1use super::*;
2use crate::{
3 JoinLines,
4 code_context_menus::CodeContextMenu,
5 inline_completion_tests::FakeInlineCompletionProvider,
6 linked_editing_ranges::LinkedEditingRanges,
7 scroll::scroll_amount::ScrollAmount,
8 test::{
9 assert_text_with_selections, build_editor,
10 editor_lsp_test_context::{EditorLspTestContext, git_commit_lang},
11 editor_test_context::EditorTestContext,
12 select_ranges,
13 },
14};
15use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
16use futures::StreamExt;
17use gpui::{
18 BackgroundExecutor, DismissEvent, Rgba, SemanticVersion, TestAppContext, UpdateGlobal,
19 VisualTestContext, WindowBounds, WindowOptions, div,
20};
21use indoc::indoc;
22use language::{
23 BracketPairConfig,
24 Capability::ReadWrite,
25 DiagnosticSourceKind, FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher,
26 LanguageName, Override, Point,
27 language_settings::{
28 AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings, FormatterList,
29 LanguageSettingsContent, LspInsertMode, PrettierSettings, SelectedFormatter,
30 },
31 tree_sitter_python,
32};
33use language_settings::{Formatter, IndentGuideSettings};
34use lsp::CompletionParams;
35use multi_buffer::{IndentGuide, PathKey};
36use parking_lot::Mutex;
37use pretty_assertions::{assert_eq, assert_ne};
38use project::{
39 FakeFs,
40 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
41 project_settings::{LspSettings, ProjectSettings},
42};
43use serde_json::{self, json};
44use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
45use std::{
46 iter,
47 sync::atomic::{self, AtomicUsize},
48};
49use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
50use text::ToPoint as _;
51use unindent::Unindent;
52use util::{
53 assert_set_eq, path,
54 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
55 uri,
56};
57use workspace::{
58 CloseActiveItem, CloseAllItems, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry,
59 OpenOptions, ViewId,
60 item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
61};
62
63#[gpui::test]
64fn test_edit_events(cx: &mut TestAppContext) {
65 init_test(cx, |_| {});
66
67 let buffer = cx.new(|cx| {
68 let mut buffer = language::Buffer::local("123456", cx);
69 buffer.set_group_interval(Duration::from_secs(1));
70 buffer
71 });
72
73 let events = Rc::new(RefCell::new(Vec::new()));
74 let editor1 = cx.add_window({
75 let events = events.clone();
76 |window, cx| {
77 let entity = cx.entity().clone();
78 cx.subscribe_in(
79 &entity,
80 window,
81 move |_, _, event: &EditorEvent, _, _| match event {
82 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
83 EditorEvent::BufferEdited => {
84 events.borrow_mut().push(("editor1", "buffer edited"))
85 }
86 _ => {}
87 },
88 )
89 .detach();
90 Editor::for_buffer(buffer.clone(), None, window, cx)
91 }
92 });
93
94 let editor2 = cx.add_window({
95 let events = events.clone();
96 |window, cx| {
97 cx.subscribe_in(
98 &cx.entity().clone(),
99 window,
100 move |_, _, event: &EditorEvent, _, _| match event {
101 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
102 EditorEvent::BufferEdited => {
103 events.borrow_mut().push(("editor2", "buffer edited"))
104 }
105 _ => {}
106 },
107 )
108 .detach();
109 Editor::for_buffer(buffer.clone(), None, window, cx)
110 }
111 });
112
113 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
114
115 // Mutating editor 1 will emit an `Edited` event only for that editor.
116 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
117 assert_eq!(
118 mem::take(&mut *events.borrow_mut()),
119 [
120 ("editor1", "edited"),
121 ("editor1", "buffer edited"),
122 ("editor2", "buffer edited"),
123 ]
124 );
125
126 // Mutating editor 2 will emit an `Edited` event only for that editor.
127 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
128 assert_eq!(
129 mem::take(&mut *events.borrow_mut()),
130 [
131 ("editor2", "edited"),
132 ("editor1", "buffer edited"),
133 ("editor2", "buffer edited"),
134 ]
135 );
136
137 // Undoing on editor 1 will emit an `Edited` event only for that editor.
138 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
139 assert_eq!(
140 mem::take(&mut *events.borrow_mut()),
141 [
142 ("editor1", "edited"),
143 ("editor1", "buffer edited"),
144 ("editor2", "buffer edited"),
145 ]
146 );
147
148 // Redoing on editor 1 will emit an `Edited` event only for that editor.
149 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
150 assert_eq!(
151 mem::take(&mut *events.borrow_mut()),
152 [
153 ("editor1", "edited"),
154 ("editor1", "buffer edited"),
155 ("editor2", "buffer edited"),
156 ]
157 );
158
159 // Undoing on editor 2 will emit an `Edited` event only for that editor.
160 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
161 assert_eq!(
162 mem::take(&mut *events.borrow_mut()),
163 [
164 ("editor2", "edited"),
165 ("editor1", "buffer edited"),
166 ("editor2", "buffer edited"),
167 ]
168 );
169
170 // Redoing on editor 2 will emit an `Edited` event only for that editor.
171 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
172 assert_eq!(
173 mem::take(&mut *events.borrow_mut()),
174 [
175 ("editor2", "edited"),
176 ("editor1", "buffer edited"),
177 ("editor2", "buffer edited"),
178 ]
179 );
180
181 // No event is emitted when the mutation is a no-op.
182 _ = editor2.update(cx, |editor, window, cx| {
183 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
184 s.select_ranges([0..0])
185 });
186
187 editor.backspace(&Backspace, window, cx);
188 });
189 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
190}
191
192#[gpui::test]
193fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
194 init_test(cx, |_| {});
195
196 let mut now = Instant::now();
197 let group_interval = Duration::from_millis(1);
198 let buffer = cx.new(|cx| {
199 let mut buf = language::Buffer::local("123456", cx);
200 buf.set_group_interval(group_interval);
201 buf
202 });
203 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
204 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
205
206 _ = editor.update(cx, |editor, window, cx| {
207 editor.start_transaction_at(now, window, cx);
208 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
209 s.select_ranges([2..4])
210 });
211
212 editor.insert("cd", window, cx);
213 editor.end_transaction_at(now, cx);
214 assert_eq!(editor.text(cx), "12cd56");
215 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
216
217 editor.start_transaction_at(now, window, cx);
218 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
219 s.select_ranges([4..5])
220 });
221 editor.insert("e", window, cx);
222 editor.end_transaction_at(now, cx);
223 assert_eq!(editor.text(cx), "12cde6");
224 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
225
226 now += group_interval + Duration::from_millis(1);
227 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
228 s.select_ranges([2..2])
229 });
230
231 // Simulate an edit in another editor
232 buffer.update(cx, |buffer, cx| {
233 buffer.start_transaction_at(now, cx);
234 buffer.edit([(0..1, "a")], None, cx);
235 buffer.edit([(1..1, "b")], None, cx);
236 buffer.end_transaction_at(now, cx);
237 });
238
239 assert_eq!(editor.text(cx), "ab2cde6");
240 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
241
242 // Last transaction happened past the group interval in a different editor.
243 // Undo it individually and don't restore selections.
244 editor.undo(&Undo, window, cx);
245 assert_eq!(editor.text(cx), "12cde6");
246 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
247
248 // First two transactions happened within the group interval in this editor.
249 // Undo them together and restore selections.
250 editor.undo(&Undo, window, cx);
251 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
252 assert_eq!(editor.text(cx), "123456");
253 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
254
255 // Redo the first two transactions together.
256 editor.redo(&Redo, window, cx);
257 assert_eq!(editor.text(cx), "12cde6");
258 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
259
260 // Redo the last transaction on its own.
261 editor.redo(&Redo, window, cx);
262 assert_eq!(editor.text(cx), "ab2cde6");
263 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
264
265 // Test empty transactions.
266 editor.start_transaction_at(now, window, cx);
267 editor.end_transaction_at(now, cx);
268 editor.undo(&Undo, window, cx);
269 assert_eq!(editor.text(cx), "12cde6");
270 });
271}
272
273#[gpui::test]
274fn test_ime_composition(cx: &mut TestAppContext) {
275 init_test(cx, |_| {});
276
277 let buffer = cx.new(|cx| {
278 let mut buffer = language::Buffer::local("abcde", cx);
279 // Ensure automatic grouping doesn't occur.
280 buffer.set_group_interval(Duration::ZERO);
281 buffer
282 });
283
284 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
285 cx.add_window(|window, cx| {
286 let mut editor = build_editor(buffer.clone(), window, cx);
287
288 // Start a new IME composition.
289 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
290 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
291 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
292 assert_eq!(editor.text(cx), "äbcde");
293 assert_eq!(
294 editor.marked_text_ranges(cx),
295 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
296 );
297
298 // Finalize IME composition.
299 editor.replace_text_in_range(None, "ā", window, cx);
300 assert_eq!(editor.text(cx), "ābcde");
301 assert_eq!(editor.marked_text_ranges(cx), None);
302
303 // IME composition edits are grouped and are undone/redone at once.
304 editor.undo(&Default::default(), window, cx);
305 assert_eq!(editor.text(cx), "abcde");
306 assert_eq!(editor.marked_text_ranges(cx), None);
307 editor.redo(&Default::default(), window, cx);
308 assert_eq!(editor.text(cx), "ābcde");
309 assert_eq!(editor.marked_text_ranges(cx), None);
310
311 // Start a new IME composition.
312 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
313 assert_eq!(
314 editor.marked_text_ranges(cx),
315 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
316 );
317
318 // Undoing during an IME composition cancels it.
319 editor.undo(&Default::default(), window, cx);
320 assert_eq!(editor.text(cx), "ābcde");
321 assert_eq!(editor.marked_text_ranges(cx), None);
322
323 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
324 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
325 assert_eq!(editor.text(cx), "ābcdè");
326 assert_eq!(
327 editor.marked_text_ranges(cx),
328 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
329 );
330
331 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
332 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
333 assert_eq!(editor.text(cx), "ābcdę");
334 assert_eq!(editor.marked_text_ranges(cx), None);
335
336 // Start a new IME composition with multiple cursors.
337 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
338 s.select_ranges([
339 OffsetUtf16(1)..OffsetUtf16(1),
340 OffsetUtf16(3)..OffsetUtf16(3),
341 OffsetUtf16(5)..OffsetUtf16(5),
342 ])
343 });
344 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
345 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
346 assert_eq!(
347 editor.marked_text_ranges(cx),
348 Some(vec![
349 OffsetUtf16(0)..OffsetUtf16(3),
350 OffsetUtf16(4)..OffsetUtf16(7),
351 OffsetUtf16(8)..OffsetUtf16(11)
352 ])
353 );
354
355 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
356 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
357 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
358 assert_eq!(
359 editor.marked_text_ranges(cx),
360 Some(vec![
361 OffsetUtf16(1)..OffsetUtf16(2),
362 OffsetUtf16(5)..OffsetUtf16(6),
363 OffsetUtf16(9)..OffsetUtf16(10)
364 ])
365 );
366
367 // Finalize IME composition with multiple cursors.
368 editor.replace_text_in_range(Some(9..10), "2", window, cx);
369 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
370 assert_eq!(editor.marked_text_ranges(cx), None);
371
372 editor
373 });
374}
375
376#[gpui::test]
377fn test_selection_with_mouse(cx: &mut TestAppContext) {
378 init_test(cx, |_| {});
379
380 let editor = cx.add_window(|window, cx| {
381 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
382 build_editor(buffer, window, cx)
383 });
384
385 _ = editor.update(cx, |editor, window, cx| {
386 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
387 });
388 assert_eq!(
389 editor
390 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
391 .unwrap(),
392 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
393 );
394
395 _ = editor.update(cx, |editor, window, cx| {
396 editor.update_selection(
397 DisplayPoint::new(DisplayRow(3), 3),
398 0,
399 gpui::Point::<f32>::default(),
400 window,
401 cx,
402 );
403 });
404
405 assert_eq!(
406 editor
407 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
408 .unwrap(),
409 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
410 );
411
412 _ = editor.update(cx, |editor, window, cx| {
413 editor.update_selection(
414 DisplayPoint::new(DisplayRow(1), 1),
415 0,
416 gpui::Point::<f32>::default(),
417 window,
418 cx,
419 );
420 });
421
422 assert_eq!(
423 editor
424 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
425 .unwrap(),
426 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
427 );
428
429 _ = editor.update(cx, |editor, window, cx| {
430 editor.end_selection(window, cx);
431 editor.update_selection(
432 DisplayPoint::new(DisplayRow(3), 3),
433 0,
434 gpui::Point::<f32>::default(),
435 window,
436 cx,
437 );
438 });
439
440 assert_eq!(
441 editor
442 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
443 .unwrap(),
444 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
445 );
446
447 _ = editor.update(cx, |editor, window, cx| {
448 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
449 editor.update_selection(
450 DisplayPoint::new(DisplayRow(0), 0),
451 0,
452 gpui::Point::<f32>::default(),
453 window,
454 cx,
455 );
456 });
457
458 assert_eq!(
459 editor
460 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
461 .unwrap(),
462 [
463 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
464 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
465 ]
466 );
467
468 _ = editor.update(cx, |editor, window, cx| {
469 editor.end_selection(window, cx);
470 });
471
472 assert_eq!(
473 editor
474 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
475 .unwrap(),
476 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
477 );
478}
479
480#[gpui::test]
481fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
482 init_test(cx, |_| {});
483
484 let editor = cx.add_window(|window, cx| {
485 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
486 build_editor(buffer, window, cx)
487 });
488
489 _ = editor.update(cx, |editor, window, cx| {
490 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
491 });
492
493 _ = editor.update(cx, |editor, window, cx| {
494 editor.end_selection(window, cx);
495 });
496
497 _ = editor.update(cx, |editor, window, cx| {
498 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
499 });
500
501 _ = editor.update(cx, |editor, window, cx| {
502 editor.end_selection(window, cx);
503 });
504
505 assert_eq!(
506 editor
507 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
508 .unwrap(),
509 [
510 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
511 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
512 ]
513 );
514
515 _ = editor.update(cx, |editor, window, cx| {
516 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
517 });
518
519 _ = editor.update(cx, |editor, window, cx| {
520 editor.end_selection(window, cx);
521 });
522
523 assert_eq!(
524 editor
525 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
526 .unwrap(),
527 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
528 );
529}
530
531#[gpui::test]
532fn test_canceling_pending_selection(cx: &mut TestAppContext) {
533 init_test(cx, |_| {});
534
535 let editor = cx.add_window(|window, cx| {
536 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
537 build_editor(buffer, window, cx)
538 });
539
540 _ = editor.update(cx, |editor, window, cx| {
541 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
542 assert_eq!(
543 editor.selections.display_ranges(cx),
544 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
545 );
546 });
547
548 _ = editor.update(cx, |editor, window, cx| {
549 editor.update_selection(
550 DisplayPoint::new(DisplayRow(3), 3),
551 0,
552 gpui::Point::<f32>::default(),
553 window,
554 cx,
555 );
556 assert_eq!(
557 editor.selections.display_ranges(cx),
558 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
559 );
560 });
561
562 _ = editor.update(cx, |editor, window, cx| {
563 editor.cancel(&Cancel, window, cx);
564 editor.update_selection(
565 DisplayPoint::new(DisplayRow(1), 1),
566 0,
567 gpui::Point::<f32>::default(),
568 window,
569 cx,
570 );
571 assert_eq!(
572 editor.selections.display_ranges(cx),
573 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
574 );
575 });
576}
577
578#[gpui::test]
579fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
580 init_test(cx, |_| {});
581
582 let editor = cx.add_window(|window, cx| {
583 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
584 build_editor(buffer, window, cx)
585 });
586
587 _ = editor.update(cx, |editor, window, cx| {
588 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
589 assert_eq!(
590 editor.selections.display_ranges(cx),
591 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
592 );
593
594 editor.move_down(&Default::default(), window, cx);
595 assert_eq!(
596 editor.selections.display_ranges(cx),
597 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
598 );
599
600 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
601 assert_eq!(
602 editor.selections.display_ranges(cx),
603 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
604 );
605
606 editor.move_up(&Default::default(), window, cx);
607 assert_eq!(
608 editor.selections.display_ranges(cx),
609 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
610 );
611 });
612}
613
614#[gpui::test]
615fn test_clone(cx: &mut TestAppContext) {
616 init_test(cx, |_| {});
617
618 let (text, selection_ranges) = marked_text_ranges(
619 indoc! {"
620 one
621 two
622 threeˇ
623 four
624 fiveˇ
625 "},
626 true,
627 );
628
629 let editor = cx.add_window(|window, cx| {
630 let buffer = MultiBuffer::build_simple(&text, cx);
631 build_editor(buffer, window, cx)
632 });
633
634 _ = editor.update(cx, |editor, window, cx| {
635 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
636 s.select_ranges(selection_ranges.clone())
637 });
638 editor.fold_creases(
639 vec![
640 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
641 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
642 ],
643 true,
644 window,
645 cx,
646 );
647 });
648
649 let cloned_editor = editor
650 .update(cx, |editor, _, cx| {
651 cx.open_window(Default::default(), |window, cx| {
652 cx.new(|cx| editor.clone(window, cx))
653 })
654 })
655 .unwrap()
656 .unwrap();
657
658 let snapshot = editor
659 .update(cx, |e, window, cx| e.snapshot(window, cx))
660 .unwrap();
661 let cloned_snapshot = cloned_editor
662 .update(cx, |e, window, cx| e.snapshot(window, cx))
663 .unwrap();
664
665 assert_eq!(
666 cloned_editor
667 .update(cx, |e, _, cx| e.display_text(cx))
668 .unwrap(),
669 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
670 );
671 assert_eq!(
672 cloned_snapshot
673 .folds_in_range(0..text.len())
674 .collect::<Vec<_>>(),
675 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
676 );
677 assert_set_eq!(
678 cloned_editor
679 .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
680 .unwrap(),
681 editor
682 .update(cx, |editor, _, cx| editor.selections.ranges(cx))
683 .unwrap()
684 );
685 assert_set_eq!(
686 cloned_editor
687 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
688 .unwrap(),
689 editor
690 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
691 .unwrap()
692 );
693}
694
695#[gpui::test]
696async fn test_navigation_history(cx: &mut TestAppContext) {
697 init_test(cx, |_| {});
698
699 use workspace::item::Item;
700
701 let fs = FakeFs::new(cx.executor());
702 let project = Project::test(fs, [], cx).await;
703 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
704 let pane = workspace
705 .update(cx, |workspace, _, _| workspace.active_pane().clone())
706 .unwrap();
707
708 _ = workspace.update(cx, |_v, window, cx| {
709 cx.new(|cx| {
710 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
711 let mut editor = build_editor(buffer.clone(), window, cx);
712 let handle = cx.entity();
713 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
714
715 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
716 editor.nav_history.as_mut().unwrap().pop_backward(cx)
717 }
718
719 // Move the cursor a small distance.
720 // Nothing is added to the navigation history.
721 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
722 s.select_display_ranges([
723 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
724 ])
725 });
726 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
727 s.select_display_ranges([
728 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
729 ])
730 });
731 assert!(pop_history(&mut editor, cx).is_none());
732
733 // Move the cursor a large distance.
734 // The history can jump back to the previous position.
735 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
736 s.select_display_ranges([
737 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
738 ])
739 });
740 let nav_entry = pop_history(&mut editor, cx).unwrap();
741 editor.navigate(nav_entry.data.unwrap(), window, cx);
742 assert_eq!(nav_entry.item.id(), cx.entity_id());
743 assert_eq!(
744 editor.selections.display_ranges(cx),
745 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
746 );
747 assert!(pop_history(&mut editor, cx).is_none());
748
749 // Move the cursor a small distance via the mouse.
750 // Nothing is added to the navigation history.
751 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
752 editor.end_selection(window, cx);
753 assert_eq!(
754 editor.selections.display_ranges(cx),
755 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
756 );
757 assert!(pop_history(&mut editor, cx).is_none());
758
759 // Move the cursor a large distance via the mouse.
760 // The history can jump back to the previous position.
761 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
762 editor.end_selection(window, cx);
763 assert_eq!(
764 editor.selections.display_ranges(cx),
765 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
766 );
767 let nav_entry = pop_history(&mut editor, cx).unwrap();
768 editor.navigate(nav_entry.data.unwrap(), window, cx);
769 assert_eq!(nav_entry.item.id(), cx.entity_id());
770 assert_eq!(
771 editor.selections.display_ranges(cx),
772 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
773 );
774 assert!(pop_history(&mut editor, cx).is_none());
775
776 // Set scroll position to check later
777 editor.set_scroll_position(gpui::Point::<f32>::new(5.5, 5.5), window, cx);
778 let original_scroll_position = editor.scroll_manager.anchor();
779
780 // Jump to the end of the document and adjust scroll
781 editor.move_to_end(&MoveToEnd, window, cx);
782 editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), window, cx);
783 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
784
785 let nav_entry = pop_history(&mut editor, cx).unwrap();
786 editor.navigate(nav_entry.data.unwrap(), window, cx);
787 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
788
789 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
790 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
791 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
792 let invalid_point = Point::new(9999, 0);
793 editor.navigate(
794 Box::new(NavigationData {
795 cursor_anchor: invalid_anchor,
796 cursor_position: invalid_point,
797 scroll_anchor: ScrollAnchor {
798 anchor: invalid_anchor,
799 offset: Default::default(),
800 },
801 scroll_top_row: invalid_point.row,
802 }),
803 window,
804 cx,
805 );
806 assert_eq!(
807 editor.selections.display_ranges(cx),
808 &[editor.max_point(cx)..editor.max_point(cx)]
809 );
810 assert_eq!(
811 editor.scroll_position(cx),
812 gpui::Point::new(0., editor.max_point(cx).row().as_f32())
813 );
814
815 editor
816 })
817 });
818}
819
820#[gpui::test]
821fn test_cancel(cx: &mut TestAppContext) {
822 init_test(cx, |_| {});
823
824 let editor = cx.add_window(|window, cx| {
825 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
826 build_editor(buffer, window, cx)
827 });
828
829 _ = editor.update(cx, |editor, window, cx| {
830 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
831 editor.update_selection(
832 DisplayPoint::new(DisplayRow(1), 1),
833 0,
834 gpui::Point::<f32>::default(),
835 window,
836 cx,
837 );
838 editor.end_selection(window, cx);
839
840 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
841 editor.update_selection(
842 DisplayPoint::new(DisplayRow(0), 3),
843 0,
844 gpui::Point::<f32>::default(),
845 window,
846 cx,
847 );
848 editor.end_selection(window, cx);
849 assert_eq!(
850 editor.selections.display_ranges(cx),
851 [
852 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
853 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
854 ]
855 );
856 });
857
858 _ = editor.update(cx, |editor, window, cx| {
859 editor.cancel(&Cancel, window, cx);
860 assert_eq!(
861 editor.selections.display_ranges(cx),
862 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
863 );
864 });
865
866 _ = editor.update(cx, |editor, window, cx| {
867 editor.cancel(&Cancel, window, cx);
868 assert_eq!(
869 editor.selections.display_ranges(cx),
870 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
871 );
872 });
873}
874
875#[gpui::test]
876fn test_fold_action(cx: &mut TestAppContext) {
877 init_test(cx, |_| {});
878
879 let editor = cx.add_window(|window, cx| {
880 let buffer = MultiBuffer::build_simple(
881 &"
882 impl Foo {
883 // Hello!
884
885 fn a() {
886 1
887 }
888
889 fn b() {
890 2
891 }
892
893 fn c() {
894 3
895 }
896 }
897 "
898 .unindent(),
899 cx,
900 );
901 build_editor(buffer.clone(), window, cx)
902 });
903
904 _ = editor.update(cx, |editor, window, cx| {
905 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
906 s.select_display_ranges([
907 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
908 ]);
909 });
910 editor.fold(&Fold, window, cx);
911 assert_eq!(
912 editor.display_text(cx),
913 "
914 impl Foo {
915 // Hello!
916
917 fn a() {
918 1
919 }
920
921 fn b() {⋯
922 }
923
924 fn c() {⋯
925 }
926 }
927 "
928 .unindent(),
929 );
930
931 editor.fold(&Fold, window, cx);
932 assert_eq!(
933 editor.display_text(cx),
934 "
935 impl Foo {⋯
936 }
937 "
938 .unindent(),
939 );
940
941 editor.unfold_lines(&UnfoldLines, window, cx);
942 assert_eq!(
943 editor.display_text(cx),
944 "
945 impl Foo {
946 // Hello!
947
948 fn a() {
949 1
950 }
951
952 fn b() {⋯
953 }
954
955 fn c() {⋯
956 }
957 }
958 "
959 .unindent(),
960 );
961
962 editor.unfold_lines(&UnfoldLines, window, cx);
963 assert_eq!(
964 editor.display_text(cx),
965 editor.buffer.read(cx).read(cx).text()
966 );
967 });
968}
969
970#[gpui::test]
971fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
972 init_test(cx, |_| {});
973
974 let editor = cx.add_window(|window, cx| {
975 let buffer = MultiBuffer::build_simple(
976 &"
977 class Foo:
978 # Hello!
979
980 def a():
981 print(1)
982
983 def b():
984 print(2)
985
986 def c():
987 print(3)
988 "
989 .unindent(),
990 cx,
991 );
992 build_editor(buffer.clone(), window, cx)
993 });
994
995 _ = editor.update(cx, |editor, window, cx| {
996 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
997 s.select_display_ranges([
998 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
999 ]);
1000 });
1001 editor.fold(&Fold, window, cx);
1002 assert_eq!(
1003 editor.display_text(cx),
1004 "
1005 class Foo:
1006 # Hello!
1007
1008 def a():
1009 print(1)
1010
1011 def b():⋯
1012
1013 def c():⋯
1014 "
1015 .unindent(),
1016 );
1017
1018 editor.fold(&Fold, window, cx);
1019 assert_eq!(
1020 editor.display_text(cx),
1021 "
1022 class Foo:⋯
1023 "
1024 .unindent(),
1025 );
1026
1027 editor.unfold_lines(&UnfoldLines, window, cx);
1028 assert_eq!(
1029 editor.display_text(cx),
1030 "
1031 class Foo:
1032 # Hello!
1033
1034 def a():
1035 print(1)
1036
1037 def b():⋯
1038
1039 def c():⋯
1040 "
1041 .unindent(),
1042 );
1043
1044 editor.unfold_lines(&UnfoldLines, window, cx);
1045 assert_eq!(
1046 editor.display_text(cx),
1047 editor.buffer.read(cx).read(cx).text()
1048 );
1049 });
1050}
1051
1052#[gpui::test]
1053fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1054 init_test(cx, |_| {});
1055
1056 let editor = cx.add_window(|window, cx| {
1057 let buffer = MultiBuffer::build_simple(
1058 &"
1059 class Foo:
1060 # Hello!
1061
1062 def a():
1063 print(1)
1064
1065 def b():
1066 print(2)
1067
1068
1069 def c():
1070 print(3)
1071
1072
1073 "
1074 .unindent(),
1075 cx,
1076 );
1077 build_editor(buffer.clone(), window, cx)
1078 });
1079
1080 _ = editor.update(cx, |editor, window, cx| {
1081 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1082 s.select_display_ranges([
1083 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1084 ]);
1085 });
1086 editor.fold(&Fold, window, cx);
1087 assert_eq!(
1088 editor.display_text(cx),
1089 "
1090 class Foo:
1091 # Hello!
1092
1093 def a():
1094 print(1)
1095
1096 def b():⋯
1097
1098
1099 def c():⋯
1100
1101
1102 "
1103 .unindent(),
1104 );
1105
1106 editor.fold(&Fold, window, cx);
1107 assert_eq!(
1108 editor.display_text(cx),
1109 "
1110 class Foo:⋯
1111
1112
1113 "
1114 .unindent(),
1115 );
1116
1117 editor.unfold_lines(&UnfoldLines, window, cx);
1118 assert_eq!(
1119 editor.display_text(cx),
1120 "
1121 class Foo:
1122 # Hello!
1123
1124 def a():
1125 print(1)
1126
1127 def b():⋯
1128
1129
1130 def c():⋯
1131
1132
1133 "
1134 .unindent(),
1135 );
1136
1137 editor.unfold_lines(&UnfoldLines, window, cx);
1138 assert_eq!(
1139 editor.display_text(cx),
1140 editor.buffer.read(cx).read(cx).text()
1141 );
1142 });
1143}
1144
1145#[gpui::test]
1146fn test_fold_at_level(cx: &mut TestAppContext) {
1147 init_test(cx, |_| {});
1148
1149 let editor = cx.add_window(|window, cx| {
1150 let buffer = MultiBuffer::build_simple(
1151 &"
1152 class Foo:
1153 # Hello!
1154
1155 def a():
1156 print(1)
1157
1158 def b():
1159 print(2)
1160
1161
1162 class Bar:
1163 # World!
1164
1165 def a():
1166 print(1)
1167
1168 def b():
1169 print(2)
1170
1171
1172 "
1173 .unindent(),
1174 cx,
1175 );
1176 build_editor(buffer.clone(), window, cx)
1177 });
1178
1179 _ = editor.update(cx, |editor, window, cx| {
1180 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1181 assert_eq!(
1182 editor.display_text(cx),
1183 "
1184 class Foo:
1185 # Hello!
1186
1187 def a():⋯
1188
1189 def b():⋯
1190
1191
1192 class Bar:
1193 # World!
1194
1195 def a():⋯
1196
1197 def b():⋯
1198
1199
1200 "
1201 .unindent(),
1202 );
1203
1204 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1205 assert_eq!(
1206 editor.display_text(cx),
1207 "
1208 class Foo:⋯
1209
1210
1211 class Bar:⋯
1212
1213
1214 "
1215 .unindent(),
1216 );
1217
1218 editor.unfold_all(&UnfoldAll, window, cx);
1219 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1220 assert_eq!(
1221 editor.display_text(cx),
1222 "
1223 class Foo:
1224 # Hello!
1225
1226 def a():
1227 print(1)
1228
1229 def b():
1230 print(2)
1231
1232
1233 class Bar:
1234 # World!
1235
1236 def a():
1237 print(1)
1238
1239 def b():
1240 print(2)
1241
1242
1243 "
1244 .unindent(),
1245 );
1246
1247 assert_eq!(
1248 editor.display_text(cx),
1249 editor.buffer.read(cx).read(cx).text()
1250 );
1251 });
1252}
1253
1254#[gpui::test]
1255fn test_move_cursor(cx: &mut TestAppContext) {
1256 init_test(cx, |_| {});
1257
1258 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1259 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1260
1261 buffer.update(cx, |buffer, cx| {
1262 buffer.edit(
1263 vec![
1264 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1265 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1266 ],
1267 None,
1268 cx,
1269 );
1270 });
1271 _ = editor.update(cx, |editor, window, cx| {
1272 assert_eq!(
1273 editor.selections.display_ranges(cx),
1274 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1275 );
1276
1277 editor.move_down(&MoveDown, window, cx);
1278 assert_eq!(
1279 editor.selections.display_ranges(cx),
1280 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1281 );
1282
1283 editor.move_right(&MoveRight, window, cx);
1284 assert_eq!(
1285 editor.selections.display_ranges(cx),
1286 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1287 );
1288
1289 editor.move_left(&MoveLeft, window, cx);
1290 assert_eq!(
1291 editor.selections.display_ranges(cx),
1292 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1293 );
1294
1295 editor.move_up(&MoveUp, window, cx);
1296 assert_eq!(
1297 editor.selections.display_ranges(cx),
1298 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1299 );
1300
1301 editor.move_to_end(&MoveToEnd, window, cx);
1302 assert_eq!(
1303 editor.selections.display_ranges(cx),
1304 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1305 );
1306
1307 editor.move_to_beginning(&MoveToBeginning, window, cx);
1308 assert_eq!(
1309 editor.selections.display_ranges(cx),
1310 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1311 );
1312
1313 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1314 s.select_display_ranges([
1315 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1316 ]);
1317 });
1318 editor.select_to_beginning(&SelectToBeginning, window, cx);
1319 assert_eq!(
1320 editor.selections.display_ranges(cx),
1321 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1322 );
1323
1324 editor.select_to_end(&SelectToEnd, window, cx);
1325 assert_eq!(
1326 editor.selections.display_ranges(cx),
1327 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1328 );
1329 });
1330}
1331
1332#[gpui::test]
1333fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1334 init_test(cx, |_| {});
1335
1336 let editor = cx.add_window(|window, cx| {
1337 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1338 build_editor(buffer.clone(), window, cx)
1339 });
1340
1341 assert_eq!('🟥'.len_utf8(), 4);
1342 assert_eq!('α'.len_utf8(), 2);
1343
1344 _ = editor.update(cx, |editor, window, cx| {
1345 editor.fold_creases(
1346 vec![
1347 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1348 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1349 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1350 ],
1351 true,
1352 window,
1353 cx,
1354 );
1355 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1356
1357 editor.move_right(&MoveRight, window, cx);
1358 assert_eq!(
1359 editor.selections.display_ranges(cx),
1360 &[empty_range(0, "🟥".len())]
1361 );
1362 editor.move_right(&MoveRight, window, cx);
1363 assert_eq!(
1364 editor.selections.display_ranges(cx),
1365 &[empty_range(0, "🟥🟧".len())]
1366 );
1367 editor.move_right(&MoveRight, window, cx);
1368 assert_eq!(
1369 editor.selections.display_ranges(cx),
1370 &[empty_range(0, "🟥🟧⋯".len())]
1371 );
1372
1373 editor.move_down(&MoveDown, window, cx);
1374 assert_eq!(
1375 editor.selections.display_ranges(cx),
1376 &[empty_range(1, "ab⋯e".len())]
1377 );
1378 editor.move_left(&MoveLeft, window, cx);
1379 assert_eq!(
1380 editor.selections.display_ranges(cx),
1381 &[empty_range(1, "ab⋯".len())]
1382 );
1383 editor.move_left(&MoveLeft, window, cx);
1384 assert_eq!(
1385 editor.selections.display_ranges(cx),
1386 &[empty_range(1, "ab".len())]
1387 );
1388 editor.move_left(&MoveLeft, window, cx);
1389 assert_eq!(
1390 editor.selections.display_ranges(cx),
1391 &[empty_range(1, "a".len())]
1392 );
1393
1394 editor.move_down(&MoveDown, window, cx);
1395 assert_eq!(
1396 editor.selections.display_ranges(cx),
1397 &[empty_range(2, "α".len())]
1398 );
1399 editor.move_right(&MoveRight, window, cx);
1400 assert_eq!(
1401 editor.selections.display_ranges(cx),
1402 &[empty_range(2, "αβ".len())]
1403 );
1404 editor.move_right(&MoveRight, window, cx);
1405 assert_eq!(
1406 editor.selections.display_ranges(cx),
1407 &[empty_range(2, "αβ⋯".len())]
1408 );
1409 editor.move_right(&MoveRight, window, cx);
1410 assert_eq!(
1411 editor.selections.display_ranges(cx),
1412 &[empty_range(2, "αβ⋯ε".len())]
1413 );
1414
1415 editor.move_up(&MoveUp, window, cx);
1416 assert_eq!(
1417 editor.selections.display_ranges(cx),
1418 &[empty_range(1, "ab⋯e".len())]
1419 );
1420 editor.move_down(&MoveDown, window, cx);
1421 assert_eq!(
1422 editor.selections.display_ranges(cx),
1423 &[empty_range(2, "αβ⋯ε".len())]
1424 );
1425 editor.move_up(&MoveUp, window, cx);
1426 assert_eq!(
1427 editor.selections.display_ranges(cx),
1428 &[empty_range(1, "ab⋯e".len())]
1429 );
1430
1431 editor.move_up(&MoveUp, window, cx);
1432 assert_eq!(
1433 editor.selections.display_ranges(cx),
1434 &[empty_range(0, "🟥🟧".len())]
1435 );
1436 editor.move_left(&MoveLeft, window, cx);
1437 assert_eq!(
1438 editor.selections.display_ranges(cx),
1439 &[empty_range(0, "🟥".len())]
1440 );
1441 editor.move_left(&MoveLeft, window, cx);
1442 assert_eq!(
1443 editor.selections.display_ranges(cx),
1444 &[empty_range(0, "".len())]
1445 );
1446 });
1447}
1448
1449#[gpui::test]
1450fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1451 init_test(cx, |_| {});
1452
1453 let editor = cx.add_window(|window, cx| {
1454 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1455 build_editor(buffer.clone(), window, cx)
1456 });
1457 _ = editor.update(cx, |editor, window, cx| {
1458 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1459 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1460 });
1461
1462 // moving above start of document should move selection to start of document,
1463 // but the next move down should still be at the original goal_x
1464 editor.move_up(&MoveUp, window, cx);
1465 assert_eq!(
1466 editor.selections.display_ranges(cx),
1467 &[empty_range(0, "".len())]
1468 );
1469
1470 editor.move_down(&MoveDown, window, cx);
1471 assert_eq!(
1472 editor.selections.display_ranges(cx),
1473 &[empty_range(1, "abcd".len())]
1474 );
1475
1476 editor.move_down(&MoveDown, window, cx);
1477 assert_eq!(
1478 editor.selections.display_ranges(cx),
1479 &[empty_range(2, "αβγ".len())]
1480 );
1481
1482 editor.move_down(&MoveDown, window, cx);
1483 assert_eq!(
1484 editor.selections.display_ranges(cx),
1485 &[empty_range(3, "abcd".len())]
1486 );
1487
1488 editor.move_down(&MoveDown, window, cx);
1489 assert_eq!(
1490 editor.selections.display_ranges(cx),
1491 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1492 );
1493
1494 // moving past end of document should not change goal_x
1495 editor.move_down(&MoveDown, window, cx);
1496 assert_eq!(
1497 editor.selections.display_ranges(cx),
1498 &[empty_range(5, "".len())]
1499 );
1500
1501 editor.move_down(&MoveDown, window, cx);
1502 assert_eq!(
1503 editor.selections.display_ranges(cx),
1504 &[empty_range(5, "".len())]
1505 );
1506
1507 editor.move_up(&MoveUp, window, cx);
1508 assert_eq!(
1509 editor.selections.display_ranges(cx),
1510 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1511 );
1512
1513 editor.move_up(&MoveUp, window, cx);
1514 assert_eq!(
1515 editor.selections.display_ranges(cx),
1516 &[empty_range(3, "abcd".len())]
1517 );
1518
1519 editor.move_up(&MoveUp, window, cx);
1520 assert_eq!(
1521 editor.selections.display_ranges(cx),
1522 &[empty_range(2, "αβγ".len())]
1523 );
1524 });
1525}
1526
1527#[gpui::test]
1528fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1529 init_test(cx, |_| {});
1530 let move_to_beg = MoveToBeginningOfLine {
1531 stop_at_soft_wraps: true,
1532 stop_at_indent: true,
1533 };
1534
1535 let delete_to_beg = DeleteToBeginningOfLine {
1536 stop_at_indent: false,
1537 };
1538
1539 let move_to_end = MoveToEndOfLine {
1540 stop_at_soft_wraps: true,
1541 };
1542
1543 let editor = cx.add_window(|window, cx| {
1544 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1545 build_editor(buffer, window, cx)
1546 });
1547 _ = editor.update(cx, |editor, window, cx| {
1548 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1549 s.select_display_ranges([
1550 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1551 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1552 ]);
1553 });
1554 });
1555
1556 _ = editor.update(cx, |editor, window, cx| {
1557 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1558 assert_eq!(
1559 editor.selections.display_ranges(cx),
1560 &[
1561 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1562 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1563 ]
1564 );
1565 });
1566
1567 _ = editor.update(cx, |editor, window, cx| {
1568 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1569 assert_eq!(
1570 editor.selections.display_ranges(cx),
1571 &[
1572 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1573 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1574 ]
1575 );
1576 });
1577
1578 _ = editor.update(cx, |editor, window, cx| {
1579 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1580 assert_eq!(
1581 editor.selections.display_ranges(cx),
1582 &[
1583 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1584 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1585 ]
1586 );
1587 });
1588
1589 _ = editor.update(cx, |editor, window, cx| {
1590 editor.move_to_end_of_line(&move_to_end, window, cx);
1591 assert_eq!(
1592 editor.selections.display_ranges(cx),
1593 &[
1594 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1595 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1596 ]
1597 );
1598 });
1599
1600 // Moving to the end of line again is a no-op.
1601 _ = editor.update(cx, |editor, window, cx| {
1602 editor.move_to_end_of_line(&move_to_end, window, cx);
1603 assert_eq!(
1604 editor.selections.display_ranges(cx),
1605 &[
1606 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1607 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1608 ]
1609 );
1610 });
1611
1612 _ = editor.update(cx, |editor, window, cx| {
1613 editor.move_left(&MoveLeft, window, cx);
1614 editor.select_to_beginning_of_line(
1615 &SelectToBeginningOfLine {
1616 stop_at_soft_wraps: true,
1617 stop_at_indent: true,
1618 },
1619 window,
1620 cx,
1621 );
1622 assert_eq!(
1623 editor.selections.display_ranges(cx),
1624 &[
1625 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1626 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1627 ]
1628 );
1629 });
1630
1631 _ = editor.update(cx, |editor, window, cx| {
1632 editor.select_to_beginning_of_line(
1633 &SelectToBeginningOfLine {
1634 stop_at_soft_wraps: true,
1635 stop_at_indent: true,
1636 },
1637 window,
1638 cx,
1639 );
1640 assert_eq!(
1641 editor.selections.display_ranges(cx),
1642 &[
1643 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1644 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1645 ]
1646 );
1647 });
1648
1649 _ = editor.update(cx, |editor, window, cx| {
1650 editor.select_to_beginning_of_line(
1651 &SelectToBeginningOfLine {
1652 stop_at_soft_wraps: true,
1653 stop_at_indent: true,
1654 },
1655 window,
1656 cx,
1657 );
1658 assert_eq!(
1659 editor.selections.display_ranges(cx),
1660 &[
1661 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1662 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1663 ]
1664 );
1665 });
1666
1667 _ = editor.update(cx, |editor, window, cx| {
1668 editor.select_to_end_of_line(
1669 &SelectToEndOfLine {
1670 stop_at_soft_wraps: true,
1671 },
1672 window,
1673 cx,
1674 );
1675 assert_eq!(
1676 editor.selections.display_ranges(cx),
1677 &[
1678 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1679 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1680 ]
1681 );
1682 });
1683
1684 _ = editor.update(cx, |editor, window, cx| {
1685 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1686 assert_eq!(editor.display_text(cx), "ab\n de");
1687 assert_eq!(
1688 editor.selections.display_ranges(cx),
1689 &[
1690 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1691 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1692 ]
1693 );
1694 });
1695
1696 _ = editor.update(cx, |editor, window, cx| {
1697 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1698 assert_eq!(editor.display_text(cx), "\n");
1699 assert_eq!(
1700 editor.selections.display_ranges(cx),
1701 &[
1702 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1703 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1704 ]
1705 );
1706 });
1707}
1708
1709#[gpui::test]
1710fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1711 init_test(cx, |_| {});
1712 let move_to_beg = MoveToBeginningOfLine {
1713 stop_at_soft_wraps: false,
1714 stop_at_indent: false,
1715 };
1716
1717 let move_to_end = MoveToEndOfLine {
1718 stop_at_soft_wraps: false,
1719 };
1720
1721 let editor = cx.add_window(|window, cx| {
1722 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1723 build_editor(buffer, window, cx)
1724 });
1725
1726 _ = editor.update(cx, |editor, window, cx| {
1727 editor.set_wrap_width(Some(140.0.into()), cx);
1728
1729 // We expect the following lines after wrapping
1730 // ```
1731 // thequickbrownfox
1732 // jumpedoverthelazydo
1733 // gs
1734 // ```
1735 // The final `gs` was soft-wrapped onto a new line.
1736 assert_eq!(
1737 "thequickbrownfox\njumpedoverthelaz\nydogs",
1738 editor.display_text(cx),
1739 );
1740
1741 // First, let's assert behavior on the first line, that was not soft-wrapped.
1742 // Start the cursor at the `k` on the first line
1743 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1744 s.select_display_ranges([
1745 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1746 ]);
1747 });
1748
1749 // Moving to the beginning of the line should put us at the beginning of the line.
1750 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1751 assert_eq!(
1752 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1753 editor.selections.display_ranges(cx)
1754 );
1755
1756 // Moving to the end of the line should put us at the end of the line.
1757 editor.move_to_end_of_line(&move_to_end, window, cx);
1758 assert_eq!(
1759 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1760 editor.selections.display_ranges(cx)
1761 );
1762
1763 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1764 // Start the cursor at the last line (`y` that was wrapped to a new line)
1765 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1766 s.select_display_ranges([
1767 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1768 ]);
1769 });
1770
1771 // Moving to the beginning of the line should put us at the start of the second line of
1772 // display text, i.e., the `j`.
1773 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1774 assert_eq!(
1775 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1776 editor.selections.display_ranges(cx)
1777 );
1778
1779 // Moving to the beginning of the line again should be a no-op.
1780 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1781 assert_eq!(
1782 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1783 editor.selections.display_ranges(cx)
1784 );
1785
1786 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1787 // next display line.
1788 editor.move_to_end_of_line(&move_to_end, window, cx);
1789 assert_eq!(
1790 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1791 editor.selections.display_ranges(cx)
1792 );
1793
1794 // Moving to the end of the line again should be a no-op.
1795 editor.move_to_end_of_line(&move_to_end, window, cx);
1796 assert_eq!(
1797 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1798 editor.selections.display_ranges(cx)
1799 );
1800 });
1801}
1802
1803#[gpui::test]
1804fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1805 init_test(cx, |_| {});
1806
1807 let move_to_beg = MoveToBeginningOfLine {
1808 stop_at_soft_wraps: true,
1809 stop_at_indent: true,
1810 };
1811
1812 let select_to_beg = SelectToBeginningOfLine {
1813 stop_at_soft_wraps: true,
1814 stop_at_indent: true,
1815 };
1816
1817 let delete_to_beg = DeleteToBeginningOfLine {
1818 stop_at_indent: true,
1819 };
1820
1821 let move_to_end = MoveToEndOfLine {
1822 stop_at_soft_wraps: false,
1823 };
1824
1825 let editor = cx.add_window(|window, cx| {
1826 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1827 build_editor(buffer, window, cx)
1828 });
1829
1830 _ = editor.update(cx, |editor, window, cx| {
1831 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1832 s.select_display_ranges([
1833 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1834 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1835 ]);
1836 });
1837
1838 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1839 // and the second cursor at the first non-whitespace character in the line.
1840 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1841 assert_eq!(
1842 editor.selections.display_ranges(cx),
1843 &[
1844 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1845 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1846 ]
1847 );
1848
1849 // Moving to the beginning of the line again should be a no-op for the first cursor,
1850 // and should move the second cursor to the beginning of the line.
1851 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1852 assert_eq!(
1853 editor.selections.display_ranges(cx),
1854 &[
1855 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1856 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1857 ]
1858 );
1859
1860 // Moving to the beginning of the line again should still be a no-op for the first cursor,
1861 // and should move the second cursor back to the first non-whitespace character in the line.
1862 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1863 assert_eq!(
1864 editor.selections.display_ranges(cx),
1865 &[
1866 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1867 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1868 ]
1869 );
1870
1871 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
1872 // and to the first non-whitespace character in the line for the second cursor.
1873 editor.move_to_end_of_line(&move_to_end, window, cx);
1874 editor.move_left(&MoveLeft, window, cx);
1875 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1876 assert_eq!(
1877 editor.selections.display_ranges(cx),
1878 &[
1879 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1880 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1881 ]
1882 );
1883
1884 // Selecting to the beginning of the line again should be a no-op for the first cursor,
1885 // and should select to the beginning of the line for the second cursor.
1886 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1887 assert_eq!(
1888 editor.selections.display_ranges(cx),
1889 &[
1890 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1891 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1892 ]
1893 );
1894
1895 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
1896 // and should delete to the first non-whitespace character in the line for the second cursor.
1897 editor.move_to_end_of_line(&move_to_end, window, cx);
1898 editor.move_left(&MoveLeft, window, cx);
1899 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1900 assert_eq!(editor.text(cx), "c\n f");
1901 });
1902}
1903
1904#[gpui::test]
1905fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1906 init_test(cx, |_| {});
1907
1908 let editor = cx.add_window(|window, cx| {
1909 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1910 build_editor(buffer, window, cx)
1911 });
1912 _ = editor.update(cx, |editor, window, cx| {
1913 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1914 s.select_display_ranges([
1915 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
1916 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
1917 ])
1918 });
1919 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1920 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1921
1922 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1923 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
1924
1925 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1926 assert_selection_ranges("use ˇstd::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1927
1928 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1929 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1930
1931 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1932 assert_selection_ranges("ˇuse std::str::{foo, ˇbar}\n\n {baz.qux()}", editor, cx);
1933
1934 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1935 assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1936
1937 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1938 assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
1939
1940 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1941 assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1942
1943 editor.move_right(&MoveRight, window, cx);
1944 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1945 assert_selection_ranges(
1946 "use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}",
1947 editor,
1948 cx,
1949 );
1950
1951 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1952 assert_selection_ranges(
1953 "use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}",
1954 editor,
1955 cx,
1956 );
1957
1958 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
1959 assert_selection_ranges(
1960 "use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}",
1961 editor,
1962 cx,
1963 );
1964 });
1965}
1966
1967#[gpui::test]
1968fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
1969 init_test(cx, |_| {});
1970
1971 let editor = cx.add_window(|window, cx| {
1972 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
1973 build_editor(buffer, window, cx)
1974 });
1975
1976 _ = editor.update(cx, |editor, window, cx| {
1977 editor.set_wrap_width(Some(140.0.into()), cx);
1978 assert_eq!(
1979 editor.display_text(cx),
1980 "use one::{\n two::three::\n four::five\n};"
1981 );
1982
1983 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1984 s.select_display_ranges([
1985 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
1986 ]);
1987 });
1988
1989 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1990 assert_eq!(
1991 editor.selections.display_ranges(cx),
1992 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
1993 );
1994
1995 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1996 assert_eq!(
1997 editor.selections.display_ranges(cx),
1998 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
1999 );
2000
2001 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2002 assert_eq!(
2003 editor.selections.display_ranges(cx),
2004 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2005 );
2006
2007 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2008 assert_eq!(
2009 editor.selections.display_ranges(cx),
2010 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2011 );
2012
2013 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2014 assert_eq!(
2015 editor.selections.display_ranges(cx),
2016 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2017 );
2018
2019 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2020 assert_eq!(
2021 editor.selections.display_ranges(cx),
2022 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2023 );
2024 });
2025}
2026
2027#[gpui::test]
2028async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2029 init_test(cx, |_| {});
2030 let mut cx = EditorTestContext::new(cx).await;
2031
2032 let line_height = cx.editor(|editor, window, _| {
2033 editor
2034 .style()
2035 .unwrap()
2036 .text
2037 .line_height_in_pixels(window.rem_size())
2038 });
2039 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2040
2041 cx.set_state(
2042 &r#"ˇone
2043 two
2044
2045 three
2046 fourˇ
2047 five
2048
2049 six"#
2050 .unindent(),
2051 );
2052
2053 cx.update_editor(|editor, window, cx| {
2054 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2055 });
2056 cx.assert_editor_state(
2057 &r#"one
2058 two
2059 ˇ
2060 three
2061 four
2062 five
2063 ˇ
2064 six"#
2065 .unindent(),
2066 );
2067
2068 cx.update_editor(|editor, window, cx| {
2069 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2070 });
2071 cx.assert_editor_state(
2072 &r#"one
2073 two
2074
2075 three
2076 four
2077 five
2078 ˇ
2079 sixˇ"#
2080 .unindent(),
2081 );
2082
2083 cx.update_editor(|editor, window, cx| {
2084 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2085 });
2086 cx.assert_editor_state(
2087 &r#"one
2088 two
2089
2090 three
2091 four
2092 five
2093
2094 sixˇ"#
2095 .unindent(),
2096 );
2097
2098 cx.update_editor(|editor, window, cx| {
2099 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2100 });
2101 cx.assert_editor_state(
2102 &r#"one
2103 two
2104
2105 three
2106 four
2107 five
2108 ˇ
2109 six"#
2110 .unindent(),
2111 );
2112
2113 cx.update_editor(|editor, window, cx| {
2114 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2115 });
2116 cx.assert_editor_state(
2117 &r#"one
2118 two
2119 ˇ
2120 three
2121 four
2122 five
2123
2124 six"#
2125 .unindent(),
2126 );
2127
2128 cx.update_editor(|editor, window, cx| {
2129 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2130 });
2131 cx.assert_editor_state(
2132 &r#"ˇone
2133 two
2134
2135 three
2136 four
2137 five
2138
2139 six"#
2140 .unindent(),
2141 );
2142}
2143
2144#[gpui::test]
2145async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2146 init_test(cx, |_| {});
2147 let mut cx = EditorTestContext::new(cx).await;
2148 let line_height = cx.editor(|editor, window, _| {
2149 editor
2150 .style()
2151 .unwrap()
2152 .text
2153 .line_height_in_pixels(window.rem_size())
2154 });
2155 let window = cx.window;
2156 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2157
2158 cx.set_state(
2159 r#"ˇone
2160 two
2161 three
2162 four
2163 five
2164 six
2165 seven
2166 eight
2167 nine
2168 ten
2169 "#,
2170 );
2171
2172 cx.update_editor(|editor, window, cx| {
2173 assert_eq!(
2174 editor.snapshot(window, cx).scroll_position(),
2175 gpui::Point::new(0., 0.)
2176 );
2177 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2178 assert_eq!(
2179 editor.snapshot(window, cx).scroll_position(),
2180 gpui::Point::new(0., 3.)
2181 );
2182 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2183 assert_eq!(
2184 editor.snapshot(window, cx).scroll_position(),
2185 gpui::Point::new(0., 6.)
2186 );
2187 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2188 assert_eq!(
2189 editor.snapshot(window, cx).scroll_position(),
2190 gpui::Point::new(0., 3.)
2191 );
2192
2193 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2194 assert_eq!(
2195 editor.snapshot(window, cx).scroll_position(),
2196 gpui::Point::new(0., 1.)
2197 );
2198 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2199 assert_eq!(
2200 editor.snapshot(window, cx).scroll_position(),
2201 gpui::Point::new(0., 3.)
2202 );
2203 });
2204}
2205
2206#[gpui::test]
2207async fn test_autoscroll(cx: &mut TestAppContext) {
2208 init_test(cx, |_| {});
2209 let mut cx = EditorTestContext::new(cx).await;
2210
2211 let line_height = cx.update_editor(|editor, window, cx| {
2212 editor.set_vertical_scroll_margin(2, cx);
2213 editor
2214 .style()
2215 .unwrap()
2216 .text
2217 .line_height_in_pixels(window.rem_size())
2218 });
2219 let window = cx.window;
2220 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2221
2222 cx.set_state(
2223 r#"ˇone
2224 two
2225 three
2226 four
2227 five
2228 six
2229 seven
2230 eight
2231 nine
2232 ten
2233 "#,
2234 );
2235 cx.update_editor(|editor, window, cx| {
2236 assert_eq!(
2237 editor.snapshot(window, cx).scroll_position(),
2238 gpui::Point::new(0., 0.0)
2239 );
2240 });
2241
2242 // Add a cursor below the visible area. Since both cursors cannot fit
2243 // on screen, the editor autoscrolls to reveal the newest cursor, and
2244 // allows the vertical scroll margin below that cursor.
2245 cx.update_editor(|editor, window, cx| {
2246 editor.change_selections(Default::default(), window, cx, |selections| {
2247 selections.select_ranges([
2248 Point::new(0, 0)..Point::new(0, 0),
2249 Point::new(6, 0)..Point::new(6, 0),
2250 ]);
2251 })
2252 });
2253 cx.update_editor(|editor, window, cx| {
2254 assert_eq!(
2255 editor.snapshot(window, cx).scroll_position(),
2256 gpui::Point::new(0., 3.0)
2257 );
2258 });
2259
2260 // Move down. The editor cursor scrolls down to track the newest cursor.
2261 cx.update_editor(|editor, window, cx| {
2262 editor.move_down(&Default::default(), window, cx);
2263 });
2264 cx.update_editor(|editor, window, cx| {
2265 assert_eq!(
2266 editor.snapshot(window, cx).scroll_position(),
2267 gpui::Point::new(0., 4.0)
2268 );
2269 });
2270
2271 // Add a cursor above the visible area. Since both cursors fit on screen,
2272 // the editor scrolls to show both.
2273 cx.update_editor(|editor, window, cx| {
2274 editor.change_selections(Default::default(), window, cx, |selections| {
2275 selections.select_ranges([
2276 Point::new(1, 0)..Point::new(1, 0),
2277 Point::new(6, 0)..Point::new(6, 0),
2278 ]);
2279 })
2280 });
2281 cx.update_editor(|editor, window, cx| {
2282 assert_eq!(
2283 editor.snapshot(window, cx).scroll_position(),
2284 gpui::Point::new(0., 1.0)
2285 );
2286 });
2287}
2288
2289#[gpui::test]
2290async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2291 init_test(cx, |_| {});
2292 let mut cx = EditorTestContext::new(cx).await;
2293
2294 let line_height = cx.editor(|editor, window, _cx| {
2295 editor
2296 .style()
2297 .unwrap()
2298 .text
2299 .line_height_in_pixels(window.rem_size())
2300 });
2301 let window = cx.window;
2302 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2303 cx.set_state(
2304 &r#"
2305 ˇone
2306 two
2307 threeˇ
2308 four
2309 five
2310 six
2311 seven
2312 eight
2313 nine
2314 ten
2315 "#
2316 .unindent(),
2317 );
2318
2319 cx.update_editor(|editor, window, cx| {
2320 editor.move_page_down(&MovePageDown::default(), window, cx)
2321 });
2322 cx.assert_editor_state(
2323 &r#"
2324 one
2325 two
2326 three
2327 ˇfour
2328 five
2329 sixˇ
2330 seven
2331 eight
2332 nine
2333 ten
2334 "#
2335 .unindent(),
2336 );
2337
2338 cx.update_editor(|editor, window, cx| {
2339 editor.move_page_down(&MovePageDown::default(), window, cx)
2340 });
2341 cx.assert_editor_state(
2342 &r#"
2343 one
2344 two
2345 three
2346 four
2347 five
2348 six
2349 ˇseven
2350 eight
2351 nineˇ
2352 ten
2353 "#
2354 .unindent(),
2355 );
2356
2357 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2358 cx.assert_editor_state(
2359 &r#"
2360 one
2361 two
2362 three
2363 ˇfour
2364 five
2365 sixˇ
2366 seven
2367 eight
2368 nine
2369 ten
2370 "#
2371 .unindent(),
2372 );
2373
2374 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2375 cx.assert_editor_state(
2376 &r#"
2377 ˇone
2378 two
2379 threeˇ
2380 four
2381 five
2382 six
2383 seven
2384 eight
2385 nine
2386 ten
2387 "#
2388 .unindent(),
2389 );
2390
2391 // Test select collapsing
2392 cx.update_editor(|editor, window, cx| {
2393 editor.move_page_down(&MovePageDown::default(), window, cx);
2394 editor.move_page_down(&MovePageDown::default(), window, cx);
2395 editor.move_page_down(&MovePageDown::default(), window, cx);
2396 });
2397 cx.assert_editor_state(
2398 &r#"
2399 one
2400 two
2401 three
2402 four
2403 five
2404 six
2405 seven
2406 eight
2407 nine
2408 ˇten
2409 ˇ"#
2410 .unindent(),
2411 );
2412}
2413
2414#[gpui::test]
2415async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2416 init_test(cx, |_| {});
2417 let mut cx = EditorTestContext::new(cx).await;
2418 cx.set_state("one «two threeˇ» four");
2419 cx.update_editor(|editor, window, cx| {
2420 editor.delete_to_beginning_of_line(
2421 &DeleteToBeginningOfLine {
2422 stop_at_indent: false,
2423 },
2424 window,
2425 cx,
2426 );
2427 assert_eq!(editor.text(cx), " four");
2428 });
2429}
2430
2431#[gpui::test]
2432fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2433 init_test(cx, |_| {});
2434
2435 let editor = cx.add_window(|window, cx| {
2436 let buffer = MultiBuffer::build_simple("one two three four", cx);
2437 build_editor(buffer.clone(), window, cx)
2438 });
2439
2440 _ = editor.update(cx, |editor, window, cx| {
2441 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2442 s.select_display_ranges([
2443 // an empty selection - the preceding word fragment is deleted
2444 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2445 // characters selected - they are deleted
2446 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 12),
2447 ])
2448 });
2449 editor.delete_to_previous_word_start(
2450 &DeleteToPreviousWordStart {
2451 ignore_newlines: false,
2452 },
2453 window,
2454 cx,
2455 );
2456 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e two te four");
2457 });
2458
2459 _ = editor.update(cx, |editor, window, cx| {
2460 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2461 s.select_display_ranges([
2462 // an empty selection - the following word fragment is deleted
2463 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
2464 // characters selected - they are deleted
2465 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 10),
2466 ])
2467 });
2468 editor.delete_to_next_word_end(
2469 &DeleteToNextWordEnd {
2470 ignore_newlines: false,
2471 },
2472 window,
2473 cx,
2474 );
2475 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e t te our");
2476 });
2477}
2478
2479#[gpui::test]
2480fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2481 init_test(cx, |_| {});
2482
2483 let editor = cx.add_window(|window, cx| {
2484 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2485 build_editor(buffer.clone(), window, cx)
2486 });
2487 let del_to_prev_word_start = DeleteToPreviousWordStart {
2488 ignore_newlines: false,
2489 };
2490 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2491 ignore_newlines: true,
2492 };
2493
2494 _ = editor.update(cx, |editor, window, cx| {
2495 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2496 s.select_display_ranges([
2497 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2498 ])
2499 });
2500 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2501 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2502 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2503 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2504 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2505 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2506 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2507 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
2508 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2509 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
2510 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2511 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2512 });
2513}
2514
2515#[gpui::test]
2516fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2517 init_test(cx, |_| {});
2518
2519 let editor = cx.add_window(|window, cx| {
2520 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2521 build_editor(buffer.clone(), window, cx)
2522 });
2523 let del_to_next_word_end = DeleteToNextWordEnd {
2524 ignore_newlines: false,
2525 };
2526 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2527 ignore_newlines: true,
2528 };
2529
2530 _ = editor.update(cx, |editor, window, cx| {
2531 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2532 s.select_display_ranges([
2533 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2534 ])
2535 });
2536 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2537 assert_eq!(
2538 editor.buffer.read(cx).read(cx).text(),
2539 "one\n two\nthree\n four"
2540 );
2541 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2542 assert_eq!(
2543 editor.buffer.read(cx).read(cx).text(),
2544 "\n two\nthree\n four"
2545 );
2546 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2547 assert_eq!(
2548 editor.buffer.read(cx).read(cx).text(),
2549 "two\nthree\n four"
2550 );
2551 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2552 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
2553 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2554 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
2555 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2556 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2557 });
2558}
2559
2560#[gpui::test]
2561fn test_newline(cx: &mut TestAppContext) {
2562 init_test(cx, |_| {});
2563
2564 let editor = cx.add_window(|window, cx| {
2565 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2566 build_editor(buffer.clone(), window, cx)
2567 });
2568
2569 _ = editor.update(cx, |editor, window, cx| {
2570 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2571 s.select_display_ranges([
2572 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2573 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2574 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2575 ])
2576 });
2577
2578 editor.newline(&Newline, window, cx);
2579 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2580 });
2581}
2582
2583#[gpui::test]
2584fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2585 init_test(cx, |_| {});
2586
2587 let editor = cx.add_window(|window, cx| {
2588 let buffer = MultiBuffer::build_simple(
2589 "
2590 a
2591 b(
2592 X
2593 )
2594 c(
2595 X
2596 )
2597 "
2598 .unindent()
2599 .as_str(),
2600 cx,
2601 );
2602 let mut editor = build_editor(buffer.clone(), window, cx);
2603 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2604 s.select_ranges([
2605 Point::new(2, 4)..Point::new(2, 5),
2606 Point::new(5, 4)..Point::new(5, 5),
2607 ])
2608 });
2609 editor
2610 });
2611
2612 _ = editor.update(cx, |editor, window, cx| {
2613 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2614 editor.buffer.update(cx, |buffer, cx| {
2615 buffer.edit(
2616 [
2617 (Point::new(1, 2)..Point::new(3, 0), ""),
2618 (Point::new(4, 2)..Point::new(6, 0), ""),
2619 ],
2620 None,
2621 cx,
2622 );
2623 assert_eq!(
2624 buffer.read(cx).text(),
2625 "
2626 a
2627 b()
2628 c()
2629 "
2630 .unindent()
2631 );
2632 });
2633 assert_eq!(
2634 editor.selections.ranges(cx),
2635 &[
2636 Point::new(1, 2)..Point::new(1, 2),
2637 Point::new(2, 2)..Point::new(2, 2),
2638 ],
2639 );
2640
2641 editor.newline(&Newline, window, cx);
2642 assert_eq!(
2643 editor.text(cx),
2644 "
2645 a
2646 b(
2647 )
2648 c(
2649 )
2650 "
2651 .unindent()
2652 );
2653
2654 // The selections are moved after the inserted newlines
2655 assert_eq!(
2656 editor.selections.ranges(cx),
2657 &[
2658 Point::new(2, 0)..Point::new(2, 0),
2659 Point::new(4, 0)..Point::new(4, 0),
2660 ],
2661 );
2662 });
2663}
2664
2665#[gpui::test]
2666async fn test_newline_above(cx: &mut TestAppContext) {
2667 init_test(cx, |settings| {
2668 settings.defaults.tab_size = NonZeroU32::new(4)
2669 });
2670
2671 let language = Arc::new(
2672 Language::new(
2673 LanguageConfig::default(),
2674 Some(tree_sitter_rust::LANGUAGE.into()),
2675 )
2676 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2677 .unwrap(),
2678 );
2679
2680 let mut cx = EditorTestContext::new(cx).await;
2681 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2682 cx.set_state(indoc! {"
2683 const a: ˇA = (
2684 (ˇ
2685 «const_functionˇ»(ˇ),
2686 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2687 )ˇ
2688 ˇ);ˇ
2689 "});
2690
2691 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
2692 cx.assert_editor_state(indoc! {"
2693 ˇ
2694 const a: A = (
2695 ˇ
2696 (
2697 ˇ
2698 ˇ
2699 const_function(),
2700 ˇ
2701 ˇ
2702 ˇ
2703 ˇ
2704 something_else,
2705 ˇ
2706 )
2707 ˇ
2708 ˇ
2709 );
2710 "});
2711}
2712
2713#[gpui::test]
2714async fn test_newline_below(cx: &mut TestAppContext) {
2715 init_test(cx, |settings| {
2716 settings.defaults.tab_size = NonZeroU32::new(4)
2717 });
2718
2719 let language = Arc::new(
2720 Language::new(
2721 LanguageConfig::default(),
2722 Some(tree_sitter_rust::LANGUAGE.into()),
2723 )
2724 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2725 .unwrap(),
2726 );
2727
2728 let mut cx = EditorTestContext::new(cx).await;
2729 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2730 cx.set_state(indoc! {"
2731 const a: ˇA = (
2732 (ˇ
2733 «const_functionˇ»(ˇ),
2734 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2735 )ˇ
2736 ˇ);ˇ
2737 "});
2738
2739 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
2740 cx.assert_editor_state(indoc! {"
2741 const a: A = (
2742 ˇ
2743 (
2744 ˇ
2745 const_function(),
2746 ˇ
2747 ˇ
2748 something_else,
2749 ˇ
2750 ˇ
2751 ˇ
2752 ˇ
2753 )
2754 ˇ
2755 );
2756 ˇ
2757 ˇ
2758 "});
2759}
2760
2761#[gpui::test]
2762async fn test_newline_comments(cx: &mut TestAppContext) {
2763 init_test(cx, |settings| {
2764 settings.defaults.tab_size = NonZeroU32::new(4)
2765 });
2766
2767 let language = Arc::new(Language::new(
2768 LanguageConfig {
2769 line_comments: vec!["// ".into()],
2770 ..LanguageConfig::default()
2771 },
2772 None,
2773 ));
2774 {
2775 let mut cx = EditorTestContext::new(cx).await;
2776 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2777 cx.set_state(indoc! {"
2778 // Fooˇ
2779 "});
2780
2781 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2782 cx.assert_editor_state(indoc! {"
2783 // Foo
2784 // ˇ
2785 "});
2786 // Ensure that we add comment prefix when existing line contains space
2787 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2788 cx.assert_editor_state(
2789 indoc! {"
2790 // Foo
2791 //s
2792 // ˇ
2793 "}
2794 .replace("s", " ") // s is used as space placeholder to prevent format on save
2795 .as_str(),
2796 );
2797 // Ensure that we add comment prefix when existing line does not contain space
2798 cx.set_state(indoc! {"
2799 // Foo
2800 //ˇ
2801 "});
2802 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2803 cx.assert_editor_state(indoc! {"
2804 // Foo
2805 //
2806 // ˇ
2807 "});
2808 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
2809 cx.set_state(indoc! {"
2810 ˇ// Foo
2811 "});
2812 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2813 cx.assert_editor_state(indoc! {"
2814
2815 ˇ// Foo
2816 "});
2817 }
2818 // Ensure that comment continuations can be disabled.
2819 update_test_language_settings(cx, |settings| {
2820 settings.defaults.extend_comment_on_newline = Some(false);
2821 });
2822 let mut cx = EditorTestContext::new(cx).await;
2823 cx.set_state(indoc! {"
2824 // Fooˇ
2825 "});
2826 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2827 cx.assert_editor_state(indoc! {"
2828 // Foo
2829 ˇ
2830 "});
2831}
2832
2833#[gpui::test]
2834async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
2835 init_test(cx, |settings| {
2836 settings.defaults.tab_size = NonZeroU32::new(4)
2837 });
2838
2839 let language = Arc::new(Language::new(
2840 LanguageConfig {
2841 line_comments: vec!["// ".into(), "/// ".into()],
2842 ..LanguageConfig::default()
2843 },
2844 None,
2845 ));
2846 {
2847 let mut cx = EditorTestContext::new(cx).await;
2848 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2849 cx.set_state(indoc! {"
2850 //ˇ
2851 "});
2852 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2853 cx.assert_editor_state(indoc! {"
2854 //
2855 // ˇ
2856 "});
2857
2858 cx.set_state(indoc! {"
2859 ///ˇ
2860 "});
2861 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2862 cx.assert_editor_state(indoc! {"
2863 ///
2864 /// ˇ
2865 "});
2866 }
2867}
2868
2869#[gpui::test]
2870async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
2871 init_test(cx, |settings| {
2872 settings.defaults.tab_size = NonZeroU32::new(4)
2873 });
2874
2875 let language = Arc::new(
2876 Language::new(
2877 LanguageConfig {
2878 documentation_comment: Some(language::BlockCommentConfig {
2879 start: "/**".into(),
2880 end: "*/".into(),
2881 prefix: "* ".into(),
2882 tab_size: 1,
2883 }),
2884
2885 ..LanguageConfig::default()
2886 },
2887 Some(tree_sitter_rust::LANGUAGE.into()),
2888 )
2889 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
2890 .unwrap(),
2891 );
2892
2893 {
2894 let mut cx = EditorTestContext::new(cx).await;
2895 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2896 cx.set_state(indoc! {"
2897 /**ˇ
2898 "});
2899
2900 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2901 cx.assert_editor_state(indoc! {"
2902 /**
2903 * ˇ
2904 "});
2905 // Ensure that if cursor is before the comment start,
2906 // we do not actually insert a comment prefix.
2907 cx.set_state(indoc! {"
2908 ˇ/**
2909 "});
2910 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2911 cx.assert_editor_state(indoc! {"
2912
2913 ˇ/**
2914 "});
2915 // Ensure that if cursor is between it doesn't add comment prefix.
2916 cx.set_state(indoc! {"
2917 /*ˇ*
2918 "});
2919 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2920 cx.assert_editor_state(indoc! {"
2921 /*
2922 ˇ*
2923 "});
2924 // Ensure that if suffix exists on same line after cursor it adds new line.
2925 cx.set_state(indoc! {"
2926 /**ˇ*/
2927 "});
2928 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2929 cx.assert_editor_state(indoc! {"
2930 /**
2931 * ˇ
2932 */
2933 "});
2934 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2935 cx.set_state(indoc! {"
2936 /**ˇ */
2937 "});
2938 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2939 cx.assert_editor_state(indoc! {"
2940 /**
2941 * ˇ
2942 */
2943 "});
2944 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2945 cx.set_state(indoc! {"
2946 /** ˇ*/
2947 "});
2948 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2949 cx.assert_editor_state(
2950 indoc! {"
2951 /**s
2952 * ˇ
2953 */
2954 "}
2955 .replace("s", " ") // s is used as space placeholder to prevent format on save
2956 .as_str(),
2957 );
2958 // Ensure that delimiter space is preserved when newline on already
2959 // spaced delimiter.
2960 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2961 cx.assert_editor_state(
2962 indoc! {"
2963 /**s
2964 *s
2965 * ˇ
2966 */
2967 "}
2968 .replace("s", " ") // s is used as space placeholder to prevent format on save
2969 .as_str(),
2970 );
2971 // Ensure that delimiter space is preserved when space is not
2972 // on existing delimiter.
2973 cx.set_state(indoc! {"
2974 /**
2975 *ˇ
2976 */
2977 "});
2978 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2979 cx.assert_editor_state(indoc! {"
2980 /**
2981 *
2982 * ˇ
2983 */
2984 "});
2985 // Ensure that if suffix exists on same line after cursor it
2986 // doesn't add extra new line if prefix is not on same line.
2987 cx.set_state(indoc! {"
2988 /**
2989 ˇ*/
2990 "});
2991 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2992 cx.assert_editor_state(indoc! {"
2993 /**
2994
2995 ˇ*/
2996 "});
2997 // Ensure that it detects suffix after existing prefix.
2998 cx.set_state(indoc! {"
2999 /**ˇ/
3000 "});
3001 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3002 cx.assert_editor_state(indoc! {"
3003 /**
3004 ˇ/
3005 "});
3006 // Ensure that if suffix exists on same line before
3007 // cursor it does not add comment prefix.
3008 cx.set_state(indoc! {"
3009 /** */ˇ
3010 "});
3011 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3012 cx.assert_editor_state(indoc! {"
3013 /** */
3014 ˇ
3015 "});
3016 // Ensure that if suffix exists on same line before
3017 // cursor it does not add comment prefix.
3018 cx.set_state(indoc! {"
3019 /**
3020 *
3021 */ˇ
3022 "});
3023 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3024 cx.assert_editor_state(indoc! {"
3025 /**
3026 *
3027 */
3028 ˇ
3029 "});
3030
3031 // Ensure that inline comment followed by code
3032 // doesn't add comment prefix on newline
3033 cx.set_state(indoc! {"
3034 /** */ textˇ
3035 "});
3036 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3037 cx.assert_editor_state(indoc! {"
3038 /** */ text
3039 ˇ
3040 "});
3041
3042 // Ensure that text after comment end tag
3043 // doesn't add comment prefix on newline
3044 cx.set_state(indoc! {"
3045 /**
3046 *
3047 */ˇtext
3048 "});
3049 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3050 cx.assert_editor_state(indoc! {"
3051 /**
3052 *
3053 */
3054 ˇtext
3055 "});
3056
3057 // Ensure if not comment block it doesn't
3058 // add comment prefix on newline
3059 cx.set_state(indoc! {"
3060 * textˇ
3061 "});
3062 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3063 cx.assert_editor_state(indoc! {"
3064 * text
3065 ˇ
3066 "});
3067 }
3068 // Ensure that comment continuations can be disabled.
3069 update_test_language_settings(cx, |settings| {
3070 settings.defaults.extend_comment_on_newline = Some(false);
3071 });
3072 let mut cx = EditorTestContext::new(cx).await;
3073 cx.set_state(indoc! {"
3074 /**ˇ
3075 "});
3076 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3077 cx.assert_editor_state(indoc! {"
3078 /**
3079 ˇ
3080 "});
3081}
3082
3083#[gpui::test]
3084async fn test_newline_comments_with_block_comment(cx: &mut TestAppContext) {
3085 init_test(cx, |settings| {
3086 settings.defaults.tab_size = NonZeroU32::new(4)
3087 });
3088
3089 let lua_language = Arc::new(Language::new(
3090 LanguageConfig {
3091 line_comments: vec!["--".into()],
3092 block_comment: Some(language::BlockCommentConfig {
3093 start: "--[[".into(),
3094 prefix: "".into(),
3095 end: "]]".into(),
3096 tab_size: 0,
3097 }),
3098 ..LanguageConfig::default()
3099 },
3100 None,
3101 ));
3102
3103 let mut cx = EditorTestContext::new(cx).await;
3104 cx.update_buffer(|buffer, cx| buffer.set_language(Some(lua_language), cx));
3105
3106 // Line with line comment should extend
3107 cx.set_state(indoc! {"
3108 --ˇ
3109 "});
3110 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3111 cx.assert_editor_state(indoc! {"
3112 --
3113 --ˇ
3114 "});
3115
3116 // Line with block comment that matches line comment should not extend
3117 cx.set_state(indoc! {"
3118 --[[ˇ
3119 "});
3120 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3121 cx.assert_editor_state(indoc! {"
3122 --[[
3123 ˇ
3124 "});
3125}
3126
3127#[gpui::test]
3128fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3129 init_test(cx, |_| {});
3130
3131 let editor = cx.add_window(|window, cx| {
3132 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3133 let mut editor = build_editor(buffer.clone(), window, cx);
3134 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3135 s.select_ranges([3..4, 11..12, 19..20])
3136 });
3137 editor
3138 });
3139
3140 _ = editor.update(cx, |editor, window, cx| {
3141 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3142 editor.buffer.update(cx, |buffer, cx| {
3143 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3144 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3145 });
3146 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
3147
3148 editor.insert("Z", window, cx);
3149 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3150
3151 // The selections are moved after the inserted characters
3152 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
3153 });
3154}
3155
3156#[gpui::test]
3157async fn test_tab(cx: &mut TestAppContext) {
3158 init_test(cx, |settings| {
3159 settings.defaults.tab_size = NonZeroU32::new(3)
3160 });
3161
3162 let mut cx = EditorTestContext::new(cx).await;
3163 cx.set_state(indoc! {"
3164 ˇabˇc
3165 ˇ🏀ˇ🏀ˇefg
3166 dˇ
3167 "});
3168 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3169 cx.assert_editor_state(indoc! {"
3170 ˇab ˇc
3171 ˇ🏀 ˇ🏀 ˇefg
3172 d ˇ
3173 "});
3174
3175 cx.set_state(indoc! {"
3176 a
3177 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3178 "});
3179 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3180 cx.assert_editor_state(indoc! {"
3181 a
3182 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3183 "});
3184}
3185
3186#[gpui::test]
3187async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3188 init_test(cx, |_| {});
3189
3190 let mut cx = EditorTestContext::new(cx).await;
3191 let language = Arc::new(
3192 Language::new(
3193 LanguageConfig::default(),
3194 Some(tree_sitter_rust::LANGUAGE.into()),
3195 )
3196 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3197 .unwrap(),
3198 );
3199 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3200
3201 // test when all cursors are not at suggested indent
3202 // then simply move to their suggested indent location
3203 cx.set_state(indoc! {"
3204 const a: B = (
3205 c(
3206 ˇ
3207 ˇ )
3208 );
3209 "});
3210 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3211 cx.assert_editor_state(indoc! {"
3212 const a: B = (
3213 c(
3214 ˇ
3215 ˇ)
3216 );
3217 "});
3218
3219 // test cursor already at suggested indent not moving when
3220 // other cursors are yet to reach their suggested indents
3221 cx.set_state(indoc! {"
3222 ˇ
3223 const a: B = (
3224 c(
3225 d(
3226 ˇ
3227 )
3228 ˇ
3229 ˇ )
3230 );
3231 "});
3232 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3233 cx.assert_editor_state(indoc! {"
3234 ˇ
3235 const a: B = (
3236 c(
3237 d(
3238 ˇ
3239 )
3240 ˇ
3241 ˇ)
3242 );
3243 "});
3244 // test when all cursors are at suggested indent then tab is inserted
3245 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3246 cx.assert_editor_state(indoc! {"
3247 ˇ
3248 const a: B = (
3249 c(
3250 d(
3251 ˇ
3252 )
3253 ˇ
3254 ˇ)
3255 );
3256 "});
3257
3258 // test when current indent is less than suggested indent,
3259 // we adjust line to match suggested indent and move cursor to it
3260 //
3261 // when no other cursor is at word boundary, all of them should move
3262 cx.set_state(indoc! {"
3263 const a: B = (
3264 c(
3265 d(
3266 ˇ
3267 ˇ )
3268 ˇ )
3269 );
3270 "});
3271 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3272 cx.assert_editor_state(indoc! {"
3273 const a: B = (
3274 c(
3275 d(
3276 ˇ
3277 ˇ)
3278 ˇ)
3279 );
3280 "});
3281
3282 // test when current indent is less than suggested indent,
3283 // we adjust line to match suggested indent and move cursor to it
3284 //
3285 // when some other cursor is at word boundary, it should not move
3286 cx.set_state(indoc! {"
3287 const a: B = (
3288 c(
3289 d(
3290 ˇ
3291 ˇ )
3292 ˇ)
3293 );
3294 "});
3295 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3296 cx.assert_editor_state(indoc! {"
3297 const a: B = (
3298 c(
3299 d(
3300 ˇ
3301 ˇ)
3302 ˇ)
3303 );
3304 "});
3305
3306 // test when current indent is more than suggested indent,
3307 // we just move cursor to current indent instead of suggested indent
3308 //
3309 // when no other cursor is at word boundary, all of them should move
3310 cx.set_state(indoc! {"
3311 const a: B = (
3312 c(
3313 d(
3314 ˇ
3315 ˇ )
3316 ˇ )
3317 );
3318 "});
3319 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3320 cx.assert_editor_state(indoc! {"
3321 const a: B = (
3322 c(
3323 d(
3324 ˇ
3325 ˇ)
3326 ˇ)
3327 );
3328 "});
3329 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3330 cx.assert_editor_state(indoc! {"
3331 const a: B = (
3332 c(
3333 d(
3334 ˇ
3335 ˇ)
3336 ˇ)
3337 );
3338 "});
3339
3340 // test when current indent is more than suggested indent,
3341 // we just move cursor to current indent instead of suggested indent
3342 //
3343 // when some other cursor is at word boundary, it doesn't move
3344 cx.set_state(indoc! {"
3345 const a: B = (
3346 c(
3347 d(
3348 ˇ
3349 ˇ )
3350 ˇ)
3351 );
3352 "});
3353 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3354 cx.assert_editor_state(indoc! {"
3355 const a: B = (
3356 c(
3357 d(
3358 ˇ
3359 ˇ)
3360 ˇ)
3361 );
3362 "});
3363
3364 // handle auto-indent when there are multiple cursors on the same line
3365 cx.set_state(indoc! {"
3366 const a: B = (
3367 c(
3368 ˇ ˇ
3369 ˇ )
3370 );
3371 "});
3372 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3373 cx.assert_editor_state(indoc! {"
3374 const a: B = (
3375 c(
3376 ˇ
3377 ˇ)
3378 );
3379 "});
3380}
3381
3382#[gpui::test]
3383async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3384 init_test(cx, |settings| {
3385 settings.defaults.tab_size = NonZeroU32::new(3)
3386 });
3387
3388 let mut cx = EditorTestContext::new(cx).await;
3389 cx.set_state(indoc! {"
3390 ˇ
3391 \t ˇ
3392 \t ˇ
3393 \t ˇ
3394 \t \t\t \t \t\t \t\t \t \t ˇ
3395 "});
3396
3397 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3398 cx.assert_editor_state(indoc! {"
3399 ˇ
3400 \t ˇ
3401 \t ˇ
3402 \t ˇ
3403 \t \t\t \t \t\t \t\t \t \t ˇ
3404 "});
3405}
3406
3407#[gpui::test]
3408async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3409 init_test(cx, |settings| {
3410 settings.defaults.tab_size = NonZeroU32::new(4)
3411 });
3412
3413 let language = Arc::new(
3414 Language::new(
3415 LanguageConfig::default(),
3416 Some(tree_sitter_rust::LANGUAGE.into()),
3417 )
3418 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3419 .unwrap(),
3420 );
3421
3422 let mut cx = EditorTestContext::new(cx).await;
3423 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3424 cx.set_state(indoc! {"
3425 fn a() {
3426 if b {
3427 \t ˇc
3428 }
3429 }
3430 "});
3431
3432 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3433 cx.assert_editor_state(indoc! {"
3434 fn a() {
3435 if b {
3436 ˇc
3437 }
3438 }
3439 "});
3440}
3441
3442#[gpui::test]
3443async fn test_indent_outdent(cx: &mut TestAppContext) {
3444 init_test(cx, |settings| {
3445 settings.defaults.tab_size = NonZeroU32::new(4);
3446 });
3447
3448 let mut cx = EditorTestContext::new(cx).await;
3449
3450 cx.set_state(indoc! {"
3451 «oneˇ» «twoˇ»
3452 three
3453 four
3454 "});
3455 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3456 cx.assert_editor_state(indoc! {"
3457 «oneˇ» «twoˇ»
3458 three
3459 four
3460 "});
3461
3462 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3463 cx.assert_editor_state(indoc! {"
3464 «oneˇ» «twoˇ»
3465 three
3466 four
3467 "});
3468
3469 // select across line ending
3470 cx.set_state(indoc! {"
3471 one two
3472 t«hree
3473 ˇ» four
3474 "});
3475 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3476 cx.assert_editor_state(indoc! {"
3477 one two
3478 t«hree
3479 ˇ» four
3480 "});
3481
3482 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3483 cx.assert_editor_state(indoc! {"
3484 one two
3485 t«hree
3486 ˇ» four
3487 "});
3488
3489 // Ensure that indenting/outdenting works when the cursor is at column 0.
3490 cx.set_state(indoc! {"
3491 one two
3492 ˇthree
3493 four
3494 "});
3495 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3496 cx.assert_editor_state(indoc! {"
3497 one two
3498 ˇthree
3499 four
3500 "});
3501
3502 cx.set_state(indoc! {"
3503 one two
3504 ˇ three
3505 four
3506 "});
3507 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3508 cx.assert_editor_state(indoc! {"
3509 one two
3510 ˇthree
3511 four
3512 "});
3513}
3514
3515#[gpui::test]
3516async fn test_indent_yaml_comments_with_multiple_cursors(cx: &mut TestAppContext) {
3517 // This is a regression test for issue #33761
3518 init_test(cx, |_| {});
3519
3520 let mut cx = EditorTestContext::new(cx).await;
3521 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3522 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3523
3524 cx.set_state(
3525 r#"ˇ# ingress:
3526ˇ# api:
3527ˇ# enabled: false
3528ˇ# pathType: Prefix
3529ˇ# console:
3530ˇ# enabled: false
3531ˇ# pathType: Prefix
3532"#,
3533 );
3534
3535 // Press tab to indent all lines
3536 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3537
3538 cx.assert_editor_state(
3539 r#" ˇ# ingress:
3540 ˇ# api:
3541 ˇ# enabled: false
3542 ˇ# pathType: Prefix
3543 ˇ# console:
3544 ˇ# enabled: false
3545 ˇ# pathType: Prefix
3546"#,
3547 );
3548}
3549
3550#[gpui::test]
3551async fn test_indent_yaml_non_comments_with_multiple_cursors(cx: &mut TestAppContext) {
3552 // This is a test to make sure our fix for issue #33761 didn't break anything
3553 init_test(cx, |_| {});
3554
3555 let mut cx = EditorTestContext::new(cx).await;
3556 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3557 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3558
3559 cx.set_state(
3560 r#"ˇingress:
3561ˇ api:
3562ˇ enabled: false
3563ˇ pathType: Prefix
3564"#,
3565 );
3566
3567 // Press tab to indent all lines
3568 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3569
3570 cx.assert_editor_state(
3571 r#"ˇingress:
3572 ˇapi:
3573 ˇenabled: false
3574 ˇpathType: Prefix
3575"#,
3576 );
3577}
3578
3579#[gpui::test]
3580async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3581 init_test(cx, |settings| {
3582 settings.defaults.hard_tabs = Some(true);
3583 });
3584
3585 let mut cx = EditorTestContext::new(cx).await;
3586
3587 // select two ranges on one line
3588 cx.set_state(indoc! {"
3589 «oneˇ» «twoˇ»
3590 three
3591 four
3592 "});
3593 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3594 cx.assert_editor_state(indoc! {"
3595 \t«oneˇ» «twoˇ»
3596 three
3597 four
3598 "});
3599 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3600 cx.assert_editor_state(indoc! {"
3601 \t\t«oneˇ» «twoˇ»
3602 three
3603 four
3604 "});
3605 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3606 cx.assert_editor_state(indoc! {"
3607 \t«oneˇ» «twoˇ»
3608 three
3609 four
3610 "});
3611 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3612 cx.assert_editor_state(indoc! {"
3613 «oneˇ» «twoˇ»
3614 three
3615 four
3616 "});
3617
3618 // select across a line ending
3619 cx.set_state(indoc! {"
3620 one two
3621 t«hree
3622 ˇ»four
3623 "});
3624 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3625 cx.assert_editor_state(indoc! {"
3626 one two
3627 \tt«hree
3628 ˇ»four
3629 "});
3630 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3631 cx.assert_editor_state(indoc! {"
3632 one two
3633 \t\tt«hree
3634 ˇ»four
3635 "});
3636 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3637 cx.assert_editor_state(indoc! {"
3638 one two
3639 \tt«hree
3640 ˇ»four
3641 "});
3642 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3643 cx.assert_editor_state(indoc! {"
3644 one two
3645 t«hree
3646 ˇ»four
3647 "});
3648
3649 // Ensure that indenting/outdenting works when the cursor is at column 0.
3650 cx.set_state(indoc! {"
3651 one two
3652 ˇthree
3653 four
3654 "});
3655 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3656 cx.assert_editor_state(indoc! {"
3657 one two
3658 ˇthree
3659 four
3660 "});
3661 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3662 cx.assert_editor_state(indoc! {"
3663 one two
3664 \tˇthree
3665 four
3666 "});
3667 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3668 cx.assert_editor_state(indoc! {"
3669 one two
3670 ˇthree
3671 four
3672 "});
3673}
3674
3675#[gpui::test]
3676fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3677 init_test(cx, |settings| {
3678 settings.languages.0.extend([
3679 (
3680 "TOML".into(),
3681 LanguageSettingsContent {
3682 tab_size: NonZeroU32::new(2),
3683 ..Default::default()
3684 },
3685 ),
3686 (
3687 "Rust".into(),
3688 LanguageSettingsContent {
3689 tab_size: NonZeroU32::new(4),
3690 ..Default::default()
3691 },
3692 ),
3693 ]);
3694 });
3695
3696 let toml_language = Arc::new(Language::new(
3697 LanguageConfig {
3698 name: "TOML".into(),
3699 ..Default::default()
3700 },
3701 None,
3702 ));
3703 let rust_language = Arc::new(Language::new(
3704 LanguageConfig {
3705 name: "Rust".into(),
3706 ..Default::default()
3707 },
3708 None,
3709 ));
3710
3711 let toml_buffer =
3712 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3713 let rust_buffer =
3714 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3715 let multibuffer = cx.new(|cx| {
3716 let mut multibuffer = MultiBuffer::new(ReadWrite);
3717 multibuffer.push_excerpts(
3718 toml_buffer.clone(),
3719 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
3720 cx,
3721 );
3722 multibuffer.push_excerpts(
3723 rust_buffer.clone(),
3724 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
3725 cx,
3726 );
3727 multibuffer
3728 });
3729
3730 cx.add_window(|window, cx| {
3731 let mut editor = build_editor(multibuffer, window, cx);
3732
3733 assert_eq!(
3734 editor.text(cx),
3735 indoc! {"
3736 a = 1
3737 b = 2
3738
3739 const c: usize = 3;
3740 "}
3741 );
3742
3743 select_ranges(
3744 &mut editor,
3745 indoc! {"
3746 «aˇ» = 1
3747 b = 2
3748
3749 «const c:ˇ» usize = 3;
3750 "},
3751 window,
3752 cx,
3753 );
3754
3755 editor.tab(&Tab, window, cx);
3756 assert_text_with_selections(
3757 &mut editor,
3758 indoc! {"
3759 «aˇ» = 1
3760 b = 2
3761
3762 «const c:ˇ» usize = 3;
3763 "},
3764 cx,
3765 );
3766 editor.backtab(&Backtab, window, cx);
3767 assert_text_with_selections(
3768 &mut editor,
3769 indoc! {"
3770 «aˇ» = 1
3771 b = 2
3772
3773 «const c:ˇ» usize = 3;
3774 "},
3775 cx,
3776 );
3777
3778 editor
3779 });
3780}
3781
3782#[gpui::test]
3783async fn test_backspace(cx: &mut TestAppContext) {
3784 init_test(cx, |_| {});
3785
3786 let mut cx = EditorTestContext::new(cx).await;
3787
3788 // Basic backspace
3789 cx.set_state(indoc! {"
3790 onˇe two three
3791 fou«rˇ» five six
3792 seven «ˇeight nine
3793 »ten
3794 "});
3795 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3796 cx.assert_editor_state(indoc! {"
3797 oˇe two three
3798 fouˇ five six
3799 seven ˇten
3800 "});
3801
3802 // Test backspace inside and around indents
3803 cx.set_state(indoc! {"
3804 zero
3805 ˇone
3806 ˇtwo
3807 ˇ ˇ ˇ three
3808 ˇ ˇ four
3809 "});
3810 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3811 cx.assert_editor_state(indoc! {"
3812 zero
3813 ˇone
3814 ˇtwo
3815 ˇ threeˇ four
3816 "});
3817}
3818
3819#[gpui::test]
3820async fn test_delete(cx: &mut TestAppContext) {
3821 init_test(cx, |_| {});
3822
3823 let mut cx = EditorTestContext::new(cx).await;
3824 cx.set_state(indoc! {"
3825 onˇe two three
3826 fou«rˇ» five six
3827 seven «ˇeight nine
3828 »ten
3829 "});
3830 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3831 cx.assert_editor_state(indoc! {"
3832 onˇ two three
3833 fouˇ five six
3834 seven ˇten
3835 "});
3836}
3837
3838#[gpui::test]
3839fn test_delete_line(cx: &mut TestAppContext) {
3840 init_test(cx, |_| {});
3841
3842 let editor = cx.add_window(|window, cx| {
3843 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3844 build_editor(buffer, window, cx)
3845 });
3846 _ = editor.update(cx, |editor, window, cx| {
3847 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3848 s.select_display_ranges([
3849 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3850 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3851 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3852 ])
3853 });
3854 editor.delete_line(&DeleteLine, window, cx);
3855 assert_eq!(editor.display_text(cx), "ghi");
3856 assert_eq!(
3857 editor.selections.display_ranges(cx),
3858 vec![
3859 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3860 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3861 ]
3862 );
3863 });
3864
3865 let editor = cx.add_window(|window, cx| {
3866 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3867 build_editor(buffer, window, cx)
3868 });
3869 _ = editor.update(cx, |editor, window, cx| {
3870 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3871 s.select_display_ranges([
3872 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3873 ])
3874 });
3875 editor.delete_line(&DeleteLine, window, cx);
3876 assert_eq!(editor.display_text(cx), "ghi\n");
3877 assert_eq!(
3878 editor.selections.display_ranges(cx),
3879 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3880 );
3881 });
3882}
3883
3884#[gpui::test]
3885fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3886 init_test(cx, |_| {});
3887
3888 cx.add_window(|window, cx| {
3889 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3890 let mut editor = build_editor(buffer.clone(), window, cx);
3891 let buffer = buffer.read(cx).as_singleton().unwrap();
3892
3893 assert_eq!(
3894 editor.selections.ranges::<Point>(cx),
3895 &[Point::new(0, 0)..Point::new(0, 0)]
3896 );
3897
3898 // When on single line, replace newline at end by space
3899 editor.join_lines(&JoinLines, window, cx);
3900 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3901 assert_eq!(
3902 editor.selections.ranges::<Point>(cx),
3903 &[Point::new(0, 3)..Point::new(0, 3)]
3904 );
3905
3906 // When multiple lines are selected, remove newlines that are spanned by the selection
3907 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3908 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3909 });
3910 editor.join_lines(&JoinLines, window, cx);
3911 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3912 assert_eq!(
3913 editor.selections.ranges::<Point>(cx),
3914 &[Point::new(0, 11)..Point::new(0, 11)]
3915 );
3916
3917 // Undo should be transactional
3918 editor.undo(&Undo, window, cx);
3919 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3920 assert_eq!(
3921 editor.selections.ranges::<Point>(cx),
3922 &[Point::new(0, 5)..Point::new(2, 2)]
3923 );
3924
3925 // When joining an empty line don't insert a space
3926 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3927 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3928 });
3929 editor.join_lines(&JoinLines, window, cx);
3930 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3931 assert_eq!(
3932 editor.selections.ranges::<Point>(cx),
3933 [Point::new(2, 3)..Point::new(2, 3)]
3934 );
3935
3936 // We can remove trailing newlines
3937 editor.join_lines(&JoinLines, window, cx);
3938 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3939 assert_eq!(
3940 editor.selections.ranges::<Point>(cx),
3941 [Point::new(2, 3)..Point::new(2, 3)]
3942 );
3943
3944 // We don't blow up on the last line
3945 editor.join_lines(&JoinLines, window, cx);
3946 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3947 assert_eq!(
3948 editor.selections.ranges::<Point>(cx),
3949 [Point::new(2, 3)..Point::new(2, 3)]
3950 );
3951
3952 // reset to test indentation
3953 editor.buffer.update(cx, |buffer, cx| {
3954 buffer.edit(
3955 [
3956 (Point::new(1, 0)..Point::new(1, 2), " "),
3957 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3958 ],
3959 None,
3960 cx,
3961 )
3962 });
3963
3964 // We remove any leading spaces
3965 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3966 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3967 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3968 });
3969 editor.join_lines(&JoinLines, window, cx);
3970 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3971
3972 // We don't insert a space for a line containing only spaces
3973 editor.join_lines(&JoinLines, window, cx);
3974 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3975
3976 // We ignore any leading tabs
3977 editor.join_lines(&JoinLines, window, cx);
3978 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3979
3980 editor
3981 });
3982}
3983
3984#[gpui::test]
3985fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3986 init_test(cx, |_| {});
3987
3988 cx.add_window(|window, cx| {
3989 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3990 let mut editor = build_editor(buffer.clone(), window, cx);
3991 let buffer = buffer.read(cx).as_singleton().unwrap();
3992
3993 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3994 s.select_ranges([
3995 Point::new(0, 2)..Point::new(1, 1),
3996 Point::new(1, 2)..Point::new(1, 2),
3997 Point::new(3, 1)..Point::new(3, 2),
3998 ])
3999 });
4000
4001 editor.join_lines(&JoinLines, window, cx);
4002 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
4003
4004 assert_eq!(
4005 editor.selections.ranges::<Point>(cx),
4006 [
4007 Point::new(0, 7)..Point::new(0, 7),
4008 Point::new(1, 3)..Point::new(1, 3)
4009 ]
4010 );
4011 editor
4012 });
4013}
4014
4015#[gpui::test]
4016async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
4017 init_test(cx, |_| {});
4018
4019 let mut cx = EditorTestContext::new(cx).await;
4020
4021 let diff_base = r#"
4022 Line 0
4023 Line 1
4024 Line 2
4025 Line 3
4026 "#
4027 .unindent();
4028
4029 cx.set_state(
4030 &r#"
4031 ˇLine 0
4032 Line 1
4033 Line 2
4034 Line 3
4035 "#
4036 .unindent(),
4037 );
4038
4039 cx.set_head_text(&diff_base);
4040 executor.run_until_parked();
4041
4042 // Join lines
4043 cx.update_editor(|editor, window, cx| {
4044 editor.join_lines(&JoinLines, window, cx);
4045 });
4046 executor.run_until_parked();
4047
4048 cx.assert_editor_state(
4049 &r#"
4050 Line 0ˇ Line 1
4051 Line 2
4052 Line 3
4053 "#
4054 .unindent(),
4055 );
4056 // Join again
4057 cx.update_editor(|editor, window, cx| {
4058 editor.join_lines(&JoinLines, window, cx);
4059 });
4060 executor.run_until_parked();
4061
4062 cx.assert_editor_state(
4063 &r#"
4064 Line 0 Line 1ˇ Line 2
4065 Line 3
4066 "#
4067 .unindent(),
4068 );
4069}
4070
4071#[gpui::test]
4072async fn test_custom_newlines_cause_no_false_positive_diffs(
4073 executor: BackgroundExecutor,
4074 cx: &mut TestAppContext,
4075) {
4076 init_test(cx, |_| {});
4077 let mut cx = EditorTestContext::new(cx).await;
4078 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
4079 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
4080 executor.run_until_parked();
4081
4082 cx.update_editor(|editor, window, cx| {
4083 let snapshot = editor.snapshot(window, cx);
4084 assert_eq!(
4085 snapshot
4086 .buffer_snapshot
4087 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
4088 .collect::<Vec<_>>(),
4089 Vec::new(),
4090 "Should not have any diffs for files with custom newlines"
4091 );
4092 });
4093}
4094
4095#[gpui::test]
4096async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
4097 init_test(cx, |_| {});
4098
4099 let mut cx = EditorTestContext::new(cx).await;
4100
4101 // Test sort_lines_case_insensitive()
4102 cx.set_state(indoc! {"
4103 «z
4104 y
4105 x
4106 Z
4107 Y
4108 Xˇ»
4109 "});
4110 cx.update_editor(|e, window, cx| {
4111 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
4112 });
4113 cx.assert_editor_state(indoc! {"
4114 «x
4115 X
4116 y
4117 Y
4118 z
4119 Zˇ»
4120 "});
4121
4122 // Test sort_lines_by_length()
4123 //
4124 // Demonstrates:
4125 // - ∞ is 3 bytes UTF-8, but sorted by its char count (1)
4126 // - sort is stable
4127 cx.set_state(indoc! {"
4128 «123
4129 æ
4130 12
4131 ∞
4132 1
4133 æˇ»
4134 "});
4135 cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx));
4136 cx.assert_editor_state(indoc! {"
4137 «æ
4138 ∞
4139 1
4140 æ
4141 12
4142 123ˇ»
4143 "});
4144
4145 // Test reverse_lines()
4146 cx.set_state(indoc! {"
4147 «5
4148 4
4149 3
4150 2
4151 1ˇ»
4152 "});
4153 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4154 cx.assert_editor_state(indoc! {"
4155 «1
4156 2
4157 3
4158 4
4159 5ˇ»
4160 "});
4161
4162 // Skip testing shuffle_line()
4163
4164 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
4165 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
4166
4167 // Don't manipulate when cursor is on single line, but expand the selection
4168 cx.set_state(indoc! {"
4169 ddˇdd
4170 ccc
4171 bb
4172 a
4173 "});
4174 cx.update_editor(|e, window, cx| {
4175 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4176 });
4177 cx.assert_editor_state(indoc! {"
4178 «ddddˇ»
4179 ccc
4180 bb
4181 a
4182 "});
4183
4184 // Basic manipulate case
4185 // Start selection moves to column 0
4186 // End of selection shrinks to fit shorter line
4187 cx.set_state(indoc! {"
4188 dd«d
4189 ccc
4190 bb
4191 aaaaaˇ»
4192 "});
4193 cx.update_editor(|e, window, cx| {
4194 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4195 });
4196 cx.assert_editor_state(indoc! {"
4197 «aaaaa
4198 bb
4199 ccc
4200 dddˇ»
4201 "});
4202
4203 // Manipulate case with newlines
4204 cx.set_state(indoc! {"
4205 dd«d
4206 ccc
4207
4208 bb
4209 aaaaa
4210
4211 ˇ»
4212 "});
4213 cx.update_editor(|e, window, cx| {
4214 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4215 });
4216 cx.assert_editor_state(indoc! {"
4217 «
4218
4219 aaaaa
4220 bb
4221 ccc
4222 dddˇ»
4223
4224 "});
4225
4226 // Adding new line
4227 cx.set_state(indoc! {"
4228 aa«a
4229 bbˇ»b
4230 "});
4231 cx.update_editor(|e, window, cx| {
4232 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
4233 });
4234 cx.assert_editor_state(indoc! {"
4235 «aaa
4236 bbb
4237 added_lineˇ»
4238 "});
4239
4240 // Removing line
4241 cx.set_state(indoc! {"
4242 aa«a
4243 bbbˇ»
4244 "});
4245 cx.update_editor(|e, window, cx| {
4246 e.manipulate_immutable_lines(window, cx, |lines| {
4247 lines.pop();
4248 })
4249 });
4250 cx.assert_editor_state(indoc! {"
4251 «aaaˇ»
4252 "});
4253
4254 // Removing all lines
4255 cx.set_state(indoc! {"
4256 aa«a
4257 bbbˇ»
4258 "});
4259 cx.update_editor(|e, window, cx| {
4260 e.manipulate_immutable_lines(window, cx, |lines| {
4261 lines.drain(..);
4262 })
4263 });
4264 cx.assert_editor_state(indoc! {"
4265 ˇ
4266 "});
4267}
4268
4269#[gpui::test]
4270async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4271 init_test(cx, |_| {});
4272
4273 let mut cx = EditorTestContext::new(cx).await;
4274
4275 // Consider continuous selection as single selection
4276 cx.set_state(indoc! {"
4277 Aaa«aa
4278 cˇ»c«c
4279 bb
4280 aaaˇ»aa
4281 "});
4282 cx.update_editor(|e, window, cx| {
4283 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4284 });
4285 cx.assert_editor_state(indoc! {"
4286 «Aaaaa
4287 ccc
4288 bb
4289 aaaaaˇ»
4290 "});
4291
4292 cx.set_state(indoc! {"
4293 Aaa«aa
4294 cˇ»c«c
4295 bb
4296 aaaˇ»aa
4297 "});
4298 cx.update_editor(|e, window, cx| {
4299 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4300 });
4301 cx.assert_editor_state(indoc! {"
4302 «Aaaaa
4303 ccc
4304 bbˇ»
4305 "});
4306
4307 // Consider non continuous selection as distinct dedup operations
4308 cx.set_state(indoc! {"
4309 «aaaaa
4310 bb
4311 aaaaa
4312 aaaaaˇ»
4313
4314 aaa«aaˇ»
4315 "});
4316 cx.update_editor(|e, window, cx| {
4317 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4318 });
4319 cx.assert_editor_state(indoc! {"
4320 «aaaaa
4321 bbˇ»
4322
4323 «aaaaaˇ»
4324 "});
4325}
4326
4327#[gpui::test]
4328async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4329 init_test(cx, |_| {});
4330
4331 let mut cx = EditorTestContext::new(cx).await;
4332
4333 cx.set_state(indoc! {"
4334 «Aaa
4335 aAa
4336 Aaaˇ»
4337 "});
4338 cx.update_editor(|e, window, cx| {
4339 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4340 });
4341 cx.assert_editor_state(indoc! {"
4342 «Aaa
4343 aAaˇ»
4344 "});
4345
4346 cx.set_state(indoc! {"
4347 «Aaa
4348 aAa
4349 aaAˇ»
4350 "});
4351 cx.update_editor(|e, window, cx| {
4352 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4353 });
4354 cx.assert_editor_state(indoc! {"
4355 «Aaaˇ»
4356 "});
4357}
4358
4359#[gpui::test]
4360async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
4361 init_test(cx, |_| {});
4362
4363 let mut cx = EditorTestContext::new(cx).await;
4364
4365 // Manipulate with multiple selections on a single line
4366 cx.set_state(indoc! {"
4367 dd«dd
4368 cˇ»c«c
4369 bb
4370 aaaˇ»aa
4371 "});
4372 cx.update_editor(|e, window, cx| {
4373 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4374 });
4375 cx.assert_editor_state(indoc! {"
4376 «aaaaa
4377 bb
4378 ccc
4379 ddddˇ»
4380 "});
4381
4382 // Manipulate with multiple disjoin selections
4383 cx.set_state(indoc! {"
4384 5«
4385 4
4386 3
4387 2
4388 1ˇ»
4389
4390 dd«dd
4391 ccc
4392 bb
4393 aaaˇ»aa
4394 "});
4395 cx.update_editor(|e, window, cx| {
4396 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4397 });
4398 cx.assert_editor_state(indoc! {"
4399 «1
4400 2
4401 3
4402 4
4403 5ˇ»
4404
4405 «aaaaa
4406 bb
4407 ccc
4408 ddddˇ»
4409 "});
4410
4411 // Adding lines on each selection
4412 cx.set_state(indoc! {"
4413 2«
4414 1ˇ»
4415
4416 bb«bb
4417 aaaˇ»aa
4418 "});
4419 cx.update_editor(|e, window, cx| {
4420 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
4421 });
4422 cx.assert_editor_state(indoc! {"
4423 «2
4424 1
4425 added lineˇ»
4426
4427 «bbbb
4428 aaaaa
4429 added lineˇ»
4430 "});
4431
4432 // Removing lines on each selection
4433 cx.set_state(indoc! {"
4434 2«
4435 1ˇ»
4436
4437 bb«bb
4438 aaaˇ»aa
4439 "});
4440 cx.update_editor(|e, window, cx| {
4441 e.manipulate_immutable_lines(window, cx, |lines| {
4442 lines.pop();
4443 })
4444 });
4445 cx.assert_editor_state(indoc! {"
4446 «2ˇ»
4447
4448 «bbbbˇ»
4449 "});
4450}
4451
4452#[gpui::test]
4453async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
4454 init_test(cx, |settings| {
4455 settings.defaults.tab_size = NonZeroU32::new(3)
4456 });
4457
4458 let mut cx = EditorTestContext::new(cx).await;
4459
4460 // MULTI SELECTION
4461 // Ln.1 "«" tests empty lines
4462 // Ln.9 tests just leading whitespace
4463 cx.set_state(indoc! {"
4464 «
4465 abc // No indentationˇ»
4466 «\tabc // 1 tabˇ»
4467 \t\tabc « ˇ» // 2 tabs
4468 \t ab«c // Tab followed by space
4469 \tabc // Space followed by tab (3 spaces should be the result)
4470 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4471 abˇ»ˇc ˇ ˇ // Already space indented«
4472 \t
4473 \tabc\tdef // Only the leading tab is manipulatedˇ»
4474 "});
4475 cx.update_editor(|e, window, cx| {
4476 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4477 });
4478 cx.assert_editor_state(
4479 indoc! {"
4480 «
4481 abc // No indentation
4482 abc // 1 tab
4483 abc // 2 tabs
4484 abc // Tab followed by space
4485 abc // Space followed by tab (3 spaces should be the result)
4486 abc // Mixed indentation (tab conversion depends on the column)
4487 abc // Already space indented
4488 ·
4489 abc\tdef // Only the leading tab is manipulatedˇ»
4490 "}
4491 .replace("·", "")
4492 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4493 );
4494
4495 // Test on just a few lines, the others should remain unchanged
4496 // Only lines (3, 5, 10, 11) should change
4497 cx.set_state(
4498 indoc! {"
4499 ·
4500 abc // No indentation
4501 \tabcˇ // 1 tab
4502 \t\tabc // 2 tabs
4503 \t abcˇ // Tab followed by space
4504 \tabc // Space followed by tab (3 spaces should be the result)
4505 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4506 abc // Already space indented
4507 «\t
4508 \tabc\tdef // Only the leading tab is manipulatedˇ»
4509 "}
4510 .replace("·", "")
4511 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4512 );
4513 cx.update_editor(|e, window, cx| {
4514 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4515 });
4516 cx.assert_editor_state(
4517 indoc! {"
4518 ·
4519 abc // No indentation
4520 « abc // 1 tabˇ»
4521 \t\tabc // 2 tabs
4522 « abc // Tab followed by spaceˇ»
4523 \tabc // Space followed by tab (3 spaces should be the result)
4524 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4525 abc // Already space indented
4526 « ·
4527 abc\tdef // Only the leading tab is manipulatedˇ»
4528 "}
4529 .replace("·", "")
4530 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4531 );
4532
4533 // SINGLE SELECTION
4534 // Ln.1 "«" tests empty lines
4535 // Ln.9 tests just leading whitespace
4536 cx.set_state(indoc! {"
4537 «
4538 abc // No indentation
4539 \tabc // 1 tab
4540 \t\tabc // 2 tabs
4541 \t abc // Tab followed by space
4542 \tabc // Space followed by tab (3 spaces should be the result)
4543 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4544 abc // Already space indented
4545 \t
4546 \tabc\tdef // Only the leading tab is manipulatedˇ»
4547 "});
4548 cx.update_editor(|e, window, cx| {
4549 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4550 });
4551 cx.assert_editor_state(
4552 indoc! {"
4553 «
4554 abc // No indentation
4555 abc // 1 tab
4556 abc // 2 tabs
4557 abc // Tab followed by space
4558 abc // Space followed by tab (3 spaces should be the result)
4559 abc // Mixed indentation (tab conversion depends on the column)
4560 abc // Already space indented
4561 ·
4562 abc\tdef // Only the leading tab is manipulatedˇ»
4563 "}
4564 .replace("·", "")
4565 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4566 );
4567}
4568
4569#[gpui::test]
4570async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
4571 init_test(cx, |settings| {
4572 settings.defaults.tab_size = NonZeroU32::new(3)
4573 });
4574
4575 let mut cx = EditorTestContext::new(cx).await;
4576
4577 // MULTI SELECTION
4578 // Ln.1 "«" tests empty lines
4579 // Ln.11 tests just leading whitespace
4580 cx.set_state(indoc! {"
4581 «
4582 abˇ»ˇc // No indentation
4583 abc ˇ ˇ // 1 space (< 3 so dont convert)
4584 abc « // 2 spaces (< 3 so dont convert)
4585 abc // 3 spaces (convert)
4586 abc ˇ» // 5 spaces (1 tab + 2 spaces)
4587 «\tˇ»\t«\tˇ»abc // Already tab indented
4588 «\t abc // Tab followed by space
4589 \tabc // Space followed by tab (should be consumed due to tab)
4590 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4591 \tˇ» «\t
4592 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
4593 "});
4594 cx.update_editor(|e, window, cx| {
4595 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4596 });
4597 cx.assert_editor_state(indoc! {"
4598 «
4599 abc // No indentation
4600 abc // 1 space (< 3 so dont convert)
4601 abc // 2 spaces (< 3 so dont convert)
4602 \tabc // 3 spaces (convert)
4603 \t abc // 5 spaces (1 tab + 2 spaces)
4604 \t\t\tabc // Already tab indented
4605 \t abc // Tab followed by space
4606 \tabc // Space followed by tab (should be consumed due to tab)
4607 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4608 \t\t\t
4609 \tabc \t // Only the leading spaces should be convertedˇ»
4610 "});
4611
4612 // Test on just a few lines, the other should remain unchanged
4613 // Only lines (4, 8, 11, 12) should change
4614 cx.set_state(
4615 indoc! {"
4616 ·
4617 abc // No indentation
4618 abc // 1 space (< 3 so dont convert)
4619 abc // 2 spaces (< 3 so dont convert)
4620 « abc // 3 spaces (convert)ˇ»
4621 abc // 5 spaces (1 tab + 2 spaces)
4622 \t\t\tabc // Already tab indented
4623 \t abc // Tab followed by space
4624 \tabc ˇ // Space followed by tab (should be consumed due to tab)
4625 \t\t \tabc // Mixed indentation
4626 \t \t \t \tabc // Mixed indentation
4627 \t \tˇ
4628 « abc \t // Only the leading spaces should be convertedˇ»
4629 "}
4630 .replace("·", "")
4631 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4632 );
4633 cx.update_editor(|e, window, cx| {
4634 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4635 });
4636 cx.assert_editor_state(
4637 indoc! {"
4638 ·
4639 abc // No indentation
4640 abc // 1 space (< 3 so dont convert)
4641 abc // 2 spaces (< 3 so dont convert)
4642 «\tabc // 3 spaces (convert)ˇ»
4643 abc // 5 spaces (1 tab + 2 spaces)
4644 \t\t\tabc // Already tab indented
4645 \t abc // Tab followed by space
4646 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
4647 \t\t \tabc // Mixed indentation
4648 \t \t \t \tabc // Mixed indentation
4649 «\t\t\t
4650 \tabc \t // Only the leading spaces should be convertedˇ»
4651 "}
4652 .replace("·", "")
4653 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4654 );
4655
4656 // SINGLE SELECTION
4657 // Ln.1 "«" tests empty lines
4658 // Ln.11 tests just leading whitespace
4659 cx.set_state(indoc! {"
4660 «
4661 abc // No indentation
4662 abc // 1 space (< 3 so dont convert)
4663 abc // 2 spaces (< 3 so dont convert)
4664 abc // 3 spaces (convert)
4665 abc // 5 spaces (1 tab + 2 spaces)
4666 \t\t\tabc // Already tab indented
4667 \t abc // Tab followed by space
4668 \tabc // Space followed by tab (should be consumed due to tab)
4669 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4670 \t \t
4671 abc \t // Only the leading spaces should be convertedˇ»
4672 "});
4673 cx.update_editor(|e, window, cx| {
4674 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4675 });
4676 cx.assert_editor_state(indoc! {"
4677 «
4678 abc // No indentation
4679 abc // 1 space (< 3 so dont convert)
4680 abc // 2 spaces (< 3 so dont convert)
4681 \tabc // 3 spaces (convert)
4682 \t abc // 5 spaces (1 tab + 2 spaces)
4683 \t\t\tabc // Already tab indented
4684 \t abc // Tab followed by space
4685 \tabc // Space followed by tab (should be consumed due to tab)
4686 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4687 \t\t\t
4688 \tabc \t // Only the leading spaces should be convertedˇ»
4689 "});
4690}
4691
4692#[gpui::test]
4693async fn test_toggle_case(cx: &mut TestAppContext) {
4694 init_test(cx, |_| {});
4695
4696 let mut cx = EditorTestContext::new(cx).await;
4697
4698 // If all lower case -> upper case
4699 cx.set_state(indoc! {"
4700 «hello worldˇ»
4701 "});
4702 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4703 cx.assert_editor_state(indoc! {"
4704 «HELLO WORLDˇ»
4705 "});
4706
4707 // If all upper case -> lower case
4708 cx.set_state(indoc! {"
4709 «HELLO WORLDˇ»
4710 "});
4711 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4712 cx.assert_editor_state(indoc! {"
4713 «hello worldˇ»
4714 "});
4715
4716 // If any upper case characters are identified -> lower case
4717 // This matches JetBrains IDEs
4718 cx.set_state(indoc! {"
4719 «hEllo worldˇ»
4720 "});
4721 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4722 cx.assert_editor_state(indoc! {"
4723 «hello worldˇ»
4724 "});
4725}
4726
4727#[gpui::test]
4728async fn test_manipulate_text(cx: &mut TestAppContext) {
4729 init_test(cx, |_| {});
4730
4731 let mut cx = EditorTestContext::new(cx).await;
4732
4733 // Test convert_to_upper_case()
4734 cx.set_state(indoc! {"
4735 «hello worldˇ»
4736 "});
4737 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4738 cx.assert_editor_state(indoc! {"
4739 «HELLO WORLDˇ»
4740 "});
4741
4742 // Test convert_to_lower_case()
4743 cx.set_state(indoc! {"
4744 «HELLO WORLDˇ»
4745 "});
4746 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
4747 cx.assert_editor_state(indoc! {"
4748 «hello worldˇ»
4749 "});
4750
4751 // Test multiple line, single selection case
4752 cx.set_state(indoc! {"
4753 «The quick brown
4754 fox jumps over
4755 the lazy dogˇ»
4756 "});
4757 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
4758 cx.assert_editor_state(indoc! {"
4759 «The Quick Brown
4760 Fox Jumps Over
4761 The Lazy Dogˇ»
4762 "});
4763
4764 // Test multiple line, single selection case
4765 cx.set_state(indoc! {"
4766 «The quick brown
4767 fox jumps over
4768 the lazy dogˇ»
4769 "});
4770 cx.update_editor(|e, window, cx| {
4771 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
4772 });
4773 cx.assert_editor_state(indoc! {"
4774 «TheQuickBrown
4775 FoxJumpsOver
4776 TheLazyDogˇ»
4777 "});
4778
4779 // From here on out, test more complex cases of manipulate_text()
4780
4781 // Test no selection case - should affect words cursors are in
4782 // Cursor at beginning, middle, and end of word
4783 cx.set_state(indoc! {"
4784 ˇhello big beauˇtiful worldˇ
4785 "});
4786 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4787 cx.assert_editor_state(indoc! {"
4788 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
4789 "});
4790
4791 // Test multiple selections on a single line and across multiple lines
4792 cx.set_state(indoc! {"
4793 «Theˇ» quick «brown
4794 foxˇ» jumps «overˇ»
4795 the «lazyˇ» dog
4796 "});
4797 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4798 cx.assert_editor_state(indoc! {"
4799 «THEˇ» quick «BROWN
4800 FOXˇ» jumps «OVERˇ»
4801 the «LAZYˇ» dog
4802 "});
4803
4804 // Test case where text length grows
4805 cx.set_state(indoc! {"
4806 «tschüߡ»
4807 "});
4808 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4809 cx.assert_editor_state(indoc! {"
4810 «TSCHÜSSˇ»
4811 "});
4812
4813 // Test to make sure we don't crash when text shrinks
4814 cx.set_state(indoc! {"
4815 aaa_bbbˇ
4816 "});
4817 cx.update_editor(|e, window, cx| {
4818 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4819 });
4820 cx.assert_editor_state(indoc! {"
4821 «aaaBbbˇ»
4822 "});
4823
4824 // Test to make sure we all aware of the fact that each word can grow and shrink
4825 // Final selections should be aware of this fact
4826 cx.set_state(indoc! {"
4827 aaa_bˇbb bbˇb_ccc ˇccc_ddd
4828 "});
4829 cx.update_editor(|e, window, cx| {
4830 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4831 });
4832 cx.assert_editor_state(indoc! {"
4833 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
4834 "});
4835
4836 cx.set_state(indoc! {"
4837 «hElLo, WoRld!ˇ»
4838 "});
4839 cx.update_editor(|e, window, cx| {
4840 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
4841 });
4842 cx.assert_editor_state(indoc! {"
4843 «HeLlO, wOrLD!ˇ»
4844 "});
4845}
4846
4847#[gpui::test]
4848fn test_duplicate_line(cx: &mut TestAppContext) {
4849 init_test(cx, |_| {});
4850
4851 let editor = cx.add_window(|window, cx| {
4852 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4853 build_editor(buffer, window, cx)
4854 });
4855 _ = editor.update(cx, |editor, window, cx| {
4856 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4857 s.select_display_ranges([
4858 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4859 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4860 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4861 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4862 ])
4863 });
4864 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4865 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4866 assert_eq!(
4867 editor.selections.display_ranges(cx),
4868 vec![
4869 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4870 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
4871 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4872 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4873 ]
4874 );
4875 });
4876
4877 let editor = cx.add_window(|window, cx| {
4878 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4879 build_editor(buffer, window, cx)
4880 });
4881 _ = editor.update(cx, |editor, window, cx| {
4882 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4883 s.select_display_ranges([
4884 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4885 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4886 ])
4887 });
4888 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4889 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4890 assert_eq!(
4891 editor.selections.display_ranges(cx),
4892 vec![
4893 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
4894 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
4895 ]
4896 );
4897 });
4898
4899 // With `move_upwards` the selections stay in place, except for
4900 // the lines inserted above them
4901 let editor = cx.add_window(|window, cx| {
4902 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4903 build_editor(buffer, window, cx)
4904 });
4905 _ = editor.update(cx, |editor, window, cx| {
4906 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4907 s.select_display_ranges([
4908 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4909 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4910 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4911 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4912 ])
4913 });
4914 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4915 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4916 assert_eq!(
4917 editor.selections.display_ranges(cx),
4918 vec![
4919 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4920 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4921 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4922 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4923 ]
4924 );
4925 });
4926
4927 let editor = cx.add_window(|window, cx| {
4928 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4929 build_editor(buffer, window, cx)
4930 });
4931 _ = editor.update(cx, |editor, window, cx| {
4932 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4933 s.select_display_ranges([
4934 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4935 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4936 ])
4937 });
4938 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4939 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4940 assert_eq!(
4941 editor.selections.display_ranges(cx),
4942 vec![
4943 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4944 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4945 ]
4946 );
4947 });
4948
4949 let editor = cx.add_window(|window, cx| {
4950 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4951 build_editor(buffer, window, cx)
4952 });
4953 _ = editor.update(cx, |editor, window, cx| {
4954 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4955 s.select_display_ranges([
4956 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4957 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4958 ])
4959 });
4960 editor.duplicate_selection(&DuplicateSelection, window, cx);
4961 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
4962 assert_eq!(
4963 editor.selections.display_ranges(cx),
4964 vec![
4965 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4966 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4967 ]
4968 );
4969 });
4970}
4971
4972#[gpui::test]
4973fn test_move_line_up_down(cx: &mut TestAppContext) {
4974 init_test(cx, |_| {});
4975
4976 let editor = cx.add_window(|window, cx| {
4977 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4978 build_editor(buffer, window, cx)
4979 });
4980 _ = editor.update(cx, |editor, window, cx| {
4981 editor.fold_creases(
4982 vec![
4983 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4984 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4985 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4986 ],
4987 true,
4988 window,
4989 cx,
4990 );
4991 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4992 s.select_display_ranges([
4993 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4994 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4995 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4996 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
4997 ])
4998 });
4999 assert_eq!(
5000 editor.display_text(cx),
5001 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
5002 );
5003
5004 editor.move_line_up(&MoveLineUp, window, cx);
5005 assert_eq!(
5006 editor.display_text(cx),
5007 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
5008 );
5009 assert_eq!(
5010 editor.selections.display_ranges(cx),
5011 vec![
5012 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5013 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5014 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5015 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5016 ]
5017 );
5018 });
5019
5020 _ = editor.update(cx, |editor, window, cx| {
5021 editor.move_line_down(&MoveLineDown, window, cx);
5022 assert_eq!(
5023 editor.display_text(cx),
5024 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
5025 );
5026 assert_eq!(
5027 editor.selections.display_ranges(cx),
5028 vec![
5029 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5030 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5031 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5032 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5033 ]
5034 );
5035 });
5036
5037 _ = editor.update(cx, |editor, window, cx| {
5038 editor.move_line_down(&MoveLineDown, window, cx);
5039 assert_eq!(
5040 editor.display_text(cx),
5041 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
5042 );
5043 assert_eq!(
5044 editor.selections.display_ranges(cx),
5045 vec![
5046 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5047 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5048 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5049 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5050 ]
5051 );
5052 });
5053
5054 _ = editor.update(cx, |editor, window, cx| {
5055 editor.move_line_up(&MoveLineUp, window, cx);
5056 assert_eq!(
5057 editor.display_text(cx),
5058 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
5059 );
5060 assert_eq!(
5061 editor.selections.display_ranges(cx),
5062 vec![
5063 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5064 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5065 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5066 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5067 ]
5068 );
5069 });
5070}
5071
5072#[gpui::test]
5073fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
5074 init_test(cx, |_| {});
5075
5076 let editor = cx.add_window(|window, cx| {
5077 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5078 build_editor(buffer, window, cx)
5079 });
5080 _ = editor.update(cx, |editor, window, cx| {
5081 let snapshot = editor.buffer.read(cx).snapshot(cx);
5082 editor.insert_blocks(
5083 [BlockProperties {
5084 style: BlockStyle::Fixed,
5085 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
5086 height: Some(1),
5087 render: Arc::new(|_| div().into_any()),
5088 priority: 0,
5089 }],
5090 Some(Autoscroll::fit()),
5091 cx,
5092 );
5093 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5094 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
5095 });
5096 editor.move_line_down(&MoveLineDown, window, cx);
5097 });
5098}
5099
5100#[gpui::test]
5101async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
5102 init_test(cx, |_| {});
5103
5104 let mut cx = EditorTestContext::new(cx).await;
5105 cx.set_state(
5106 &"
5107 ˇzero
5108 one
5109 two
5110 three
5111 four
5112 five
5113 "
5114 .unindent(),
5115 );
5116
5117 // Create a four-line block that replaces three lines of text.
5118 cx.update_editor(|editor, window, cx| {
5119 let snapshot = editor.snapshot(window, cx);
5120 let snapshot = &snapshot.buffer_snapshot;
5121 let placement = BlockPlacement::Replace(
5122 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
5123 );
5124 editor.insert_blocks(
5125 [BlockProperties {
5126 placement,
5127 height: Some(4),
5128 style: BlockStyle::Sticky,
5129 render: Arc::new(|_| gpui::div().into_any_element()),
5130 priority: 0,
5131 }],
5132 None,
5133 cx,
5134 );
5135 });
5136
5137 // Move down so that the cursor touches the block.
5138 cx.update_editor(|editor, window, cx| {
5139 editor.move_down(&Default::default(), window, cx);
5140 });
5141 cx.assert_editor_state(
5142 &"
5143 zero
5144 «one
5145 two
5146 threeˇ»
5147 four
5148 five
5149 "
5150 .unindent(),
5151 );
5152
5153 // Move down past the block.
5154 cx.update_editor(|editor, window, cx| {
5155 editor.move_down(&Default::default(), window, cx);
5156 });
5157 cx.assert_editor_state(
5158 &"
5159 zero
5160 one
5161 two
5162 three
5163 ˇfour
5164 five
5165 "
5166 .unindent(),
5167 );
5168}
5169
5170#[gpui::test]
5171fn test_transpose(cx: &mut TestAppContext) {
5172 init_test(cx, |_| {});
5173
5174 _ = cx.add_window(|window, cx| {
5175 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
5176 editor.set_style(EditorStyle::default(), window, cx);
5177 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5178 s.select_ranges([1..1])
5179 });
5180 editor.transpose(&Default::default(), window, cx);
5181 assert_eq!(editor.text(cx), "bac");
5182 assert_eq!(editor.selections.ranges(cx), [2..2]);
5183
5184 editor.transpose(&Default::default(), window, cx);
5185 assert_eq!(editor.text(cx), "bca");
5186 assert_eq!(editor.selections.ranges(cx), [3..3]);
5187
5188 editor.transpose(&Default::default(), window, cx);
5189 assert_eq!(editor.text(cx), "bac");
5190 assert_eq!(editor.selections.ranges(cx), [3..3]);
5191
5192 editor
5193 });
5194
5195 _ = cx.add_window(|window, cx| {
5196 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5197 editor.set_style(EditorStyle::default(), window, cx);
5198 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5199 s.select_ranges([3..3])
5200 });
5201 editor.transpose(&Default::default(), window, cx);
5202 assert_eq!(editor.text(cx), "acb\nde");
5203 assert_eq!(editor.selections.ranges(cx), [3..3]);
5204
5205 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5206 s.select_ranges([4..4])
5207 });
5208 editor.transpose(&Default::default(), window, cx);
5209 assert_eq!(editor.text(cx), "acbd\ne");
5210 assert_eq!(editor.selections.ranges(cx), [5..5]);
5211
5212 editor.transpose(&Default::default(), window, cx);
5213 assert_eq!(editor.text(cx), "acbde\n");
5214 assert_eq!(editor.selections.ranges(cx), [6..6]);
5215
5216 editor.transpose(&Default::default(), window, cx);
5217 assert_eq!(editor.text(cx), "acbd\ne");
5218 assert_eq!(editor.selections.ranges(cx), [6..6]);
5219
5220 editor
5221 });
5222
5223 _ = cx.add_window(|window, cx| {
5224 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5225 editor.set_style(EditorStyle::default(), window, cx);
5226 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5227 s.select_ranges([1..1, 2..2, 4..4])
5228 });
5229 editor.transpose(&Default::default(), window, cx);
5230 assert_eq!(editor.text(cx), "bacd\ne");
5231 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
5232
5233 editor.transpose(&Default::default(), window, cx);
5234 assert_eq!(editor.text(cx), "bcade\n");
5235 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
5236
5237 editor.transpose(&Default::default(), window, cx);
5238 assert_eq!(editor.text(cx), "bcda\ne");
5239 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5240
5241 editor.transpose(&Default::default(), window, cx);
5242 assert_eq!(editor.text(cx), "bcade\n");
5243 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5244
5245 editor.transpose(&Default::default(), window, cx);
5246 assert_eq!(editor.text(cx), "bcaed\n");
5247 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
5248
5249 editor
5250 });
5251
5252 _ = cx.add_window(|window, cx| {
5253 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
5254 editor.set_style(EditorStyle::default(), window, cx);
5255 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5256 s.select_ranges([4..4])
5257 });
5258 editor.transpose(&Default::default(), window, cx);
5259 assert_eq!(editor.text(cx), "🏀🍐✋");
5260 assert_eq!(editor.selections.ranges(cx), [8..8]);
5261
5262 editor.transpose(&Default::default(), window, cx);
5263 assert_eq!(editor.text(cx), "🏀✋🍐");
5264 assert_eq!(editor.selections.ranges(cx), [11..11]);
5265
5266 editor.transpose(&Default::default(), window, cx);
5267 assert_eq!(editor.text(cx), "🏀🍐✋");
5268 assert_eq!(editor.selections.ranges(cx), [11..11]);
5269
5270 editor
5271 });
5272}
5273
5274#[gpui::test]
5275async fn test_rewrap(cx: &mut TestAppContext) {
5276 init_test(cx, |settings| {
5277 settings.languages.0.extend([
5278 (
5279 "Markdown".into(),
5280 LanguageSettingsContent {
5281 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5282 preferred_line_length: Some(40),
5283 ..Default::default()
5284 },
5285 ),
5286 (
5287 "Plain Text".into(),
5288 LanguageSettingsContent {
5289 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5290 preferred_line_length: Some(40),
5291 ..Default::default()
5292 },
5293 ),
5294 (
5295 "C++".into(),
5296 LanguageSettingsContent {
5297 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5298 preferred_line_length: Some(40),
5299 ..Default::default()
5300 },
5301 ),
5302 (
5303 "Python".into(),
5304 LanguageSettingsContent {
5305 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5306 preferred_line_length: Some(40),
5307 ..Default::default()
5308 },
5309 ),
5310 (
5311 "Rust".into(),
5312 LanguageSettingsContent {
5313 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5314 preferred_line_length: Some(40),
5315 ..Default::default()
5316 },
5317 ),
5318 ])
5319 });
5320
5321 let mut cx = EditorTestContext::new(cx).await;
5322
5323 let cpp_language = Arc::new(Language::new(
5324 LanguageConfig {
5325 name: "C++".into(),
5326 line_comments: vec!["// ".into()],
5327 ..LanguageConfig::default()
5328 },
5329 None,
5330 ));
5331 let python_language = Arc::new(Language::new(
5332 LanguageConfig {
5333 name: "Python".into(),
5334 line_comments: vec!["# ".into()],
5335 ..LanguageConfig::default()
5336 },
5337 None,
5338 ));
5339 let markdown_language = Arc::new(Language::new(
5340 LanguageConfig {
5341 name: "Markdown".into(),
5342 rewrap_prefixes: vec![
5343 regex::Regex::new("\\d+\\.\\s+").unwrap(),
5344 regex::Regex::new("[-*+]\\s+").unwrap(),
5345 ],
5346 ..LanguageConfig::default()
5347 },
5348 None,
5349 ));
5350 let rust_language = Arc::new(Language::new(
5351 LanguageConfig {
5352 name: "Rust".into(),
5353 line_comments: vec!["// ".into(), "/// ".into()],
5354 ..LanguageConfig::default()
5355 },
5356 Some(tree_sitter_rust::LANGUAGE.into()),
5357 ));
5358
5359 let plaintext_language = Arc::new(Language::new(
5360 LanguageConfig {
5361 name: "Plain Text".into(),
5362 ..LanguageConfig::default()
5363 },
5364 None,
5365 ));
5366
5367 // Test basic rewrapping of a long line with a cursor
5368 assert_rewrap(
5369 indoc! {"
5370 // ˇThis is a long comment that needs to be wrapped.
5371 "},
5372 indoc! {"
5373 // ˇThis is a long comment that needs to
5374 // be wrapped.
5375 "},
5376 cpp_language.clone(),
5377 &mut cx,
5378 );
5379
5380 // Test rewrapping a full selection
5381 assert_rewrap(
5382 indoc! {"
5383 «// This selected long comment needs to be wrapped.ˇ»"
5384 },
5385 indoc! {"
5386 «// This selected long comment needs to
5387 // be wrapped.ˇ»"
5388 },
5389 cpp_language.clone(),
5390 &mut cx,
5391 );
5392
5393 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
5394 assert_rewrap(
5395 indoc! {"
5396 // ˇThis is the first line.
5397 // Thisˇ is the second line.
5398 // This is the thirdˇ line, all part of one paragraph.
5399 "},
5400 indoc! {"
5401 // ˇThis is the first line. Thisˇ is the
5402 // second line. This is the thirdˇ line,
5403 // all part of one paragraph.
5404 "},
5405 cpp_language.clone(),
5406 &mut cx,
5407 );
5408
5409 // Test multiple cursors in different paragraphs trigger separate rewraps
5410 assert_rewrap(
5411 indoc! {"
5412 // ˇThis is the first paragraph, first line.
5413 // ˇThis is the first paragraph, second line.
5414
5415 // ˇThis is the second paragraph, first line.
5416 // ˇThis is the second paragraph, second line.
5417 "},
5418 indoc! {"
5419 // ˇThis is the first paragraph, first
5420 // line. ˇThis is the first paragraph,
5421 // second line.
5422
5423 // ˇThis is the second paragraph, first
5424 // line. ˇThis is the second paragraph,
5425 // second line.
5426 "},
5427 cpp_language.clone(),
5428 &mut cx,
5429 );
5430
5431 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
5432 assert_rewrap(
5433 indoc! {"
5434 «// A regular long long comment to be wrapped.
5435 /// A documentation long comment to be wrapped.ˇ»
5436 "},
5437 indoc! {"
5438 «// A regular long long comment to be
5439 // wrapped.
5440 /// A documentation long comment to be
5441 /// wrapped.ˇ»
5442 "},
5443 rust_language.clone(),
5444 &mut cx,
5445 );
5446
5447 // Test that change in indentation level trigger seperate rewraps
5448 assert_rewrap(
5449 indoc! {"
5450 fn foo() {
5451 «// This is a long comment at the base indent.
5452 // This is a long comment at the next indent.ˇ»
5453 }
5454 "},
5455 indoc! {"
5456 fn foo() {
5457 «// This is a long comment at the
5458 // base indent.
5459 // This is a long comment at the
5460 // next indent.ˇ»
5461 }
5462 "},
5463 rust_language.clone(),
5464 &mut cx,
5465 );
5466
5467 // Test that different comment prefix characters (e.g., '#') are handled correctly
5468 assert_rewrap(
5469 indoc! {"
5470 # ˇThis is a long comment using a pound sign.
5471 "},
5472 indoc! {"
5473 # ˇThis is a long comment using a pound
5474 # sign.
5475 "},
5476 python_language.clone(),
5477 &mut cx,
5478 );
5479
5480 // Test rewrapping only affects comments, not code even when selected
5481 assert_rewrap(
5482 indoc! {"
5483 «/// This doc comment is long and should be wrapped.
5484 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
5485 "},
5486 indoc! {"
5487 «/// This doc comment is long and should
5488 /// be wrapped.
5489 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
5490 "},
5491 rust_language.clone(),
5492 &mut cx,
5493 );
5494
5495 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
5496 assert_rewrap(
5497 indoc! {"
5498 # Header
5499
5500 A long long long line of markdown text to wrap.ˇ
5501 "},
5502 indoc! {"
5503 # Header
5504
5505 A long long long line of markdown text
5506 to wrap.ˇ
5507 "},
5508 markdown_language.clone(),
5509 &mut cx,
5510 );
5511
5512 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
5513 assert_rewrap(
5514 indoc! {"
5515 «1. This is a numbered list item that is very long and needs to be wrapped properly.
5516 2. This is a numbered list item that is very long and needs to be wrapped properly.
5517 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
5518 "},
5519 indoc! {"
5520 «1. This is a numbered list item that is
5521 very long and needs to be wrapped
5522 properly.
5523 2. This is a numbered list item that is
5524 very long and needs to be wrapped
5525 properly.
5526 - This is an unordered list item that is
5527 also very long and should not merge
5528 with the numbered item.ˇ»
5529 "},
5530 markdown_language.clone(),
5531 &mut cx,
5532 );
5533
5534 // Test that rewrapping add indents for rewrapping boundary if not exists already.
5535 assert_rewrap(
5536 indoc! {"
5537 «1. This is a numbered list item that is
5538 very long and needs to be wrapped
5539 properly.
5540 2. This is a numbered list item that is
5541 very long and needs to be wrapped
5542 properly.
5543 - This is an unordered list item that is
5544 also very long and should not merge with
5545 the numbered item.ˇ»
5546 "},
5547 indoc! {"
5548 «1. This is a numbered list item that is
5549 very long and needs to be wrapped
5550 properly.
5551 2. This is a numbered list item that is
5552 very long and needs to be wrapped
5553 properly.
5554 - This is an unordered list item that is
5555 also very long and should not merge
5556 with the numbered item.ˇ»
5557 "},
5558 markdown_language.clone(),
5559 &mut cx,
5560 );
5561
5562 // Test that rewrapping maintain indents even when they already exists.
5563 assert_rewrap(
5564 indoc! {"
5565 «1. This is a numbered list
5566 item that is very long and needs to be wrapped properly.
5567 2. This is a numbered list
5568 item that is very long and needs to be wrapped properly.
5569 - This is an unordered list item that is also very long and
5570 should not merge with the numbered item.ˇ»
5571 "},
5572 indoc! {"
5573 «1. This is a numbered list item that is
5574 very long and needs to be wrapped
5575 properly.
5576 2. This is a numbered list item that is
5577 very long and needs to be wrapped
5578 properly.
5579 - This is an unordered list item that is
5580 also very long and should not merge
5581 with the numbered item.ˇ»
5582 "},
5583 markdown_language.clone(),
5584 &mut cx,
5585 );
5586
5587 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
5588 assert_rewrap(
5589 indoc! {"
5590 ˇThis is a very long line of plain text that will be wrapped.
5591 "},
5592 indoc! {"
5593 ˇThis is a very long line of plain text
5594 that will be wrapped.
5595 "},
5596 plaintext_language.clone(),
5597 &mut cx,
5598 );
5599
5600 // Test that non-commented code acts as a paragraph boundary within a selection
5601 assert_rewrap(
5602 indoc! {"
5603 «// This is the first long comment block to be wrapped.
5604 fn my_func(a: u32);
5605 // This is the second long comment block to be wrapped.ˇ»
5606 "},
5607 indoc! {"
5608 «// This is the first long comment block
5609 // to be wrapped.
5610 fn my_func(a: u32);
5611 // This is the second long comment block
5612 // to be wrapped.ˇ»
5613 "},
5614 rust_language.clone(),
5615 &mut cx,
5616 );
5617
5618 // Test rewrapping multiple selections, including ones with blank lines or tabs
5619 assert_rewrap(
5620 indoc! {"
5621 «ˇThis is a very long line that will be wrapped.
5622
5623 This is another paragraph in the same selection.»
5624
5625 «\tThis is a very long indented line that will be wrapped.ˇ»
5626 "},
5627 indoc! {"
5628 «ˇThis is a very long line that will be
5629 wrapped.
5630
5631 This is another paragraph in the same
5632 selection.»
5633
5634 «\tThis is a very long indented line
5635 \tthat will be wrapped.ˇ»
5636 "},
5637 plaintext_language.clone(),
5638 &mut cx,
5639 );
5640
5641 // Test that an empty comment line acts as a paragraph boundary
5642 assert_rewrap(
5643 indoc! {"
5644 // ˇThis is a long comment that will be wrapped.
5645 //
5646 // And this is another long comment that will also be wrapped.ˇ
5647 "},
5648 indoc! {"
5649 // ˇThis is a long comment that will be
5650 // wrapped.
5651 //
5652 // And this is another long comment that
5653 // will also be wrapped.ˇ
5654 "},
5655 cpp_language,
5656 &mut cx,
5657 );
5658
5659 #[track_caller]
5660 fn assert_rewrap(
5661 unwrapped_text: &str,
5662 wrapped_text: &str,
5663 language: Arc<Language>,
5664 cx: &mut EditorTestContext,
5665 ) {
5666 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
5667 cx.set_state(unwrapped_text);
5668 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
5669 cx.assert_editor_state(wrapped_text);
5670 }
5671}
5672
5673#[gpui::test]
5674async fn test_hard_wrap(cx: &mut TestAppContext) {
5675 init_test(cx, |_| {});
5676 let mut cx = EditorTestContext::new(cx).await;
5677
5678 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
5679 cx.update_editor(|editor, _, cx| {
5680 editor.set_hard_wrap(Some(14), cx);
5681 });
5682
5683 cx.set_state(indoc!(
5684 "
5685 one two three ˇ
5686 "
5687 ));
5688 cx.simulate_input("four");
5689 cx.run_until_parked();
5690
5691 cx.assert_editor_state(indoc!(
5692 "
5693 one two three
5694 fourˇ
5695 "
5696 ));
5697
5698 cx.update_editor(|editor, window, cx| {
5699 editor.newline(&Default::default(), window, cx);
5700 });
5701 cx.run_until_parked();
5702 cx.assert_editor_state(indoc!(
5703 "
5704 one two three
5705 four
5706 ˇ
5707 "
5708 ));
5709
5710 cx.simulate_input("five");
5711 cx.run_until_parked();
5712 cx.assert_editor_state(indoc!(
5713 "
5714 one two three
5715 four
5716 fiveˇ
5717 "
5718 ));
5719
5720 cx.update_editor(|editor, window, cx| {
5721 editor.newline(&Default::default(), window, cx);
5722 });
5723 cx.run_until_parked();
5724 cx.simulate_input("# ");
5725 cx.run_until_parked();
5726 cx.assert_editor_state(indoc!(
5727 "
5728 one two three
5729 four
5730 five
5731 # ˇ
5732 "
5733 ));
5734
5735 cx.update_editor(|editor, window, cx| {
5736 editor.newline(&Default::default(), window, cx);
5737 });
5738 cx.run_until_parked();
5739 cx.assert_editor_state(indoc!(
5740 "
5741 one two three
5742 four
5743 five
5744 #\x20
5745 #ˇ
5746 "
5747 ));
5748
5749 cx.simulate_input(" 6");
5750 cx.run_until_parked();
5751 cx.assert_editor_state(indoc!(
5752 "
5753 one two three
5754 four
5755 five
5756 #
5757 # 6ˇ
5758 "
5759 ));
5760}
5761
5762#[gpui::test]
5763async fn test_clipboard(cx: &mut TestAppContext) {
5764 init_test(cx, |_| {});
5765
5766 let mut cx = EditorTestContext::new(cx).await;
5767
5768 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
5769 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5770 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
5771
5772 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
5773 cx.set_state("two ˇfour ˇsix ˇ");
5774 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5775 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
5776
5777 // Paste again but with only two cursors. Since the number of cursors doesn't
5778 // match the number of slices in the clipboard, the entire clipboard text
5779 // is pasted at each cursor.
5780 cx.set_state("ˇtwo one✅ four three six five ˇ");
5781 cx.update_editor(|e, window, cx| {
5782 e.handle_input("( ", window, cx);
5783 e.paste(&Paste, window, cx);
5784 e.handle_input(") ", window, cx);
5785 });
5786 cx.assert_editor_state(
5787 &([
5788 "( one✅ ",
5789 "three ",
5790 "five ) ˇtwo one✅ four three six five ( one✅ ",
5791 "three ",
5792 "five ) ˇ",
5793 ]
5794 .join("\n")),
5795 );
5796
5797 // Cut with three selections, one of which is full-line.
5798 cx.set_state(indoc! {"
5799 1«2ˇ»3
5800 4ˇ567
5801 «8ˇ»9"});
5802 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5803 cx.assert_editor_state(indoc! {"
5804 1ˇ3
5805 ˇ9"});
5806
5807 // Paste with three selections, noticing how the copied selection that was full-line
5808 // gets inserted before the second cursor.
5809 cx.set_state(indoc! {"
5810 1ˇ3
5811 9ˇ
5812 «oˇ»ne"});
5813 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5814 cx.assert_editor_state(indoc! {"
5815 12ˇ3
5816 4567
5817 9ˇ
5818 8ˇne"});
5819
5820 // Copy with a single cursor only, which writes the whole line into the clipboard.
5821 cx.set_state(indoc! {"
5822 The quick brown
5823 fox juˇmps over
5824 the lazy dog"});
5825 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5826 assert_eq!(
5827 cx.read_from_clipboard()
5828 .and_then(|item| item.text().as_deref().map(str::to_string)),
5829 Some("fox jumps over\n".to_string())
5830 );
5831
5832 // Paste with three selections, noticing how the copied full-line selection is inserted
5833 // before the empty selections but replaces the selection that is non-empty.
5834 cx.set_state(indoc! {"
5835 Tˇhe quick brown
5836 «foˇ»x jumps over
5837 tˇhe lazy dog"});
5838 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5839 cx.assert_editor_state(indoc! {"
5840 fox jumps over
5841 Tˇhe quick brown
5842 fox jumps over
5843 ˇx jumps over
5844 fox jumps over
5845 tˇhe lazy dog"});
5846}
5847
5848#[gpui::test]
5849async fn test_copy_trim(cx: &mut TestAppContext) {
5850 init_test(cx, |_| {});
5851
5852 let mut cx = EditorTestContext::new(cx).await;
5853 cx.set_state(
5854 r#" «for selection in selections.iter() {
5855 let mut start = selection.start;
5856 let mut end = selection.end;
5857 let is_entire_line = selection.is_empty();
5858 if is_entire_line {
5859 start = Point::new(start.row, 0);ˇ»
5860 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5861 }
5862 "#,
5863 );
5864 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5865 assert_eq!(
5866 cx.read_from_clipboard()
5867 .and_then(|item| item.text().as_deref().map(str::to_string)),
5868 Some(
5869 "for selection in selections.iter() {
5870 let mut start = selection.start;
5871 let mut end = selection.end;
5872 let is_entire_line = selection.is_empty();
5873 if is_entire_line {
5874 start = Point::new(start.row, 0);"
5875 .to_string()
5876 ),
5877 "Regular copying preserves all indentation selected",
5878 );
5879 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5880 assert_eq!(
5881 cx.read_from_clipboard()
5882 .and_then(|item| item.text().as_deref().map(str::to_string)),
5883 Some(
5884 "for selection in selections.iter() {
5885let mut start = selection.start;
5886let mut end = selection.end;
5887let is_entire_line = selection.is_empty();
5888if is_entire_line {
5889 start = Point::new(start.row, 0);"
5890 .to_string()
5891 ),
5892 "Copying with stripping should strip all leading whitespaces"
5893 );
5894
5895 cx.set_state(
5896 r#" « for selection in selections.iter() {
5897 let mut start = selection.start;
5898 let mut end = selection.end;
5899 let is_entire_line = selection.is_empty();
5900 if is_entire_line {
5901 start = Point::new(start.row, 0);ˇ»
5902 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5903 }
5904 "#,
5905 );
5906 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5907 assert_eq!(
5908 cx.read_from_clipboard()
5909 .and_then(|item| item.text().as_deref().map(str::to_string)),
5910 Some(
5911 " for selection in selections.iter() {
5912 let mut start = selection.start;
5913 let mut end = selection.end;
5914 let is_entire_line = selection.is_empty();
5915 if is_entire_line {
5916 start = Point::new(start.row, 0);"
5917 .to_string()
5918 ),
5919 "Regular copying preserves all indentation selected",
5920 );
5921 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5922 assert_eq!(
5923 cx.read_from_clipboard()
5924 .and_then(|item| item.text().as_deref().map(str::to_string)),
5925 Some(
5926 "for selection in selections.iter() {
5927let mut start = selection.start;
5928let mut end = selection.end;
5929let is_entire_line = selection.is_empty();
5930if is_entire_line {
5931 start = Point::new(start.row, 0);"
5932 .to_string()
5933 ),
5934 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
5935 );
5936
5937 cx.set_state(
5938 r#" «ˇ for selection in selections.iter() {
5939 let mut start = selection.start;
5940 let mut end = selection.end;
5941 let is_entire_line = selection.is_empty();
5942 if is_entire_line {
5943 start = Point::new(start.row, 0);»
5944 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5945 }
5946 "#,
5947 );
5948 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5949 assert_eq!(
5950 cx.read_from_clipboard()
5951 .and_then(|item| item.text().as_deref().map(str::to_string)),
5952 Some(
5953 " for selection in selections.iter() {
5954 let mut start = selection.start;
5955 let mut end = selection.end;
5956 let is_entire_line = selection.is_empty();
5957 if is_entire_line {
5958 start = Point::new(start.row, 0);"
5959 .to_string()
5960 ),
5961 "Regular copying for reverse selection works the same",
5962 );
5963 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5964 assert_eq!(
5965 cx.read_from_clipboard()
5966 .and_then(|item| item.text().as_deref().map(str::to_string)),
5967 Some(
5968 "for selection in selections.iter() {
5969let mut start = selection.start;
5970let mut end = selection.end;
5971let is_entire_line = selection.is_empty();
5972if is_entire_line {
5973 start = Point::new(start.row, 0);"
5974 .to_string()
5975 ),
5976 "Copying with stripping for reverse selection works the same"
5977 );
5978
5979 cx.set_state(
5980 r#" for selection «in selections.iter() {
5981 let mut start = selection.start;
5982 let mut end = selection.end;
5983 let is_entire_line = selection.is_empty();
5984 if is_entire_line {
5985 start = Point::new(start.row, 0);ˇ»
5986 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5987 }
5988 "#,
5989 );
5990 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5991 assert_eq!(
5992 cx.read_from_clipboard()
5993 .and_then(|item| item.text().as_deref().map(str::to_string)),
5994 Some(
5995 "in selections.iter() {
5996 let mut start = selection.start;
5997 let mut end = selection.end;
5998 let is_entire_line = selection.is_empty();
5999 if is_entire_line {
6000 start = Point::new(start.row, 0);"
6001 .to_string()
6002 ),
6003 "When selecting past the indent, the copying works as usual",
6004 );
6005 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6006 assert_eq!(
6007 cx.read_from_clipboard()
6008 .and_then(|item| item.text().as_deref().map(str::to_string)),
6009 Some(
6010 "in selections.iter() {
6011 let mut start = selection.start;
6012 let mut end = selection.end;
6013 let is_entire_line = selection.is_empty();
6014 if is_entire_line {
6015 start = Point::new(start.row, 0);"
6016 .to_string()
6017 ),
6018 "When selecting past the indent, nothing is trimmed"
6019 );
6020
6021 cx.set_state(
6022 r#" «for selection in selections.iter() {
6023 let mut start = selection.start;
6024
6025 let mut end = selection.end;
6026 let is_entire_line = selection.is_empty();
6027 if is_entire_line {
6028 start = Point::new(start.row, 0);
6029ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
6030 }
6031 "#,
6032 );
6033 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6034 assert_eq!(
6035 cx.read_from_clipboard()
6036 .and_then(|item| item.text().as_deref().map(str::to_string)),
6037 Some(
6038 "for selection in selections.iter() {
6039let mut start = selection.start;
6040
6041let mut end = selection.end;
6042let is_entire_line = selection.is_empty();
6043if is_entire_line {
6044 start = Point::new(start.row, 0);
6045"
6046 .to_string()
6047 ),
6048 "Copying with stripping should ignore empty lines"
6049 );
6050}
6051
6052#[gpui::test]
6053async fn test_paste_multiline(cx: &mut TestAppContext) {
6054 init_test(cx, |_| {});
6055
6056 let mut cx = EditorTestContext::new(cx).await;
6057 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
6058
6059 // Cut an indented block, without the leading whitespace.
6060 cx.set_state(indoc! {"
6061 const a: B = (
6062 c(),
6063 «d(
6064 e,
6065 f
6066 )ˇ»
6067 );
6068 "});
6069 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6070 cx.assert_editor_state(indoc! {"
6071 const a: B = (
6072 c(),
6073 ˇ
6074 );
6075 "});
6076
6077 // Paste it at the same position.
6078 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6079 cx.assert_editor_state(indoc! {"
6080 const a: B = (
6081 c(),
6082 d(
6083 e,
6084 f
6085 )ˇ
6086 );
6087 "});
6088
6089 // Paste it at a line with a lower indent level.
6090 cx.set_state(indoc! {"
6091 ˇ
6092 const a: B = (
6093 c(),
6094 );
6095 "});
6096 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6097 cx.assert_editor_state(indoc! {"
6098 d(
6099 e,
6100 f
6101 )ˇ
6102 const a: B = (
6103 c(),
6104 );
6105 "});
6106
6107 // Cut an indented block, with the leading whitespace.
6108 cx.set_state(indoc! {"
6109 const a: B = (
6110 c(),
6111 « d(
6112 e,
6113 f
6114 )
6115 ˇ»);
6116 "});
6117 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6118 cx.assert_editor_state(indoc! {"
6119 const a: B = (
6120 c(),
6121 ˇ);
6122 "});
6123
6124 // Paste it at the same position.
6125 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6126 cx.assert_editor_state(indoc! {"
6127 const a: B = (
6128 c(),
6129 d(
6130 e,
6131 f
6132 )
6133 ˇ);
6134 "});
6135
6136 // Paste it at a line with a higher indent level.
6137 cx.set_state(indoc! {"
6138 const a: B = (
6139 c(),
6140 d(
6141 e,
6142 fˇ
6143 )
6144 );
6145 "});
6146 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6147 cx.assert_editor_state(indoc! {"
6148 const a: B = (
6149 c(),
6150 d(
6151 e,
6152 f d(
6153 e,
6154 f
6155 )
6156 ˇ
6157 )
6158 );
6159 "});
6160
6161 // Copy an indented block, starting mid-line
6162 cx.set_state(indoc! {"
6163 const a: B = (
6164 c(),
6165 somethin«g(
6166 e,
6167 f
6168 )ˇ»
6169 );
6170 "});
6171 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6172
6173 // Paste it on a line with a lower indent level
6174 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
6175 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6176 cx.assert_editor_state(indoc! {"
6177 const a: B = (
6178 c(),
6179 something(
6180 e,
6181 f
6182 )
6183 );
6184 g(
6185 e,
6186 f
6187 )ˇ"});
6188}
6189
6190#[gpui::test]
6191async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
6192 init_test(cx, |_| {});
6193
6194 cx.write_to_clipboard(ClipboardItem::new_string(
6195 " d(\n e\n );\n".into(),
6196 ));
6197
6198 let mut cx = EditorTestContext::new(cx).await;
6199 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
6200
6201 cx.set_state(indoc! {"
6202 fn a() {
6203 b();
6204 if c() {
6205 ˇ
6206 }
6207 }
6208 "});
6209
6210 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6211 cx.assert_editor_state(indoc! {"
6212 fn a() {
6213 b();
6214 if c() {
6215 d(
6216 e
6217 );
6218 ˇ
6219 }
6220 }
6221 "});
6222
6223 cx.set_state(indoc! {"
6224 fn a() {
6225 b();
6226 ˇ
6227 }
6228 "});
6229
6230 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6231 cx.assert_editor_state(indoc! {"
6232 fn a() {
6233 b();
6234 d(
6235 e
6236 );
6237 ˇ
6238 }
6239 "});
6240}
6241
6242#[gpui::test]
6243fn test_select_all(cx: &mut TestAppContext) {
6244 init_test(cx, |_| {});
6245
6246 let editor = cx.add_window(|window, cx| {
6247 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
6248 build_editor(buffer, window, cx)
6249 });
6250 _ = editor.update(cx, |editor, window, cx| {
6251 editor.select_all(&SelectAll, window, cx);
6252 assert_eq!(
6253 editor.selections.display_ranges(cx),
6254 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
6255 );
6256 });
6257}
6258
6259#[gpui::test]
6260fn test_select_line(cx: &mut TestAppContext) {
6261 init_test(cx, |_| {});
6262
6263 let editor = cx.add_window(|window, cx| {
6264 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
6265 build_editor(buffer, window, cx)
6266 });
6267 _ = editor.update(cx, |editor, window, cx| {
6268 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6269 s.select_display_ranges([
6270 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6271 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
6272 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
6273 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
6274 ])
6275 });
6276 editor.select_line(&SelectLine, window, cx);
6277 assert_eq!(
6278 editor.selections.display_ranges(cx),
6279 vec![
6280 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
6281 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
6282 ]
6283 );
6284 });
6285
6286 _ = editor.update(cx, |editor, window, cx| {
6287 editor.select_line(&SelectLine, window, cx);
6288 assert_eq!(
6289 editor.selections.display_ranges(cx),
6290 vec![
6291 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
6292 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
6293 ]
6294 );
6295 });
6296
6297 _ = editor.update(cx, |editor, window, cx| {
6298 editor.select_line(&SelectLine, window, cx);
6299 assert_eq!(
6300 editor.selections.display_ranges(cx),
6301 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
6302 );
6303 });
6304}
6305
6306#[gpui::test]
6307async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
6308 init_test(cx, |_| {});
6309 let mut cx = EditorTestContext::new(cx).await;
6310
6311 #[track_caller]
6312 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
6313 cx.set_state(initial_state);
6314 cx.update_editor(|e, window, cx| {
6315 e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
6316 });
6317 cx.assert_editor_state(expected_state);
6318 }
6319
6320 // Selection starts and ends at the middle of lines, left-to-right
6321 test(
6322 &mut cx,
6323 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
6324 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
6325 );
6326 // Same thing, right-to-left
6327 test(
6328 &mut cx,
6329 "aa\nb«b\ncc\ndd\neˇ»e\nff",
6330 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
6331 );
6332
6333 // Whole buffer, left-to-right, last line *doesn't* end with newline
6334 test(
6335 &mut cx,
6336 "«ˇaa\nbb\ncc\ndd\nee\nff»",
6337 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
6338 );
6339 // Same thing, right-to-left
6340 test(
6341 &mut cx,
6342 "«aa\nbb\ncc\ndd\nee\nffˇ»",
6343 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
6344 );
6345
6346 // Whole buffer, left-to-right, last line ends with newline
6347 test(
6348 &mut cx,
6349 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
6350 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
6351 );
6352 // Same thing, right-to-left
6353 test(
6354 &mut cx,
6355 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
6356 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
6357 );
6358
6359 // Starts at the end of a line, ends at the start of another
6360 test(
6361 &mut cx,
6362 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
6363 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
6364 );
6365}
6366
6367#[gpui::test]
6368async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
6369 init_test(cx, |_| {});
6370
6371 let editor = cx.add_window(|window, cx| {
6372 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
6373 build_editor(buffer, window, cx)
6374 });
6375
6376 // setup
6377 _ = editor.update(cx, |editor, window, cx| {
6378 editor.fold_creases(
6379 vec![
6380 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
6381 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
6382 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
6383 ],
6384 true,
6385 window,
6386 cx,
6387 );
6388 assert_eq!(
6389 editor.display_text(cx),
6390 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
6391 );
6392 });
6393
6394 _ = editor.update(cx, |editor, window, cx| {
6395 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6396 s.select_display_ranges([
6397 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6398 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
6399 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
6400 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
6401 ])
6402 });
6403 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
6404 assert_eq!(
6405 editor.display_text(cx),
6406 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
6407 );
6408 });
6409 EditorTestContext::for_editor(editor, cx)
6410 .await
6411 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
6412
6413 _ = editor.update(cx, |editor, window, cx| {
6414 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6415 s.select_display_ranges([
6416 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
6417 ])
6418 });
6419 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
6420 assert_eq!(
6421 editor.display_text(cx),
6422 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
6423 );
6424 assert_eq!(
6425 editor.selections.display_ranges(cx),
6426 [
6427 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
6428 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
6429 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
6430 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
6431 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
6432 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
6433 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
6434 ]
6435 );
6436 });
6437 EditorTestContext::for_editor(editor, cx)
6438 .await
6439 .assert_editor_state(
6440 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
6441 );
6442}
6443
6444#[gpui::test]
6445async fn test_add_selection_above_below(cx: &mut TestAppContext) {
6446 init_test(cx, |_| {});
6447
6448 let mut cx = EditorTestContext::new(cx).await;
6449
6450 cx.set_state(indoc!(
6451 r#"abc
6452 defˇghi
6453
6454 jk
6455 nlmo
6456 "#
6457 ));
6458
6459 cx.update_editor(|editor, window, cx| {
6460 editor.add_selection_above(&Default::default(), window, cx);
6461 });
6462
6463 cx.assert_editor_state(indoc!(
6464 r#"abcˇ
6465 defˇghi
6466
6467 jk
6468 nlmo
6469 "#
6470 ));
6471
6472 cx.update_editor(|editor, window, cx| {
6473 editor.add_selection_above(&Default::default(), window, cx);
6474 });
6475
6476 cx.assert_editor_state(indoc!(
6477 r#"abcˇ
6478 defˇghi
6479
6480 jk
6481 nlmo
6482 "#
6483 ));
6484
6485 cx.update_editor(|editor, window, cx| {
6486 editor.add_selection_below(&Default::default(), window, cx);
6487 });
6488
6489 cx.assert_editor_state(indoc!(
6490 r#"abc
6491 defˇghi
6492
6493 jk
6494 nlmo
6495 "#
6496 ));
6497
6498 cx.update_editor(|editor, window, cx| {
6499 editor.undo_selection(&Default::default(), window, cx);
6500 });
6501
6502 cx.assert_editor_state(indoc!(
6503 r#"abcˇ
6504 defˇghi
6505
6506 jk
6507 nlmo
6508 "#
6509 ));
6510
6511 cx.update_editor(|editor, window, cx| {
6512 editor.redo_selection(&Default::default(), window, cx);
6513 });
6514
6515 cx.assert_editor_state(indoc!(
6516 r#"abc
6517 defˇghi
6518
6519 jk
6520 nlmo
6521 "#
6522 ));
6523
6524 cx.update_editor(|editor, window, cx| {
6525 editor.add_selection_below(&Default::default(), window, cx);
6526 });
6527
6528 cx.assert_editor_state(indoc!(
6529 r#"abc
6530 defˇghi
6531 ˇ
6532 jk
6533 nlmo
6534 "#
6535 ));
6536
6537 cx.update_editor(|editor, window, cx| {
6538 editor.add_selection_below(&Default::default(), window, cx);
6539 });
6540
6541 cx.assert_editor_state(indoc!(
6542 r#"abc
6543 defˇghi
6544 ˇ
6545 jkˇ
6546 nlmo
6547 "#
6548 ));
6549
6550 cx.update_editor(|editor, window, cx| {
6551 editor.add_selection_below(&Default::default(), window, cx);
6552 });
6553
6554 cx.assert_editor_state(indoc!(
6555 r#"abc
6556 defˇghi
6557 ˇ
6558 jkˇ
6559 nlmˇo
6560 "#
6561 ));
6562
6563 cx.update_editor(|editor, window, cx| {
6564 editor.add_selection_below(&Default::default(), window, cx);
6565 });
6566
6567 cx.assert_editor_state(indoc!(
6568 r#"abc
6569 defˇghi
6570 ˇ
6571 jkˇ
6572 nlmˇo
6573 ˇ"#
6574 ));
6575
6576 // change selections
6577 cx.set_state(indoc!(
6578 r#"abc
6579 def«ˇg»hi
6580
6581 jk
6582 nlmo
6583 "#
6584 ));
6585
6586 cx.update_editor(|editor, window, cx| {
6587 editor.add_selection_below(&Default::default(), window, cx);
6588 });
6589
6590 cx.assert_editor_state(indoc!(
6591 r#"abc
6592 def«ˇg»hi
6593
6594 jk
6595 nlm«ˇo»
6596 "#
6597 ));
6598
6599 cx.update_editor(|editor, window, cx| {
6600 editor.add_selection_below(&Default::default(), window, cx);
6601 });
6602
6603 cx.assert_editor_state(indoc!(
6604 r#"abc
6605 def«ˇg»hi
6606
6607 jk
6608 nlm«ˇo»
6609 "#
6610 ));
6611
6612 cx.update_editor(|editor, window, cx| {
6613 editor.add_selection_above(&Default::default(), window, cx);
6614 });
6615
6616 cx.assert_editor_state(indoc!(
6617 r#"abc
6618 def«ˇg»hi
6619
6620 jk
6621 nlmo
6622 "#
6623 ));
6624
6625 cx.update_editor(|editor, window, cx| {
6626 editor.add_selection_above(&Default::default(), window, cx);
6627 });
6628
6629 cx.assert_editor_state(indoc!(
6630 r#"abc
6631 def«ˇg»hi
6632
6633 jk
6634 nlmo
6635 "#
6636 ));
6637
6638 // Change selections again
6639 cx.set_state(indoc!(
6640 r#"a«bc
6641 defgˇ»hi
6642
6643 jk
6644 nlmo
6645 "#
6646 ));
6647
6648 cx.update_editor(|editor, window, cx| {
6649 editor.add_selection_below(&Default::default(), window, cx);
6650 });
6651
6652 cx.assert_editor_state(indoc!(
6653 r#"a«bcˇ»
6654 d«efgˇ»hi
6655
6656 j«kˇ»
6657 nlmo
6658 "#
6659 ));
6660
6661 cx.update_editor(|editor, window, cx| {
6662 editor.add_selection_below(&Default::default(), window, cx);
6663 });
6664 cx.assert_editor_state(indoc!(
6665 r#"a«bcˇ»
6666 d«efgˇ»hi
6667
6668 j«kˇ»
6669 n«lmoˇ»
6670 "#
6671 ));
6672 cx.update_editor(|editor, window, cx| {
6673 editor.add_selection_above(&Default::default(), window, cx);
6674 });
6675
6676 cx.assert_editor_state(indoc!(
6677 r#"a«bcˇ»
6678 d«efgˇ»hi
6679
6680 j«kˇ»
6681 nlmo
6682 "#
6683 ));
6684
6685 // Change selections again
6686 cx.set_state(indoc!(
6687 r#"abc
6688 d«ˇefghi
6689
6690 jk
6691 nlm»o
6692 "#
6693 ));
6694
6695 cx.update_editor(|editor, window, cx| {
6696 editor.add_selection_above(&Default::default(), window, cx);
6697 });
6698
6699 cx.assert_editor_state(indoc!(
6700 r#"a«ˇbc»
6701 d«ˇef»ghi
6702
6703 j«ˇk»
6704 n«ˇlm»o
6705 "#
6706 ));
6707
6708 cx.update_editor(|editor, window, cx| {
6709 editor.add_selection_below(&Default::default(), window, cx);
6710 });
6711
6712 cx.assert_editor_state(indoc!(
6713 r#"abc
6714 d«ˇef»ghi
6715
6716 j«ˇk»
6717 n«ˇlm»o
6718 "#
6719 ));
6720}
6721
6722#[gpui::test]
6723async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
6724 init_test(cx, |_| {});
6725 let mut cx = EditorTestContext::new(cx).await;
6726
6727 cx.set_state(indoc!(
6728 r#"line onˇe
6729 liˇne two
6730 line three
6731 line four"#
6732 ));
6733
6734 cx.update_editor(|editor, window, cx| {
6735 editor.add_selection_below(&Default::default(), window, cx);
6736 });
6737
6738 // test multiple cursors expand in the same direction
6739 cx.assert_editor_state(indoc!(
6740 r#"line onˇe
6741 liˇne twˇo
6742 liˇne three
6743 line four"#
6744 ));
6745
6746 cx.update_editor(|editor, window, cx| {
6747 editor.add_selection_below(&Default::default(), window, cx);
6748 });
6749
6750 cx.update_editor(|editor, window, cx| {
6751 editor.add_selection_below(&Default::default(), window, cx);
6752 });
6753
6754 // test multiple cursors expand below overflow
6755 cx.assert_editor_state(indoc!(
6756 r#"line onˇe
6757 liˇne twˇo
6758 liˇne thˇree
6759 liˇne foˇur"#
6760 ));
6761
6762 cx.update_editor(|editor, window, cx| {
6763 editor.add_selection_above(&Default::default(), window, cx);
6764 });
6765
6766 // test multiple cursors retrieves back correctly
6767 cx.assert_editor_state(indoc!(
6768 r#"line onˇe
6769 liˇne twˇo
6770 liˇne thˇree
6771 line four"#
6772 ));
6773
6774 cx.update_editor(|editor, window, cx| {
6775 editor.add_selection_above(&Default::default(), window, cx);
6776 });
6777
6778 cx.update_editor(|editor, window, cx| {
6779 editor.add_selection_above(&Default::default(), window, cx);
6780 });
6781
6782 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
6783 cx.assert_editor_state(indoc!(
6784 r#"liˇne onˇe
6785 liˇne two
6786 line three
6787 line four"#
6788 ));
6789
6790 cx.update_editor(|editor, window, cx| {
6791 editor.undo_selection(&Default::default(), window, cx);
6792 });
6793
6794 // test undo
6795 cx.assert_editor_state(indoc!(
6796 r#"line onˇe
6797 liˇne twˇo
6798 line three
6799 line four"#
6800 ));
6801
6802 cx.update_editor(|editor, window, cx| {
6803 editor.redo_selection(&Default::default(), window, cx);
6804 });
6805
6806 // test redo
6807 cx.assert_editor_state(indoc!(
6808 r#"liˇne onˇe
6809 liˇne two
6810 line three
6811 line four"#
6812 ));
6813
6814 cx.set_state(indoc!(
6815 r#"abcd
6816 ef«ghˇ»
6817 ijkl
6818 «mˇ»nop"#
6819 ));
6820
6821 cx.update_editor(|editor, window, cx| {
6822 editor.add_selection_above(&Default::default(), window, cx);
6823 });
6824
6825 // test multiple selections expand in the same direction
6826 cx.assert_editor_state(indoc!(
6827 r#"ab«cdˇ»
6828 ef«ghˇ»
6829 «iˇ»jkl
6830 «mˇ»nop"#
6831 ));
6832
6833 cx.update_editor(|editor, window, cx| {
6834 editor.add_selection_above(&Default::default(), window, cx);
6835 });
6836
6837 // test multiple selection upward overflow
6838 cx.assert_editor_state(indoc!(
6839 r#"ab«cdˇ»
6840 «eˇ»f«ghˇ»
6841 «iˇ»jkl
6842 «mˇ»nop"#
6843 ));
6844
6845 cx.update_editor(|editor, window, cx| {
6846 editor.add_selection_below(&Default::default(), window, cx);
6847 });
6848
6849 // test multiple selection retrieves back correctly
6850 cx.assert_editor_state(indoc!(
6851 r#"abcd
6852 ef«ghˇ»
6853 «iˇ»jkl
6854 «mˇ»nop"#
6855 ));
6856
6857 cx.update_editor(|editor, window, cx| {
6858 editor.add_selection_below(&Default::default(), window, cx);
6859 });
6860
6861 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
6862 cx.assert_editor_state(indoc!(
6863 r#"abcd
6864 ef«ghˇ»
6865 ij«klˇ»
6866 «mˇ»nop"#
6867 ));
6868
6869 cx.update_editor(|editor, window, cx| {
6870 editor.undo_selection(&Default::default(), window, cx);
6871 });
6872
6873 // test undo
6874 cx.assert_editor_state(indoc!(
6875 r#"abcd
6876 ef«ghˇ»
6877 «iˇ»jkl
6878 «mˇ»nop"#
6879 ));
6880
6881 cx.update_editor(|editor, window, cx| {
6882 editor.redo_selection(&Default::default(), window, cx);
6883 });
6884
6885 // test redo
6886 cx.assert_editor_state(indoc!(
6887 r#"abcd
6888 ef«ghˇ»
6889 ij«klˇ»
6890 «mˇ»nop"#
6891 ));
6892}
6893
6894#[gpui::test]
6895async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
6896 init_test(cx, |_| {});
6897 let mut cx = EditorTestContext::new(cx).await;
6898
6899 cx.set_state(indoc!(
6900 r#"line onˇe
6901 liˇne two
6902 line three
6903 line four"#
6904 ));
6905
6906 cx.update_editor(|editor, window, cx| {
6907 editor.add_selection_below(&Default::default(), window, cx);
6908 editor.add_selection_below(&Default::default(), window, cx);
6909 editor.add_selection_below(&Default::default(), window, cx);
6910 });
6911
6912 // initial state with two multi cursor groups
6913 cx.assert_editor_state(indoc!(
6914 r#"line onˇe
6915 liˇne twˇo
6916 liˇne thˇree
6917 liˇne foˇur"#
6918 ));
6919
6920 // add single cursor in middle - simulate opt click
6921 cx.update_editor(|editor, window, cx| {
6922 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
6923 editor.begin_selection(new_cursor_point, true, 1, window, cx);
6924 editor.end_selection(window, cx);
6925 });
6926
6927 cx.assert_editor_state(indoc!(
6928 r#"line onˇe
6929 liˇne twˇo
6930 liˇneˇ thˇree
6931 liˇne foˇur"#
6932 ));
6933
6934 cx.update_editor(|editor, window, cx| {
6935 editor.add_selection_above(&Default::default(), window, cx);
6936 });
6937
6938 // test new added selection expands above and existing selection shrinks
6939 cx.assert_editor_state(indoc!(
6940 r#"line onˇe
6941 liˇneˇ twˇo
6942 liˇneˇ thˇree
6943 line four"#
6944 ));
6945
6946 cx.update_editor(|editor, window, cx| {
6947 editor.add_selection_above(&Default::default(), window, cx);
6948 });
6949
6950 // test new added selection expands above and existing selection shrinks
6951 cx.assert_editor_state(indoc!(
6952 r#"lineˇ onˇe
6953 liˇneˇ twˇo
6954 lineˇ three
6955 line four"#
6956 ));
6957
6958 // intial state with two selection groups
6959 cx.set_state(indoc!(
6960 r#"abcd
6961 ef«ghˇ»
6962 ijkl
6963 «mˇ»nop"#
6964 ));
6965
6966 cx.update_editor(|editor, window, cx| {
6967 editor.add_selection_above(&Default::default(), window, cx);
6968 editor.add_selection_above(&Default::default(), window, cx);
6969 });
6970
6971 cx.assert_editor_state(indoc!(
6972 r#"ab«cdˇ»
6973 «eˇ»f«ghˇ»
6974 «iˇ»jkl
6975 «mˇ»nop"#
6976 ));
6977
6978 // add single selection in middle - simulate opt drag
6979 cx.update_editor(|editor, window, cx| {
6980 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
6981 editor.begin_selection(new_cursor_point, true, 1, window, cx);
6982 editor.update_selection(
6983 DisplayPoint::new(DisplayRow(2), 4),
6984 0,
6985 gpui::Point::<f32>::default(),
6986 window,
6987 cx,
6988 );
6989 editor.end_selection(window, cx);
6990 });
6991
6992 cx.assert_editor_state(indoc!(
6993 r#"ab«cdˇ»
6994 «eˇ»f«ghˇ»
6995 «iˇ»jk«lˇ»
6996 «mˇ»nop"#
6997 ));
6998
6999 cx.update_editor(|editor, window, cx| {
7000 editor.add_selection_below(&Default::default(), window, cx);
7001 });
7002
7003 // test new added selection expands below, others shrinks from above
7004 cx.assert_editor_state(indoc!(
7005 r#"abcd
7006 ef«ghˇ»
7007 «iˇ»jk«lˇ»
7008 «mˇ»no«pˇ»"#
7009 ));
7010}
7011
7012#[gpui::test]
7013async fn test_select_next(cx: &mut TestAppContext) {
7014 init_test(cx, |_| {});
7015
7016 let mut cx = EditorTestContext::new(cx).await;
7017 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
7018
7019 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7020 .unwrap();
7021 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7022
7023 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7024 .unwrap();
7025 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
7026
7027 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7028 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7029
7030 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7031 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
7032
7033 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7034 .unwrap();
7035 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7036
7037 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7038 .unwrap();
7039 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7040
7041 // Test selection direction should be preserved
7042 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
7043
7044 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7045 .unwrap();
7046 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
7047}
7048
7049#[gpui::test]
7050async fn test_select_all_matches(cx: &mut TestAppContext) {
7051 init_test(cx, |_| {});
7052
7053 let mut cx = EditorTestContext::new(cx).await;
7054
7055 // Test caret-only selections
7056 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
7057 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7058 .unwrap();
7059 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7060
7061 // Test left-to-right selections
7062 cx.set_state("abc\n«abcˇ»\nabc");
7063 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7064 .unwrap();
7065 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
7066
7067 // Test right-to-left selections
7068 cx.set_state("abc\n«ˇabc»\nabc");
7069 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7070 .unwrap();
7071 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
7072
7073 // Test selecting whitespace with caret selection
7074 cx.set_state("abc\nˇ abc\nabc");
7075 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7076 .unwrap();
7077 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
7078
7079 // Test selecting whitespace with left-to-right selection
7080 cx.set_state("abc\n«ˇ »abc\nabc");
7081 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7082 .unwrap();
7083 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
7084
7085 // Test no matches with right-to-left selection
7086 cx.set_state("abc\n« ˇ»abc\nabc");
7087 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7088 .unwrap();
7089 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
7090
7091 // Test with a single word and clip_at_line_ends=true (#29823)
7092 cx.set_state("aˇbc");
7093 cx.update_editor(|e, window, cx| {
7094 e.set_clip_at_line_ends(true, cx);
7095 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
7096 e.set_clip_at_line_ends(false, cx);
7097 });
7098 cx.assert_editor_state("«abcˇ»");
7099}
7100
7101#[gpui::test]
7102async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
7103 init_test(cx, |_| {});
7104
7105 let mut cx = EditorTestContext::new(cx).await;
7106
7107 let large_body_1 = "\nd".repeat(200);
7108 let large_body_2 = "\ne".repeat(200);
7109
7110 cx.set_state(&format!(
7111 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
7112 ));
7113 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
7114 let scroll_position = editor.scroll_position(cx);
7115 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
7116 scroll_position
7117 });
7118
7119 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7120 .unwrap();
7121 cx.assert_editor_state(&format!(
7122 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
7123 ));
7124 let scroll_position_after_selection =
7125 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
7126 assert_eq!(
7127 initial_scroll_position, scroll_position_after_selection,
7128 "Scroll position should not change after selecting all matches"
7129 );
7130}
7131
7132#[gpui::test]
7133async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
7134 init_test(cx, |_| {});
7135
7136 let mut cx = EditorLspTestContext::new_rust(
7137 lsp::ServerCapabilities {
7138 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7139 ..Default::default()
7140 },
7141 cx,
7142 )
7143 .await;
7144
7145 cx.set_state(indoc! {"
7146 line 1
7147 line 2
7148 linˇe 3
7149 line 4
7150 line 5
7151 "});
7152
7153 // Make an edit
7154 cx.update_editor(|editor, window, cx| {
7155 editor.handle_input("X", window, cx);
7156 });
7157
7158 // Move cursor to a different position
7159 cx.update_editor(|editor, window, cx| {
7160 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7161 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
7162 });
7163 });
7164
7165 cx.assert_editor_state(indoc! {"
7166 line 1
7167 line 2
7168 linXe 3
7169 line 4
7170 liˇne 5
7171 "});
7172
7173 cx.lsp
7174 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
7175 Ok(Some(vec![lsp::TextEdit::new(
7176 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
7177 "PREFIX ".to_string(),
7178 )]))
7179 });
7180
7181 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
7182 .unwrap()
7183 .await
7184 .unwrap();
7185
7186 cx.assert_editor_state(indoc! {"
7187 PREFIX line 1
7188 line 2
7189 linXe 3
7190 line 4
7191 liˇne 5
7192 "});
7193
7194 // Undo formatting
7195 cx.update_editor(|editor, window, cx| {
7196 editor.undo(&Default::default(), window, cx);
7197 });
7198
7199 // Verify cursor moved back to position after edit
7200 cx.assert_editor_state(indoc! {"
7201 line 1
7202 line 2
7203 linXˇe 3
7204 line 4
7205 line 5
7206 "});
7207}
7208
7209#[gpui::test]
7210async fn test_undo_inline_completion_scrolls_to_edit_pos(cx: &mut TestAppContext) {
7211 init_test(cx, |_| {});
7212
7213 let mut cx = EditorTestContext::new(cx).await;
7214
7215 let provider = cx.new(|_| FakeInlineCompletionProvider::default());
7216 cx.update_editor(|editor, window, cx| {
7217 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
7218 });
7219
7220 cx.set_state(indoc! {"
7221 line 1
7222 line 2
7223 linˇe 3
7224 line 4
7225 line 5
7226 line 6
7227 line 7
7228 line 8
7229 line 9
7230 line 10
7231 "});
7232
7233 let snapshot = cx.buffer_snapshot();
7234 let edit_position = snapshot.anchor_after(Point::new(2, 4));
7235
7236 cx.update(|_, cx| {
7237 provider.update(cx, |provider, _| {
7238 provider.set_inline_completion(Some(inline_completion::InlineCompletion {
7239 id: None,
7240 edits: vec![(edit_position..edit_position, "X".into())],
7241 edit_preview: None,
7242 }))
7243 })
7244 });
7245
7246 cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx));
7247 cx.update_editor(|editor, window, cx| {
7248 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
7249 });
7250
7251 cx.assert_editor_state(indoc! {"
7252 line 1
7253 line 2
7254 lineXˇ 3
7255 line 4
7256 line 5
7257 line 6
7258 line 7
7259 line 8
7260 line 9
7261 line 10
7262 "});
7263
7264 cx.update_editor(|editor, window, cx| {
7265 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7266 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
7267 });
7268 });
7269
7270 cx.assert_editor_state(indoc! {"
7271 line 1
7272 line 2
7273 lineX 3
7274 line 4
7275 line 5
7276 line 6
7277 line 7
7278 line 8
7279 line 9
7280 liˇne 10
7281 "});
7282
7283 cx.update_editor(|editor, window, cx| {
7284 editor.undo(&Default::default(), window, cx);
7285 });
7286
7287 cx.assert_editor_state(indoc! {"
7288 line 1
7289 line 2
7290 lineˇ 3
7291 line 4
7292 line 5
7293 line 6
7294 line 7
7295 line 8
7296 line 9
7297 line 10
7298 "});
7299}
7300
7301#[gpui::test]
7302async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
7303 init_test(cx, |_| {});
7304
7305 let mut cx = EditorTestContext::new(cx).await;
7306 cx.set_state(
7307 r#"let foo = 2;
7308lˇet foo = 2;
7309let fooˇ = 2;
7310let foo = 2;
7311let foo = ˇ2;"#,
7312 );
7313
7314 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7315 .unwrap();
7316 cx.assert_editor_state(
7317 r#"let foo = 2;
7318«letˇ» foo = 2;
7319let «fooˇ» = 2;
7320let foo = 2;
7321let foo = «2ˇ»;"#,
7322 );
7323
7324 // noop for multiple selections with different contents
7325 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7326 .unwrap();
7327 cx.assert_editor_state(
7328 r#"let foo = 2;
7329«letˇ» foo = 2;
7330let «fooˇ» = 2;
7331let foo = 2;
7332let foo = «2ˇ»;"#,
7333 );
7334
7335 // Test last selection direction should be preserved
7336 cx.set_state(
7337 r#"let foo = 2;
7338let foo = 2;
7339let «fooˇ» = 2;
7340let «ˇfoo» = 2;
7341let foo = 2;"#,
7342 );
7343
7344 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7345 .unwrap();
7346 cx.assert_editor_state(
7347 r#"let foo = 2;
7348let foo = 2;
7349let «fooˇ» = 2;
7350let «ˇfoo» = 2;
7351let «ˇfoo» = 2;"#,
7352 );
7353}
7354
7355#[gpui::test]
7356async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
7357 init_test(cx, |_| {});
7358
7359 let mut cx =
7360 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
7361
7362 cx.assert_editor_state(indoc! {"
7363 ˇbbb
7364 ccc
7365
7366 bbb
7367 ccc
7368 "});
7369 cx.dispatch_action(SelectPrevious::default());
7370 cx.assert_editor_state(indoc! {"
7371 «bbbˇ»
7372 ccc
7373
7374 bbb
7375 ccc
7376 "});
7377 cx.dispatch_action(SelectPrevious::default());
7378 cx.assert_editor_state(indoc! {"
7379 «bbbˇ»
7380 ccc
7381
7382 «bbbˇ»
7383 ccc
7384 "});
7385}
7386
7387#[gpui::test]
7388async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
7389 init_test(cx, |_| {});
7390
7391 let mut cx = EditorTestContext::new(cx).await;
7392 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
7393
7394 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7395 .unwrap();
7396 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7397
7398 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7399 .unwrap();
7400 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
7401
7402 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7403 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7404
7405 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7406 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
7407
7408 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7409 .unwrap();
7410 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
7411
7412 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7413 .unwrap();
7414 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7415}
7416
7417#[gpui::test]
7418async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
7419 init_test(cx, |_| {});
7420
7421 let mut cx = EditorTestContext::new(cx).await;
7422 cx.set_state("aˇ");
7423
7424 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7425 .unwrap();
7426 cx.assert_editor_state("«aˇ»");
7427 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7428 .unwrap();
7429 cx.assert_editor_state("«aˇ»");
7430}
7431
7432#[gpui::test]
7433async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
7434 init_test(cx, |_| {});
7435
7436 let mut cx = EditorTestContext::new(cx).await;
7437 cx.set_state(
7438 r#"let foo = 2;
7439lˇet foo = 2;
7440let fooˇ = 2;
7441let foo = 2;
7442let foo = ˇ2;"#,
7443 );
7444
7445 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7446 .unwrap();
7447 cx.assert_editor_state(
7448 r#"let foo = 2;
7449«letˇ» foo = 2;
7450let «fooˇ» = 2;
7451let foo = 2;
7452let foo = «2ˇ»;"#,
7453 );
7454
7455 // noop for multiple selections with different contents
7456 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7457 .unwrap();
7458 cx.assert_editor_state(
7459 r#"let foo = 2;
7460«letˇ» foo = 2;
7461let «fooˇ» = 2;
7462let foo = 2;
7463let foo = «2ˇ»;"#,
7464 );
7465}
7466
7467#[gpui::test]
7468async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
7469 init_test(cx, |_| {});
7470
7471 let mut cx = EditorTestContext::new(cx).await;
7472 cx.set_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 // selection direction is preserved
7477 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
7478
7479 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7480 .unwrap();
7481 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
7482
7483 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7484 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
7485
7486 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7487 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
7488
7489 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7490 .unwrap();
7491 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
7492
7493 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7494 .unwrap();
7495 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
7496}
7497
7498#[gpui::test]
7499async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
7500 init_test(cx, |_| {});
7501
7502 let language = Arc::new(Language::new(
7503 LanguageConfig::default(),
7504 Some(tree_sitter_rust::LANGUAGE.into()),
7505 ));
7506
7507 let text = r#"
7508 use mod1::mod2::{mod3, mod4};
7509
7510 fn fn_1(param1: bool, param2: &str) {
7511 let var1 = "text";
7512 }
7513 "#
7514 .unindent();
7515
7516 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7517 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7518 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7519
7520 editor
7521 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7522 .await;
7523
7524 editor.update_in(cx, |editor, window, cx| {
7525 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7526 s.select_display_ranges([
7527 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
7528 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
7529 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
7530 ]);
7531 });
7532 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7533 });
7534 editor.update(cx, |editor, cx| {
7535 assert_text_with_selections(
7536 editor,
7537 indoc! {r#"
7538 use mod1::mod2::{mod3, «mod4ˇ»};
7539
7540 fn fn_1«ˇ(param1: bool, param2: &str)» {
7541 let var1 = "«ˇtext»";
7542 }
7543 "#},
7544 cx,
7545 );
7546 });
7547
7548 editor.update_in(cx, |editor, window, cx| {
7549 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7550 });
7551 editor.update(cx, |editor, cx| {
7552 assert_text_with_selections(
7553 editor,
7554 indoc! {r#"
7555 use mod1::mod2::«{mod3, mod4}ˇ»;
7556
7557 «ˇfn fn_1(param1: bool, param2: &str) {
7558 let var1 = "text";
7559 }»
7560 "#},
7561 cx,
7562 );
7563 });
7564
7565 editor.update_in(cx, |editor, window, cx| {
7566 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7567 });
7568 assert_eq!(
7569 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
7570 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
7571 );
7572
7573 // Trying to expand the selected syntax node one more time has no effect.
7574 editor.update_in(cx, |editor, window, cx| {
7575 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7576 });
7577 assert_eq!(
7578 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
7579 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
7580 );
7581
7582 editor.update_in(cx, |editor, window, cx| {
7583 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7584 });
7585 editor.update(cx, |editor, cx| {
7586 assert_text_with_selections(
7587 editor,
7588 indoc! {r#"
7589 use mod1::mod2::«{mod3, mod4}ˇ»;
7590
7591 «ˇfn fn_1(param1: bool, param2: &str) {
7592 let var1 = "text";
7593 }»
7594 "#},
7595 cx,
7596 );
7597 });
7598
7599 editor.update_in(cx, |editor, window, cx| {
7600 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7601 });
7602 editor.update(cx, |editor, cx| {
7603 assert_text_with_selections(
7604 editor,
7605 indoc! {r#"
7606 use mod1::mod2::{mod3, «mod4ˇ»};
7607
7608 fn fn_1«ˇ(param1: bool, param2: &str)» {
7609 let var1 = "«ˇtext»";
7610 }
7611 "#},
7612 cx,
7613 );
7614 });
7615
7616 editor.update_in(cx, |editor, window, cx| {
7617 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7618 });
7619 editor.update(cx, |editor, cx| {
7620 assert_text_with_selections(
7621 editor,
7622 indoc! {r#"
7623 use mod1::mod2::{mod3, mo«ˇ»d4};
7624
7625 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
7626 let var1 = "te«ˇ»xt";
7627 }
7628 "#},
7629 cx,
7630 );
7631 });
7632
7633 // Trying to shrink the selected syntax node one more time has no effect.
7634 editor.update_in(cx, |editor, window, cx| {
7635 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7636 });
7637 editor.update_in(cx, |editor, _, cx| {
7638 assert_text_with_selections(
7639 editor,
7640 indoc! {r#"
7641 use mod1::mod2::{mod3, mo«ˇ»d4};
7642
7643 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
7644 let var1 = "te«ˇ»xt";
7645 }
7646 "#},
7647 cx,
7648 );
7649 });
7650
7651 // Ensure that we keep expanding the selection if the larger selection starts or ends within
7652 // a fold.
7653 editor.update_in(cx, |editor, window, cx| {
7654 editor.fold_creases(
7655 vec![
7656 Crease::simple(
7657 Point::new(0, 21)..Point::new(0, 24),
7658 FoldPlaceholder::test(),
7659 ),
7660 Crease::simple(
7661 Point::new(3, 20)..Point::new(3, 22),
7662 FoldPlaceholder::test(),
7663 ),
7664 ],
7665 true,
7666 window,
7667 cx,
7668 );
7669 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7670 });
7671 editor.update(cx, |editor, cx| {
7672 assert_text_with_selections(
7673 editor,
7674 indoc! {r#"
7675 use mod1::mod2::«{mod3, mod4}ˇ»;
7676
7677 fn fn_1«ˇ(param1: bool, param2: &str)» {
7678 let var1 = "«ˇtext»";
7679 }
7680 "#},
7681 cx,
7682 );
7683 });
7684}
7685
7686#[gpui::test]
7687async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
7688 init_test(cx, |_| {});
7689
7690 let language = Arc::new(Language::new(
7691 LanguageConfig::default(),
7692 Some(tree_sitter_rust::LANGUAGE.into()),
7693 ));
7694
7695 let text = "let a = 2;";
7696
7697 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7698 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7699 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7700
7701 editor
7702 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7703 .await;
7704
7705 // Test case 1: Cursor at end of word
7706 editor.update_in(cx, |editor, window, cx| {
7707 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7708 s.select_display_ranges([
7709 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
7710 ]);
7711 });
7712 });
7713 editor.update(cx, |editor, cx| {
7714 assert_text_with_selections(editor, "let aˇ = 2;", cx);
7715 });
7716 editor.update_in(cx, |editor, window, cx| {
7717 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7718 });
7719 editor.update(cx, |editor, cx| {
7720 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
7721 });
7722 editor.update_in(cx, |editor, window, cx| {
7723 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7724 });
7725 editor.update(cx, |editor, cx| {
7726 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
7727 });
7728
7729 // Test case 2: Cursor at end of statement
7730 editor.update_in(cx, |editor, window, cx| {
7731 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7732 s.select_display_ranges([
7733 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
7734 ]);
7735 });
7736 });
7737 editor.update(cx, |editor, cx| {
7738 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
7739 });
7740 editor.update_in(cx, |editor, window, cx| {
7741 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7742 });
7743 editor.update(cx, |editor, cx| {
7744 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
7745 });
7746}
7747
7748#[gpui::test]
7749async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
7750 init_test(cx, |_| {});
7751
7752 let language = Arc::new(Language::new(
7753 LanguageConfig::default(),
7754 Some(tree_sitter_rust::LANGUAGE.into()),
7755 ));
7756
7757 let text = r#"
7758 use mod1::mod2::{mod3, mod4};
7759
7760 fn fn_1(param1: bool, param2: &str) {
7761 let var1 = "hello world";
7762 }
7763 "#
7764 .unindent();
7765
7766 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7767 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7768 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7769
7770 editor
7771 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7772 .await;
7773
7774 // Test 1: Cursor on a letter of a string word
7775 editor.update_in(cx, |editor, window, cx| {
7776 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7777 s.select_display_ranges([
7778 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
7779 ]);
7780 });
7781 });
7782 editor.update_in(cx, |editor, window, cx| {
7783 assert_text_with_selections(
7784 editor,
7785 indoc! {r#"
7786 use mod1::mod2::{mod3, mod4};
7787
7788 fn fn_1(param1: bool, param2: &str) {
7789 let var1 = "hˇello world";
7790 }
7791 "#},
7792 cx,
7793 );
7794 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7795 assert_text_with_selections(
7796 editor,
7797 indoc! {r#"
7798 use mod1::mod2::{mod3, mod4};
7799
7800 fn fn_1(param1: bool, param2: &str) {
7801 let var1 = "«ˇhello» world";
7802 }
7803 "#},
7804 cx,
7805 );
7806 });
7807
7808 // Test 2: Partial selection within a word
7809 editor.update_in(cx, |editor, window, cx| {
7810 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7811 s.select_display_ranges([
7812 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
7813 ]);
7814 });
7815 });
7816 editor.update_in(cx, |editor, window, cx| {
7817 assert_text_with_selections(
7818 editor,
7819 indoc! {r#"
7820 use mod1::mod2::{mod3, mod4};
7821
7822 fn fn_1(param1: bool, param2: &str) {
7823 let var1 = "h«elˇ»lo world";
7824 }
7825 "#},
7826 cx,
7827 );
7828 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7829 assert_text_with_selections(
7830 editor,
7831 indoc! {r#"
7832 use mod1::mod2::{mod3, mod4};
7833
7834 fn fn_1(param1: bool, param2: &str) {
7835 let var1 = "«ˇhello» world";
7836 }
7837 "#},
7838 cx,
7839 );
7840 });
7841
7842 // Test 3: Complete word already selected
7843 editor.update_in(cx, |editor, window, cx| {
7844 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7845 s.select_display_ranges([
7846 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
7847 ]);
7848 });
7849 });
7850 editor.update_in(cx, |editor, window, cx| {
7851 assert_text_with_selections(
7852 editor,
7853 indoc! {r#"
7854 use mod1::mod2::{mod3, mod4};
7855
7856 fn fn_1(param1: bool, param2: &str) {
7857 let var1 = "«helloˇ» world";
7858 }
7859 "#},
7860 cx,
7861 );
7862 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7863 assert_text_with_selections(
7864 editor,
7865 indoc! {r#"
7866 use mod1::mod2::{mod3, mod4};
7867
7868 fn fn_1(param1: bool, param2: &str) {
7869 let var1 = "«hello worldˇ»";
7870 }
7871 "#},
7872 cx,
7873 );
7874 });
7875
7876 // Test 4: Selection spanning across words
7877 editor.update_in(cx, |editor, window, cx| {
7878 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7879 s.select_display_ranges([
7880 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
7881 ]);
7882 });
7883 });
7884 editor.update_in(cx, |editor, window, cx| {
7885 assert_text_with_selections(
7886 editor,
7887 indoc! {r#"
7888 use mod1::mod2::{mod3, mod4};
7889
7890 fn fn_1(param1: bool, param2: &str) {
7891 let var1 = "hel«lo woˇ»rld";
7892 }
7893 "#},
7894 cx,
7895 );
7896 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7897 assert_text_with_selections(
7898 editor,
7899 indoc! {r#"
7900 use mod1::mod2::{mod3, mod4};
7901
7902 fn fn_1(param1: bool, param2: &str) {
7903 let var1 = "«ˇhello world»";
7904 }
7905 "#},
7906 cx,
7907 );
7908 });
7909
7910 // Test 5: Expansion beyond string
7911 editor.update_in(cx, |editor, window, cx| {
7912 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7913 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7914 assert_text_with_selections(
7915 editor,
7916 indoc! {r#"
7917 use mod1::mod2::{mod3, mod4};
7918
7919 fn fn_1(param1: bool, param2: &str) {
7920 «ˇlet var1 = "hello world";»
7921 }
7922 "#},
7923 cx,
7924 );
7925 });
7926}
7927
7928#[gpui::test]
7929async fn test_fold_function_bodies(cx: &mut TestAppContext) {
7930 init_test(cx, |_| {});
7931
7932 let base_text = r#"
7933 impl A {
7934 // this is an uncommitted comment
7935
7936 fn b() {
7937 c();
7938 }
7939
7940 // this is another uncommitted comment
7941
7942 fn d() {
7943 // e
7944 // f
7945 }
7946 }
7947
7948 fn g() {
7949 // h
7950 }
7951 "#
7952 .unindent();
7953
7954 let text = r#"
7955 ˇimpl A {
7956
7957 fn b() {
7958 c();
7959 }
7960
7961 fn d() {
7962 // e
7963 // f
7964 }
7965 }
7966
7967 fn g() {
7968 // h
7969 }
7970 "#
7971 .unindent();
7972
7973 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
7974 cx.set_state(&text);
7975 cx.set_head_text(&base_text);
7976 cx.update_editor(|editor, window, cx| {
7977 editor.expand_all_diff_hunks(&Default::default(), window, cx);
7978 });
7979
7980 cx.assert_state_with_diff(
7981 "
7982 ˇimpl A {
7983 - // this is an uncommitted comment
7984
7985 fn b() {
7986 c();
7987 }
7988
7989 - // this is another uncommitted comment
7990 -
7991 fn d() {
7992 // e
7993 // f
7994 }
7995 }
7996
7997 fn g() {
7998 // h
7999 }
8000 "
8001 .unindent(),
8002 );
8003
8004 let expected_display_text = "
8005 impl A {
8006 // this is an uncommitted comment
8007
8008 fn b() {
8009 ⋯
8010 }
8011
8012 // this is another uncommitted comment
8013
8014 fn d() {
8015 ⋯
8016 }
8017 }
8018
8019 fn g() {
8020 ⋯
8021 }
8022 "
8023 .unindent();
8024
8025 cx.update_editor(|editor, window, cx| {
8026 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
8027 assert_eq!(editor.display_text(cx), expected_display_text);
8028 });
8029}
8030
8031#[gpui::test]
8032async fn test_autoindent(cx: &mut TestAppContext) {
8033 init_test(cx, |_| {});
8034
8035 let language = Arc::new(
8036 Language::new(
8037 LanguageConfig {
8038 brackets: BracketPairConfig {
8039 pairs: vec![
8040 BracketPair {
8041 start: "{".to_string(),
8042 end: "}".to_string(),
8043 close: false,
8044 surround: false,
8045 newline: true,
8046 },
8047 BracketPair {
8048 start: "(".to_string(),
8049 end: ")".to_string(),
8050 close: false,
8051 surround: false,
8052 newline: true,
8053 },
8054 ],
8055 ..Default::default()
8056 },
8057 ..Default::default()
8058 },
8059 Some(tree_sitter_rust::LANGUAGE.into()),
8060 )
8061 .with_indents_query(
8062 r#"
8063 (_ "(" ")" @end) @indent
8064 (_ "{" "}" @end) @indent
8065 "#,
8066 )
8067 .unwrap(),
8068 );
8069
8070 let text = "fn a() {}";
8071
8072 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8073 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8074 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8075 editor
8076 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8077 .await;
8078
8079 editor.update_in(cx, |editor, window, cx| {
8080 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8081 s.select_ranges([5..5, 8..8, 9..9])
8082 });
8083 editor.newline(&Newline, window, cx);
8084 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
8085 assert_eq!(
8086 editor.selections.ranges(cx),
8087 &[
8088 Point::new(1, 4)..Point::new(1, 4),
8089 Point::new(3, 4)..Point::new(3, 4),
8090 Point::new(5, 0)..Point::new(5, 0)
8091 ]
8092 );
8093 });
8094}
8095
8096#[gpui::test]
8097async fn test_autoindent_selections(cx: &mut TestAppContext) {
8098 init_test(cx, |_| {});
8099
8100 {
8101 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
8102 cx.set_state(indoc! {"
8103 impl A {
8104
8105 fn b() {}
8106
8107 «fn c() {
8108
8109 }ˇ»
8110 }
8111 "});
8112
8113 cx.update_editor(|editor, window, cx| {
8114 editor.autoindent(&Default::default(), window, cx);
8115 });
8116
8117 cx.assert_editor_state(indoc! {"
8118 impl A {
8119
8120 fn b() {}
8121
8122 «fn c() {
8123
8124 }ˇ»
8125 }
8126 "});
8127 }
8128
8129 {
8130 let mut cx = EditorTestContext::new_multibuffer(
8131 cx,
8132 [indoc! { "
8133 impl A {
8134 «
8135 // a
8136 fn b(){}
8137 »
8138 «
8139 }
8140 fn c(){}
8141 »
8142 "}],
8143 );
8144
8145 let buffer = cx.update_editor(|editor, _, cx| {
8146 let buffer = editor.buffer().update(cx, |buffer, _| {
8147 buffer.all_buffers().iter().next().unwrap().clone()
8148 });
8149 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
8150 buffer
8151 });
8152
8153 cx.run_until_parked();
8154 cx.update_editor(|editor, window, cx| {
8155 editor.select_all(&Default::default(), window, cx);
8156 editor.autoindent(&Default::default(), window, cx)
8157 });
8158 cx.run_until_parked();
8159
8160 cx.update(|_, cx| {
8161 assert_eq!(
8162 buffer.read(cx).text(),
8163 indoc! { "
8164 impl A {
8165
8166 // a
8167 fn b(){}
8168
8169
8170 }
8171 fn c(){}
8172
8173 " }
8174 )
8175 });
8176 }
8177}
8178
8179#[gpui::test]
8180async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
8181 init_test(cx, |_| {});
8182
8183 let mut cx = EditorTestContext::new(cx).await;
8184
8185 let language = Arc::new(Language::new(
8186 LanguageConfig {
8187 brackets: BracketPairConfig {
8188 pairs: vec![
8189 BracketPair {
8190 start: "{".to_string(),
8191 end: "}".to_string(),
8192 close: true,
8193 surround: true,
8194 newline: true,
8195 },
8196 BracketPair {
8197 start: "(".to_string(),
8198 end: ")".to_string(),
8199 close: true,
8200 surround: true,
8201 newline: true,
8202 },
8203 BracketPair {
8204 start: "/*".to_string(),
8205 end: " */".to_string(),
8206 close: true,
8207 surround: true,
8208 newline: true,
8209 },
8210 BracketPair {
8211 start: "[".to_string(),
8212 end: "]".to_string(),
8213 close: false,
8214 surround: false,
8215 newline: true,
8216 },
8217 BracketPair {
8218 start: "\"".to_string(),
8219 end: "\"".to_string(),
8220 close: true,
8221 surround: true,
8222 newline: false,
8223 },
8224 BracketPair {
8225 start: "<".to_string(),
8226 end: ">".to_string(),
8227 close: false,
8228 surround: true,
8229 newline: true,
8230 },
8231 ],
8232 ..Default::default()
8233 },
8234 autoclose_before: "})]".to_string(),
8235 ..Default::default()
8236 },
8237 Some(tree_sitter_rust::LANGUAGE.into()),
8238 ));
8239
8240 cx.language_registry().add(language.clone());
8241 cx.update_buffer(|buffer, cx| {
8242 buffer.set_language(Some(language), cx);
8243 });
8244
8245 cx.set_state(
8246 &r#"
8247 🏀ˇ
8248 εˇ
8249 ❤️ˇ
8250 "#
8251 .unindent(),
8252 );
8253
8254 // autoclose multiple nested brackets at multiple cursors
8255 cx.update_editor(|editor, window, cx| {
8256 editor.handle_input("{", window, cx);
8257 editor.handle_input("{", window, cx);
8258 editor.handle_input("{", window, cx);
8259 });
8260 cx.assert_editor_state(
8261 &"
8262 🏀{{{ˇ}}}
8263 ε{{{ˇ}}}
8264 ❤️{{{ˇ}}}
8265 "
8266 .unindent(),
8267 );
8268
8269 // insert a different closing bracket
8270 cx.update_editor(|editor, window, cx| {
8271 editor.handle_input(")", window, cx);
8272 });
8273 cx.assert_editor_state(
8274 &"
8275 🏀{{{)ˇ}}}
8276 ε{{{)ˇ}}}
8277 ❤️{{{)ˇ}}}
8278 "
8279 .unindent(),
8280 );
8281
8282 // skip over the auto-closed brackets when typing a closing bracket
8283 cx.update_editor(|editor, window, cx| {
8284 editor.move_right(&MoveRight, window, cx);
8285 editor.handle_input("}", window, cx);
8286 editor.handle_input("}", window, cx);
8287 editor.handle_input("}", window, cx);
8288 });
8289 cx.assert_editor_state(
8290 &"
8291 🏀{{{)}}}}ˇ
8292 ε{{{)}}}}ˇ
8293 ❤️{{{)}}}}ˇ
8294 "
8295 .unindent(),
8296 );
8297
8298 // autoclose multi-character pairs
8299 cx.set_state(
8300 &"
8301 ˇ
8302 ˇ
8303 "
8304 .unindent(),
8305 );
8306 cx.update_editor(|editor, window, cx| {
8307 editor.handle_input("/", window, cx);
8308 editor.handle_input("*", window, cx);
8309 });
8310 cx.assert_editor_state(
8311 &"
8312 /*ˇ */
8313 /*ˇ */
8314 "
8315 .unindent(),
8316 );
8317
8318 // one cursor autocloses a multi-character pair, one cursor
8319 // does not autoclose.
8320 cx.set_state(
8321 &"
8322 /ˇ
8323 ˇ
8324 "
8325 .unindent(),
8326 );
8327 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
8328 cx.assert_editor_state(
8329 &"
8330 /*ˇ */
8331 *ˇ
8332 "
8333 .unindent(),
8334 );
8335
8336 // Don't autoclose if the next character isn't whitespace and isn't
8337 // listed in the language's "autoclose_before" section.
8338 cx.set_state("ˇa b");
8339 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8340 cx.assert_editor_state("{ˇa b");
8341
8342 // Don't autoclose if `close` is false for the bracket pair
8343 cx.set_state("ˇ");
8344 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
8345 cx.assert_editor_state("[ˇ");
8346
8347 // Surround with brackets if text is selected
8348 cx.set_state("«aˇ» b");
8349 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8350 cx.assert_editor_state("{«aˇ»} b");
8351
8352 // Autoclose when not immediately after a word character
8353 cx.set_state("a ˇ");
8354 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8355 cx.assert_editor_state("a \"ˇ\"");
8356
8357 // Autoclose pair where the start and end characters are the same
8358 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8359 cx.assert_editor_state("a \"\"ˇ");
8360
8361 // Don't autoclose when immediately after a word character
8362 cx.set_state("aˇ");
8363 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8364 cx.assert_editor_state("a\"ˇ");
8365
8366 // Do autoclose when after a non-word character
8367 cx.set_state("{ˇ");
8368 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8369 cx.assert_editor_state("{\"ˇ\"");
8370
8371 // Non identical pairs autoclose regardless of preceding character
8372 cx.set_state("aˇ");
8373 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8374 cx.assert_editor_state("a{ˇ}");
8375
8376 // Don't autoclose pair if autoclose is disabled
8377 cx.set_state("ˇ");
8378 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
8379 cx.assert_editor_state("<ˇ");
8380
8381 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
8382 cx.set_state("«aˇ» b");
8383 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
8384 cx.assert_editor_state("<«aˇ»> b");
8385}
8386
8387#[gpui::test]
8388async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
8389 init_test(cx, |settings| {
8390 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
8391 });
8392
8393 let mut cx = EditorTestContext::new(cx).await;
8394
8395 let language = Arc::new(Language::new(
8396 LanguageConfig {
8397 brackets: BracketPairConfig {
8398 pairs: vec![
8399 BracketPair {
8400 start: "{".to_string(),
8401 end: "}".to_string(),
8402 close: true,
8403 surround: true,
8404 newline: true,
8405 },
8406 BracketPair {
8407 start: "(".to_string(),
8408 end: ")".to_string(),
8409 close: true,
8410 surround: true,
8411 newline: true,
8412 },
8413 BracketPair {
8414 start: "[".to_string(),
8415 end: "]".to_string(),
8416 close: false,
8417 surround: false,
8418 newline: true,
8419 },
8420 ],
8421 ..Default::default()
8422 },
8423 autoclose_before: "})]".to_string(),
8424 ..Default::default()
8425 },
8426 Some(tree_sitter_rust::LANGUAGE.into()),
8427 ));
8428
8429 cx.language_registry().add(language.clone());
8430 cx.update_buffer(|buffer, cx| {
8431 buffer.set_language(Some(language), cx);
8432 });
8433
8434 cx.set_state(
8435 &"
8436 ˇ
8437 ˇ
8438 ˇ
8439 "
8440 .unindent(),
8441 );
8442
8443 // ensure only matching closing brackets are skipped over
8444 cx.update_editor(|editor, window, cx| {
8445 editor.handle_input("}", window, cx);
8446 editor.move_left(&MoveLeft, window, cx);
8447 editor.handle_input(")", window, cx);
8448 editor.move_left(&MoveLeft, window, cx);
8449 });
8450 cx.assert_editor_state(
8451 &"
8452 ˇ)}
8453 ˇ)}
8454 ˇ)}
8455 "
8456 .unindent(),
8457 );
8458
8459 // skip-over closing brackets at multiple cursors
8460 cx.update_editor(|editor, window, cx| {
8461 editor.handle_input(")", window, cx);
8462 editor.handle_input("}", window, cx);
8463 });
8464 cx.assert_editor_state(
8465 &"
8466 )}ˇ
8467 )}ˇ
8468 )}ˇ
8469 "
8470 .unindent(),
8471 );
8472
8473 // ignore non-close brackets
8474 cx.update_editor(|editor, window, cx| {
8475 editor.handle_input("]", window, cx);
8476 editor.move_left(&MoveLeft, window, cx);
8477 editor.handle_input("]", window, cx);
8478 });
8479 cx.assert_editor_state(
8480 &"
8481 )}]ˇ]
8482 )}]ˇ]
8483 )}]ˇ]
8484 "
8485 .unindent(),
8486 );
8487}
8488
8489#[gpui::test]
8490async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
8491 init_test(cx, |_| {});
8492
8493 let mut cx = EditorTestContext::new(cx).await;
8494
8495 let html_language = Arc::new(
8496 Language::new(
8497 LanguageConfig {
8498 name: "HTML".into(),
8499 brackets: BracketPairConfig {
8500 pairs: vec![
8501 BracketPair {
8502 start: "<".into(),
8503 end: ">".into(),
8504 close: true,
8505 ..Default::default()
8506 },
8507 BracketPair {
8508 start: "{".into(),
8509 end: "}".into(),
8510 close: true,
8511 ..Default::default()
8512 },
8513 BracketPair {
8514 start: "(".into(),
8515 end: ")".into(),
8516 close: true,
8517 ..Default::default()
8518 },
8519 ],
8520 ..Default::default()
8521 },
8522 autoclose_before: "})]>".into(),
8523 ..Default::default()
8524 },
8525 Some(tree_sitter_html::LANGUAGE.into()),
8526 )
8527 .with_injection_query(
8528 r#"
8529 (script_element
8530 (raw_text) @injection.content
8531 (#set! injection.language "javascript"))
8532 "#,
8533 )
8534 .unwrap(),
8535 );
8536
8537 let javascript_language = Arc::new(Language::new(
8538 LanguageConfig {
8539 name: "JavaScript".into(),
8540 brackets: BracketPairConfig {
8541 pairs: vec![
8542 BracketPair {
8543 start: "/*".into(),
8544 end: " */".into(),
8545 close: true,
8546 ..Default::default()
8547 },
8548 BracketPair {
8549 start: "{".into(),
8550 end: "}".into(),
8551 close: true,
8552 ..Default::default()
8553 },
8554 BracketPair {
8555 start: "(".into(),
8556 end: ")".into(),
8557 close: true,
8558 ..Default::default()
8559 },
8560 ],
8561 ..Default::default()
8562 },
8563 autoclose_before: "})]>".into(),
8564 ..Default::default()
8565 },
8566 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
8567 ));
8568
8569 cx.language_registry().add(html_language.clone());
8570 cx.language_registry().add(javascript_language.clone());
8571
8572 cx.update_buffer(|buffer, cx| {
8573 buffer.set_language(Some(html_language), cx);
8574 });
8575
8576 cx.set_state(
8577 &r#"
8578 <body>ˇ
8579 <script>
8580 var x = 1;ˇ
8581 </script>
8582 </body>ˇ
8583 "#
8584 .unindent(),
8585 );
8586
8587 // Precondition: different languages are active at different locations.
8588 cx.update_editor(|editor, window, cx| {
8589 let snapshot = editor.snapshot(window, cx);
8590 let cursors = editor.selections.ranges::<usize>(cx);
8591 let languages = cursors
8592 .iter()
8593 .map(|c| snapshot.language_at(c.start).unwrap().name())
8594 .collect::<Vec<_>>();
8595 assert_eq!(
8596 languages,
8597 &["HTML".into(), "JavaScript".into(), "HTML".into()]
8598 );
8599 });
8600
8601 // Angle brackets autoclose in HTML, but not JavaScript.
8602 cx.update_editor(|editor, window, cx| {
8603 editor.handle_input("<", window, cx);
8604 editor.handle_input("a", window, cx);
8605 });
8606 cx.assert_editor_state(
8607 &r#"
8608 <body><aˇ>
8609 <script>
8610 var x = 1;<aˇ
8611 </script>
8612 </body><aˇ>
8613 "#
8614 .unindent(),
8615 );
8616
8617 // Curly braces and parens autoclose in both HTML and JavaScript.
8618 cx.update_editor(|editor, window, cx| {
8619 editor.handle_input(" b=", window, cx);
8620 editor.handle_input("{", window, cx);
8621 editor.handle_input("c", window, cx);
8622 editor.handle_input("(", window, cx);
8623 });
8624 cx.assert_editor_state(
8625 &r#"
8626 <body><a b={c(ˇ)}>
8627 <script>
8628 var x = 1;<a b={c(ˇ)}
8629 </script>
8630 </body><a b={c(ˇ)}>
8631 "#
8632 .unindent(),
8633 );
8634
8635 // Brackets that were already autoclosed are skipped.
8636 cx.update_editor(|editor, window, cx| {
8637 editor.handle_input(")", window, cx);
8638 editor.handle_input("d", window, cx);
8639 editor.handle_input("}", window, cx);
8640 });
8641 cx.assert_editor_state(
8642 &r#"
8643 <body><a b={c()d}ˇ>
8644 <script>
8645 var x = 1;<a b={c()d}ˇ
8646 </script>
8647 </body><a b={c()d}ˇ>
8648 "#
8649 .unindent(),
8650 );
8651 cx.update_editor(|editor, window, cx| {
8652 editor.handle_input(">", window, cx);
8653 });
8654 cx.assert_editor_state(
8655 &r#"
8656 <body><a b={c()d}>ˇ
8657 <script>
8658 var x = 1;<a b={c()d}>ˇ
8659 </script>
8660 </body><a b={c()d}>ˇ
8661 "#
8662 .unindent(),
8663 );
8664
8665 // Reset
8666 cx.set_state(
8667 &r#"
8668 <body>ˇ
8669 <script>
8670 var x = 1;ˇ
8671 </script>
8672 </body>ˇ
8673 "#
8674 .unindent(),
8675 );
8676
8677 cx.update_editor(|editor, window, cx| {
8678 editor.handle_input("<", window, cx);
8679 });
8680 cx.assert_editor_state(
8681 &r#"
8682 <body><ˇ>
8683 <script>
8684 var x = 1;<ˇ
8685 </script>
8686 </body><ˇ>
8687 "#
8688 .unindent(),
8689 );
8690
8691 // When backspacing, the closing angle brackets are removed.
8692 cx.update_editor(|editor, window, cx| {
8693 editor.backspace(&Backspace, window, cx);
8694 });
8695 cx.assert_editor_state(
8696 &r#"
8697 <body>ˇ
8698 <script>
8699 var x = 1;ˇ
8700 </script>
8701 </body>ˇ
8702 "#
8703 .unindent(),
8704 );
8705
8706 // Block comments autoclose in JavaScript, but not HTML.
8707 cx.update_editor(|editor, window, cx| {
8708 editor.handle_input("/", window, cx);
8709 editor.handle_input("*", window, cx);
8710 });
8711 cx.assert_editor_state(
8712 &r#"
8713 <body>/*ˇ
8714 <script>
8715 var x = 1;/*ˇ */
8716 </script>
8717 </body>/*ˇ
8718 "#
8719 .unindent(),
8720 );
8721}
8722
8723#[gpui::test]
8724async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
8725 init_test(cx, |_| {});
8726
8727 let mut cx = EditorTestContext::new(cx).await;
8728
8729 let rust_language = Arc::new(
8730 Language::new(
8731 LanguageConfig {
8732 name: "Rust".into(),
8733 brackets: serde_json::from_value(json!([
8734 { "start": "{", "end": "}", "close": true, "newline": true },
8735 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
8736 ]))
8737 .unwrap(),
8738 autoclose_before: "})]>".into(),
8739 ..Default::default()
8740 },
8741 Some(tree_sitter_rust::LANGUAGE.into()),
8742 )
8743 .with_override_query("(string_literal) @string")
8744 .unwrap(),
8745 );
8746
8747 cx.language_registry().add(rust_language.clone());
8748 cx.update_buffer(|buffer, cx| {
8749 buffer.set_language(Some(rust_language), cx);
8750 });
8751
8752 cx.set_state(
8753 &r#"
8754 let x = ˇ
8755 "#
8756 .unindent(),
8757 );
8758
8759 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
8760 cx.update_editor(|editor, window, cx| {
8761 editor.handle_input("\"", window, cx);
8762 });
8763 cx.assert_editor_state(
8764 &r#"
8765 let x = "ˇ"
8766 "#
8767 .unindent(),
8768 );
8769
8770 // Inserting another quotation mark. The cursor moves across the existing
8771 // automatically-inserted quotation mark.
8772 cx.update_editor(|editor, window, cx| {
8773 editor.handle_input("\"", window, cx);
8774 });
8775 cx.assert_editor_state(
8776 &r#"
8777 let x = ""ˇ
8778 "#
8779 .unindent(),
8780 );
8781
8782 // Reset
8783 cx.set_state(
8784 &r#"
8785 let x = ˇ
8786 "#
8787 .unindent(),
8788 );
8789
8790 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
8791 cx.update_editor(|editor, window, cx| {
8792 editor.handle_input("\"", window, cx);
8793 editor.handle_input(" ", window, cx);
8794 editor.move_left(&Default::default(), window, cx);
8795 editor.handle_input("\\", window, cx);
8796 editor.handle_input("\"", window, cx);
8797 });
8798 cx.assert_editor_state(
8799 &r#"
8800 let x = "\"ˇ "
8801 "#
8802 .unindent(),
8803 );
8804
8805 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
8806 // mark. Nothing is inserted.
8807 cx.update_editor(|editor, window, cx| {
8808 editor.move_right(&Default::default(), window, cx);
8809 editor.handle_input("\"", window, cx);
8810 });
8811 cx.assert_editor_state(
8812 &r#"
8813 let x = "\" "ˇ
8814 "#
8815 .unindent(),
8816 );
8817}
8818
8819#[gpui::test]
8820async fn test_surround_with_pair(cx: &mut TestAppContext) {
8821 init_test(cx, |_| {});
8822
8823 let language = Arc::new(Language::new(
8824 LanguageConfig {
8825 brackets: BracketPairConfig {
8826 pairs: vec![
8827 BracketPair {
8828 start: "{".to_string(),
8829 end: "}".to_string(),
8830 close: true,
8831 surround: true,
8832 newline: true,
8833 },
8834 BracketPair {
8835 start: "/* ".to_string(),
8836 end: "*/".to_string(),
8837 close: true,
8838 surround: true,
8839 ..Default::default()
8840 },
8841 ],
8842 ..Default::default()
8843 },
8844 ..Default::default()
8845 },
8846 Some(tree_sitter_rust::LANGUAGE.into()),
8847 ));
8848
8849 let text = r#"
8850 a
8851 b
8852 c
8853 "#
8854 .unindent();
8855
8856 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8857 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8858 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8859 editor
8860 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8861 .await;
8862
8863 editor.update_in(cx, |editor, window, cx| {
8864 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8865 s.select_display_ranges([
8866 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8867 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8868 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
8869 ])
8870 });
8871
8872 editor.handle_input("{", window, cx);
8873 editor.handle_input("{", window, cx);
8874 editor.handle_input("{", window, cx);
8875 assert_eq!(
8876 editor.text(cx),
8877 "
8878 {{{a}}}
8879 {{{b}}}
8880 {{{c}}}
8881 "
8882 .unindent()
8883 );
8884 assert_eq!(
8885 editor.selections.display_ranges(cx),
8886 [
8887 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
8888 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
8889 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
8890 ]
8891 );
8892
8893 editor.undo(&Undo, window, cx);
8894 editor.undo(&Undo, window, cx);
8895 editor.undo(&Undo, window, cx);
8896 assert_eq!(
8897 editor.text(cx),
8898 "
8899 a
8900 b
8901 c
8902 "
8903 .unindent()
8904 );
8905 assert_eq!(
8906 editor.selections.display_ranges(cx),
8907 [
8908 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8909 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8910 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8911 ]
8912 );
8913
8914 // Ensure inserting the first character of a multi-byte bracket pair
8915 // doesn't surround the selections with the bracket.
8916 editor.handle_input("/", window, cx);
8917 assert_eq!(
8918 editor.text(cx),
8919 "
8920 /
8921 /
8922 /
8923 "
8924 .unindent()
8925 );
8926 assert_eq!(
8927 editor.selections.display_ranges(cx),
8928 [
8929 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8930 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8931 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8932 ]
8933 );
8934
8935 editor.undo(&Undo, window, cx);
8936 assert_eq!(
8937 editor.text(cx),
8938 "
8939 a
8940 b
8941 c
8942 "
8943 .unindent()
8944 );
8945 assert_eq!(
8946 editor.selections.display_ranges(cx),
8947 [
8948 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8949 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8950 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8951 ]
8952 );
8953
8954 // Ensure inserting the last character of a multi-byte bracket pair
8955 // doesn't surround the selections with the bracket.
8956 editor.handle_input("*", window, cx);
8957 assert_eq!(
8958 editor.text(cx),
8959 "
8960 *
8961 *
8962 *
8963 "
8964 .unindent()
8965 );
8966 assert_eq!(
8967 editor.selections.display_ranges(cx),
8968 [
8969 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8970 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8971 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8972 ]
8973 );
8974 });
8975}
8976
8977#[gpui::test]
8978async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
8979 init_test(cx, |_| {});
8980
8981 let language = Arc::new(Language::new(
8982 LanguageConfig {
8983 brackets: BracketPairConfig {
8984 pairs: vec![BracketPair {
8985 start: "{".to_string(),
8986 end: "}".to_string(),
8987 close: true,
8988 surround: true,
8989 newline: true,
8990 }],
8991 ..Default::default()
8992 },
8993 autoclose_before: "}".to_string(),
8994 ..Default::default()
8995 },
8996 Some(tree_sitter_rust::LANGUAGE.into()),
8997 ));
8998
8999 let text = r#"
9000 a
9001 b
9002 c
9003 "#
9004 .unindent();
9005
9006 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9007 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9008 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9009 editor
9010 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9011 .await;
9012
9013 editor.update_in(cx, |editor, window, cx| {
9014 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9015 s.select_ranges([
9016 Point::new(0, 1)..Point::new(0, 1),
9017 Point::new(1, 1)..Point::new(1, 1),
9018 Point::new(2, 1)..Point::new(2, 1),
9019 ])
9020 });
9021
9022 editor.handle_input("{", window, cx);
9023 editor.handle_input("{", window, cx);
9024 editor.handle_input("_", window, cx);
9025 assert_eq!(
9026 editor.text(cx),
9027 "
9028 a{{_}}
9029 b{{_}}
9030 c{{_}}
9031 "
9032 .unindent()
9033 );
9034 assert_eq!(
9035 editor.selections.ranges::<Point>(cx),
9036 [
9037 Point::new(0, 4)..Point::new(0, 4),
9038 Point::new(1, 4)..Point::new(1, 4),
9039 Point::new(2, 4)..Point::new(2, 4)
9040 ]
9041 );
9042
9043 editor.backspace(&Default::default(), window, cx);
9044 editor.backspace(&Default::default(), window, cx);
9045 assert_eq!(
9046 editor.text(cx),
9047 "
9048 a{}
9049 b{}
9050 c{}
9051 "
9052 .unindent()
9053 );
9054 assert_eq!(
9055 editor.selections.ranges::<Point>(cx),
9056 [
9057 Point::new(0, 2)..Point::new(0, 2),
9058 Point::new(1, 2)..Point::new(1, 2),
9059 Point::new(2, 2)..Point::new(2, 2)
9060 ]
9061 );
9062
9063 editor.delete_to_previous_word_start(&Default::default(), window, cx);
9064 assert_eq!(
9065 editor.text(cx),
9066 "
9067 a
9068 b
9069 c
9070 "
9071 .unindent()
9072 );
9073 assert_eq!(
9074 editor.selections.ranges::<Point>(cx),
9075 [
9076 Point::new(0, 1)..Point::new(0, 1),
9077 Point::new(1, 1)..Point::new(1, 1),
9078 Point::new(2, 1)..Point::new(2, 1)
9079 ]
9080 );
9081 });
9082}
9083
9084#[gpui::test]
9085async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
9086 init_test(cx, |settings| {
9087 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
9088 });
9089
9090 let mut cx = EditorTestContext::new(cx).await;
9091
9092 let language = Arc::new(Language::new(
9093 LanguageConfig {
9094 brackets: BracketPairConfig {
9095 pairs: vec![
9096 BracketPair {
9097 start: "{".to_string(),
9098 end: "}".to_string(),
9099 close: true,
9100 surround: true,
9101 newline: true,
9102 },
9103 BracketPair {
9104 start: "(".to_string(),
9105 end: ")".to_string(),
9106 close: true,
9107 surround: true,
9108 newline: true,
9109 },
9110 BracketPair {
9111 start: "[".to_string(),
9112 end: "]".to_string(),
9113 close: false,
9114 surround: true,
9115 newline: true,
9116 },
9117 ],
9118 ..Default::default()
9119 },
9120 autoclose_before: "})]".to_string(),
9121 ..Default::default()
9122 },
9123 Some(tree_sitter_rust::LANGUAGE.into()),
9124 ));
9125
9126 cx.language_registry().add(language.clone());
9127 cx.update_buffer(|buffer, cx| {
9128 buffer.set_language(Some(language), cx);
9129 });
9130
9131 cx.set_state(
9132 &"
9133 {(ˇ)}
9134 [[ˇ]]
9135 {(ˇ)}
9136 "
9137 .unindent(),
9138 );
9139
9140 cx.update_editor(|editor, window, cx| {
9141 editor.backspace(&Default::default(), window, cx);
9142 editor.backspace(&Default::default(), window, cx);
9143 });
9144
9145 cx.assert_editor_state(
9146 &"
9147 ˇ
9148 ˇ]]
9149 ˇ
9150 "
9151 .unindent(),
9152 );
9153
9154 cx.update_editor(|editor, window, cx| {
9155 editor.handle_input("{", window, cx);
9156 editor.handle_input("{", window, cx);
9157 editor.move_right(&MoveRight, window, cx);
9158 editor.move_right(&MoveRight, window, cx);
9159 editor.move_left(&MoveLeft, window, cx);
9160 editor.move_left(&MoveLeft, window, cx);
9161 editor.backspace(&Default::default(), window, cx);
9162 });
9163
9164 cx.assert_editor_state(
9165 &"
9166 {ˇ}
9167 {ˇ}]]
9168 {ˇ}
9169 "
9170 .unindent(),
9171 );
9172
9173 cx.update_editor(|editor, window, cx| {
9174 editor.backspace(&Default::default(), window, cx);
9175 });
9176
9177 cx.assert_editor_state(
9178 &"
9179 ˇ
9180 ˇ]]
9181 ˇ
9182 "
9183 .unindent(),
9184 );
9185}
9186
9187#[gpui::test]
9188async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
9189 init_test(cx, |_| {});
9190
9191 let language = Arc::new(Language::new(
9192 LanguageConfig::default(),
9193 Some(tree_sitter_rust::LANGUAGE.into()),
9194 ));
9195
9196 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
9197 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9198 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9199 editor
9200 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9201 .await;
9202
9203 editor.update_in(cx, |editor, window, cx| {
9204 editor.set_auto_replace_emoji_shortcode(true);
9205
9206 editor.handle_input("Hello ", window, cx);
9207 editor.handle_input(":wave", window, cx);
9208 assert_eq!(editor.text(cx), "Hello :wave".unindent());
9209
9210 editor.handle_input(":", window, cx);
9211 assert_eq!(editor.text(cx), "Hello 👋".unindent());
9212
9213 editor.handle_input(" :smile", window, cx);
9214 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
9215
9216 editor.handle_input(":", window, cx);
9217 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
9218
9219 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
9220 editor.handle_input(":wave", window, cx);
9221 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
9222
9223 editor.handle_input(":", window, cx);
9224 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
9225
9226 editor.handle_input(":1", window, cx);
9227 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
9228
9229 editor.handle_input(":", window, cx);
9230 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
9231
9232 // Ensure shortcode does not get replaced when it is part of a word
9233 editor.handle_input(" Test:wave", window, cx);
9234 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
9235
9236 editor.handle_input(":", window, cx);
9237 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
9238
9239 editor.set_auto_replace_emoji_shortcode(false);
9240
9241 // Ensure shortcode does not get replaced when auto replace is off
9242 editor.handle_input(" :wave", window, cx);
9243 assert_eq!(
9244 editor.text(cx),
9245 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
9246 );
9247
9248 editor.handle_input(":", window, cx);
9249 assert_eq!(
9250 editor.text(cx),
9251 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
9252 );
9253 });
9254}
9255
9256#[gpui::test]
9257async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
9258 init_test(cx, |_| {});
9259
9260 let (text, insertion_ranges) = marked_text_ranges(
9261 indoc! {"
9262 ˇ
9263 "},
9264 false,
9265 );
9266
9267 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
9268 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9269
9270 _ = editor.update_in(cx, |editor, window, cx| {
9271 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
9272
9273 editor
9274 .insert_snippet(&insertion_ranges, snippet, window, cx)
9275 .unwrap();
9276
9277 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
9278 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
9279 assert_eq!(editor.text(cx), expected_text);
9280 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
9281 }
9282
9283 assert(
9284 editor,
9285 cx,
9286 indoc! {"
9287 type «» =•
9288 "},
9289 );
9290
9291 assert!(editor.context_menu_visible(), "There should be a matches");
9292 });
9293}
9294
9295#[gpui::test]
9296async fn test_snippets(cx: &mut TestAppContext) {
9297 init_test(cx, |_| {});
9298
9299 let mut cx = EditorTestContext::new(cx).await;
9300
9301 cx.set_state(indoc! {"
9302 a.ˇ b
9303 a.ˇ b
9304 a.ˇ b
9305 "});
9306
9307 cx.update_editor(|editor, window, cx| {
9308 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
9309 let insertion_ranges = editor
9310 .selections
9311 .all(cx)
9312 .iter()
9313 .map(|s| s.range().clone())
9314 .collect::<Vec<_>>();
9315 editor
9316 .insert_snippet(&insertion_ranges, snippet, window, cx)
9317 .unwrap();
9318 });
9319
9320 cx.assert_editor_state(indoc! {"
9321 a.f(«oneˇ», two, «threeˇ») b
9322 a.f(«oneˇ», two, «threeˇ») b
9323 a.f(«oneˇ», two, «threeˇ») b
9324 "});
9325
9326 // Can't move earlier than the first tab stop
9327 cx.update_editor(|editor, window, cx| {
9328 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
9329 });
9330 cx.assert_editor_state(indoc! {"
9331 a.f(«oneˇ», two, «threeˇ») b
9332 a.f(«oneˇ», two, «threeˇ») b
9333 a.f(«oneˇ», two, «threeˇ») b
9334 "});
9335
9336 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9337 cx.assert_editor_state(indoc! {"
9338 a.f(one, «twoˇ», three) b
9339 a.f(one, «twoˇ», three) b
9340 a.f(one, «twoˇ», three) b
9341 "});
9342
9343 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
9344 cx.assert_editor_state(indoc! {"
9345 a.f(«oneˇ», two, «threeˇ») b
9346 a.f(«oneˇ», two, «threeˇ») b
9347 a.f(«oneˇ», two, «threeˇ») b
9348 "});
9349
9350 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9351 cx.assert_editor_state(indoc! {"
9352 a.f(one, «twoˇ», three) b
9353 a.f(one, «twoˇ», three) b
9354 a.f(one, «twoˇ», three) b
9355 "});
9356 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9357 cx.assert_editor_state(indoc! {"
9358 a.f(one, two, three)ˇ b
9359 a.f(one, two, three)ˇ b
9360 a.f(one, two, three)ˇ b
9361 "});
9362
9363 // As soon as the last tab stop is reached, snippet state is gone
9364 cx.update_editor(|editor, window, cx| {
9365 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
9366 });
9367 cx.assert_editor_state(indoc! {"
9368 a.f(one, two, three)ˇ b
9369 a.f(one, two, three)ˇ b
9370 a.f(one, two, three)ˇ b
9371 "});
9372}
9373
9374#[gpui::test]
9375async fn test_snippet_indentation(cx: &mut TestAppContext) {
9376 init_test(cx, |_| {});
9377
9378 let mut cx = EditorTestContext::new(cx).await;
9379
9380 cx.update_editor(|editor, window, cx| {
9381 let snippet = Snippet::parse(indoc! {"
9382 /*
9383 * Multiline comment with leading indentation
9384 *
9385 * $1
9386 */
9387 $0"})
9388 .unwrap();
9389 let insertion_ranges = editor
9390 .selections
9391 .all(cx)
9392 .iter()
9393 .map(|s| s.range().clone())
9394 .collect::<Vec<_>>();
9395 editor
9396 .insert_snippet(&insertion_ranges, snippet, window, cx)
9397 .unwrap();
9398 });
9399
9400 cx.assert_editor_state(indoc! {"
9401 /*
9402 * Multiline comment with leading indentation
9403 *
9404 * ˇ
9405 */
9406 "});
9407
9408 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9409 cx.assert_editor_state(indoc! {"
9410 /*
9411 * Multiline comment with leading indentation
9412 *
9413 *•
9414 */
9415 ˇ"});
9416}
9417
9418#[gpui::test]
9419async fn test_document_format_during_save(cx: &mut TestAppContext) {
9420 init_test(cx, |_| {});
9421
9422 let fs = FakeFs::new(cx.executor());
9423 fs.insert_file(path!("/file.rs"), Default::default()).await;
9424
9425 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
9426
9427 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9428 language_registry.add(rust_lang());
9429 let mut fake_servers = language_registry.register_fake_lsp(
9430 "Rust",
9431 FakeLspAdapter {
9432 capabilities: lsp::ServerCapabilities {
9433 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9434 ..Default::default()
9435 },
9436 ..Default::default()
9437 },
9438 );
9439
9440 let buffer = project
9441 .update(cx, |project, cx| {
9442 project.open_local_buffer(path!("/file.rs"), cx)
9443 })
9444 .await
9445 .unwrap();
9446
9447 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9448 let (editor, cx) = cx.add_window_view(|window, cx| {
9449 build_editor_with_project(project.clone(), buffer, window, cx)
9450 });
9451 editor.update_in(cx, |editor, window, cx| {
9452 editor.set_text("one\ntwo\nthree\n", window, cx)
9453 });
9454 assert!(cx.read(|cx| editor.is_dirty(cx)));
9455
9456 cx.executor().start_waiting();
9457 let fake_server = fake_servers.next().await.unwrap();
9458
9459 {
9460 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9461 move |params, _| async move {
9462 assert_eq!(
9463 params.text_document.uri,
9464 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9465 );
9466 assert_eq!(params.options.tab_size, 4);
9467 Ok(Some(vec![lsp::TextEdit::new(
9468 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9469 ", ".to_string(),
9470 )]))
9471 },
9472 );
9473 let save = editor
9474 .update_in(cx, |editor, window, cx| {
9475 editor.save(
9476 SaveOptions {
9477 format: true,
9478 autosave: false,
9479 },
9480 project.clone(),
9481 window,
9482 cx,
9483 )
9484 })
9485 .unwrap();
9486 cx.executor().start_waiting();
9487 save.await;
9488
9489 assert_eq!(
9490 editor.update(cx, |editor, cx| editor.text(cx)),
9491 "one, two\nthree\n"
9492 );
9493 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9494 }
9495
9496 {
9497 editor.update_in(cx, |editor, window, cx| {
9498 editor.set_text("one\ntwo\nthree\n", window, cx)
9499 });
9500 assert!(cx.read(|cx| editor.is_dirty(cx)));
9501
9502 // Ensure we can still save even if formatting hangs.
9503 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9504 move |params, _| async move {
9505 assert_eq!(
9506 params.text_document.uri,
9507 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9508 );
9509 futures::future::pending::<()>().await;
9510 unreachable!()
9511 },
9512 );
9513 let save = editor
9514 .update_in(cx, |editor, window, cx| {
9515 editor.save(
9516 SaveOptions {
9517 format: true,
9518 autosave: false,
9519 },
9520 project.clone(),
9521 window,
9522 cx,
9523 )
9524 })
9525 .unwrap();
9526 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9527 cx.executor().start_waiting();
9528 save.await;
9529 assert_eq!(
9530 editor.update(cx, |editor, cx| editor.text(cx)),
9531 "one\ntwo\nthree\n"
9532 );
9533 }
9534
9535 // Set rust language override and assert overridden tabsize is sent to language server
9536 update_test_language_settings(cx, |settings| {
9537 settings.languages.0.insert(
9538 "Rust".into(),
9539 LanguageSettingsContent {
9540 tab_size: NonZeroU32::new(8),
9541 ..Default::default()
9542 },
9543 );
9544 });
9545
9546 {
9547 editor.update_in(cx, |editor, window, cx| {
9548 editor.set_text("somehting_new\n", window, cx)
9549 });
9550 assert!(cx.read(|cx| editor.is_dirty(cx)));
9551 let _formatting_request_signal = fake_server
9552 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
9553 assert_eq!(
9554 params.text_document.uri,
9555 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9556 );
9557 assert_eq!(params.options.tab_size, 8);
9558 Ok(Some(vec![]))
9559 });
9560 let save = editor
9561 .update_in(cx, |editor, window, cx| {
9562 editor.save(
9563 SaveOptions {
9564 format: true,
9565 autosave: false,
9566 },
9567 project.clone(),
9568 window,
9569 cx,
9570 )
9571 })
9572 .unwrap();
9573 cx.executor().start_waiting();
9574 save.await;
9575 }
9576}
9577
9578#[gpui::test]
9579async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
9580 init_test(cx, |settings| {
9581 settings.defaults.ensure_final_newline_on_save = Some(false);
9582 });
9583
9584 let fs = FakeFs::new(cx.executor());
9585 fs.insert_file(path!("/file.txt"), "foo".into()).await;
9586
9587 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
9588
9589 let buffer = project
9590 .update(cx, |project, cx| {
9591 project.open_local_buffer(path!("/file.txt"), cx)
9592 })
9593 .await
9594 .unwrap();
9595
9596 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9597 let (editor, cx) = cx.add_window_view(|window, cx| {
9598 build_editor_with_project(project.clone(), buffer, window, cx)
9599 });
9600 editor.update_in(cx, |editor, window, cx| {
9601 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
9602 s.select_ranges([0..0])
9603 });
9604 });
9605 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9606
9607 editor.update_in(cx, |editor, window, cx| {
9608 editor.handle_input("\n", window, cx)
9609 });
9610 cx.run_until_parked();
9611 save(&editor, &project, cx).await;
9612 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
9613
9614 editor.update_in(cx, |editor, window, cx| {
9615 editor.undo(&Default::default(), window, cx);
9616 });
9617 save(&editor, &project, cx).await;
9618 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
9619
9620 editor.update_in(cx, |editor, window, cx| {
9621 editor.redo(&Default::default(), window, cx);
9622 });
9623 cx.run_until_parked();
9624 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
9625
9626 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
9627 let save = editor
9628 .update_in(cx, |editor, window, cx| {
9629 editor.save(
9630 SaveOptions {
9631 format: true,
9632 autosave: false,
9633 },
9634 project.clone(),
9635 window,
9636 cx,
9637 )
9638 })
9639 .unwrap();
9640 cx.executor().start_waiting();
9641 save.await;
9642 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9643 }
9644}
9645
9646#[gpui::test]
9647async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
9648 init_test(cx, |_| {});
9649
9650 let cols = 4;
9651 let rows = 10;
9652 let sample_text_1 = sample_text(rows, cols, 'a');
9653 assert_eq!(
9654 sample_text_1,
9655 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
9656 );
9657 let sample_text_2 = sample_text(rows, cols, 'l');
9658 assert_eq!(
9659 sample_text_2,
9660 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
9661 );
9662 let sample_text_3 = sample_text(rows, cols, 'v');
9663 assert_eq!(
9664 sample_text_3,
9665 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
9666 );
9667
9668 let fs = FakeFs::new(cx.executor());
9669 fs.insert_tree(
9670 path!("/a"),
9671 json!({
9672 "main.rs": sample_text_1,
9673 "other.rs": sample_text_2,
9674 "lib.rs": sample_text_3,
9675 }),
9676 )
9677 .await;
9678
9679 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
9680 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9681 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
9682
9683 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9684 language_registry.add(rust_lang());
9685 let mut fake_servers = language_registry.register_fake_lsp(
9686 "Rust",
9687 FakeLspAdapter {
9688 capabilities: lsp::ServerCapabilities {
9689 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9690 ..Default::default()
9691 },
9692 ..Default::default()
9693 },
9694 );
9695
9696 let worktree = project.update(cx, |project, cx| {
9697 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
9698 assert_eq!(worktrees.len(), 1);
9699 worktrees.pop().unwrap()
9700 });
9701 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
9702
9703 let buffer_1 = project
9704 .update(cx, |project, cx| {
9705 project.open_buffer((worktree_id, "main.rs"), cx)
9706 })
9707 .await
9708 .unwrap();
9709 let buffer_2 = project
9710 .update(cx, |project, cx| {
9711 project.open_buffer((worktree_id, "other.rs"), cx)
9712 })
9713 .await
9714 .unwrap();
9715 let buffer_3 = project
9716 .update(cx, |project, cx| {
9717 project.open_buffer((worktree_id, "lib.rs"), cx)
9718 })
9719 .await
9720 .unwrap();
9721
9722 let multi_buffer = cx.new(|cx| {
9723 let mut multi_buffer = MultiBuffer::new(ReadWrite);
9724 multi_buffer.push_excerpts(
9725 buffer_1.clone(),
9726 [
9727 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9728 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9729 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9730 ],
9731 cx,
9732 );
9733 multi_buffer.push_excerpts(
9734 buffer_2.clone(),
9735 [
9736 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9737 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9738 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9739 ],
9740 cx,
9741 );
9742 multi_buffer.push_excerpts(
9743 buffer_3.clone(),
9744 [
9745 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9746 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9747 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9748 ],
9749 cx,
9750 );
9751 multi_buffer
9752 });
9753 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
9754 Editor::new(
9755 EditorMode::full(),
9756 multi_buffer,
9757 Some(project.clone()),
9758 window,
9759 cx,
9760 )
9761 });
9762
9763 multi_buffer_editor.update_in(cx, |editor, window, cx| {
9764 editor.change_selections(
9765 SelectionEffects::scroll(Autoscroll::Next),
9766 window,
9767 cx,
9768 |s| s.select_ranges(Some(1..2)),
9769 );
9770 editor.insert("|one|two|three|", window, cx);
9771 });
9772 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
9773 multi_buffer_editor.update_in(cx, |editor, window, cx| {
9774 editor.change_selections(
9775 SelectionEffects::scroll(Autoscroll::Next),
9776 window,
9777 cx,
9778 |s| s.select_ranges(Some(60..70)),
9779 );
9780 editor.insert("|four|five|six|", window, cx);
9781 });
9782 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
9783
9784 // First two buffers should be edited, but not the third one.
9785 assert_eq!(
9786 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
9787 "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}",
9788 );
9789 buffer_1.update(cx, |buffer, _| {
9790 assert!(buffer.is_dirty());
9791 assert_eq!(
9792 buffer.text(),
9793 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
9794 )
9795 });
9796 buffer_2.update(cx, |buffer, _| {
9797 assert!(buffer.is_dirty());
9798 assert_eq!(
9799 buffer.text(),
9800 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
9801 )
9802 });
9803 buffer_3.update(cx, |buffer, _| {
9804 assert!(!buffer.is_dirty());
9805 assert_eq!(buffer.text(), sample_text_3,)
9806 });
9807 cx.executor().run_until_parked();
9808
9809 cx.executor().start_waiting();
9810 let save = multi_buffer_editor
9811 .update_in(cx, |editor, window, cx| {
9812 editor.save(
9813 SaveOptions {
9814 format: true,
9815 autosave: false,
9816 },
9817 project.clone(),
9818 window,
9819 cx,
9820 )
9821 })
9822 .unwrap();
9823
9824 let fake_server = fake_servers.next().await.unwrap();
9825 fake_server
9826 .server
9827 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
9828 Ok(Some(vec![lsp::TextEdit::new(
9829 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9830 format!("[{} formatted]", params.text_document.uri),
9831 )]))
9832 })
9833 .detach();
9834 save.await;
9835
9836 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
9837 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
9838 assert_eq!(
9839 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
9840 uri!(
9841 "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}"
9842 ),
9843 );
9844 buffer_1.update(cx, |buffer, _| {
9845 assert!(!buffer.is_dirty());
9846 assert_eq!(
9847 buffer.text(),
9848 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
9849 )
9850 });
9851 buffer_2.update(cx, |buffer, _| {
9852 assert!(!buffer.is_dirty());
9853 assert_eq!(
9854 buffer.text(),
9855 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
9856 )
9857 });
9858 buffer_3.update(cx, |buffer, _| {
9859 assert!(!buffer.is_dirty());
9860 assert_eq!(buffer.text(), sample_text_3,)
9861 });
9862}
9863
9864#[gpui::test]
9865async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
9866 init_test(cx, |_| {});
9867
9868 let fs = FakeFs::new(cx.executor());
9869 fs.insert_tree(
9870 path!("/dir"),
9871 json!({
9872 "file1.rs": "fn main() { println!(\"hello\"); }",
9873 "file2.rs": "fn test() { println!(\"test\"); }",
9874 "file3.rs": "fn other() { println!(\"other\"); }\n",
9875 }),
9876 )
9877 .await;
9878
9879 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
9880 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9881 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
9882
9883 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9884 language_registry.add(rust_lang());
9885
9886 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
9887 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
9888
9889 // Open three buffers
9890 let buffer_1 = project
9891 .update(cx, |project, cx| {
9892 project.open_buffer((worktree_id, "file1.rs"), cx)
9893 })
9894 .await
9895 .unwrap();
9896 let buffer_2 = project
9897 .update(cx, |project, cx| {
9898 project.open_buffer((worktree_id, "file2.rs"), cx)
9899 })
9900 .await
9901 .unwrap();
9902 let buffer_3 = project
9903 .update(cx, |project, cx| {
9904 project.open_buffer((worktree_id, "file3.rs"), cx)
9905 })
9906 .await
9907 .unwrap();
9908
9909 // Create a multi-buffer with all three buffers
9910 let multi_buffer = cx.new(|cx| {
9911 let mut multi_buffer = MultiBuffer::new(ReadWrite);
9912 multi_buffer.push_excerpts(
9913 buffer_1.clone(),
9914 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9915 cx,
9916 );
9917 multi_buffer.push_excerpts(
9918 buffer_2.clone(),
9919 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9920 cx,
9921 );
9922 multi_buffer.push_excerpts(
9923 buffer_3.clone(),
9924 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9925 cx,
9926 );
9927 multi_buffer
9928 });
9929
9930 let editor = cx.new_window_entity(|window, cx| {
9931 Editor::new(
9932 EditorMode::full(),
9933 multi_buffer,
9934 Some(project.clone()),
9935 window,
9936 cx,
9937 )
9938 });
9939
9940 // Edit only the first buffer
9941 editor.update_in(cx, |editor, window, cx| {
9942 editor.change_selections(
9943 SelectionEffects::scroll(Autoscroll::Next),
9944 window,
9945 cx,
9946 |s| s.select_ranges(Some(10..10)),
9947 );
9948 editor.insert("// edited", window, cx);
9949 });
9950
9951 // Verify that only buffer 1 is dirty
9952 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
9953 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9954 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9955
9956 // Get write counts after file creation (files were created with initial content)
9957 // We expect each file to have been written once during creation
9958 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
9959 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
9960 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
9961
9962 // Perform autosave
9963 let save_task = editor.update_in(cx, |editor, window, cx| {
9964 editor.save(
9965 SaveOptions {
9966 format: true,
9967 autosave: true,
9968 },
9969 project.clone(),
9970 window,
9971 cx,
9972 )
9973 });
9974 save_task.await.unwrap();
9975
9976 // Only the dirty buffer should have been saved
9977 assert_eq!(
9978 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
9979 1,
9980 "Buffer 1 was dirty, so it should have been written once during autosave"
9981 );
9982 assert_eq!(
9983 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
9984 0,
9985 "Buffer 2 was clean, so it should not have been written during autosave"
9986 );
9987 assert_eq!(
9988 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
9989 0,
9990 "Buffer 3 was clean, so it should not have been written during autosave"
9991 );
9992
9993 // Verify buffer states after autosave
9994 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9995 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9996 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9997
9998 // Now perform a manual save (format = true)
9999 let save_task = editor.update_in(cx, |editor, window, cx| {
10000 editor.save(
10001 SaveOptions {
10002 format: true,
10003 autosave: false,
10004 },
10005 project.clone(),
10006 window,
10007 cx,
10008 )
10009 });
10010 save_task.await.unwrap();
10011
10012 // During manual save, clean buffers don't get written to disk
10013 // They just get did_save called for language server notifications
10014 assert_eq!(
10015 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
10016 1,
10017 "Buffer 1 should only have been written once total (during autosave, not manual save)"
10018 );
10019 assert_eq!(
10020 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
10021 0,
10022 "Buffer 2 should not have been written at all"
10023 );
10024 assert_eq!(
10025 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
10026 0,
10027 "Buffer 3 should not have been written at all"
10028 );
10029}
10030
10031#[gpui::test]
10032async fn test_range_format_during_save(cx: &mut TestAppContext) {
10033 init_test(cx, |_| {});
10034
10035 let fs = FakeFs::new(cx.executor());
10036 fs.insert_file(path!("/file.rs"), Default::default()).await;
10037
10038 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10039
10040 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10041 language_registry.add(rust_lang());
10042 let mut fake_servers = language_registry.register_fake_lsp(
10043 "Rust",
10044 FakeLspAdapter {
10045 capabilities: lsp::ServerCapabilities {
10046 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
10047 ..Default::default()
10048 },
10049 ..Default::default()
10050 },
10051 );
10052
10053 let buffer = project
10054 .update(cx, |project, cx| {
10055 project.open_local_buffer(path!("/file.rs"), cx)
10056 })
10057 .await
10058 .unwrap();
10059
10060 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10061 let (editor, cx) = cx.add_window_view(|window, cx| {
10062 build_editor_with_project(project.clone(), buffer, window, cx)
10063 });
10064 editor.update_in(cx, |editor, window, cx| {
10065 editor.set_text("one\ntwo\nthree\n", window, cx)
10066 });
10067 assert!(cx.read(|cx| editor.is_dirty(cx)));
10068
10069 cx.executor().start_waiting();
10070 let fake_server = fake_servers.next().await.unwrap();
10071
10072 let save = editor
10073 .update_in(cx, |editor, window, cx| {
10074 editor.save(
10075 SaveOptions {
10076 format: true,
10077 autosave: false,
10078 },
10079 project.clone(),
10080 window,
10081 cx,
10082 )
10083 })
10084 .unwrap();
10085 fake_server
10086 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
10087 assert_eq!(
10088 params.text_document.uri,
10089 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10090 );
10091 assert_eq!(params.options.tab_size, 4);
10092 Ok(Some(vec![lsp::TextEdit::new(
10093 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10094 ", ".to_string(),
10095 )]))
10096 })
10097 .next()
10098 .await;
10099 cx.executor().start_waiting();
10100 save.await;
10101 assert_eq!(
10102 editor.update(cx, |editor, cx| editor.text(cx)),
10103 "one, two\nthree\n"
10104 );
10105 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10106
10107 editor.update_in(cx, |editor, window, cx| {
10108 editor.set_text("one\ntwo\nthree\n", window, cx)
10109 });
10110 assert!(cx.read(|cx| editor.is_dirty(cx)));
10111
10112 // Ensure we can still save even if formatting hangs.
10113 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
10114 move |params, _| async move {
10115 assert_eq!(
10116 params.text_document.uri,
10117 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10118 );
10119 futures::future::pending::<()>().await;
10120 unreachable!()
10121 },
10122 );
10123 let save = editor
10124 .update_in(cx, |editor, window, cx| {
10125 editor.save(
10126 SaveOptions {
10127 format: true,
10128 autosave: false,
10129 },
10130 project.clone(),
10131 window,
10132 cx,
10133 )
10134 })
10135 .unwrap();
10136 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
10137 cx.executor().start_waiting();
10138 save.await;
10139 assert_eq!(
10140 editor.update(cx, |editor, cx| editor.text(cx)),
10141 "one\ntwo\nthree\n"
10142 );
10143 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10144
10145 // For non-dirty buffer, no formatting request should be sent
10146 let save = editor
10147 .update_in(cx, |editor, window, cx| {
10148 editor.save(
10149 SaveOptions {
10150 format: false,
10151 autosave: false,
10152 },
10153 project.clone(),
10154 window,
10155 cx,
10156 )
10157 })
10158 .unwrap();
10159 let _pending_format_request = fake_server
10160 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
10161 panic!("Should not be invoked");
10162 })
10163 .next();
10164 cx.executor().start_waiting();
10165 save.await;
10166
10167 // Set Rust language override and assert overridden tabsize is sent to language server
10168 update_test_language_settings(cx, |settings| {
10169 settings.languages.0.insert(
10170 "Rust".into(),
10171 LanguageSettingsContent {
10172 tab_size: NonZeroU32::new(8),
10173 ..Default::default()
10174 },
10175 );
10176 });
10177
10178 editor.update_in(cx, |editor, window, cx| {
10179 editor.set_text("somehting_new\n", window, cx)
10180 });
10181 assert!(cx.read(|cx| editor.is_dirty(cx)));
10182 let save = editor
10183 .update_in(cx, |editor, window, cx| {
10184 editor.save(
10185 SaveOptions {
10186 format: true,
10187 autosave: false,
10188 },
10189 project.clone(),
10190 window,
10191 cx,
10192 )
10193 })
10194 .unwrap();
10195 fake_server
10196 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
10197 assert_eq!(
10198 params.text_document.uri,
10199 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10200 );
10201 assert_eq!(params.options.tab_size, 8);
10202 Ok(Some(Vec::new()))
10203 })
10204 .next()
10205 .await;
10206 save.await;
10207}
10208
10209#[gpui::test]
10210async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
10211 init_test(cx, |settings| {
10212 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
10213 Formatter::LanguageServer { name: None },
10214 )))
10215 });
10216
10217 let fs = FakeFs::new(cx.executor());
10218 fs.insert_file(path!("/file.rs"), Default::default()).await;
10219
10220 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10221
10222 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10223 language_registry.add(Arc::new(Language::new(
10224 LanguageConfig {
10225 name: "Rust".into(),
10226 matcher: LanguageMatcher {
10227 path_suffixes: vec!["rs".to_string()],
10228 ..Default::default()
10229 },
10230 ..LanguageConfig::default()
10231 },
10232 Some(tree_sitter_rust::LANGUAGE.into()),
10233 )));
10234 update_test_language_settings(cx, |settings| {
10235 // Enable Prettier formatting for the same buffer, and ensure
10236 // LSP is called instead of Prettier.
10237 settings.defaults.prettier = Some(PrettierSettings {
10238 allowed: true,
10239 ..PrettierSettings::default()
10240 });
10241 });
10242 let mut fake_servers = language_registry.register_fake_lsp(
10243 "Rust",
10244 FakeLspAdapter {
10245 capabilities: lsp::ServerCapabilities {
10246 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10247 ..Default::default()
10248 },
10249 ..Default::default()
10250 },
10251 );
10252
10253 let buffer = project
10254 .update(cx, |project, cx| {
10255 project.open_local_buffer(path!("/file.rs"), cx)
10256 })
10257 .await
10258 .unwrap();
10259
10260 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10261 let (editor, cx) = cx.add_window_view(|window, cx| {
10262 build_editor_with_project(project.clone(), buffer, window, cx)
10263 });
10264 editor.update_in(cx, |editor, window, cx| {
10265 editor.set_text("one\ntwo\nthree\n", window, cx)
10266 });
10267
10268 cx.executor().start_waiting();
10269 let fake_server = fake_servers.next().await.unwrap();
10270
10271 let format = editor
10272 .update_in(cx, |editor, window, cx| {
10273 editor.perform_format(
10274 project.clone(),
10275 FormatTrigger::Manual,
10276 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10277 window,
10278 cx,
10279 )
10280 })
10281 .unwrap();
10282 fake_server
10283 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
10284 assert_eq!(
10285 params.text_document.uri,
10286 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10287 );
10288 assert_eq!(params.options.tab_size, 4);
10289 Ok(Some(vec![lsp::TextEdit::new(
10290 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10291 ", ".to_string(),
10292 )]))
10293 })
10294 .next()
10295 .await;
10296 cx.executor().start_waiting();
10297 format.await;
10298 assert_eq!(
10299 editor.update(cx, |editor, cx| editor.text(cx)),
10300 "one, two\nthree\n"
10301 );
10302
10303 editor.update_in(cx, |editor, window, cx| {
10304 editor.set_text("one\ntwo\nthree\n", window, cx)
10305 });
10306 // Ensure we don't lock if formatting hangs.
10307 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10308 move |params, _| async move {
10309 assert_eq!(
10310 params.text_document.uri,
10311 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10312 );
10313 futures::future::pending::<()>().await;
10314 unreachable!()
10315 },
10316 );
10317 let format = editor
10318 .update_in(cx, |editor, window, cx| {
10319 editor.perform_format(
10320 project,
10321 FormatTrigger::Manual,
10322 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10323 window,
10324 cx,
10325 )
10326 })
10327 .unwrap();
10328 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
10329 cx.executor().start_waiting();
10330 format.await;
10331 assert_eq!(
10332 editor.update(cx, |editor, cx| editor.text(cx)),
10333 "one\ntwo\nthree\n"
10334 );
10335}
10336
10337#[gpui::test]
10338async fn test_multiple_formatters(cx: &mut TestAppContext) {
10339 init_test(cx, |settings| {
10340 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
10341 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
10342 Formatter::LanguageServer { name: None },
10343 Formatter::CodeActions(
10344 [
10345 ("code-action-1".into(), true),
10346 ("code-action-2".into(), true),
10347 ]
10348 .into_iter()
10349 .collect(),
10350 ),
10351 ])))
10352 });
10353
10354 let fs = FakeFs::new(cx.executor());
10355 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
10356 .await;
10357
10358 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10359 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10360 language_registry.add(rust_lang());
10361
10362 let mut fake_servers = language_registry.register_fake_lsp(
10363 "Rust",
10364 FakeLspAdapter {
10365 capabilities: lsp::ServerCapabilities {
10366 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10367 execute_command_provider: Some(lsp::ExecuteCommandOptions {
10368 commands: vec!["the-command-for-code-action-1".into()],
10369 ..Default::default()
10370 }),
10371 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
10372 ..Default::default()
10373 },
10374 ..Default::default()
10375 },
10376 );
10377
10378 let buffer = project
10379 .update(cx, |project, cx| {
10380 project.open_local_buffer(path!("/file.rs"), cx)
10381 })
10382 .await
10383 .unwrap();
10384
10385 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10386 let (editor, cx) = cx.add_window_view(|window, cx| {
10387 build_editor_with_project(project.clone(), buffer, window, cx)
10388 });
10389
10390 cx.executor().start_waiting();
10391
10392 let fake_server = fake_servers.next().await.unwrap();
10393 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10394 move |_params, _| async move {
10395 Ok(Some(vec![lsp::TextEdit::new(
10396 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10397 "applied-formatting\n".to_string(),
10398 )]))
10399 },
10400 );
10401 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
10402 move |params, _| async move {
10403 assert_eq!(
10404 params.context.only,
10405 Some(vec!["code-action-1".into(), "code-action-2".into()])
10406 );
10407 let uri = lsp::Url::from_file_path(path!("/file.rs")).unwrap();
10408 Ok(Some(vec![
10409 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
10410 kind: Some("code-action-1".into()),
10411 edit: Some(lsp::WorkspaceEdit::new(
10412 [(
10413 uri.clone(),
10414 vec![lsp::TextEdit::new(
10415 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10416 "applied-code-action-1-edit\n".to_string(),
10417 )],
10418 )]
10419 .into_iter()
10420 .collect(),
10421 )),
10422 command: Some(lsp::Command {
10423 command: "the-command-for-code-action-1".into(),
10424 ..Default::default()
10425 }),
10426 ..Default::default()
10427 }),
10428 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
10429 kind: Some("code-action-2".into()),
10430 edit: Some(lsp::WorkspaceEdit::new(
10431 [(
10432 uri.clone(),
10433 vec![lsp::TextEdit::new(
10434 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10435 "applied-code-action-2-edit\n".to_string(),
10436 )],
10437 )]
10438 .into_iter()
10439 .collect(),
10440 )),
10441 ..Default::default()
10442 }),
10443 ]))
10444 },
10445 );
10446
10447 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
10448 move |params, _| async move { Ok(params) }
10449 });
10450
10451 let command_lock = Arc::new(futures::lock::Mutex::new(()));
10452 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
10453 let fake = fake_server.clone();
10454 let lock = command_lock.clone();
10455 move |params, _| {
10456 assert_eq!(params.command, "the-command-for-code-action-1");
10457 let fake = fake.clone();
10458 let lock = lock.clone();
10459 async move {
10460 lock.lock().await;
10461 fake.server
10462 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
10463 label: None,
10464 edit: lsp::WorkspaceEdit {
10465 changes: Some(
10466 [(
10467 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
10468 vec![lsp::TextEdit {
10469 range: lsp::Range::new(
10470 lsp::Position::new(0, 0),
10471 lsp::Position::new(0, 0),
10472 ),
10473 new_text: "applied-code-action-1-command\n".into(),
10474 }],
10475 )]
10476 .into_iter()
10477 .collect(),
10478 ),
10479 ..Default::default()
10480 },
10481 })
10482 .await
10483 .into_response()
10484 .unwrap();
10485 Ok(Some(json!(null)))
10486 }
10487 }
10488 });
10489
10490 cx.executor().start_waiting();
10491 editor
10492 .update_in(cx, |editor, window, cx| {
10493 editor.perform_format(
10494 project.clone(),
10495 FormatTrigger::Manual,
10496 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10497 window,
10498 cx,
10499 )
10500 })
10501 .unwrap()
10502 .await;
10503 editor.update(cx, |editor, cx| {
10504 assert_eq!(
10505 editor.text(cx),
10506 r#"
10507 applied-code-action-2-edit
10508 applied-code-action-1-command
10509 applied-code-action-1-edit
10510 applied-formatting
10511 one
10512 two
10513 three
10514 "#
10515 .unindent()
10516 );
10517 });
10518
10519 editor.update_in(cx, |editor, window, cx| {
10520 editor.undo(&Default::default(), window, cx);
10521 assert_eq!(editor.text(cx), "one \ntwo \nthree");
10522 });
10523
10524 // Perform a manual edit while waiting for an LSP command
10525 // that's being run as part of a formatting code action.
10526 let lock_guard = command_lock.lock().await;
10527 let format = editor
10528 .update_in(cx, |editor, window, cx| {
10529 editor.perform_format(
10530 project.clone(),
10531 FormatTrigger::Manual,
10532 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10533 window,
10534 cx,
10535 )
10536 })
10537 .unwrap();
10538 cx.run_until_parked();
10539 editor.update(cx, |editor, cx| {
10540 assert_eq!(
10541 editor.text(cx),
10542 r#"
10543 applied-code-action-1-edit
10544 applied-formatting
10545 one
10546 two
10547 three
10548 "#
10549 .unindent()
10550 );
10551
10552 editor.buffer.update(cx, |buffer, cx| {
10553 let ix = buffer.len(cx);
10554 buffer.edit([(ix..ix, "edited\n")], None, cx);
10555 });
10556 });
10557
10558 // Allow the LSP command to proceed. Because the buffer was edited,
10559 // the second code action will not be run.
10560 drop(lock_guard);
10561 format.await;
10562 editor.update_in(cx, |editor, window, cx| {
10563 assert_eq!(
10564 editor.text(cx),
10565 r#"
10566 applied-code-action-1-command
10567 applied-code-action-1-edit
10568 applied-formatting
10569 one
10570 two
10571 three
10572 edited
10573 "#
10574 .unindent()
10575 );
10576
10577 // The manual edit is undone first, because it is the last thing the user did
10578 // (even though the command completed afterwards).
10579 editor.undo(&Default::default(), window, cx);
10580 assert_eq!(
10581 editor.text(cx),
10582 r#"
10583 applied-code-action-1-command
10584 applied-code-action-1-edit
10585 applied-formatting
10586 one
10587 two
10588 three
10589 "#
10590 .unindent()
10591 );
10592
10593 // All the formatting (including the command, which completed after the manual edit)
10594 // is undone together.
10595 editor.undo(&Default::default(), window, cx);
10596 assert_eq!(editor.text(cx), "one \ntwo \nthree");
10597 });
10598}
10599
10600#[gpui::test]
10601async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
10602 init_test(cx, |settings| {
10603 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
10604 Formatter::LanguageServer { name: None },
10605 ])))
10606 });
10607
10608 let fs = FakeFs::new(cx.executor());
10609 fs.insert_file(path!("/file.ts"), Default::default()).await;
10610
10611 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10612
10613 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10614 language_registry.add(Arc::new(Language::new(
10615 LanguageConfig {
10616 name: "TypeScript".into(),
10617 matcher: LanguageMatcher {
10618 path_suffixes: vec!["ts".to_string()],
10619 ..Default::default()
10620 },
10621 ..LanguageConfig::default()
10622 },
10623 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
10624 )));
10625 update_test_language_settings(cx, |settings| {
10626 settings.defaults.prettier = Some(PrettierSettings {
10627 allowed: true,
10628 ..PrettierSettings::default()
10629 });
10630 });
10631 let mut fake_servers = language_registry.register_fake_lsp(
10632 "TypeScript",
10633 FakeLspAdapter {
10634 capabilities: lsp::ServerCapabilities {
10635 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
10636 ..Default::default()
10637 },
10638 ..Default::default()
10639 },
10640 );
10641
10642 let buffer = project
10643 .update(cx, |project, cx| {
10644 project.open_local_buffer(path!("/file.ts"), cx)
10645 })
10646 .await
10647 .unwrap();
10648
10649 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10650 let (editor, cx) = cx.add_window_view(|window, cx| {
10651 build_editor_with_project(project.clone(), buffer, window, cx)
10652 });
10653 editor.update_in(cx, |editor, window, cx| {
10654 editor.set_text(
10655 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
10656 window,
10657 cx,
10658 )
10659 });
10660
10661 cx.executor().start_waiting();
10662 let fake_server = fake_servers.next().await.unwrap();
10663
10664 let format = editor
10665 .update_in(cx, |editor, window, cx| {
10666 editor.perform_code_action_kind(
10667 project.clone(),
10668 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
10669 window,
10670 cx,
10671 )
10672 })
10673 .unwrap();
10674 fake_server
10675 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
10676 assert_eq!(
10677 params.text_document.uri,
10678 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
10679 );
10680 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
10681 lsp::CodeAction {
10682 title: "Organize Imports".to_string(),
10683 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
10684 edit: Some(lsp::WorkspaceEdit {
10685 changes: Some(
10686 [(
10687 params.text_document.uri.clone(),
10688 vec![lsp::TextEdit::new(
10689 lsp::Range::new(
10690 lsp::Position::new(1, 0),
10691 lsp::Position::new(2, 0),
10692 ),
10693 "".to_string(),
10694 )],
10695 )]
10696 .into_iter()
10697 .collect(),
10698 ),
10699 ..Default::default()
10700 }),
10701 ..Default::default()
10702 },
10703 )]))
10704 })
10705 .next()
10706 .await;
10707 cx.executor().start_waiting();
10708 format.await;
10709 assert_eq!(
10710 editor.update(cx, |editor, cx| editor.text(cx)),
10711 "import { a } from 'module';\n\nconst x = a;\n"
10712 );
10713
10714 editor.update_in(cx, |editor, window, cx| {
10715 editor.set_text(
10716 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
10717 window,
10718 cx,
10719 )
10720 });
10721 // Ensure we don't lock if code action hangs.
10722 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
10723 move |params, _| async move {
10724 assert_eq!(
10725 params.text_document.uri,
10726 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
10727 );
10728 futures::future::pending::<()>().await;
10729 unreachable!()
10730 },
10731 );
10732 let format = editor
10733 .update_in(cx, |editor, window, cx| {
10734 editor.perform_code_action_kind(
10735 project,
10736 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
10737 window,
10738 cx,
10739 )
10740 })
10741 .unwrap();
10742 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
10743 cx.executor().start_waiting();
10744 format.await;
10745 assert_eq!(
10746 editor.update(cx, |editor, cx| editor.text(cx)),
10747 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
10748 );
10749}
10750
10751#[gpui::test]
10752async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
10753 init_test(cx, |_| {});
10754
10755 let mut cx = EditorLspTestContext::new_rust(
10756 lsp::ServerCapabilities {
10757 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10758 ..Default::default()
10759 },
10760 cx,
10761 )
10762 .await;
10763
10764 cx.set_state(indoc! {"
10765 one.twoˇ
10766 "});
10767
10768 // The format request takes a long time. When it completes, it inserts
10769 // a newline and an indent before the `.`
10770 cx.lsp
10771 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
10772 let executor = cx.background_executor().clone();
10773 async move {
10774 executor.timer(Duration::from_millis(100)).await;
10775 Ok(Some(vec![lsp::TextEdit {
10776 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
10777 new_text: "\n ".into(),
10778 }]))
10779 }
10780 });
10781
10782 // Submit a format request.
10783 let format_1 = cx
10784 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10785 .unwrap();
10786 cx.executor().run_until_parked();
10787
10788 // Submit a second format request.
10789 let format_2 = cx
10790 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10791 .unwrap();
10792 cx.executor().run_until_parked();
10793
10794 // Wait for both format requests to complete
10795 cx.executor().advance_clock(Duration::from_millis(200));
10796 cx.executor().start_waiting();
10797 format_1.await.unwrap();
10798 cx.executor().start_waiting();
10799 format_2.await.unwrap();
10800
10801 // The formatting edits only happens once.
10802 cx.assert_editor_state(indoc! {"
10803 one
10804 .twoˇ
10805 "});
10806}
10807
10808#[gpui::test]
10809async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
10810 init_test(cx, |settings| {
10811 settings.defaults.formatter = Some(SelectedFormatter::Auto)
10812 });
10813
10814 let mut cx = EditorLspTestContext::new_rust(
10815 lsp::ServerCapabilities {
10816 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10817 ..Default::default()
10818 },
10819 cx,
10820 )
10821 .await;
10822
10823 // Set up a buffer white some trailing whitespace and no trailing newline.
10824 cx.set_state(
10825 &[
10826 "one ", //
10827 "twoˇ", //
10828 "three ", //
10829 "four", //
10830 ]
10831 .join("\n"),
10832 );
10833
10834 // Submit a format request.
10835 let format = cx
10836 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10837 .unwrap();
10838
10839 // Record which buffer changes have been sent to the language server
10840 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
10841 cx.lsp
10842 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
10843 let buffer_changes = buffer_changes.clone();
10844 move |params, _| {
10845 buffer_changes.lock().extend(
10846 params
10847 .content_changes
10848 .into_iter()
10849 .map(|e| (e.range.unwrap(), e.text)),
10850 );
10851 }
10852 });
10853
10854 // Handle formatting requests to the language server.
10855 cx.lsp
10856 .set_request_handler::<lsp::request::Formatting, _, _>({
10857 let buffer_changes = buffer_changes.clone();
10858 move |_, _| {
10859 // When formatting is requested, trailing whitespace has already been stripped,
10860 // and the trailing newline has already been added.
10861 assert_eq!(
10862 &buffer_changes.lock()[1..],
10863 &[
10864 (
10865 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
10866 "".into()
10867 ),
10868 (
10869 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
10870 "".into()
10871 ),
10872 (
10873 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
10874 "\n".into()
10875 ),
10876 ]
10877 );
10878
10879 // Insert blank lines between each line of the buffer.
10880 async move {
10881 Ok(Some(vec![
10882 lsp::TextEdit {
10883 range: lsp::Range::new(
10884 lsp::Position::new(1, 0),
10885 lsp::Position::new(1, 0),
10886 ),
10887 new_text: "\n".into(),
10888 },
10889 lsp::TextEdit {
10890 range: lsp::Range::new(
10891 lsp::Position::new(2, 0),
10892 lsp::Position::new(2, 0),
10893 ),
10894 new_text: "\n".into(),
10895 },
10896 ]))
10897 }
10898 }
10899 });
10900
10901 // After formatting the buffer, the trailing whitespace is stripped,
10902 // a newline is appended, and the edits provided by the language server
10903 // have been applied.
10904 format.await.unwrap();
10905 cx.assert_editor_state(
10906 &[
10907 "one", //
10908 "", //
10909 "twoˇ", //
10910 "", //
10911 "three", //
10912 "four", //
10913 "", //
10914 ]
10915 .join("\n"),
10916 );
10917
10918 // Undoing the formatting undoes the trailing whitespace removal, the
10919 // trailing newline, and the LSP edits.
10920 cx.update_buffer(|buffer, cx| buffer.undo(cx));
10921 cx.assert_editor_state(
10922 &[
10923 "one ", //
10924 "twoˇ", //
10925 "three ", //
10926 "four", //
10927 ]
10928 .join("\n"),
10929 );
10930}
10931
10932#[gpui::test]
10933async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
10934 cx: &mut TestAppContext,
10935) {
10936 init_test(cx, |_| {});
10937
10938 cx.update(|cx| {
10939 cx.update_global::<SettingsStore, _>(|settings, cx| {
10940 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10941 settings.auto_signature_help = Some(true);
10942 });
10943 });
10944 });
10945
10946 let mut cx = EditorLspTestContext::new_rust(
10947 lsp::ServerCapabilities {
10948 signature_help_provider: Some(lsp::SignatureHelpOptions {
10949 ..Default::default()
10950 }),
10951 ..Default::default()
10952 },
10953 cx,
10954 )
10955 .await;
10956
10957 let language = Language::new(
10958 LanguageConfig {
10959 name: "Rust".into(),
10960 brackets: BracketPairConfig {
10961 pairs: vec![
10962 BracketPair {
10963 start: "{".to_string(),
10964 end: "}".to_string(),
10965 close: true,
10966 surround: true,
10967 newline: true,
10968 },
10969 BracketPair {
10970 start: "(".to_string(),
10971 end: ")".to_string(),
10972 close: true,
10973 surround: true,
10974 newline: true,
10975 },
10976 BracketPair {
10977 start: "/*".to_string(),
10978 end: " */".to_string(),
10979 close: true,
10980 surround: true,
10981 newline: true,
10982 },
10983 BracketPair {
10984 start: "[".to_string(),
10985 end: "]".to_string(),
10986 close: false,
10987 surround: false,
10988 newline: true,
10989 },
10990 BracketPair {
10991 start: "\"".to_string(),
10992 end: "\"".to_string(),
10993 close: true,
10994 surround: true,
10995 newline: false,
10996 },
10997 BracketPair {
10998 start: "<".to_string(),
10999 end: ">".to_string(),
11000 close: false,
11001 surround: true,
11002 newline: true,
11003 },
11004 ],
11005 ..Default::default()
11006 },
11007 autoclose_before: "})]".to_string(),
11008 ..Default::default()
11009 },
11010 Some(tree_sitter_rust::LANGUAGE.into()),
11011 );
11012 let language = Arc::new(language);
11013
11014 cx.language_registry().add(language.clone());
11015 cx.update_buffer(|buffer, cx| {
11016 buffer.set_language(Some(language), cx);
11017 });
11018
11019 cx.set_state(
11020 &r#"
11021 fn main() {
11022 sampleˇ
11023 }
11024 "#
11025 .unindent(),
11026 );
11027
11028 cx.update_editor(|editor, window, cx| {
11029 editor.handle_input("(", window, cx);
11030 });
11031 cx.assert_editor_state(
11032 &"
11033 fn main() {
11034 sample(ˇ)
11035 }
11036 "
11037 .unindent(),
11038 );
11039
11040 let mocked_response = lsp::SignatureHelp {
11041 signatures: vec![lsp::SignatureInformation {
11042 label: "fn sample(param1: u8, param2: u8)".to_string(),
11043 documentation: None,
11044 parameters: Some(vec![
11045 lsp::ParameterInformation {
11046 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11047 documentation: None,
11048 },
11049 lsp::ParameterInformation {
11050 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11051 documentation: None,
11052 },
11053 ]),
11054 active_parameter: None,
11055 }],
11056 active_signature: Some(0),
11057 active_parameter: Some(0),
11058 };
11059 handle_signature_help_request(&mut cx, mocked_response).await;
11060
11061 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11062 .await;
11063
11064 cx.editor(|editor, _, _| {
11065 let signature_help_state = editor.signature_help_state.popover().cloned();
11066 let signature = signature_help_state.unwrap();
11067 assert_eq!(
11068 signature.signatures[signature.current_signature].label,
11069 "fn sample(param1: u8, param2: u8)"
11070 );
11071 });
11072}
11073
11074#[gpui::test]
11075async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
11076 init_test(cx, |_| {});
11077
11078 cx.update(|cx| {
11079 cx.update_global::<SettingsStore, _>(|settings, cx| {
11080 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11081 settings.auto_signature_help = Some(false);
11082 settings.show_signature_help_after_edits = Some(false);
11083 });
11084 });
11085 });
11086
11087 let mut cx = EditorLspTestContext::new_rust(
11088 lsp::ServerCapabilities {
11089 signature_help_provider: Some(lsp::SignatureHelpOptions {
11090 ..Default::default()
11091 }),
11092 ..Default::default()
11093 },
11094 cx,
11095 )
11096 .await;
11097
11098 let language = Language::new(
11099 LanguageConfig {
11100 name: "Rust".into(),
11101 brackets: BracketPairConfig {
11102 pairs: vec![
11103 BracketPair {
11104 start: "{".to_string(),
11105 end: "}".to_string(),
11106 close: true,
11107 surround: true,
11108 newline: true,
11109 },
11110 BracketPair {
11111 start: "(".to_string(),
11112 end: ")".to_string(),
11113 close: true,
11114 surround: true,
11115 newline: true,
11116 },
11117 BracketPair {
11118 start: "/*".to_string(),
11119 end: " */".to_string(),
11120 close: true,
11121 surround: true,
11122 newline: true,
11123 },
11124 BracketPair {
11125 start: "[".to_string(),
11126 end: "]".to_string(),
11127 close: false,
11128 surround: false,
11129 newline: true,
11130 },
11131 BracketPair {
11132 start: "\"".to_string(),
11133 end: "\"".to_string(),
11134 close: true,
11135 surround: true,
11136 newline: false,
11137 },
11138 BracketPair {
11139 start: "<".to_string(),
11140 end: ">".to_string(),
11141 close: false,
11142 surround: true,
11143 newline: true,
11144 },
11145 ],
11146 ..Default::default()
11147 },
11148 autoclose_before: "})]".to_string(),
11149 ..Default::default()
11150 },
11151 Some(tree_sitter_rust::LANGUAGE.into()),
11152 );
11153 let language = Arc::new(language);
11154
11155 cx.language_registry().add(language.clone());
11156 cx.update_buffer(|buffer, cx| {
11157 buffer.set_language(Some(language), cx);
11158 });
11159
11160 // Ensure that signature_help is not called when no signature help is enabled.
11161 cx.set_state(
11162 &r#"
11163 fn main() {
11164 sampleˇ
11165 }
11166 "#
11167 .unindent(),
11168 );
11169 cx.update_editor(|editor, window, cx| {
11170 editor.handle_input("(", window, cx);
11171 });
11172 cx.assert_editor_state(
11173 &"
11174 fn main() {
11175 sample(ˇ)
11176 }
11177 "
11178 .unindent(),
11179 );
11180 cx.editor(|editor, _, _| {
11181 assert!(editor.signature_help_state.task().is_none());
11182 });
11183
11184 let mocked_response = lsp::SignatureHelp {
11185 signatures: vec![lsp::SignatureInformation {
11186 label: "fn sample(param1: u8, param2: u8)".to_string(),
11187 documentation: None,
11188 parameters: Some(vec![
11189 lsp::ParameterInformation {
11190 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11191 documentation: None,
11192 },
11193 lsp::ParameterInformation {
11194 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11195 documentation: None,
11196 },
11197 ]),
11198 active_parameter: None,
11199 }],
11200 active_signature: Some(0),
11201 active_parameter: Some(0),
11202 };
11203
11204 // Ensure that signature_help is called when enabled afte edits
11205 cx.update(|_, cx| {
11206 cx.update_global::<SettingsStore, _>(|settings, cx| {
11207 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11208 settings.auto_signature_help = Some(false);
11209 settings.show_signature_help_after_edits = Some(true);
11210 });
11211 });
11212 });
11213 cx.set_state(
11214 &r#"
11215 fn main() {
11216 sampleˇ
11217 }
11218 "#
11219 .unindent(),
11220 );
11221 cx.update_editor(|editor, window, cx| {
11222 editor.handle_input("(", window, cx);
11223 });
11224 cx.assert_editor_state(
11225 &"
11226 fn main() {
11227 sample(ˇ)
11228 }
11229 "
11230 .unindent(),
11231 );
11232 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11233 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11234 .await;
11235 cx.update_editor(|editor, _, _| {
11236 let signature_help_state = editor.signature_help_state.popover().cloned();
11237 assert!(signature_help_state.is_some());
11238 let signature = signature_help_state.unwrap();
11239 assert_eq!(
11240 signature.signatures[signature.current_signature].label,
11241 "fn sample(param1: u8, param2: u8)"
11242 );
11243 editor.signature_help_state = SignatureHelpState::default();
11244 });
11245
11246 // Ensure that signature_help is called when auto signature help override is enabled
11247 cx.update(|_, cx| {
11248 cx.update_global::<SettingsStore, _>(|settings, cx| {
11249 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11250 settings.auto_signature_help = Some(true);
11251 settings.show_signature_help_after_edits = Some(false);
11252 });
11253 });
11254 });
11255 cx.set_state(
11256 &r#"
11257 fn main() {
11258 sampleˇ
11259 }
11260 "#
11261 .unindent(),
11262 );
11263 cx.update_editor(|editor, window, cx| {
11264 editor.handle_input("(", window, cx);
11265 });
11266 cx.assert_editor_state(
11267 &"
11268 fn main() {
11269 sample(ˇ)
11270 }
11271 "
11272 .unindent(),
11273 );
11274 handle_signature_help_request(&mut cx, mocked_response).await;
11275 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11276 .await;
11277 cx.editor(|editor, _, _| {
11278 let signature_help_state = editor.signature_help_state.popover().cloned();
11279 assert!(signature_help_state.is_some());
11280 let signature = signature_help_state.unwrap();
11281 assert_eq!(
11282 signature.signatures[signature.current_signature].label,
11283 "fn sample(param1: u8, param2: u8)"
11284 );
11285 });
11286}
11287
11288#[gpui::test]
11289async fn test_signature_help(cx: &mut TestAppContext) {
11290 init_test(cx, |_| {});
11291 cx.update(|cx| {
11292 cx.update_global::<SettingsStore, _>(|settings, cx| {
11293 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11294 settings.auto_signature_help = Some(true);
11295 });
11296 });
11297 });
11298
11299 let mut cx = EditorLspTestContext::new_rust(
11300 lsp::ServerCapabilities {
11301 signature_help_provider: Some(lsp::SignatureHelpOptions {
11302 ..Default::default()
11303 }),
11304 ..Default::default()
11305 },
11306 cx,
11307 )
11308 .await;
11309
11310 // A test that directly calls `show_signature_help`
11311 cx.update_editor(|editor, window, cx| {
11312 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11313 });
11314
11315 let mocked_response = lsp::SignatureHelp {
11316 signatures: vec![lsp::SignatureInformation {
11317 label: "fn sample(param1: u8, param2: u8)".to_string(),
11318 documentation: None,
11319 parameters: Some(vec![
11320 lsp::ParameterInformation {
11321 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11322 documentation: None,
11323 },
11324 lsp::ParameterInformation {
11325 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11326 documentation: None,
11327 },
11328 ]),
11329 active_parameter: None,
11330 }],
11331 active_signature: Some(0),
11332 active_parameter: Some(0),
11333 };
11334 handle_signature_help_request(&mut cx, mocked_response).await;
11335
11336 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11337 .await;
11338
11339 cx.editor(|editor, _, _| {
11340 let signature_help_state = editor.signature_help_state.popover().cloned();
11341 assert!(signature_help_state.is_some());
11342 let signature = signature_help_state.unwrap();
11343 assert_eq!(
11344 signature.signatures[signature.current_signature].label,
11345 "fn sample(param1: u8, param2: u8)"
11346 );
11347 });
11348
11349 // When exiting outside from inside the brackets, `signature_help` is closed.
11350 cx.set_state(indoc! {"
11351 fn main() {
11352 sample(ˇ);
11353 }
11354
11355 fn sample(param1: u8, param2: u8) {}
11356 "});
11357
11358 cx.update_editor(|editor, window, cx| {
11359 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11360 s.select_ranges([0..0])
11361 });
11362 });
11363
11364 let mocked_response = lsp::SignatureHelp {
11365 signatures: Vec::new(),
11366 active_signature: None,
11367 active_parameter: None,
11368 };
11369 handle_signature_help_request(&mut cx, mocked_response).await;
11370
11371 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
11372 .await;
11373
11374 cx.editor(|editor, _, _| {
11375 assert!(!editor.signature_help_state.is_shown());
11376 });
11377
11378 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
11379 cx.set_state(indoc! {"
11380 fn main() {
11381 sample(ˇ);
11382 }
11383
11384 fn sample(param1: u8, param2: u8) {}
11385 "});
11386
11387 let mocked_response = lsp::SignatureHelp {
11388 signatures: vec![lsp::SignatureInformation {
11389 label: "fn sample(param1: u8, param2: u8)".to_string(),
11390 documentation: None,
11391 parameters: Some(vec![
11392 lsp::ParameterInformation {
11393 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11394 documentation: None,
11395 },
11396 lsp::ParameterInformation {
11397 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11398 documentation: None,
11399 },
11400 ]),
11401 active_parameter: None,
11402 }],
11403 active_signature: Some(0),
11404 active_parameter: Some(0),
11405 };
11406 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11407 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11408 .await;
11409 cx.editor(|editor, _, _| {
11410 assert!(editor.signature_help_state.is_shown());
11411 });
11412
11413 // Restore the popover with more parameter input
11414 cx.set_state(indoc! {"
11415 fn main() {
11416 sample(param1, param2ˇ);
11417 }
11418
11419 fn sample(param1: u8, param2: u8) {}
11420 "});
11421
11422 let mocked_response = lsp::SignatureHelp {
11423 signatures: vec![lsp::SignatureInformation {
11424 label: "fn sample(param1: u8, param2: u8)".to_string(),
11425 documentation: None,
11426 parameters: Some(vec![
11427 lsp::ParameterInformation {
11428 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11429 documentation: None,
11430 },
11431 lsp::ParameterInformation {
11432 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11433 documentation: None,
11434 },
11435 ]),
11436 active_parameter: None,
11437 }],
11438 active_signature: Some(0),
11439 active_parameter: Some(1),
11440 };
11441 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11442 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11443 .await;
11444
11445 // When selecting a range, the popover is gone.
11446 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
11447 cx.update_editor(|editor, window, cx| {
11448 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11449 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
11450 })
11451 });
11452 cx.assert_editor_state(indoc! {"
11453 fn main() {
11454 sample(param1, «ˇparam2»);
11455 }
11456
11457 fn sample(param1: u8, param2: u8) {}
11458 "});
11459 cx.editor(|editor, _, _| {
11460 assert!(!editor.signature_help_state.is_shown());
11461 });
11462
11463 // When unselecting again, the popover is back if within the brackets.
11464 cx.update_editor(|editor, window, cx| {
11465 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11466 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11467 })
11468 });
11469 cx.assert_editor_state(indoc! {"
11470 fn main() {
11471 sample(param1, ˇparam2);
11472 }
11473
11474 fn sample(param1: u8, param2: u8) {}
11475 "});
11476 handle_signature_help_request(&mut cx, mocked_response).await;
11477 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11478 .await;
11479 cx.editor(|editor, _, _| {
11480 assert!(editor.signature_help_state.is_shown());
11481 });
11482
11483 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
11484 cx.update_editor(|editor, window, cx| {
11485 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11486 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
11487 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11488 })
11489 });
11490 cx.assert_editor_state(indoc! {"
11491 fn main() {
11492 sample(param1, ˇparam2);
11493 }
11494
11495 fn sample(param1: u8, param2: u8) {}
11496 "});
11497
11498 let mocked_response = lsp::SignatureHelp {
11499 signatures: vec![lsp::SignatureInformation {
11500 label: "fn sample(param1: u8, param2: u8)".to_string(),
11501 documentation: None,
11502 parameters: Some(vec![
11503 lsp::ParameterInformation {
11504 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11505 documentation: None,
11506 },
11507 lsp::ParameterInformation {
11508 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11509 documentation: None,
11510 },
11511 ]),
11512 active_parameter: None,
11513 }],
11514 active_signature: Some(0),
11515 active_parameter: Some(1),
11516 };
11517 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11518 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11519 .await;
11520 cx.update_editor(|editor, _, cx| {
11521 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
11522 });
11523 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
11524 .await;
11525 cx.update_editor(|editor, window, cx| {
11526 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11527 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
11528 })
11529 });
11530 cx.assert_editor_state(indoc! {"
11531 fn main() {
11532 sample(param1, «ˇparam2»);
11533 }
11534
11535 fn sample(param1: u8, param2: u8) {}
11536 "});
11537 cx.update_editor(|editor, window, cx| {
11538 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11539 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11540 })
11541 });
11542 cx.assert_editor_state(indoc! {"
11543 fn main() {
11544 sample(param1, ˇparam2);
11545 }
11546
11547 fn sample(param1: u8, param2: u8) {}
11548 "});
11549 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
11550 .await;
11551}
11552
11553#[gpui::test]
11554async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
11555 init_test(cx, |_| {});
11556
11557 let mut cx = EditorLspTestContext::new_rust(
11558 lsp::ServerCapabilities {
11559 signature_help_provider: Some(lsp::SignatureHelpOptions {
11560 ..Default::default()
11561 }),
11562 ..Default::default()
11563 },
11564 cx,
11565 )
11566 .await;
11567
11568 cx.set_state(indoc! {"
11569 fn main() {
11570 overloadedˇ
11571 }
11572 "});
11573
11574 cx.update_editor(|editor, window, cx| {
11575 editor.handle_input("(", window, cx);
11576 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11577 });
11578
11579 // Mock response with 3 signatures
11580 let mocked_response = lsp::SignatureHelp {
11581 signatures: vec![
11582 lsp::SignatureInformation {
11583 label: "fn overloaded(x: i32)".to_string(),
11584 documentation: None,
11585 parameters: Some(vec![lsp::ParameterInformation {
11586 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11587 documentation: None,
11588 }]),
11589 active_parameter: None,
11590 },
11591 lsp::SignatureInformation {
11592 label: "fn overloaded(x: i32, y: i32)".to_string(),
11593 documentation: None,
11594 parameters: Some(vec![
11595 lsp::ParameterInformation {
11596 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11597 documentation: None,
11598 },
11599 lsp::ParameterInformation {
11600 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
11601 documentation: None,
11602 },
11603 ]),
11604 active_parameter: None,
11605 },
11606 lsp::SignatureInformation {
11607 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
11608 documentation: None,
11609 parameters: Some(vec![
11610 lsp::ParameterInformation {
11611 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11612 documentation: None,
11613 },
11614 lsp::ParameterInformation {
11615 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
11616 documentation: None,
11617 },
11618 lsp::ParameterInformation {
11619 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
11620 documentation: None,
11621 },
11622 ]),
11623 active_parameter: None,
11624 },
11625 ],
11626 active_signature: Some(1),
11627 active_parameter: Some(0),
11628 };
11629 handle_signature_help_request(&mut cx, mocked_response).await;
11630
11631 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11632 .await;
11633
11634 // Verify we have multiple signatures and the right one is selected
11635 cx.editor(|editor, _, _| {
11636 let popover = editor.signature_help_state.popover().cloned().unwrap();
11637 assert_eq!(popover.signatures.len(), 3);
11638 // active_signature was 1, so that should be the current
11639 assert_eq!(popover.current_signature, 1);
11640 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
11641 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
11642 assert_eq!(
11643 popover.signatures[2].label,
11644 "fn overloaded(x: i32, y: i32, z: i32)"
11645 );
11646 });
11647
11648 // Test navigation functionality
11649 cx.update_editor(|editor, window, cx| {
11650 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
11651 });
11652
11653 cx.editor(|editor, _, _| {
11654 let popover = editor.signature_help_state.popover().cloned().unwrap();
11655 assert_eq!(popover.current_signature, 2);
11656 });
11657
11658 // Test wrap around
11659 cx.update_editor(|editor, window, cx| {
11660 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
11661 });
11662
11663 cx.editor(|editor, _, _| {
11664 let popover = editor.signature_help_state.popover().cloned().unwrap();
11665 assert_eq!(popover.current_signature, 0);
11666 });
11667
11668 // Test previous navigation
11669 cx.update_editor(|editor, window, cx| {
11670 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
11671 });
11672
11673 cx.editor(|editor, _, _| {
11674 let popover = editor.signature_help_state.popover().cloned().unwrap();
11675 assert_eq!(popover.current_signature, 2);
11676 });
11677}
11678
11679#[gpui::test]
11680async fn test_completion_mode(cx: &mut TestAppContext) {
11681 init_test(cx, |_| {});
11682 let mut cx = EditorLspTestContext::new_rust(
11683 lsp::ServerCapabilities {
11684 completion_provider: Some(lsp::CompletionOptions {
11685 resolve_provider: Some(true),
11686 ..Default::default()
11687 }),
11688 ..Default::default()
11689 },
11690 cx,
11691 )
11692 .await;
11693
11694 struct Run {
11695 run_description: &'static str,
11696 initial_state: String,
11697 buffer_marked_text: String,
11698 completion_label: &'static str,
11699 completion_text: &'static str,
11700 expected_with_insert_mode: String,
11701 expected_with_replace_mode: String,
11702 expected_with_replace_subsequence_mode: String,
11703 expected_with_replace_suffix_mode: String,
11704 }
11705
11706 let runs = [
11707 Run {
11708 run_description: "Start of word matches completion text",
11709 initial_state: "before ediˇ after".into(),
11710 buffer_marked_text: "before <edi|> after".into(),
11711 completion_label: "editor",
11712 completion_text: "editor",
11713 expected_with_insert_mode: "before editorˇ after".into(),
11714 expected_with_replace_mode: "before editorˇ after".into(),
11715 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11716 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11717 },
11718 Run {
11719 run_description: "Accept same text at the middle of the word",
11720 initial_state: "before ediˇtor after".into(),
11721 buffer_marked_text: "before <edi|tor> after".into(),
11722 completion_label: "editor",
11723 completion_text: "editor",
11724 expected_with_insert_mode: "before editorˇtor after".into(),
11725 expected_with_replace_mode: "before editorˇ after".into(),
11726 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11727 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11728 },
11729 Run {
11730 run_description: "End of word matches completion text -- cursor at end",
11731 initial_state: "before torˇ after".into(),
11732 buffer_marked_text: "before <tor|> after".into(),
11733 completion_label: "editor",
11734 completion_text: "editor",
11735 expected_with_insert_mode: "before editorˇ after".into(),
11736 expected_with_replace_mode: "before editorˇ after".into(),
11737 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11738 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11739 },
11740 Run {
11741 run_description: "End of word matches completion text -- cursor at start",
11742 initial_state: "before ˇtor after".into(),
11743 buffer_marked_text: "before <|tor> after".into(),
11744 completion_label: "editor",
11745 completion_text: "editor",
11746 expected_with_insert_mode: "before editorˇtor after".into(),
11747 expected_with_replace_mode: "before editorˇ after".into(),
11748 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11749 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11750 },
11751 Run {
11752 run_description: "Prepend text containing whitespace",
11753 initial_state: "pˇfield: bool".into(),
11754 buffer_marked_text: "<p|field>: bool".into(),
11755 completion_label: "pub ",
11756 completion_text: "pub ",
11757 expected_with_insert_mode: "pub ˇfield: bool".into(),
11758 expected_with_replace_mode: "pub ˇ: bool".into(),
11759 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
11760 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
11761 },
11762 Run {
11763 run_description: "Add element to start of list",
11764 initial_state: "[element_ˇelement_2]".into(),
11765 buffer_marked_text: "[<element_|element_2>]".into(),
11766 completion_label: "element_1",
11767 completion_text: "element_1",
11768 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
11769 expected_with_replace_mode: "[element_1ˇ]".into(),
11770 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
11771 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
11772 },
11773 Run {
11774 run_description: "Add element to start of list -- first and second elements are equal",
11775 initial_state: "[elˇelement]".into(),
11776 buffer_marked_text: "[<el|element>]".into(),
11777 completion_label: "element",
11778 completion_text: "element",
11779 expected_with_insert_mode: "[elementˇelement]".into(),
11780 expected_with_replace_mode: "[elementˇ]".into(),
11781 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
11782 expected_with_replace_suffix_mode: "[elementˇ]".into(),
11783 },
11784 Run {
11785 run_description: "Ends with matching suffix",
11786 initial_state: "SubˇError".into(),
11787 buffer_marked_text: "<Sub|Error>".into(),
11788 completion_label: "SubscriptionError",
11789 completion_text: "SubscriptionError",
11790 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
11791 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11792 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11793 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
11794 },
11795 Run {
11796 run_description: "Suffix is a subsequence -- contiguous",
11797 initial_state: "SubˇErr".into(),
11798 buffer_marked_text: "<Sub|Err>".into(),
11799 completion_label: "SubscriptionError",
11800 completion_text: "SubscriptionError",
11801 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
11802 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11803 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11804 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
11805 },
11806 Run {
11807 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
11808 initial_state: "Suˇscrirr".into(),
11809 buffer_marked_text: "<Su|scrirr>".into(),
11810 completion_label: "SubscriptionError",
11811 completion_text: "SubscriptionError",
11812 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
11813 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11814 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11815 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
11816 },
11817 Run {
11818 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
11819 initial_state: "foo(indˇix)".into(),
11820 buffer_marked_text: "foo(<ind|ix>)".into(),
11821 completion_label: "node_index",
11822 completion_text: "node_index",
11823 expected_with_insert_mode: "foo(node_indexˇix)".into(),
11824 expected_with_replace_mode: "foo(node_indexˇ)".into(),
11825 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
11826 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
11827 },
11828 Run {
11829 run_description: "Replace range ends before cursor - should extend to cursor",
11830 initial_state: "before editˇo after".into(),
11831 buffer_marked_text: "before <{ed}>it|o after".into(),
11832 completion_label: "editor",
11833 completion_text: "editor",
11834 expected_with_insert_mode: "before editorˇo after".into(),
11835 expected_with_replace_mode: "before editorˇo after".into(),
11836 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
11837 expected_with_replace_suffix_mode: "before editorˇo after".into(),
11838 },
11839 Run {
11840 run_description: "Uses label for suffix matching",
11841 initial_state: "before ediˇtor after".into(),
11842 buffer_marked_text: "before <edi|tor> after".into(),
11843 completion_label: "editor",
11844 completion_text: "editor()",
11845 expected_with_insert_mode: "before editor()ˇtor after".into(),
11846 expected_with_replace_mode: "before editor()ˇ after".into(),
11847 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
11848 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
11849 },
11850 Run {
11851 run_description: "Case insensitive subsequence and suffix matching",
11852 initial_state: "before EDiˇtoR after".into(),
11853 buffer_marked_text: "before <EDi|toR> after".into(),
11854 completion_label: "editor",
11855 completion_text: "editor",
11856 expected_with_insert_mode: "before editorˇtoR after".into(),
11857 expected_with_replace_mode: "before editorˇ after".into(),
11858 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11859 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11860 },
11861 ];
11862
11863 for run in runs {
11864 let run_variations = [
11865 (LspInsertMode::Insert, run.expected_with_insert_mode),
11866 (LspInsertMode::Replace, run.expected_with_replace_mode),
11867 (
11868 LspInsertMode::ReplaceSubsequence,
11869 run.expected_with_replace_subsequence_mode,
11870 ),
11871 (
11872 LspInsertMode::ReplaceSuffix,
11873 run.expected_with_replace_suffix_mode,
11874 ),
11875 ];
11876
11877 for (lsp_insert_mode, expected_text) in run_variations {
11878 eprintln!(
11879 "run = {:?}, mode = {lsp_insert_mode:.?}",
11880 run.run_description,
11881 );
11882
11883 update_test_language_settings(&mut cx, |settings| {
11884 settings.defaults.completions = Some(CompletionSettings {
11885 lsp_insert_mode,
11886 words: WordsCompletionMode::Disabled,
11887 lsp: true,
11888 lsp_fetch_timeout_ms: 0,
11889 });
11890 });
11891
11892 cx.set_state(&run.initial_state);
11893 cx.update_editor(|editor, window, cx| {
11894 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11895 });
11896
11897 let counter = Arc::new(AtomicUsize::new(0));
11898 handle_completion_request_with_insert_and_replace(
11899 &mut cx,
11900 &run.buffer_marked_text,
11901 vec![(run.completion_label, run.completion_text)],
11902 counter.clone(),
11903 )
11904 .await;
11905 cx.condition(|editor, _| editor.context_menu_visible())
11906 .await;
11907 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11908
11909 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11910 editor
11911 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11912 .unwrap()
11913 });
11914 cx.assert_editor_state(&expected_text);
11915 handle_resolve_completion_request(&mut cx, None).await;
11916 apply_additional_edits.await.unwrap();
11917 }
11918 }
11919}
11920
11921#[gpui::test]
11922async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
11923 init_test(cx, |_| {});
11924 let mut cx = EditorLspTestContext::new_rust(
11925 lsp::ServerCapabilities {
11926 completion_provider: Some(lsp::CompletionOptions {
11927 resolve_provider: Some(true),
11928 ..Default::default()
11929 }),
11930 ..Default::default()
11931 },
11932 cx,
11933 )
11934 .await;
11935
11936 let initial_state = "SubˇError";
11937 let buffer_marked_text = "<Sub|Error>";
11938 let completion_text = "SubscriptionError";
11939 let expected_with_insert_mode = "SubscriptionErrorˇError";
11940 let expected_with_replace_mode = "SubscriptionErrorˇ";
11941
11942 update_test_language_settings(&mut cx, |settings| {
11943 settings.defaults.completions = Some(CompletionSettings {
11944 words: WordsCompletionMode::Disabled,
11945 // set the opposite here to ensure that the action is overriding the default behavior
11946 lsp_insert_mode: LspInsertMode::Insert,
11947 lsp: true,
11948 lsp_fetch_timeout_ms: 0,
11949 });
11950 });
11951
11952 cx.set_state(initial_state);
11953 cx.update_editor(|editor, window, cx| {
11954 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11955 });
11956
11957 let counter = Arc::new(AtomicUsize::new(0));
11958 handle_completion_request_with_insert_and_replace(
11959 &mut cx,
11960 &buffer_marked_text,
11961 vec![(completion_text, completion_text)],
11962 counter.clone(),
11963 )
11964 .await;
11965 cx.condition(|editor, _| editor.context_menu_visible())
11966 .await;
11967 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11968
11969 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11970 editor
11971 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11972 .unwrap()
11973 });
11974 cx.assert_editor_state(&expected_with_replace_mode);
11975 handle_resolve_completion_request(&mut cx, None).await;
11976 apply_additional_edits.await.unwrap();
11977
11978 update_test_language_settings(&mut cx, |settings| {
11979 settings.defaults.completions = Some(CompletionSettings {
11980 words: WordsCompletionMode::Disabled,
11981 // set the opposite here to ensure that the action is overriding the default behavior
11982 lsp_insert_mode: LspInsertMode::Replace,
11983 lsp: true,
11984 lsp_fetch_timeout_ms: 0,
11985 });
11986 });
11987
11988 cx.set_state(initial_state);
11989 cx.update_editor(|editor, window, cx| {
11990 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11991 });
11992 handle_completion_request_with_insert_and_replace(
11993 &mut cx,
11994 &buffer_marked_text,
11995 vec![(completion_text, completion_text)],
11996 counter.clone(),
11997 )
11998 .await;
11999 cx.condition(|editor, _| editor.context_menu_visible())
12000 .await;
12001 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12002
12003 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12004 editor
12005 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
12006 .unwrap()
12007 });
12008 cx.assert_editor_state(&expected_with_insert_mode);
12009 handle_resolve_completion_request(&mut cx, None).await;
12010 apply_additional_edits.await.unwrap();
12011}
12012
12013#[gpui::test]
12014async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
12015 init_test(cx, |_| {});
12016 let mut cx = EditorLspTestContext::new_rust(
12017 lsp::ServerCapabilities {
12018 completion_provider: Some(lsp::CompletionOptions {
12019 resolve_provider: Some(true),
12020 ..Default::default()
12021 }),
12022 ..Default::default()
12023 },
12024 cx,
12025 )
12026 .await;
12027
12028 // scenario: surrounding text matches completion text
12029 let completion_text = "to_offset";
12030 let initial_state = indoc! {"
12031 1. buf.to_offˇsuffix
12032 2. buf.to_offˇsuf
12033 3. buf.to_offˇfix
12034 4. buf.to_offˇ
12035 5. into_offˇensive
12036 6. ˇsuffix
12037 7. let ˇ //
12038 8. aaˇzz
12039 9. buf.to_off«zzzzzˇ»suffix
12040 10. buf.«ˇzzzzz»suffix
12041 11. to_off«ˇzzzzz»
12042
12043 buf.to_offˇsuffix // newest cursor
12044 "};
12045 let completion_marked_buffer = indoc! {"
12046 1. buf.to_offsuffix
12047 2. buf.to_offsuf
12048 3. buf.to_offfix
12049 4. buf.to_off
12050 5. into_offensive
12051 6. suffix
12052 7. let //
12053 8. aazz
12054 9. buf.to_offzzzzzsuffix
12055 10. buf.zzzzzsuffix
12056 11. to_offzzzzz
12057
12058 buf.<to_off|suffix> // newest cursor
12059 "};
12060 let expected = indoc! {"
12061 1. buf.to_offsetˇ
12062 2. buf.to_offsetˇsuf
12063 3. buf.to_offsetˇfix
12064 4. buf.to_offsetˇ
12065 5. into_offsetˇensive
12066 6. to_offsetˇsuffix
12067 7. let to_offsetˇ //
12068 8. aato_offsetˇzz
12069 9. buf.to_offsetˇ
12070 10. buf.to_offsetˇsuffix
12071 11. to_offsetˇ
12072
12073 buf.to_offsetˇ // newest cursor
12074 "};
12075 cx.set_state(initial_state);
12076 cx.update_editor(|editor, window, cx| {
12077 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12078 });
12079 handle_completion_request_with_insert_and_replace(
12080 &mut cx,
12081 completion_marked_buffer,
12082 vec![(completion_text, completion_text)],
12083 Arc::new(AtomicUsize::new(0)),
12084 )
12085 .await;
12086 cx.condition(|editor, _| editor.context_menu_visible())
12087 .await;
12088 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12089 editor
12090 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12091 .unwrap()
12092 });
12093 cx.assert_editor_state(expected);
12094 handle_resolve_completion_request(&mut cx, None).await;
12095 apply_additional_edits.await.unwrap();
12096
12097 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
12098 let completion_text = "foo_and_bar";
12099 let initial_state = indoc! {"
12100 1. ooanbˇ
12101 2. zooanbˇ
12102 3. ooanbˇz
12103 4. zooanbˇz
12104 5. ooanˇ
12105 6. oanbˇ
12106
12107 ooanbˇ
12108 "};
12109 let completion_marked_buffer = indoc! {"
12110 1. ooanb
12111 2. zooanb
12112 3. ooanbz
12113 4. zooanbz
12114 5. ooan
12115 6. oanb
12116
12117 <ooanb|>
12118 "};
12119 let expected = indoc! {"
12120 1. foo_and_barˇ
12121 2. zfoo_and_barˇ
12122 3. foo_and_barˇz
12123 4. zfoo_and_barˇz
12124 5. ooanfoo_and_barˇ
12125 6. oanbfoo_and_barˇ
12126
12127 foo_and_barˇ
12128 "};
12129 cx.set_state(initial_state);
12130 cx.update_editor(|editor, window, cx| {
12131 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12132 });
12133 handle_completion_request_with_insert_and_replace(
12134 &mut cx,
12135 completion_marked_buffer,
12136 vec![(completion_text, completion_text)],
12137 Arc::new(AtomicUsize::new(0)),
12138 )
12139 .await;
12140 cx.condition(|editor, _| editor.context_menu_visible())
12141 .await;
12142 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12143 editor
12144 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12145 .unwrap()
12146 });
12147 cx.assert_editor_state(expected);
12148 handle_resolve_completion_request(&mut cx, None).await;
12149 apply_additional_edits.await.unwrap();
12150
12151 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
12152 // (expects the same as if it was inserted at the end)
12153 let completion_text = "foo_and_bar";
12154 let initial_state = indoc! {"
12155 1. ooˇanb
12156 2. zooˇanb
12157 3. ooˇanbz
12158 4. zooˇanbz
12159
12160 ooˇanb
12161 "};
12162 let completion_marked_buffer = indoc! {"
12163 1. ooanb
12164 2. zooanb
12165 3. ooanbz
12166 4. zooanbz
12167
12168 <oo|anb>
12169 "};
12170 let expected = indoc! {"
12171 1. foo_and_barˇ
12172 2. zfoo_and_barˇ
12173 3. foo_and_barˇz
12174 4. zfoo_and_barˇz
12175
12176 foo_and_barˇ
12177 "};
12178 cx.set_state(initial_state);
12179 cx.update_editor(|editor, window, cx| {
12180 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12181 });
12182 handle_completion_request_with_insert_and_replace(
12183 &mut cx,
12184 completion_marked_buffer,
12185 vec![(completion_text, completion_text)],
12186 Arc::new(AtomicUsize::new(0)),
12187 )
12188 .await;
12189 cx.condition(|editor, _| editor.context_menu_visible())
12190 .await;
12191 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12192 editor
12193 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12194 .unwrap()
12195 });
12196 cx.assert_editor_state(expected);
12197 handle_resolve_completion_request(&mut cx, None).await;
12198 apply_additional_edits.await.unwrap();
12199}
12200
12201// This used to crash
12202#[gpui::test]
12203async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
12204 init_test(cx, |_| {});
12205
12206 let buffer_text = indoc! {"
12207 fn main() {
12208 10.satu;
12209
12210 //
12211 // separate cursors so they open in different excerpts (manually reproducible)
12212 //
12213
12214 10.satu20;
12215 }
12216 "};
12217 let multibuffer_text_with_selections = indoc! {"
12218 fn main() {
12219 10.satuˇ;
12220
12221 //
12222
12223 //
12224
12225 10.satuˇ20;
12226 }
12227 "};
12228 let expected_multibuffer = indoc! {"
12229 fn main() {
12230 10.saturating_sub()ˇ;
12231
12232 //
12233
12234 //
12235
12236 10.saturating_sub()ˇ;
12237 }
12238 "};
12239
12240 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
12241 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
12242
12243 let fs = FakeFs::new(cx.executor());
12244 fs.insert_tree(
12245 path!("/a"),
12246 json!({
12247 "main.rs": buffer_text,
12248 }),
12249 )
12250 .await;
12251
12252 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12253 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12254 language_registry.add(rust_lang());
12255 let mut fake_servers = language_registry.register_fake_lsp(
12256 "Rust",
12257 FakeLspAdapter {
12258 capabilities: lsp::ServerCapabilities {
12259 completion_provider: Some(lsp::CompletionOptions {
12260 resolve_provider: None,
12261 ..lsp::CompletionOptions::default()
12262 }),
12263 ..lsp::ServerCapabilities::default()
12264 },
12265 ..FakeLspAdapter::default()
12266 },
12267 );
12268 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12269 let cx = &mut VisualTestContext::from_window(*workspace, cx);
12270 let buffer = project
12271 .update(cx, |project, cx| {
12272 project.open_local_buffer(path!("/a/main.rs"), cx)
12273 })
12274 .await
12275 .unwrap();
12276
12277 let multi_buffer = cx.new(|cx| {
12278 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
12279 multi_buffer.push_excerpts(
12280 buffer.clone(),
12281 [ExcerptRange::new(0..first_excerpt_end)],
12282 cx,
12283 );
12284 multi_buffer.push_excerpts(
12285 buffer.clone(),
12286 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
12287 cx,
12288 );
12289 multi_buffer
12290 });
12291
12292 let editor = workspace
12293 .update(cx, |_, window, cx| {
12294 cx.new(|cx| {
12295 Editor::new(
12296 EditorMode::Full {
12297 scale_ui_elements_with_buffer_font_size: false,
12298 show_active_line_background: false,
12299 sized_by_content: false,
12300 },
12301 multi_buffer.clone(),
12302 Some(project.clone()),
12303 window,
12304 cx,
12305 )
12306 })
12307 })
12308 .unwrap();
12309
12310 let pane = workspace
12311 .update(cx, |workspace, _, _| workspace.active_pane().clone())
12312 .unwrap();
12313 pane.update_in(cx, |pane, window, cx| {
12314 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
12315 });
12316
12317 let fake_server = fake_servers.next().await.unwrap();
12318
12319 editor.update_in(cx, |editor, window, cx| {
12320 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12321 s.select_ranges([
12322 Point::new(1, 11)..Point::new(1, 11),
12323 Point::new(7, 11)..Point::new(7, 11),
12324 ])
12325 });
12326
12327 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
12328 });
12329
12330 editor.update_in(cx, |editor, window, cx| {
12331 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12332 });
12333
12334 fake_server
12335 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
12336 let completion_item = lsp::CompletionItem {
12337 label: "saturating_sub()".into(),
12338 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
12339 lsp::InsertReplaceEdit {
12340 new_text: "saturating_sub()".to_owned(),
12341 insert: lsp::Range::new(
12342 lsp::Position::new(7, 7),
12343 lsp::Position::new(7, 11),
12344 ),
12345 replace: lsp::Range::new(
12346 lsp::Position::new(7, 7),
12347 lsp::Position::new(7, 13),
12348 ),
12349 },
12350 )),
12351 ..lsp::CompletionItem::default()
12352 };
12353
12354 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
12355 })
12356 .next()
12357 .await
12358 .unwrap();
12359
12360 cx.condition(&editor, |editor, _| editor.context_menu_visible())
12361 .await;
12362
12363 editor
12364 .update_in(cx, |editor, window, cx| {
12365 editor
12366 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12367 .unwrap()
12368 })
12369 .await
12370 .unwrap();
12371
12372 editor.update(cx, |editor, cx| {
12373 assert_text_with_selections(editor, expected_multibuffer, cx);
12374 })
12375}
12376
12377#[gpui::test]
12378async fn test_completion(cx: &mut TestAppContext) {
12379 init_test(cx, |_| {});
12380
12381 let mut cx = EditorLspTestContext::new_rust(
12382 lsp::ServerCapabilities {
12383 completion_provider: Some(lsp::CompletionOptions {
12384 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12385 resolve_provider: Some(true),
12386 ..Default::default()
12387 }),
12388 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12389 ..Default::default()
12390 },
12391 cx,
12392 )
12393 .await;
12394 let counter = Arc::new(AtomicUsize::new(0));
12395
12396 cx.set_state(indoc! {"
12397 oneˇ
12398 two
12399 three
12400 "});
12401 cx.simulate_keystroke(".");
12402 handle_completion_request(
12403 indoc! {"
12404 one.|<>
12405 two
12406 three
12407 "},
12408 vec!["first_completion", "second_completion"],
12409 true,
12410 counter.clone(),
12411 &mut cx,
12412 )
12413 .await;
12414 cx.condition(|editor, _| editor.context_menu_visible())
12415 .await;
12416 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12417
12418 let _handler = handle_signature_help_request(
12419 &mut cx,
12420 lsp::SignatureHelp {
12421 signatures: vec![lsp::SignatureInformation {
12422 label: "test signature".to_string(),
12423 documentation: None,
12424 parameters: Some(vec![lsp::ParameterInformation {
12425 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
12426 documentation: None,
12427 }]),
12428 active_parameter: None,
12429 }],
12430 active_signature: None,
12431 active_parameter: None,
12432 },
12433 );
12434 cx.update_editor(|editor, window, cx| {
12435 assert!(
12436 !editor.signature_help_state.is_shown(),
12437 "No signature help was called for"
12438 );
12439 editor.show_signature_help(&ShowSignatureHelp, window, cx);
12440 });
12441 cx.run_until_parked();
12442 cx.update_editor(|editor, _, _| {
12443 assert!(
12444 !editor.signature_help_state.is_shown(),
12445 "No signature help should be shown when completions menu is open"
12446 );
12447 });
12448
12449 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12450 editor.context_menu_next(&Default::default(), window, cx);
12451 editor
12452 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12453 .unwrap()
12454 });
12455 cx.assert_editor_state(indoc! {"
12456 one.second_completionˇ
12457 two
12458 three
12459 "});
12460
12461 handle_resolve_completion_request(
12462 &mut cx,
12463 Some(vec![
12464 (
12465 //This overlaps with the primary completion edit which is
12466 //misbehavior from the LSP spec, test that we filter it out
12467 indoc! {"
12468 one.second_ˇcompletion
12469 two
12470 threeˇ
12471 "},
12472 "overlapping additional edit",
12473 ),
12474 (
12475 indoc! {"
12476 one.second_completion
12477 two
12478 threeˇ
12479 "},
12480 "\nadditional edit",
12481 ),
12482 ]),
12483 )
12484 .await;
12485 apply_additional_edits.await.unwrap();
12486 cx.assert_editor_state(indoc! {"
12487 one.second_completionˇ
12488 two
12489 three
12490 additional edit
12491 "});
12492
12493 cx.set_state(indoc! {"
12494 one.second_completion
12495 twoˇ
12496 threeˇ
12497 additional edit
12498 "});
12499 cx.simulate_keystroke(" ");
12500 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12501 cx.simulate_keystroke("s");
12502 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12503
12504 cx.assert_editor_state(indoc! {"
12505 one.second_completion
12506 two sˇ
12507 three sˇ
12508 additional edit
12509 "});
12510 handle_completion_request(
12511 indoc! {"
12512 one.second_completion
12513 two s
12514 three <s|>
12515 additional edit
12516 "},
12517 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
12518 true,
12519 counter.clone(),
12520 &mut cx,
12521 )
12522 .await;
12523 cx.condition(|editor, _| editor.context_menu_visible())
12524 .await;
12525 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12526
12527 cx.simulate_keystroke("i");
12528
12529 handle_completion_request(
12530 indoc! {"
12531 one.second_completion
12532 two si
12533 three <si|>
12534 additional edit
12535 "},
12536 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
12537 true,
12538 counter.clone(),
12539 &mut cx,
12540 )
12541 .await;
12542 cx.condition(|editor, _| editor.context_menu_visible())
12543 .await;
12544 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12545
12546 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12547 editor
12548 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12549 .unwrap()
12550 });
12551 cx.assert_editor_state(indoc! {"
12552 one.second_completion
12553 two sixth_completionˇ
12554 three sixth_completionˇ
12555 additional edit
12556 "});
12557
12558 apply_additional_edits.await.unwrap();
12559
12560 update_test_language_settings(&mut cx, |settings| {
12561 settings.defaults.show_completions_on_input = Some(false);
12562 });
12563 cx.set_state("editorˇ");
12564 cx.simulate_keystroke(".");
12565 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12566 cx.simulate_keystrokes("c l o");
12567 cx.assert_editor_state("editor.cloˇ");
12568 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12569 cx.update_editor(|editor, window, cx| {
12570 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12571 });
12572 handle_completion_request(
12573 "editor.<clo|>",
12574 vec!["close", "clobber"],
12575 true,
12576 counter.clone(),
12577 &mut cx,
12578 )
12579 .await;
12580 cx.condition(|editor, _| editor.context_menu_visible())
12581 .await;
12582 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
12583
12584 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12585 editor
12586 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12587 .unwrap()
12588 });
12589 cx.assert_editor_state("editor.clobberˇ");
12590 handle_resolve_completion_request(&mut cx, None).await;
12591 apply_additional_edits.await.unwrap();
12592}
12593
12594#[gpui::test]
12595async fn test_completion_reuse(cx: &mut TestAppContext) {
12596 init_test(cx, |_| {});
12597
12598 let mut cx = EditorLspTestContext::new_rust(
12599 lsp::ServerCapabilities {
12600 completion_provider: Some(lsp::CompletionOptions {
12601 trigger_characters: Some(vec![".".to_string()]),
12602 ..Default::default()
12603 }),
12604 ..Default::default()
12605 },
12606 cx,
12607 )
12608 .await;
12609
12610 let counter = Arc::new(AtomicUsize::new(0));
12611 cx.set_state("objˇ");
12612 cx.simulate_keystroke(".");
12613
12614 // Initial completion request returns complete results
12615 let is_incomplete = false;
12616 handle_completion_request(
12617 "obj.|<>",
12618 vec!["a", "ab", "abc"],
12619 is_incomplete,
12620 counter.clone(),
12621 &mut cx,
12622 )
12623 .await;
12624 cx.run_until_parked();
12625 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12626 cx.assert_editor_state("obj.ˇ");
12627 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12628
12629 // Type "a" - filters existing completions
12630 cx.simulate_keystroke("a");
12631 cx.run_until_parked();
12632 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12633 cx.assert_editor_state("obj.aˇ");
12634 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12635
12636 // Type "b" - filters existing completions
12637 cx.simulate_keystroke("b");
12638 cx.run_until_parked();
12639 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12640 cx.assert_editor_state("obj.abˇ");
12641 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12642
12643 // Type "c" - filters existing completions
12644 cx.simulate_keystroke("c");
12645 cx.run_until_parked();
12646 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12647 cx.assert_editor_state("obj.abcˇ");
12648 check_displayed_completions(vec!["abc"], &mut cx);
12649
12650 // Backspace to delete "c" - filters existing completions
12651 cx.update_editor(|editor, window, cx| {
12652 editor.backspace(&Backspace, window, cx);
12653 });
12654 cx.run_until_parked();
12655 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12656 cx.assert_editor_state("obj.abˇ");
12657 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12658
12659 // Moving cursor to the left dismisses menu.
12660 cx.update_editor(|editor, window, cx| {
12661 editor.move_left(&MoveLeft, window, cx);
12662 });
12663 cx.run_until_parked();
12664 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12665 cx.assert_editor_state("obj.aˇb");
12666 cx.update_editor(|editor, _, _| {
12667 assert_eq!(editor.context_menu_visible(), false);
12668 });
12669
12670 // Type "b" - new request
12671 cx.simulate_keystroke("b");
12672 let is_incomplete = false;
12673 handle_completion_request(
12674 "obj.<ab|>a",
12675 vec!["ab", "abc"],
12676 is_incomplete,
12677 counter.clone(),
12678 &mut cx,
12679 )
12680 .await;
12681 cx.run_until_parked();
12682 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12683 cx.assert_editor_state("obj.abˇb");
12684 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12685
12686 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
12687 cx.update_editor(|editor, window, cx| {
12688 editor.backspace(&Backspace, window, cx);
12689 });
12690 let is_incomplete = false;
12691 handle_completion_request(
12692 "obj.<a|>b",
12693 vec!["a", "ab", "abc"],
12694 is_incomplete,
12695 counter.clone(),
12696 &mut cx,
12697 )
12698 .await;
12699 cx.run_until_parked();
12700 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12701 cx.assert_editor_state("obj.aˇb");
12702 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12703
12704 // Backspace to delete "a" - dismisses menu.
12705 cx.update_editor(|editor, window, cx| {
12706 editor.backspace(&Backspace, window, cx);
12707 });
12708 cx.run_until_parked();
12709 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12710 cx.assert_editor_state("obj.ˇb");
12711 cx.update_editor(|editor, _, _| {
12712 assert_eq!(editor.context_menu_visible(), false);
12713 });
12714}
12715
12716#[gpui::test]
12717async fn test_word_completion(cx: &mut TestAppContext) {
12718 let lsp_fetch_timeout_ms = 10;
12719 init_test(cx, |language_settings| {
12720 language_settings.defaults.completions = Some(CompletionSettings {
12721 words: WordsCompletionMode::Fallback,
12722 lsp: true,
12723 lsp_fetch_timeout_ms: 10,
12724 lsp_insert_mode: LspInsertMode::Insert,
12725 });
12726 });
12727
12728 let mut cx = EditorLspTestContext::new_rust(
12729 lsp::ServerCapabilities {
12730 completion_provider: Some(lsp::CompletionOptions {
12731 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12732 ..lsp::CompletionOptions::default()
12733 }),
12734 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12735 ..lsp::ServerCapabilities::default()
12736 },
12737 cx,
12738 )
12739 .await;
12740
12741 let throttle_completions = Arc::new(AtomicBool::new(false));
12742
12743 let lsp_throttle_completions = throttle_completions.clone();
12744 let _completion_requests_handler =
12745 cx.lsp
12746 .server
12747 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
12748 let lsp_throttle_completions = lsp_throttle_completions.clone();
12749 let cx = cx.clone();
12750 async move {
12751 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
12752 cx.background_executor()
12753 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
12754 .await;
12755 }
12756 Ok(Some(lsp::CompletionResponse::Array(vec![
12757 lsp::CompletionItem {
12758 label: "first".into(),
12759 ..lsp::CompletionItem::default()
12760 },
12761 lsp::CompletionItem {
12762 label: "last".into(),
12763 ..lsp::CompletionItem::default()
12764 },
12765 ])))
12766 }
12767 });
12768
12769 cx.set_state(indoc! {"
12770 oneˇ
12771 two
12772 three
12773 "});
12774 cx.simulate_keystroke(".");
12775 cx.executor().run_until_parked();
12776 cx.condition(|editor, _| editor.context_menu_visible())
12777 .await;
12778 cx.update_editor(|editor, window, cx| {
12779 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12780 {
12781 assert_eq!(
12782 completion_menu_entries(&menu),
12783 &["first", "last"],
12784 "When LSP server is fast to reply, no fallback word completions are used"
12785 );
12786 } else {
12787 panic!("expected completion menu to be open");
12788 }
12789 editor.cancel(&Cancel, window, cx);
12790 });
12791 cx.executor().run_until_parked();
12792 cx.condition(|editor, _| !editor.context_menu_visible())
12793 .await;
12794
12795 throttle_completions.store(true, atomic::Ordering::Release);
12796 cx.simulate_keystroke(".");
12797 cx.executor()
12798 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
12799 cx.executor().run_until_parked();
12800 cx.condition(|editor, _| editor.context_menu_visible())
12801 .await;
12802 cx.update_editor(|editor, _, _| {
12803 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12804 {
12805 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
12806 "When LSP server is slow, document words can be shown instead, if configured accordingly");
12807 } else {
12808 panic!("expected completion menu to be open");
12809 }
12810 });
12811}
12812
12813#[gpui::test]
12814async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
12815 init_test(cx, |language_settings| {
12816 language_settings.defaults.completions = Some(CompletionSettings {
12817 words: WordsCompletionMode::Enabled,
12818 lsp: true,
12819 lsp_fetch_timeout_ms: 0,
12820 lsp_insert_mode: LspInsertMode::Insert,
12821 });
12822 });
12823
12824 let mut cx = EditorLspTestContext::new_rust(
12825 lsp::ServerCapabilities {
12826 completion_provider: Some(lsp::CompletionOptions {
12827 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12828 ..lsp::CompletionOptions::default()
12829 }),
12830 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12831 ..lsp::ServerCapabilities::default()
12832 },
12833 cx,
12834 )
12835 .await;
12836
12837 let _completion_requests_handler =
12838 cx.lsp
12839 .server
12840 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
12841 Ok(Some(lsp::CompletionResponse::Array(vec![
12842 lsp::CompletionItem {
12843 label: "first".into(),
12844 ..lsp::CompletionItem::default()
12845 },
12846 lsp::CompletionItem {
12847 label: "last".into(),
12848 ..lsp::CompletionItem::default()
12849 },
12850 ])))
12851 });
12852
12853 cx.set_state(indoc! {"ˇ
12854 first
12855 last
12856 second
12857 "});
12858 cx.simulate_keystroke(".");
12859 cx.executor().run_until_parked();
12860 cx.condition(|editor, _| editor.context_menu_visible())
12861 .await;
12862 cx.update_editor(|editor, _, _| {
12863 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12864 {
12865 assert_eq!(
12866 completion_menu_entries(&menu),
12867 &["first", "last", "second"],
12868 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
12869 );
12870 } else {
12871 panic!("expected completion menu to be open");
12872 }
12873 });
12874}
12875
12876#[gpui::test]
12877async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
12878 init_test(cx, |language_settings| {
12879 language_settings.defaults.completions = Some(CompletionSettings {
12880 words: WordsCompletionMode::Disabled,
12881 lsp: true,
12882 lsp_fetch_timeout_ms: 0,
12883 lsp_insert_mode: LspInsertMode::Insert,
12884 });
12885 });
12886
12887 let mut cx = EditorLspTestContext::new_rust(
12888 lsp::ServerCapabilities {
12889 completion_provider: Some(lsp::CompletionOptions {
12890 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12891 ..lsp::CompletionOptions::default()
12892 }),
12893 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12894 ..lsp::ServerCapabilities::default()
12895 },
12896 cx,
12897 )
12898 .await;
12899
12900 let _completion_requests_handler =
12901 cx.lsp
12902 .server
12903 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
12904 panic!("LSP completions should not be queried when dealing with word completions")
12905 });
12906
12907 cx.set_state(indoc! {"ˇ
12908 first
12909 last
12910 second
12911 "});
12912 cx.update_editor(|editor, window, cx| {
12913 editor.show_word_completions(&ShowWordCompletions, window, cx);
12914 });
12915 cx.executor().run_until_parked();
12916 cx.condition(|editor, _| editor.context_menu_visible())
12917 .await;
12918 cx.update_editor(|editor, _, _| {
12919 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12920 {
12921 assert_eq!(
12922 completion_menu_entries(&menu),
12923 &["first", "last", "second"],
12924 "`ShowWordCompletions` action should show word completions"
12925 );
12926 } else {
12927 panic!("expected completion menu to be open");
12928 }
12929 });
12930
12931 cx.simulate_keystroke("l");
12932 cx.executor().run_until_parked();
12933 cx.condition(|editor, _| editor.context_menu_visible())
12934 .await;
12935 cx.update_editor(|editor, _, _| {
12936 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12937 {
12938 assert_eq!(
12939 completion_menu_entries(&menu),
12940 &["last"],
12941 "After showing word completions, further editing should filter them and not query the LSP"
12942 );
12943 } else {
12944 panic!("expected completion menu to be open");
12945 }
12946 });
12947}
12948
12949#[gpui::test]
12950async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
12951 init_test(cx, |language_settings| {
12952 language_settings.defaults.completions = Some(CompletionSettings {
12953 words: WordsCompletionMode::Fallback,
12954 lsp: false,
12955 lsp_fetch_timeout_ms: 0,
12956 lsp_insert_mode: LspInsertMode::Insert,
12957 });
12958 });
12959
12960 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12961
12962 cx.set_state(indoc! {"ˇ
12963 0_usize
12964 let
12965 33
12966 4.5f32
12967 "});
12968 cx.update_editor(|editor, window, cx| {
12969 editor.show_completions(&ShowCompletions::default(), window, cx);
12970 });
12971 cx.executor().run_until_parked();
12972 cx.condition(|editor, _| editor.context_menu_visible())
12973 .await;
12974 cx.update_editor(|editor, window, cx| {
12975 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12976 {
12977 assert_eq!(
12978 completion_menu_entries(&menu),
12979 &["let"],
12980 "With no digits in the completion query, no digits should be in the word completions"
12981 );
12982 } else {
12983 panic!("expected completion menu to be open");
12984 }
12985 editor.cancel(&Cancel, window, cx);
12986 });
12987
12988 cx.set_state(indoc! {"3ˇ
12989 0_usize
12990 let
12991 3
12992 33.35f32
12993 "});
12994 cx.update_editor(|editor, window, cx| {
12995 editor.show_completions(&ShowCompletions::default(), window, cx);
12996 });
12997 cx.executor().run_until_parked();
12998 cx.condition(|editor, _| editor.context_menu_visible())
12999 .await;
13000 cx.update_editor(|editor, _, _| {
13001 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13002 {
13003 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
13004 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
13005 } else {
13006 panic!("expected completion menu to be open");
13007 }
13008 });
13009}
13010
13011fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
13012 let position = || lsp::Position {
13013 line: params.text_document_position.position.line,
13014 character: params.text_document_position.position.character,
13015 };
13016 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13017 range: lsp::Range {
13018 start: position(),
13019 end: position(),
13020 },
13021 new_text: text.to_string(),
13022 }))
13023}
13024
13025#[gpui::test]
13026async fn test_multiline_completion(cx: &mut TestAppContext) {
13027 init_test(cx, |_| {});
13028
13029 let fs = FakeFs::new(cx.executor());
13030 fs.insert_tree(
13031 path!("/a"),
13032 json!({
13033 "main.ts": "a",
13034 }),
13035 )
13036 .await;
13037
13038 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13039 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13040 let typescript_language = Arc::new(Language::new(
13041 LanguageConfig {
13042 name: "TypeScript".into(),
13043 matcher: LanguageMatcher {
13044 path_suffixes: vec!["ts".to_string()],
13045 ..LanguageMatcher::default()
13046 },
13047 line_comments: vec!["// ".into()],
13048 ..LanguageConfig::default()
13049 },
13050 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
13051 ));
13052 language_registry.add(typescript_language.clone());
13053 let mut fake_servers = language_registry.register_fake_lsp(
13054 "TypeScript",
13055 FakeLspAdapter {
13056 capabilities: lsp::ServerCapabilities {
13057 completion_provider: Some(lsp::CompletionOptions {
13058 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
13059 ..lsp::CompletionOptions::default()
13060 }),
13061 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13062 ..lsp::ServerCapabilities::default()
13063 },
13064 // Emulate vtsls label generation
13065 label_for_completion: Some(Box::new(|item, _| {
13066 let text = if let Some(description) = item
13067 .label_details
13068 .as_ref()
13069 .and_then(|label_details| label_details.description.as_ref())
13070 {
13071 format!("{} {}", item.label, description)
13072 } else if let Some(detail) = &item.detail {
13073 format!("{} {}", item.label, detail)
13074 } else {
13075 item.label.clone()
13076 };
13077 let len = text.len();
13078 Some(language::CodeLabel {
13079 text,
13080 runs: Vec::new(),
13081 filter_range: 0..len,
13082 })
13083 })),
13084 ..FakeLspAdapter::default()
13085 },
13086 );
13087 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13088 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13089 let worktree_id = workspace
13090 .update(cx, |workspace, _window, cx| {
13091 workspace.project().update(cx, |project, cx| {
13092 project.worktrees(cx).next().unwrap().read(cx).id()
13093 })
13094 })
13095 .unwrap();
13096 let _buffer = project
13097 .update(cx, |project, cx| {
13098 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
13099 })
13100 .await
13101 .unwrap();
13102 let editor = workspace
13103 .update(cx, |workspace, window, cx| {
13104 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
13105 })
13106 .unwrap()
13107 .await
13108 .unwrap()
13109 .downcast::<Editor>()
13110 .unwrap();
13111 let fake_server = fake_servers.next().await.unwrap();
13112
13113 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
13114 let multiline_label_2 = "a\nb\nc\n";
13115 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
13116 let multiline_description = "d\ne\nf\n";
13117 let multiline_detail_2 = "g\nh\ni\n";
13118
13119 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
13120 move |params, _| async move {
13121 Ok(Some(lsp::CompletionResponse::Array(vec![
13122 lsp::CompletionItem {
13123 label: multiline_label.to_string(),
13124 text_edit: gen_text_edit(¶ms, "new_text_1"),
13125 ..lsp::CompletionItem::default()
13126 },
13127 lsp::CompletionItem {
13128 label: "single line label 1".to_string(),
13129 detail: Some(multiline_detail.to_string()),
13130 text_edit: gen_text_edit(¶ms, "new_text_2"),
13131 ..lsp::CompletionItem::default()
13132 },
13133 lsp::CompletionItem {
13134 label: "single line label 2".to_string(),
13135 label_details: Some(lsp::CompletionItemLabelDetails {
13136 description: Some(multiline_description.to_string()),
13137 detail: None,
13138 }),
13139 text_edit: gen_text_edit(¶ms, "new_text_2"),
13140 ..lsp::CompletionItem::default()
13141 },
13142 lsp::CompletionItem {
13143 label: multiline_label_2.to_string(),
13144 detail: Some(multiline_detail_2.to_string()),
13145 text_edit: gen_text_edit(¶ms, "new_text_3"),
13146 ..lsp::CompletionItem::default()
13147 },
13148 lsp::CompletionItem {
13149 label: "Label with many spaces and \t but without newlines".to_string(),
13150 detail: Some(
13151 "Details with many spaces and \t but without newlines".to_string(),
13152 ),
13153 text_edit: gen_text_edit(¶ms, "new_text_4"),
13154 ..lsp::CompletionItem::default()
13155 },
13156 ])))
13157 },
13158 );
13159
13160 editor.update_in(cx, |editor, window, cx| {
13161 cx.focus_self(window);
13162 editor.move_to_end(&MoveToEnd, window, cx);
13163 editor.handle_input(".", window, cx);
13164 });
13165 cx.run_until_parked();
13166 completion_handle.next().await.unwrap();
13167
13168 editor.update(cx, |editor, _| {
13169 assert!(editor.context_menu_visible());
13170 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13171 {
13172 let completion_labels = menu
13173 .completions
13174 .borrow()
13175 .iter()
13176 .map(|c| c.label.text.clone())
13177 .collect::<Vec<_>>();
13178 assert_eq!(
13179 completion_labels,
13180 &[
13181 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
13182 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
13183 "single line label 2 d e f ",
13184 "a b c g h i ",
13185 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
13186 ],
13187 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
13188 );
13189
13190 for completion in menu
13191 .completions
13192 .borrow()
13193 .iter() {
13194 assert_eq!(
13195 completion.label.filter_range,
13196 0..completion.label.text.len(),
13197 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
13198 );
13199 }
13200 } else {
13201 panic!("expected completion menu to be open");
13202 }
13203 });
13204}
13205
13206#[gpui::test]
13207async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
13208 init_test(cx, |_| {});
13209 let mut cx = EditorLspTestContext::new_rust(
13210 lsp::ServerCapabilities {
13211 completion_provider: Some(lsp::CompletionOptions {
13212 trigger_characters: Some(vec![".".to_string()]),
13213 ..Default::default()
13214 }),
13215 ..Default::default()
13216 },
13217 cx,
13218 )
13219 .await;
13220 cx.lsp
13221 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13222 Ok(Some(lsp::CompletionResponse::Array(vec![
13223 lsp::CompletionItem {
13224 label: "first".into(),
13225 ..Default::default()
13226 },
13227 lsp::CompletionItem {
13228 label: "last".into(),
13229 ..Default::default()
13230 },
13231 ])))
13232 });
13233 cx.set_state("variableˇ");
13234 cx.simulate_keystroke(".");
13235 cx.executor().run_until_parked();
13236
13237 cx.update_editor(|editor, _, _| {
13238 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13239 {
13240 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
13241 } else {
13242 panic!("expected completion menu to be open");
13243 }
13244 });
13245
13246 cx.update_editor(|editor, window, cx| {
13247 editor.move_page_down(&MovePageDown::default(), window, cx);
13248 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13249 {
13250 assert!(
13251 menu.selected_item == 1,
13252 "expected PageDown to select the last item from the context menu"
13253 );
13254 } else {
13255 panic!("expected completion menu to stay open after PageDown");
13256 }
13257 });
13258
13259 cx.update_editor(|editor, window, cx| {
13260 editor.move_page_up(&MovePageUp::default(), window, cx);
13261 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13262 {
13263 assert!(
13264 menu.selected_item == 0,
13265 "expected PageUp to select the first item from the context menu"
13266 );
13267 } else {
13268 panic!("expected completion menu to stay open after PageUp");
13269 }
13270 });
13271}
13272
13273#[gpui::test]
13274async fn test_as_is_completions(cx: &mut TestAppContext) {
13275 init_test(cx, |_| {});
13276 let mut cx = EditorLspTestContext::new_rust(
13277 lsp::ServerCapabilities {
13278 completion_provider: Some(lsp::CompletionOptions {
13279 ..Default::default()
13280 }),
13281 ..Default::default()
13282 },
13283 cx,
13284 )
13285 .await;
13286 cx.lsp
13287 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13288 Ok(Some(lsp::CompletionResponse::Array(vec![
13289 lsp::CompletionItem {
13290 label: "unsafe".into(),
13291 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13292 range: lsp::Range {
13293 start: lsp::Position {
13294 line: 1,
13295 character: 2,
13296 },
13297 end: lsp::Position {
13298 line: 1,
13299 character: 3,
13300 },
13301 },
13302 new_text: "unsafe".to_string(),
13303 })),
13304 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
13305 ..Default::default()
13306 },
13307 ])))
13308 });
13309 cx.set_state("fn a() {}\n nˇ");
13310 cx.executor().run_until_parked();
13311 cx.update_editor(|editor, window, cx| {
13312 editor.show_completions(
13313 &ShowCompletions {
13314 trigger: Some("\n".into()),
13315 },
13316 window,
13317 cx,
13318 );
13319 });
13320 cx.executor().run_until_parked();
13321
13322 cx.update_editor(|editor, window, cx| {
13323 editor.confirm_completion(&Default::default(), window, cx)
13324 });
13325 cx.executor().run_until_parked();
13326 cx.assert_editor_state("fn a() {}\n unsafeˇ");
13327}
13328
13329#[gpui::test]
13330async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
13331 init_test(cx, |_| {});
13332
13333 let mut cx = EditorLspTestContext::new_rust(
13334 lsp::ServerCapabilities {
13335 completion_provider: Some(lsp::CompletionOptions {
13336 trigger_characters: Some(vec![".".to_string()]),
13337 resolve_provider: Some(true),
13338 ..Default::default()
13339 }),
13340 ..Default::default()
13341 },
13342 cx,
13343 )
13344 .await;
13345
13346 cx.set_state("fn main() { let a = 2ˇ; }");
13347 cx.simulate_keystroke(".");
13348 let completion_item = lsp::CompletionItem {
13349 label: "Some".into(),
13350 kind: Some(lsp::CompletionItemKind::SNIPPET),
13351 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
13352 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
13353 kind: lsp::MarkupKind::Markdown,
13354 value: "```rust\nSome(2)\n```".to_string(),
13355 })),
13356 deprecated: Some(false),
13357 sort_text: Some("Some".to_string()),
13358 filter_text: Some("Some".to_string()),
13359 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
13360 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13361 range: lsp::Range {
13362 start: lsp::Position {
13363 line: 0,
13364 character: 22,
13365 },
13366 end: lsp::Position {
13367 line: 0,
13368 character: 22,
13369 },
13370 },
13371 new_text: "Some(2)".to_string(),
13372 })),
13373 additional_text_edits: Some(vec![lsp::TextEdit {
13374 range: lsp::Range {
13375 start: lsp::Position {
13376 line: 0,
13377 character: 20,
13378 },
13379 end: lsp::Position {
13380 line: 0,
13381 character: 22,
13382 },
13383 },
13384 new_text: "".to_string(),
13385 }]),
13386 ..Default::default()
13387 };
13388
13389 let closure_completion_item = completion_item.clone();
13390 let counter = Arc::new(AtomicUsize::new(0));
13391 let counter_clone = counter.clone();
13392 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13393 let task_completion_item = closure_completion_item.clone();
13394 counter_clone.fetch_add(1, atomic::Ordering::Release);
13395 async move {
13396 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
13397 is_incomplete: true,
13398 item_defaults: None,
13399 items: vec![task_completion_item],
13400 })))
13401 }
13402 });
13403
13404 cx.condition(|editor, _| editor.context_menu_visible())
13405 .await;
13406 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
13407 assert!(request.next().await.is_some());
13408 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13409
13410 cx.simulate_keystrokes("S o m");
13411 cx.condition(|editor, _| editor.context_menu_visible())
13412 .await;
13413 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
13414 assert!(request.next().await.is_some());
13415 assert!(request.next().await.is_some());
13416 assert!(request.next().await.is_some());
13417 request.close();
13418 assert!(request.next().await.is_none());
13419 assert_eq!(
13420 counter.load(atomic::Ordering::Acquire),
13421 4,
13422 "With the completions menu open, only one LSP request should happen per input"
13423 );
13424}
13425
13426#[gpui::test]
13427async fn test_toggle_comment(cx: &mut TestAppContext) {
13428 init_test(cx, |_| {});
13429 let mut cx = EditorTestContext::new(cx).await;
13430 let language = Arc::new(Language::new(
13431 LanguageConfig {
13432 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
13433 ..Default::default()
13434 },
13435 Some(tree_sitter_rust::LANGUAGE.into()),
13436 ));
13437 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
13438
13439 // If multiple selections intersect a line, the line is only toggled once.
13440 cx.set_state(indoc! {"
13441 fn a() {
13442 «//b();
13443 ˇ»// «c();
13444 //ˇ» d();
13445 }
13446 "});
13447
13448 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13449
13450 cx.assert_editor_state(indoc! {"
13451 fn a() {
13452 «b();
13453 c();
13454 ˇ» d();
13455 }
13456 "});
13457
13458 // The comment prefix is inserted at the same column for every line in a
13459 // selection.
13460 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13461
13462 cx.assert_editor_state(indoc! {"
13463 fn a() {
13464 // «b();
13465 // c();
13466 ˇ»// d();
13467 }
13468 "});
13469
13470 // If a selection ends at the beginning of a line, that line is not toggled.
13471 cx.set_selections_state(indoc! {"
13472 fn a() {
13473 // b();
13474 «// c();
13475 ˇ» // d();
13476 }
13477 "});
13478
13479 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13480
13481 cx.assert_editor_state(indoc! {"
13482 fn a() {
13483 // b();
13484 «c();
13485 ˇ» // d();
13486 }
13487 "});
13488
13489 // If a selection span a single line and is empty, the line is toggled.
13490 cx.set_state(indoc! {"
13491 fn a() {
13492 a();
13493 b();
13494 ˇ
13495 }
13496 "});
13497
13498 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13499
13500 cx.assert_editor_state(indoc! {"
13501 fn a() {
13502 a();
13503 b();
13504 //•ˇ
13505 }
13506 "});
13507
13508 // If a selection span multiple lines, empty lines are not toggled.
13509 cx.set_state(indoc! {"
13510 fn a() {
13511 «a();
13512
13513 c();ˇ»
13514 }
13515 "});
13516
13517 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13518
13519 cx.assert_editor_state(indoc! {"
13520 fn a() {
13521 // «a();
13522
13523 // c();ˇ»
13524 }
13525 "});
13526
13527 // If a selection includes multiple comment prefixes, all lines are uncommented.
13528 cx.set_state(indoc! {"
13529 fn a() {
13530 «// a();
13531 /// b();
13532 //! c();ˇ»
13533 }
13534 "});
13535
13536 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13537
13538 cx.assert_editor_state(indoc! {"
13539 fn a() {
13540 «a();
13541 b();
13542 c();ˇ»
13543 }
13544 "});
13545}
13546
13547#[gpui::test]
13548async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
13549 init_test(cx, |_| {});
13550 let mut cx = EditorTestContext::new(cx).await;
13551 let language = Arc::new(Language::new(
13552 LanguageConfig {
13553 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
13554 ..Default::default()
13555 },
13556 Some(tree_sitter_rust::LANGUAGE.into()),
13557 ));
13558 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
13559
13560 let toggle_comments = &ToggleComments {
13561 advance_downwards: false,
13562 ignore_indent: true,
13563 };
13564
13565 // If multiple selections intersect a line, the line is only toggled once.
13566 cx.set_state(indoc! {"
13567 fn a() {
13568 // «b();
13569 // c();
13570 // ˇ» d();
13571 }
13572 "});
13573
13574 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13575
13576 cx.assert_editor_state(indoc! {"
13577 fn a() {
13578 «b();
13579 c();
13580 ˇ» d();
13581 }
13582 "});
13583
13584 // The comment prefix is inserted at the beginning of each line
13585 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13586
13587 cx.assert_editor_state(indoc! {"
13588 fn a() {
13589 // «b();
13590 // c();
13591 // ˇ» d();
13592 }
13593 "});
13594
13595 // If a selection ends at the beginning of a line, that line is not toggled.
13596 cx.set_selections_state(indoc! {"
13597 fn a() {
13598 // b();
13599 // «c();
13600 ˇ»// d();
13601 }
13602 "});
13603
13604 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13605
13606 cx.assert_editor_state(indoc! {"
13607 fn a() {
13608 // b();
13609 «c();
13610 ˇ»// d();
13611 }
13612 "});
13613
13614 // If a selection span a single line and is empty, the line is toggled.
13615 cx.set_state(indoc! {"
13616 fn a() {
13617 a();
13618 b();
13619 ˇ
13620 }
13621 "});
13622
13623 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13624
13625 cx.assert_editor_state(indoc! {"
13626 fn a() {
13627 a();
13628 b();
13629 //ˇ
13630 }
13631 "});
13632
13633 // If a selection span multiple lines, empty lines are not toggled.
13634 cx.set_state(indoc! {"
13635 fn a() {
13636 «a();
13637
13638 c();ˇ»
13639 }
13640 "});
13641
13642 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13643
13644 cx.assert_editor_state(indoc! {"
13645 fn a() {
13646 // «a();
13647
13648 // c();ˇ»
13649 }
13650 "});
13651
13652 // If a selection includes multiple comment prefixes, all lines are uncommented.
13653 cx.set_state(indoc! {"
13654 fn a() {
13655 // «a();
13656 /// b();
13657 //! c();ˇ»
13658 }
13659 "});
13660
13661 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13662
13663 cx.assert_editor_state(indoc! {"
13664 fn a() {
13665 «a();
13666 b();
13667 c();ˇ»
13668 }
13669 "});
13670}
13671
13672#[gpui::test]
13673async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
13674 init_test(cx, |_| {});
13675
13676 let language = Arc::new(Language::new(
13677 LanguageConfig {
13678 line_comments: vec!["// ".into()],
13679 ..Default::default()
13680 },
13681 Some(tree_sitter_rust::LANGUAGE.into()),
13682 ));
13683
13684 let mut cx = EditorTestContext::new(cx).await;
13685
13686 cx.language_registry().add(language.clone());
13687 cx.update_buffer(|buffer, cx| {
13688 buffer.set_language(Some(language), cx);
13689 });
13690
13691 let toggle_comments = &ToggleComments {
13692 advance_downwards: true,
13693 ignore_indent: false,
13694 };
13695
13696 // Single cursor on one line -> advance
13697 // Cursor moves horizontally 3 characters as well on non-blank line
13698 cx.set_state(indoc!(
13699 "fn a() {
13700 ˇdog();
13701 cat();
13702 }"
13703 ));
13704 cx.update_editor(|editor, window, cx| {
13705 editor.toggle_comments(toggle_comments, window, cx);
13706 });
13707 cx.assert_editor_state(indoc!(
13708 "fn a() {
13709 // dog();
13710 catˇ();
13711 }"
13712 ));
13713
13714 // Single selection on one line -> don't advance
13715 cx.set_state(indoc!(
13716 "fn a() {
13717 «dog()ˇ»;
13718 cat();
13719 }"
13720 ));
13721 cx.update_editor(|editor, window, cx| {
13722 editor.toggle_comments(toggle_comments, window, cx);
13723 });
13724 cx.assert_editor_state(indoc!(
13725 "fn a() {
13726 // «dog()ˇ»;
13727 cat();
13728 }"
13729 ));
13730
13731 // Multiple cursors on one line -> advance
13732 cx.set_state(indoc!(
13733 "fn a() {
13734 ˇdˇog();
13735 cat();
13736 }"
13737 ));
13738 cx.update_editor(|editor, window, cx| {
13739 editor.toggle_comments(toggle_comments, window, cx);
13740 });
13741 cx.assert_editor_state(indoc!(
13742 "fn a() {
13743 // dog();
13744 catˇ(ˇ);
13745 }"
13746 ));
13747
13748 // Multiple cursors on one line, with selection -> don't advance
13749 cx.set_state(indoc!(
13750 "fn a() {
13751 ˇdˇog«()ˇ»;
13752 cat();
13753 }"
13754 ));
13755 cx.update_editor(|editor, window, cx| {
13756 editor.toggle_comments(toggle_comments, window, cx);
13757 });
13758 cx.assert_editor_state(indoc!(
13759 "fn a() {
13760 // ˇdˇog«()ˇ»;
13761 cat();
13762 }"
13763 ));
13764
13765 // Single cursor on one line -> advance
13766 // Cursor moves to column 0 on blank line
13767 cx.set_state(indoc!(
13768 "fn a() {
13769 ˇdog();
13770
13771 cat();
13772 }"
13773 ));
13774 cx.update_editor(|editor, window, cx| {
13775 editor.toggle_comments(toggle_comments, window, cx);
13776 });
13777 cx.assert_editor_state(indoc!(
13778 "fn a() {
13779 // dog();
13780 ˇ
13781 cat();
13782 }"
13783 ));
13784
13785 // Single cursor on one line -> advance
13786 // Cursor starts and ends at column 0
13787 cx.set_state(indoc!(
13788 "fn a() {
13789 ˇ dog();
13790 cat();
13791 }"
13792 ));
13793 cx.update_editor(|editor, window, cx| {
13794 editor.toggle_comments(toggle_comments, window, cx);
13795 });
13796 cx.assert_editor_state(indoc!(
13797 "fn a() {
13798 // dog();
13799 ˇ cat();
13800 }"
13801 ));
13802}
13803
13804#[gpui::test]
13805async fn test_toggle_block_comment(cx: &mut TestAppContext) {
13806 init_test(cx, |_| {});
13807
13808 let mut cx = EditorTestContext::new(cx).await;
13809
13810 let html_language = Arc::new(
13811 Language::new(
13812 LanguageConfig {
13813 name: "HTML".into(),
13814 block_comment: Some(BlockCommentConfig {
13815 start: "<!-- ".into(),
13816 prefix: "".into(),
13817 end: " -->".into(),
13818 tab_size: 0,
13819 }),
13820 ..Default::default()
13821 },
13822 Some(tree_sitter_html::LANGUAGE.into()),
13823 )
13824 .with_injection_query(
13825 r#"
13826 (script_element
13827 (raw_text) @injection.content
13828 (#set! injection.language "javascript"))
13829 "#,
13830 )
13831 .unwrap(),
13832 );
13833
13834 let javascript_language = Arc::new(Language::new(
13835 LanguageConfig {
13836 name: "JavaScript".into(),
13837 line_comments: vec!["// ".into()],
13838 ..Default::default()
13839 },
13840 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
13841 ));
13842
13843 cx.language_registry().add(html_language.clone());
13844 cx.language_registry().add(javascript_language.clone());
13845 cx.update_buffer(|buffer, cx| {
13846 buffer.set_language(Some(html_language), cx);
13847 });
13848
13849 // Toggle comments for empty selections
13850 cx.set_state(
13851 &r#"
13852 <p>A</p>ˇ
13853 <p>B</p>ˇ
13854 <p>C</p>ˇ
13855 "#
13856 .unindent(),
13857 );
13858 cx.update_editor(|editor, window, cx| {
13859 editor.toggle_comments(&ToggleComments::default(), window, cx)
13860 });
13861 cx.assert_editor_state(
13862 &r#"
13863 <!-- <p>A</p>ˇ -->
13864 <!-- <p>B</p>ˇ -->
13865 <!-- <p>C</p>ˇ -->
13866 "#
13867 .unindent(),
13868 );
13869 cx.update_editor(|editor, window, cx| {
13870 editor.toggle_comments(&ToggleComments::default(), window, cx)
13871 });
13872 cx.assert_editor_state(
13873 &r#"
13874 <p>A</p>ˇ
13875 <p>B</p>ˇ
13876 <p>C</p>ˇ
13877 "#
13878 .unindent(),
13879 );
13880
13881 // Toggle comments for mixture of empty and non-empty selections, where
13882 // multiple selections occupy a given line.
13883 cx.set_state(
13884 &r#"
13885 <p>A«</p>
13886 <p>ˇ»B</p>ˇ
13887 <p>C«</p>
13888 <p>ˇ»D</p>ˇ
13889 "#
13890 .unindent(),
13891 );
13892
13893 cx.update_editor(|editor, window, cx| {
13894 editor.toggle_comments(&ToggleComments::default(), window, cx)
13895 });
13896 cx.assert_editor_state(
13897 &r#"
13898 <!-- <p>A«</p>
13899 <p>ˇ»B</p>ˇ -->
13900 <!-- <p>C«</p>
13901 <p>ˇ»D</p>ˇ -->
13902 "#
13903 .unindent(),
13904 );
13905 cx.update_editor(|editor, window, cx| {
13906 editor.toggle_comments(&ToggleComments::default(), window, cx)
13907 });
13908 cx.assert_editor_state(
13909 &r#"
13910 <p>A«</p>
13911 <p>ˇ»B</p>ˇ
13912 <p>C«</p>
13913 <p>ˇ»D</p>ˇ
13914 "#
13915 .unindent(),
13916 );
13917
13918 // Toggle comments when different languages are active for different
13919 // selections.
13920 cx.set_state(
13921 &r#"
13922 ˇ<script>
13923 ˇvar x = new Y();
13924 ˇ</script>
13925 "#
13926 .unindent(),
13927 );
13928 cx.executor().run_until_parked();
13929 cx.update_editor(|editor, window, cx| {
13930 editor.toggle_comments(&ToggleComments::default(), window, cx)
13931 });
13932 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
13933 // Uncommenting and commenting from this position brings in even more wrong artifacts.
13934 cx.assert_editor_state(
13935 &r#"
13936 <!-- ˇ<script> -->
13937 // ˇvar x = new Y();
13938 <!-- ˇ</script> -->
13939 "#
13940 .unindent(),
13941 );
13942}
13943
13944#[gpui::test]
13945fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
13946 init_test(cx, |_| {});
13947
13948 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
13949 let multibuffer = cx.new(|cx| {
13950 let mut multibuffer = MultiBuffer::new(ReadWrite);
13951 multibuffer.push_excerpts(
13952 buffer.clone(),
13953 [
13954 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
13955 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
13956 ],
13957 cx,
13958 );
13959 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
13960 multibuffer
13961 });
13962
13963 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
13964 editor.update_in(cx, |editor, window, cx| {
13965 assert_eq!(editor.text(cx), "aaaa\nbbbb");
13966 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13967 s.select_ranges([
13968 Point::new(0, 0)..Point::new(0, 0),
13969 Point::new(1, 0)..Point::new(1, 0),
13970 ])
13971 });
13972
13973 editor.handle_input("X", window, cx);
13974 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
13975 assert_eq!(
13976 editor.selections.ranges(cx),
13977 [
13978 Point::new(0, 1)..Point::new(0, 1),
13979 Point::new(1, 1)..Point::new(1, 1),
13980 ]
13981 );
13982
13983 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
13984 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13985 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
13986 });
13987 editor.backspace(&Default::default(), window, cx);
13988 assert_eq!(editor.text(cx), "Xa\nbbb");
13989 assert_eq!(
13990 editor.selections.ranges(cx),
13991 [Point::new(1, 0)..Point::new(1, 0)]
13992 );
13993
13994 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13995 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
13996 });
13997 editor.backspace(&Default::default(), window, cx);
13998 assert_eq!(editor.text(cx), "X\nbb");
13999 assert_eq!(
14000 editor.selections.ranges(cx),
14001 [Point::new(0, 1)..Point::new(0, 1)]
14002 );
14003 });
14004}
14005
14006#[gpui::test]
14007fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
14008 init_test(cx, |_| {});
14009
14010 let markers = vec![('[', ']').into(), ('(', ')').into()];
14011 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
14012 indoc! {"
14013 [aaaa
14014 (bbbb]
14015 cccc)",
14016 },
14017 markers.clone(),
14018 );
14019 let excerpt_ranges = markers.into_iter().map(|marker| {
14020 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
14021 ExcerptRange::new(context.clone())
14022 });
14023 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
14024 let multibuffer = cx.new(|cx| {
14025 let mut multibuffer = MultiBuffer::new(ReadWrite);
14026 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
14027 multibuffer
14028 });
14029
14030 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
14031 editor.update_in(cx, |editor, window, cx| {
14032 let (expected_text, selection_ranges) = marked_text_ranges(
14033 indoc! {"
14034 aaaa
14035 bˇbbb
14036 bˇbbˇb
14037 cccc"
14038 },
14039 true,
14040 );
14041 assert_eq!(editor.text(cx), expected_text);
14042 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14043 s.select_ranges(selection_ranges)
14044 });
14045
14046 editor.handle_input("X", window, cx);
14047
14048 let (expected_text, expected_selections) = marked_text_ranges(
14049 indoc! {"
14050 aaaa
14051 bXˇbbXb
14052 bXˇbbXˇb
14053 cccc"
14054 },
14055 false,
14056 );
14057 assert_eq!(editor.text(cx), expected_text);
14058 assert_eq!(editor.selections.ranges(cx), expected_selections);
14059
14060 editor.newline(&Newline, window, cx);
14061 let (expected_text, expected_selections) = marked_text_ranges(
14062 indoc! {"
14063 aaaa
14064 bX
14065 ˇbbX
14066 b
14067 bX
14068 ˇbbX
14069 ˇb
14070 cccc"
14071 },
14072 false,
14073 );
14074 assert_eq!(editor.text(cx), expected_text);
14075 assert_eq!(editor.selections.ranges(cx), expected_selections);
14076 });
14077}
14078
14079#[gpui::test]
14080fn test_refresh_selections(cx: &mut TestAppContext) {
14081 init_test(cx, |_| {});
14082
14083 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
14084 let mut excerpt1_id = None;
14085 let multibuffer = cx.new(|cx| {
14086 let mut multibuffer = MultiBuffer::new(ReadWrite);
14087 excerpt1_id = multibuffer
14088 .push_excerpts(
14089 buffer.clone(),
14090 [
14091 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
14092 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
14093 ],
14094 cx,
14095 )
14096 .into_iter()
14097 .next();
14098 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
14099 multibuffer
14100 });
14101
14102 let editor = cx.add_window(|window, cx| {
14103 let mut editor = build_editor(multibuffer.clone(), window, cx);
14104 let snapshot = editor.snapshot(window, cx);
14105 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14106 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
14107 });
14108 editor.begin_selection(
14109 Point::new(2, 1).to_display_point(&snapshot),
14110 true,
14111 1,
14112 window,
14113 cx,
14114 );
14115 assert_eq!(
14116 editor.selections.ranges(cx),
14117 [
14118 Point::new(1, 3)..Point::new(1, 3),
14119 Point::new(2, 1)..Point::new(2, 1),
14120 ]
14121 );
14122 editor
14123 });
14124
14125 // Refreshing selections is a no-op when excerpts haven't changed.
14126 _ = editor.update(cx, |editor, window, cx| {
14127 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14128 assert_eq!(
14129 editor.selections.ranges(cx),
14130 [
14131 Point::new(1, 3)..Point::new(1, 3),
14132 Point::new(2, 1)..Point::new(2, 1),
14133 ]
14134 );
14135 });
14136
14137 multibuffer.update(cx, |multibuffer, cx| {
14138 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
14139 });
14140 _ = editor.update(cx, |editor, window, cx| {
14141 // Removing an excerpt causes the first selection to become degenerate.
14142 assert_eq!(
14143 editor.selections.ranges(cx),
14144 [
14145 Point::new(0, 0)..Point::new(0, 0),
14146 Point::new(0, 1)..Point::new(0, 1)
14147 ]
14148 );
14149
14150 // Refreshing selections will relocate the first selection to the original buffer
14151 // location.
14152 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14153 assert_eq!(
14154 editor.selections.ranges(cx),
14155 [
14156 Point::new(0, 1)..Point::new(0, 1),
14157 Point::new(0, 3)..Point::new(0, 3)
14158 ]
14159 );
14160 assert!(editor.selections.pending_anchor().is_some());
14161 });
14162}
14163
14164#[gpui::test]
14165fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
14166 init_test(cx, |_| {});
14167
14168 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
14169 let mut excerpt1_id = None;
14170 let multibuffer = cx.new(|cx| {
14171 let mut multibuffer = MultiBuffer::new(ReadWrite);
14172 excerpt1_id = multibuffer
14173 .push_excerpts(
14174 buffer.clone(),
14175 [
14176 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
14177 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
14178 ],
14179 cx,
14180 )
14181 .into_iter()
14182 .next();
14183 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
14184 multibuffer
14185 });
14186
14187 let editor = cx.add_window(|window, cx| {
14188 let mut editor = build_editor(multibuffer.clone(), window, cx);
14189 let snapshot = editor.snapshot(window, cx);
14190 editor.begin_selection(
14191 Point::new(1, 3).to_display_point(&snapshot),
14192 false,
14193 1,
14194 window,
14195 cx,
14196 );
14197 assert_eq!(
14198 editor.selections.ranges(cx),
14199 [Point::new(1, 3)..Point::new(1, 3)]
14200 );
14201 editor
14202 });
14203
14204 multibuffer.update(cx, |multibuffer, cx| {
14205 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
14206 });
14207 _ = editor.update(cx, |editor, window, cx| {
14208 assert_eq!(
14209 editor.selections.ranges(cx),
14210 [Point::new(0, 0)..Point::new(0, 0)]
14211 );
14212
14213 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
14214 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14215 assert_eq!(
14216 editor.selections.ranges(cx),
14217 [Point::new(0, 3)..Point::new(0, 3)]
14218 );
14219 assert!(editor.selections.pending_anchor().is_some());
14220 });
14221}
14222
14223#[gpui::test]
14224async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
14225 init_test(cx, |_| {});
14226
14227 let language = Arc::new(
14228 Language::new(
14229 LanguageConfig {
14230 brackets: BracketPairConfig {
14231 pairs: vec![
14232 BracketPair {
14233 start: "{".to_string(),
14234 end: "}".to_string(),
14235 close: true,
14236 surround: true,
14237 newline: true,
14238 },
14239 BracketPair {
14240 start: "/* ".to_string(),
14241 end: " */".to_string(),
14242 close: true,
14243 surround: true,
14244 newline: true,
14245 },
14246 ],
14247 ..Default::default()
14248 },
14249 ..Default::default()
14250 },
14251 Some(tree_sitter_rust::LANGUAGE.into()),
14252 )
14253 .with_indents_query("")
14254 .unwrap(),
14255 );
14256
14257 let text = concat!(
14258 "{ }\n", //
14259 " x\n", //
14260 " /* */\n", //
14261 "x\n", //
14262 "{{} }\n", //
14263 );
14264
14265 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
14266 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
14267 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
14268 editor
14269 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
14270 .await;
14271
14272 editor.update_in(cx, |editor, window, cx| {
14273 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14274 s.select_display_ranges([
14275 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
14276 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
14277 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
14278 ])
14279 });
14280 editor.newline(&Newline, window, cx);
14281
14282 assert_eq!(
14283 editor.buffer().read(cx).read(cx).text(),
14284 concat!(
14285 "{ \n", // Suppress rustfmt
14286 "\n", //
14287 "}\n", //
14288 " x\n", //
14289 " /* \n", //
14290 " \n", //
14291 " */\n", //
14292 "x\n", //
14293 "{{} \n", //
14294 "}\n", //
14295 )
14296 );
14297 });
14298}
14299
14300#[gpui::test]
14301fn test_highlighted_ranges(cx: &mut TestAppContext) {
14302 init_test(cx, |_| {});
14303
14304 let editor = cx.add_window(|window, cx| {
14305 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
14306 build_editor(buffer.clone(), window, cx)
14307 });
14308
14309 _ = editor.update(cx, |editor, window, cx| {
14310 struct Type1;
14311 struct Type2;
14312
14313 let buffer = editor.buffer.read(cx).snapshot(cx);
14314
14315 let anchor_range =
14316 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
14317
14318 editor.highlight_background::<Type1>(
14319 &[
14320 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
14321 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
14322 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
14323 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
14324 ],
14325 |_| Hsla::red(),
14326 cx,
14327 );
14328 editor.highlight_background::<Type2>(
14329 &[
14330 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
14331 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
14332 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
14333 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
14334 ],
14335 |_| Hsla::green(),
14336 cx,
14337 );
14338
14339 let snapshot = editor.snapshot(window, cx);
14340 let mut highlighted_ranges = editor.background_highlights_in_range(
14341 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
14342 &snapshot,
14343 cx.theme(),
14344 );
14345 // Enforce a consistent ordering based on color without relying on the ordering of the
14346 // highlight's `TypeId` which is non-executor.
14347 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
14348 assert_eq!(
14349 highlighted_ranges,
14350 &[
14351 (
14352 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
14353 Hsla::red(),
14354 ),
14355 (
14356 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
14357 Hsla::red(),
14358 ),
14359 (
14360 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
14361 Hsla::green(),
14362 ),
14363 (
14364 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
14365 Hsla::green(),
14366 ),
14367 ]
14368 );
14369 assert_eq!(
14370 editor.background_highlights_in_range(
14371 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
14372 &snapshot,
14373 cx.theme(),
14374 ),
14375 &[(
14376 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
14377 Hsla::red(),
14378 )]
14379 );
14380 });
14381}
14382
14383#[gpui::test]
14384async fn test_following(cx: &mut TestAppContext) {
14385 init_test(cx, |_| {});
14386
14387 let fs = FakeFs::new(cx.executor());
14388 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
14389
14390 let buffer = project.update(cx, |project, cx| {
14391 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
14392 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
14393 });
14394 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
14395 let follower = cx.update(|cx| {
14396 cx.open_window(
14397 WindowOptions {
14398 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
14399 gpui::Point::new(px(0.), px(0.)),
14400 gpui::Point::new(px(10.), px(80.)),
14401 ))),
14402 ..Default::default()
14403 },
14404 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
14405 )
14406 .unwrap()
14407 });
14408
14409 let is_still_following = Rc::new(RefCell::new(true));
14410 let follower_edit_event_count = Rc::new(RefCell::new(0));
14411 let pending_update = Rc::new(RefCell::new(None));
14412 let leader_entity = leader.root(cx).unwrap();
14413 let follower_entity = follower.root(cx).unwrap();
14414 _ = follower.update(cx, {
14415 let update = pending_update.clone();
14416 let is_still_following = is_still_following.clone();
14417 let follower_edit_event_count = follower_edit_event_count.clone();
14418 |_, window, cx| {
14419 cx.subscribe_in(
14420 &leader_entity,
14421 window,
14422 move |_, leader, event, window, cx| {
14423 leader.read(cx).add_event_to_update_proto(
14424 event,
14425 &mut update.borrow_mut(),
14426 window,
14427 cx,
14428 );
14429 },
14430 )
14431 .detach();
14432
14433 cx.subscribe_in(
14434 &follower_entity,
14435 window,
14436 move |_, _, event: &EditorEvent, _window, _cx| {
14437 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
14438 *is_still_following.borrow_mut() = false;
14439 }
14440
14441 if let EditorEvent::BufferEdited = event {
14442 *follower_edit_event_count.borrow_mut() += 1;
14443 }
14444 },
14445 )
14446 .detach();
14447 }
14448 });
14449
14450 // Update the selections only
14451 _ = leader.update(cx, |leader, window, cx| {
14452 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14453 s.select_ranges([1..1])
14454 });
14455 });
14456 follower
14457 .update(cx, |follower, window, cx| {
14458 follower.apply_update_proto(
14459 &project,
14460 pending_update.borrow_mut().take().unwrap(),
14461 window,
14462 cx,
14463 )
14464 })
14465 .unwrap()
14466 .await
14467 .unwrap();
14468 _ = follower.update(cx, |follower, _, cx| {
14469 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
14470 });
14471 assert!(*is_still_following.borrow());
14472 assert_eq!(*follower_edit_event_count.borrow(), 0);
14473
14474 // Update the scroll position only
14475 _ = leader.update(cx, |leader, window, cx| {
14476 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
14477 });
14478 follower
14479 .update(cx, |follower, window, cx| {
14480 follower.apply_update_proto(
14481 &project,
14482 pending_update.borrow_mut().take().unwrap(),
14483 window,
14484 cx,
14485 )
14486 })
14487 .unwrap()
14488 .await
14489 .unwrap();
14490 assert_eq!(
14491 follower
14492 .update(cx, |follower, _, cx| follower.scroll_position(cx))
14493 .unwrap(),
14494 gpui::Point::new(1.5, 3.5)
14495 );
14496 assert!(*is_still_following.borrow());
14497 assert_eq!(*follower_edit_event_count.borrow(), 0);
14498
14499 // Update the selections and scroll position. The follower's scroll position is updated
14500 // via autoscroll, not via the leader's exact scroll position.
14501 _ = leader.update(cx, |leader, window, cx| {
14502 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14503 s.select_ranges([0..0])
14504 });
14505 leader.request_autoscroll(Autoscroll::newest(), cx);
14506 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
14507 });
14508 follower
14509 .update(cx, |follower, window, cx| {
14510 follower.apply_update_proto(
14511 &project,
14512 pending_update.borrow_mut().take().unwrap(),
14513 window,
14514 cx,
14515 )
14516 })
14517 .unwrap()
14518 .await
14519 .unwrap();
14520 _ = follower.update(cx, |follower, _, cx| {
14521 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
14522 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
14523 });
14524 assert!(*is_still_following.borrow());
14525
14526 // Creating a pending selection that precedes another selection
14527 _ = leader.update(cx, |leader, window, cx| {
14528 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14529 s.select_ranges([1..1])
14530 });
14531 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
14532 });
14533 follower
14534 .update(cx, |follower, window, cx| {
14535 follower.apply_update_proto(
14536 &project,
14537 pending_update.borrow_mut().take().unwrap(),
14538 window,
14539 cx,
14540 )
14541 })
14542 .unwrap()
14543 .await
14544 .unwrap();
14545 _ = follower.update(cx, |follower, _, cx| {
14546 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
14547 });
14548 assert!(*is_still_following.borrow());
14549
14550 // Extend the pending selection so that it surrounds another selection
14551 _ = leader.update(cx, |leader, window, cx| {
14552 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
14553 });
14554 follower
14555 .update(cx, |follower, window, cx| {
14556 follower.apply_update_proto(
14557 &project,
14558 pending_update.borrow_mut().take().unwrap(),
14559 window,
14560 cx,
14561 )
14562 })
14563 .unwrap()
14564 .await
14565 .unwrap();
14566 _ = follower.update(cx, |follower, _, cx| {
14567 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
14568 });
14569
14570 // Scrolling locally breaks the follow
14571 _ = follower.update(cx, |follower, window, cx| {
14572 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
14573 follower.set_scroll_anchor(
14574 ScrollAnchor {
14575 anchor: top_anchor,
14576 offset: gpui::Point::new(0.0, 0.5),
14577 },
14578 window,
14579 cx,
14580 );
14581 });
14582 assert!(!(*is_still_following.borrow()));
14583}
14584
14585#[gpui::test]
14586async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
14587 init_test(cx, |_| {});
14588
14589 let fs = FakeFs::new(cx.executor());
14590 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
14591 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14592 let pane = workspace
14593 .update(cx, |workspace, _, _| workspace.active_pane().clone())
14594 .unwrap();
14595
14596 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
14597
14598 let leader = pane.update_in(cx, |_, window, cx| {
14599 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
14600 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
14601 });
14602
14603 // Start following the editor when it has no excerpts.
14604 let mut state_message =
14605 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
14606 let workspace_entity = workspace.root(cx).unwrap();
14607 let follower_1 = cx
14608 .update_window(*workspace.deref(), |_, window, cx| {
14609 Editor::from_state_proto(
14610 workspace_entity,
14611 ViewId {
14612 creator: CollaboratorId::PeerId(PeerId::default()),
14613 id: 0,
14614 },
14615 &mut state_message,
14616 window,
14617 cx,
14618 )
14619 })
14620 .unwrap()
14621 .unwrap()
14622 .await
14623 .unwrap();
14624
14625 let update_message = Rc::new(RefCell::new(None));
14626 follower_1.update_in(cx, {
14627 let update = update_message.clone();
14628 |_, window, cx| {
14629 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
14630 leader.read(cx).add_event_to_update_proto(
14631 event,
14632 &mut update.borrow_mut(),
14633 window,
14634 cx,
14635 );
14636 })
14637 .detach();
14638 }
14639 });
14640
14641 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
14642 (
14643 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
14644 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
14645 )
14646 });
14647
14648 // Insert some excerpts.
14649 leader.update(cx, |leader, cx| {
14650 leader.buffer.update(cx, |multibuffer, cx| {
14651 multibuffer.set_excerpts_for_path(
14652 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
14653 buffer_1.clone(),
14654 vec![
14655 Point::row_range(0..3),
14656 Point::row_range(1..6),
14657 Point::row_range(12..15),
14658 ],
14659 0,
14660 cx,
14661 );
14662 multibuffer.set_excerpts_for_path(
14663 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
14664 buffer_2.clone(),
14665 vec![Point::row_range(0..6), Point::row_range(8..12)],
14666 0,
14667 cx,
14668 );
14669 });
14670 });
14671
14672 // Apply the update of adding the excerpts.
14673 follower_1
14674 .update_in(cx, |follower, window, cx| {
14675 follower.apply_update_proto(
14676 &project,
14677 update_message.borrow().clone().unwrap(),
14678 window,
14679 cx,
14680 )
14681 })
14682 .await
14683 .unwrap();
14684 assert_eq!(
14685 follower_1.update(cx, |editor, cx| editor.text(cx)),
14686 leader.update(cx, |editor, cx| editor.text(cx))
14687 );
14688 update_message.borrow_mut().take();
14689
14690 // Start following separately after it already has excerpts.
14691 let mut state_message =
14692 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
14693 let workspace_entity = workspace.root(cx).unwrap();
14694 let follower_2 = cx
14695 .update_window(*workspace.deref(), |_, window, cx| {
14696 Editor::from_state_proto(
14697 workspace_entity,
14698 ViewId {
14699 creator: CollaboratorId::PeerId(PeerId::default()),
14700 id: 0,
14701 },
14702 &mut state_message,
14703 window,
14704 cx,
14705 )
14706 })
14707 .unwrap()
14708 .unwrap()
14709 .await
14710 .unwrap();
14711 assert_eq!(
14712 follower_2.update(cx, |editor, cx| editor.text(cx)),
14713 leader.update(cx, |editor, cx| editor.text(cx))
14714 );
14715
14716 // Remove some excerpts.
14717 leader.update(cx, |leader, cx| {
14718 leader.buffer.update(cx, |multibuffer, cx| {
14719 let excerpt_ids = multibuffer.excerpt_ids();
14720 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
14721 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
14722 });
14723 });
14724
14725 // Apply the update of removing the excerpts.
14726 follower_1
14727 .update_in(cx, |follower, window, cx| {
14728 follower.apply_update_proto(
14729 &project,
14730 update_message.borrow().clone().unwrap(),
14731 window,
14732 cx,
14733 )
14734 })
14735 .await
14736 .unwrap();
14737 follower_2
14738 .update_in(cx, |follower, window, cx| {
14739 follower.apply_update_proto(
14740 &project,
14741 update_message.borrow().clone().unwrap(),
14742 window,
14743 cx,
14744 )
14745 })
14746 .await
14747 .unwrap();
14748 update_message.borrow_mut().take();
14749 assert_eq!(
14750 follower_1.update(cx, |editor, cx| editor.text(cx)),
14751 leader.update(cx, |editor, cx| editor.text(cx))
14752 );
14753}
14754
14755#[gpui::test]
14756async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14757 init_test(cx, |_| {});
14758
14759 let mut cx = EditorTestContext::new(cx).await;
14760 let lsp_store =
14761 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
14762
14763 cx.set_state(indoc! {"
14764 ˇfn func(abc def: i32) -> u32 {
14765 }
14766 "});
14767
14768 cx.update(|_, cx| {
14769 lsp_store.update(cx, |lsp_store, cx| {
14770 lsp_store
14771 .update_diagnostics(
14772 LanguageServerId(0),
14773 lsp::PublishDiagnosticsParams {
14774 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
14775 version: None,
14776 diagnostics: vec![
14777 lsp::Diagnostic {
14778 range: lsp::Range::new(
14779 lsp::Position::new(0, 11),
14780 lsp::Position::new(0, 12),
14781 ),
14782 severity: Some(lsp::DiagnosticSeverity::ERROR),
14783 ..Default::default()
14784 },
14785 lsp::Diagnostic {
14786 range: lsp::Range::new(
14787 lsp::Position::new(0, 12),
14788 lsp::Position::new(0, 15),
14789 ),
14790 severity: Some(lsp::DiagnosticSeverity::ERROR),
14791 ..Default::default()
14792 },
14793 lsp::Diagnostic {
14794 range: lsp::Range::new(
14795 lsp::Position::new(0, 25),
14796 lsp::Position::new(0, 28),
14797 ),
14798 severity: Some(lsp::DiagnosticSeverity::ERROR),
14799 ..Default::default()
14800 },
14801 ],
14802 },
14803 None,
14804 DiagnosticSourceKind::Pushed,
14805 &[],
14806 cx,
14807 )
14808 .unwrap()
14809 });
14810 });
14811
14812 executor.run_until_parked();
14813
14814 cx.update_editor(|editor, window, cx| {
14815 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
14816 });
14817
14818 cx.assert_editor_state(indoc! {"
14819 fn func(abc def: i32) -> ˇu32 {
14820 }
14821 "});
14822
14823 cx.update_editor(|editor, window, cx| {
14824 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
14825 });
14826
14827 cx.assert_editor_state(indoc! {"
14828 fn func(abc ˇdef: i32) -> u32 {
14829 }
14830 "});
14831
14832 cx.update_editor(|editor, window, cx| {
14833 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
14834 });
14835
14836 cx.assert_editor_state(indoc! {"
14837 fn func(abcˇ def: i32) -> u32 {
14838 }
14839 "});
14840
14841 cx.update_editor(|editor, window, cx| {
14842 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
14843 });
14844
14845 cx.assert_editor_state(indoc! {"
14846 fn func(abc def: i32) -> ˇu32 {
14847 }
14848 "});
14849}
14850
14851#[gpui::test]
14852async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14853 init_test(cx, |_| {});
14854
14855 let mut cx = EditorTestContext::new(cx).await;
14856
14857 let diff_base = r#"
14858 use some::mod;
14859
14860 const A: u32 = 42;
14861
14862 fn main() {
14863 println!("hello");
14864
14865 println!("world");
14866 }
14867 "#
14868 .unindent();
14869
14870 // Edits are modified, removed, modified, added
14871 cx.set_state(
14872 &r#"
14873 use some::modified;
14874
14875 ˇ
14876 fn main() {
14877 println!("hello there");
14878
14879 println!("around the");
14880 println!("world");
14881 }
14882 "#
14883 .unindent(),
14884 );
14885
14886 cx.set_head_text(&diff_base);
14887 executor.run_until_parked();
14888
14889 cx.update_editor(|editor, window, cx| {
14890 //Wrap around the bottom of the buffer
14891 for _ in 0..3 {
14892 editor.go_to_next_hunk(&GoToHunk, window, cx);
14893 }
14894 });
14895
14896 cx.assert_editor_state(
14897 &r#"
14898 ˇuse some::modified;
14899
14900
14901 fn main() {
14902 println!("hello there");
14903
14904 println!("around the");
14905 println!("world");
14906 }
14907 "#
14908 .unindent(),
14909 );
14910
14911 cx.update_editor(|editor, window, cx| {
14912 //Wrap around the top of the buffer
14913 for _ in 0..2 {
14914 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14915 }
14916 });
14917
14918 cx.assert_editor_state(
14919 &r#"
14920 use some::modified;
14921
14922
14923 fn main() {
14924 ˇ println!("hello there");
14925
14926 println!("around the");
14927 println!("world");
14928 }
14929 "#
14930 .unindent(),
14931 );
14932
14933 cx.update_editor(|editor, window, cx| {
14934 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14935 });
14936
14937 cx.assert_editor_state(
14938 &r#"
14939 use some::modified;
14940
14941 ˇ
14942 fn main() {
14943 println!("hello there");
14944
14945 println!("around the");
14946 println!("world");
14947 }
14948 "#
14949 .unindent(),
14950 );
14951
14952 cx.update_editor(|editor, window, cx| {
14953 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14954 });
14955
14956 cx.assert_editor_state(
14957 &r#"
14958 ˇuse some::modified;
14959
14960
14961 fn main() {
14962 println!("hello there");
14963
14964 println!("around the");
14965 println!("world");
14966 }
14967 "#
14968 .unindent(),
14969 );
14970
14971 cx.update_editor(|editor, window, cx| {
14972 for _ in 0..2 {
14973 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14974 }
14975 });
14976
14977 cx.assert_editor_state(
14978 &r#"
14979 use some::modified;
14980
14981
14982 fn main() {
14983 ˇ println!("hello there");
14984
14985 println!("around the");
14986 println!("world");
14987 }
14988 "#
14989 .unindent(),
14990 );
14991
14992 cx.update_editor(|editor, window, cx| {
14993 editor.fold(&Fold, window, cx);
14994 });
14995
14996 cx.update_editor(|editor, window, cx| {
14997 editor.go_to_next_hunk(&GoToHunk, window, cx);
14998 });
14999
15000 cx.assert_editor_state(
15001 &r#"
15002 ˇuse some::modified;
15003
15004
15005 fn main() {
15006 println!("hello there");
15007
15008 println!("around the");
15009 println!("world");
15010 }
15011 "#
15012 .unindent(),
15013 );
15014}
15015
15016#[test]
15017fn test_split_words() {
15018 fn split(text: &str) -> Vec<&str> {
15019 split_words(text).collect()
15020 }
15021
15022 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
15023 assert_eq!(split("hello_world"), &["hello_", "world"]);
15024 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
15025 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
15026 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
15027 assert_eq!(split("helloworld"), &["helloworld"]);
15028
15029 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
15030}
15031
15032#[gpui::test]
15033async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
15034 init_test(cx, |_| {});
15035
15036 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
15037 let mut assert = |before, after| {
15038 let _state_context = cx.set_state(before);
15039 cx.run_until_parked();
15040 cx.update_editor(|editor, window, cx| {
15041 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
15042 });
15043 cx.run_until_parked();
15044 cx.assert_editor_state(after);
15045 };
15046
15047 // Outside bracket jumps to outside of matching bracket
15048 assert("console.logˇ(var);", "console.log(var)ˇ;");
15049 assert("console.log(var)ˇ;", "console.logˇ(var);");
15050
15051 // Inside bracket jumps to inside of matching bracket
15052 assert("console.log(ˇvar);", "console.log(varˇ);");
15053 assert("console.log(varˇ);", "console.log(ˇvar);");
15054
15055 // When outside a bracket and inside, favor jumping to the inside bracket
15056 assert(
15057 "console.log('foo', [1, 2, 3]ˇ);",
15058 "console.log(ˇ'foo', [1, 2, 3]);",
15059 );
15060 assert(
15061 "console.log(ˇ'foo', [1, 2, 3]);",
15062 "console.log('foo', [1, 2, 3]ˇ);",
15063 );
15064
15065 // Bias forward if two options are equally likely
15066 assert(
15067 "let result = curried_fun()ˇ();",
15068 "let result = curried_fun()()ˇ;",
15069 );
15070
15071 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
15072 assert(
15073 indoc! {"
15074 function test() {
15075 console.log('test')ˇ
15076 }"},
15077 indoc! {"
15078 function test() {
15079 console.logˇ('test')
15080 }"},
15081 );
15082}
15083
15084#[gpui::test]
15085async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
15086 init_test(cx, |_| {});
15087
15088 let fs = FakeFs::new(cx.executor());
15089 fs.insert_tree(
15090 path!("/a"),
15091 json!({
15092 "main.rs": "fn main() { let a = 5; }",
15093 "other.rs": "// Test file",
15094 }),
15095 )
15096 .await;
15097 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15098
15099 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15100 language_registry.add(Arc::new(Language::new(
15101 LanguageConfig {
15102 name: "Rust".into(),
15103 matcher: LanguageMatcher {
15104 path_suffixes: vec!["rs".to_string()],
15105 ..Default::default()
15106 },
15107 brackets: BracketPairConfig {
15108 pairs: vec![BracketPair {
15109 start: "{".to_string(),
15110 end: "}".to_string(),
15111 close: true,
15112 surround: true,
15113 newline: true,
15114 }],
15115 disabled_scopes_by_bracket_ix: Vec::new(),
15116 },
15117 ..Default::default()
15118 },
15119 Some(tree_sitter_rust::LANGUAGE.into()),
15120 )));
15121 let mut fake_servers = language_registry.register_fake_lsp(
15122 "Rust",
15123 FakeLspAdapter {
15124 capabilities: lsp::ServerCapabilities {
15125 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
15126 first_trigger_character: "{".to_string(),
15127 more_trigger_character: None,
15128 }),
15129 ..Default::default()
15130 },
15131 ..Default::default()
15132 },
15133 );
15134
15135 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15136
15137 let cx = &mut VisualTestContext::from_window(*workspace, cx);
15138
15139 let worktree_id = workspace
15140 .update(cx, |workspace, _, cx| {
15141 workspace.project().update(cx, |project, cx| {
15142 project.worktrees(cx).next().unwrap().read(cx).id()
15143 })
15144 })
15145 .unwrap();
15146
15147 let buffer = project
15148 .update(cx, |project, cx| {
15149 project.open_local_buffer(path!("/a/main.rs"), cx)
15150 })
15151 .await
15152 .unwrap();
15153 let editor_handle = workspace
15154 .update(cx, |workspace, window, cx| {
15155 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
15156 })
15157 .unwrap()
15158 .await
15159 .unwrap()
15160 .downcast::<Editor>()
15161 .unwrap();
15162
15163 cx.executor().start_waiting();
15164 let fake_server = fake_servers.next().await.unwrap();
15165
15166 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
15167 |params, _| async move {
15168 assert_eq!(
15169 params.text_document_position.text_document.uri,
15170 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
15171 );
15172 assert_eq!(
15173 params.text_document_position.position,
15174 lsp::Position::new(0, 21),
15175 );
15176
15177 Ok(Some(vec![lsp::TextEdit {
15178 new_text: "]".to_string(),
15179 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15180 }]))
15181 },
15182 );
15183
15184 editor_handle.update_in(cx, |editor, window, cx| {
15185 window.focus(&editor.focus_handle(cx));
15186 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15187 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
15188 });
15189 editor.handle_input("{", window, cx);
15190 });
15191
15192 cx.executor().run_until_parked();
15193
15194 buffer.update(cx, |buffer, _| {
15195 assert_eq!(
15196 buffer.text(),
15197 "fn main() { let a = {5}; }",
15198 "No extra braces from on type formatting should appear in the buffer"
15199 )
15200 });
15201}
15202
15203#[gpui::test(iterations = 20, seeds(31))]
15204async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
15205 init_test(cx, |_| {});
15206
15207 let mut cx = EditorLspTestContext::new_rust(
15208 lsp::ServerCapabilities {
15209 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
15210 first_trigger_character: ".".to_string(),
15211 more_trigger_character: None,
15212 }),
15213 ..Default::default()
15214 },
15215 cx,
15216 )
15217 .await;
15218
15219 cx.update_buffer(|buffer, _| {
15220 // This causes autoindent to be async.
15221 buffer.set_sync_parse_timeout(Duration::ZERO)
15222 });
15223
15224 cx.set_state("fn c() {\n d()ˇ\n}\n");
15225 cx.simulate_keystroke("\n");
15226 cx.run_until_parked();
15227
15228 let buffer_cloned =
15229 cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap().clone());
15230 let mut request =
15231 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
15232 let buffer_cloned = buffer_cloned.clone();
15233 async move {
15234 buffer_cloned.update(&mut cx, |buffer, _| {
15235 assert_eq!(
15236 buffer.text(),
15237 "fn c() {\n d()\n .\n}\n",
15238 "OnTypeFormatting should triggered after autoindent applied"
15239 )
15240 })?;
15241
15242 Ok(Some(vec![]))
15243 }
15244 });
15245
15246 cx.simulate_keystroke(".");
15247 cx.run_until_parked();
15248
15249 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
15250 assert!(request.next().await.is_some());
15251 request.close();
15252 assert!(request.next().await.is_none());
15253}
15254
15255#[gpui::test]
15256async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
15257 init_test(cx, |_| {});
15258
15259 let fs = FakeFs::new(cx.executor());
15260 fs.insert_tree(
15261 path!("/a"),
15262 json!({
15263 "main.rs": "fn main() { let a = 5; }",
15264 "other.rs": "// Test file",
15265 }),
15266 )
15267 .await;
15268
15269 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15270
15271 let server_restarts = Arc::new(AtomicUsize::new(0));
15272 let closure_restarts = Arc::clone(&server_restarts);
15273 let language_server_name = "test language server";
15274 let language_name: LanguageName = "Rust".into();
15275
15276 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15277 language_registry.add(Arc::new(Language::new(
15278 LanguageConfig {
15279 name: language_name.clone(),
15280 matcher: LanguageMatcher {
15281 path_suffixes: vec!["rs".to_string()],
15282 ..Default::default()
15283 },
15284 ..Default::default()
15285 },
15286 Some(tree_sitter_rust::LANGUAGE.into()),
15287 )));
15288 let mut fake_servers = language_registry.register_fake_lsp(
15289 "Rust",
15290 FakeLspAdapter {
15291 name: language_server_name,
15292 initialization_options: Some(json!({
15293 "testOptionValue": true
15294 })),
15295 initializer: Some(Box::new(move |fake_server| {
15296 let task_restarts = Arc::clone(&closure_restarts);
15297 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
15298 task_restarts.fetch_add(1, atomic::Ordering::Release);
15299 futures::future::ready(Ok(()))
15300 });
15301 })),
15302 ..Default::default()
15303 },
15304 );
15305
15306 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15307 let _buffer = project
15308 .update(cx, |project, cx| {
15309 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
15310 })
15311 .await
15312 .unwrap();
15313 let _fake_server = fake_servers.next().await.unwrap();
15314 update_test_language_settings(cx, |language_settings| {
15315 language_settings.languages.0.insert(
15316 language_name.clone(),
15317 LanguageSettingsContent {
15318 tab_size: NonZeroU32::new(8),
15319 ..Default::default()
15320 },
15321 );
15322 });
15323 cx.executor().run_until_parked();
15324 assert_eq!(
15325 server_restarts.load(atomic::Ordering::Acquire),
15326 0,
15327 "Should not restart LSP server on an unrelated change"
15328 );
15329
15330 update_test_project_settings(cx, |project_settings| {
15331 project_settings.lsp.insert(
15332 "Some other server name".into(),
15333 LspSettings {
15334 binary: None,
15335 settings: None,
15336 initialization_options: Some(json!({
15337 "some other init value": false
15338 })),
15339 enable_lsp_tasks: false,
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 LSP settings change"
15348 );
15349
15350 update_test_project_settings(cx, |project_settings| {
15351 project_settings.lsp.insert(
15352 language_server_name.into(),
15353 LspSettings {
15354 binary: None,
15355 settings: None,
15356 initialization_options: Some(json!({
15357 "anotherInitValue": 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 1,
15367 "Should restart LSP server on a related 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 not restart LSP server on a related LSP settings change that is the same"
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: None,
15397 enable_lsp_tasks: false,
15398 },
15399 );
15400 });
15401 cx.executor().run_until_parked();
15402 assert_eq!(
15403 server_restarts.load(atomic::Ordering::Acquire),
15404 2,
15405 "Should restart LSP server on another related LSP settings change"
15406 );
15407}
15408
15409#[gpui::test]
15410async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
15411 init_test(cx, |_| {});
15412
15413 let mut cx = EditorLspTestContext::new_rust(
15414 lsp::ServerCapabilities {
15415 completion_provider: Some(lsp::CompletionOptions {
15416 trigger_characters: Some(vec![".".to_string()]),
15417 resolve_provider: Some(true),
15418 ..Default::default()
15419 }),
15420 ..Default::default()
15421 },
15422 cx,
15423 )
15424 .await;
15425
15426 cx.set_state("fn main() { let a = 2ˇ; }");
15427 cx.simulate_keystroke(".");
15428 let completion_item = lsp::CompletionItem {
15429 label: "some".into(),
15430 kind: Some(lsp::CompletionItemKind::SNIPPET),
15431 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15432 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15433 kind: lsp::MarkupKind::Markdown,
15434 value: "```rust\nSome(2)\n```".to_string(),
15435 })),
15436 deprecated: Some(false),
15437 sort_text: Some("fffffff2".to_string()),
15438 filter_text: Some("some".to_string()),
15439 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15440 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15441 range: lsp::Range {
15442 start: lsp::Position {
15443 line: 0,
15444 character: 22,
15445 },
15446 end: lsp::Position {
15447 line: 0,
15448 character: 22,
15449 },
15450 },
15451 new_text: "Some(2)".to_string(),
15452 })),
15453 additional_text_edits: Some(vec![lsp::TextEdit {
15454 range: lsp::Range {
15455 start: lsp::Position {
15456 line: 0,
15457 character: 20,
15458 },
15459 end: lsp::Position {
15460 line: 0,
15461 character: 22,
15462 },
15463 },
15464 new_text: "".to_string(),
15465 }]),
15466 ..Default::default()
15467 };
15468
15469 let closure_completion_item = completion_item.clone();
15470 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15471 let task_completion_item = closure_completion_item.clone();
15472 async move {
15473 Ok(Some(lsp::CompletionResponse::Array(vec![
15474 task_completion_item,
15475 ])))
15476 }
15477 });
15478
15479 request.next().await;
15480
15481 cx.condition(|editor, _| editor.context_menu_visible())
15482 .await;
15483 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
15484 editor
15485 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15486 .unwrap()
15487 });
15488 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
15489
15490 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
15491 let task_completion_item = completion_item.clone();
15492 async move { Ok(task_completion_item) }
15493 })
15494 .next()
15495 .await
15496 .unwrap();
15497 apply_additional_edits.await.unwrap();
15498 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
15499}
15500
15501#[gpui::test]
15502async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
15503 init_test(cx, |_| {});
15504
15505 let mut cx = EditorLspTestContext::new_rust(
15506 lsp::ServerCapabilities {
15507 completion_provider: Some(lsp::CompletionOptions {
15508 trigger_characters: Some(vec![".".to_string()]),
15509 resolve_provider: Some(true),
15510 ..Default::default()
15511 }),
15512 ..Default::default()
15513 },
15514 cx,
15515 )
15516 .await;
15517
15518 cx.set_state("fn main() { let a = 2ˇ; }");
15519 cx.simulate_keystroke(".");
15520
15521 let item1 = lsp::CompletionItem {
15522 label: "method id()".to_string(),
15523 filter_text: Some("id".to_string()),
15524 detail: None,
15525 documentation: None,
15526 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15527 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15528 new_text: ".id".to_string(),
15529 })),
15530 ..lsp::CompletionItem::default()
15531 };
15532
15533 let item2 = lsp::CompletionItem {
15534 label: "other".to_string(),
15535 filter_text: Some("other".to_string()),
15536 detail: None,
15537 documentation: None,
15538 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15539 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15540 new_text: ".other".to_string(),
15541 })),
15542 ..lsp::CompletionItem::default()
15543 };
15544
15545 let item1 = item1.clone();
15546 cx.set_request_handler::<lsp::request::Completion, _, _>({
15547 let item1 = item1.clone();
15548 move |_, _, _| {
15549 let item1 = item1.clone();
15550 let item2 = item2.clone();
15551 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
15552 }
15553 })
15554 .next()
15555 .await;
15556
15557 cx.condition(|editor, _| editor.context_menu_visible())
15558 .await;
15559 cx.update_editor(|editor, _, _| {
15560 let context_menu = editor.context_menu.borrow_mut();
15561 let context_menu = context_menu
15562 .as_ref()
15563 .expect("Should have the context menu deployed");
15564 match context_menu {
15565 CodeContextMenu::Completions(completions_menu) => {
15566 let completions = completions_menu.completions.borrow_mut();
15567 assert_eq!(
15568 completions
15569 .iter()
15570 .map(|completion| &completion.label.text)
15571 .collect::<Vec<_>>(),
15572 vec!["method id()", "other"]
15573 )
15574 }
15575 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15576 }
15577 });
15578
15579 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
15580 let item1 = item1.clone();
15581 move |_, item_to_resolve, _| {
15582 let item1 = item1.clone();
15583 async move {
15584 if item1 == item_to_resolve {
15585 Ok(lsp::CompletionItem {
15586 label: "method id()".to_string(),
15587 filter_text: Some("id".to_string()),
15588 detail: Some("Now resolved!".to_string()),
15589 documentation: Some(lsp::Documentation::String("Docs".to_string())),
15590 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15591 range: lsp::Range::new(
15592 lsp::Position::new(0, 22),
15593 lsp::Position::new(0, 22),
15594 ),
15595 new_text: ".id".to_string(),
15596 })),
15597 ..lsp::CompletionItem::default()
15598 })
15599 } else {
15600 Ok(item_to_resolve)
15601 }
15602 }
15603 }
15604 })
15605 .next()
15606 .await
15607 .unwrap();
15608 cx.run_until_parked();
15609
15610 cx.update_editor(|editor, window, cx| {
15611 editor.context_menu_next(&Default::default(), window, cx);
15612 });
15613
15614 cx.update_editor(|editor, _, _| {
15615 let context_menu = editor.context_menu.borrow_mut();
15616 let context_menu = context_menu
15617 .as_ref()
15618 .expect("Should have the context menu deployed");
15619 match context_menu {
15620 CodeContextMenu::Completions(completions_menu) => {
15621 let completions = completions_menu.completions.borrow_mut();
15622 assert_eq!(
15623 completions
15624 .iter()
15625 .map(|completion| &completion.label.text)
15626 .collect::<Vec<_>>(),
15627 vec!["method id() Now resolved!", "other"],
15628 "Should update first completion label, but not second as the filter text did not match."
15629 );
15630 }
15631 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15632 }
15633 });
15634}
15635
15636#[gpui::test]
15637async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
15638 init_test(cx, |_| {});
15639 let mut cx = EditorLspTestContext::new_rust(
15640 lsp::ServerCapabilities {
15641 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
15642 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
15643 completion_provider: Some(lsp::CompletionOptions {
15644 resolve_provider: Some(true),
15645 ..Default::default()
15646 }),
15647 ..Default::default()
15648 },
15649 cx,
15650 )
15651 .await;
15652 cx.set_state(indoc! {"
15653 struct TestStruct {
15654 field: i32
15655 }
15656
15657 fn mainˇ() {
15658 let unused_var = 42;
15659 let test_struct = TestStruct { field: 42 };
15660 }
15661 "});
15662 let symbol_range = cx.lsp_range(indoc! {"
15663 struct TestStruct {
15664 field: i32
15665 }
15666
15667 «fn main»() {
15668 let unused_var = 42;
15669 let test_struct = TestStruct { field: 42 };
15670 }
15671 "});
15672 let mut hover_requests =
15673 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
15674 Ok(Some(lsp::Hover {
15675 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
15676 kind: lsp::MarkupKind::Markdown,
15677 value: "Function documentation".to_string(),
15678 }),
15679 range: Some(symbol_range),
15680 }))
15681 });
15682
15683 // Case 1: Test that code action menu hide hover popover
15684 cx.dispatch_action(Hover);
15685 hover_requests.next().await;
15686 cx.condition(|editor, _| editor.hover_state.visible()).await;
15687 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
15688 move |_, _, _| async move {
15689 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
15690 lsp::CodeAction {
15691 title: "Remove unused variable".to_string(),
15692 kind: Some(CodeActionKind::QUICKFIX),
15693 edit: Some(lsp::WorkspaceEdit {
15694 changes: Some(
15695 [(
15696 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
15697 vec![lsp::TextEdit {
15698 range: lsp::Range::new(
15699 lsp::Position::new(5, 4),
15700 lsp::Position::new(5, 27),
15701 ),
15702 new_text: "".to_string(),
15703 }],
15704 )]
15705 .into_iter()
15706 .collect(),
15707 ),
15708 ..Default::default()
15709 }),
15710 ..Default::default()
15711 },
15712 )]))
15713 },
15714 );
15715 cx.update_editor(|editor, window, cx| {
15716 editor.toggle_code_actions(
15717 &ToggleCodeActions {
15718 deployed_from: None,
15719 quick_launch: false,
15720 },
15721 window,
15722 cx,
15723 );
15724 });
15725 code_action_requests.next().await;
15726 cx.run_until_parked();
15727 cx.condition(|editor, _| editor.context_menu_visible())
15728 .await;
15729 cx.update_editor(|editor, _, _| {
15730 assert!(
15731 !editor.hover_state.visible(),
15732 "Hover popover should be hidden when code action menu is shown"
15733 );
15734 // Hide code actions
15735 editor.context_menu.take();
15736 });
15737
15738 // Case 2: Test that code completions hide hover popover
15739 cx.dispatch_action(Hover);
15740 hover_requests.next().await;
15741 cx.condition(|editor, _| editor.hover_state.visible()).await;
15742 let counter = Arc::new(AtomicUsize::new(0));
15743 let mut completion_requests =
15744 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15745 let counter = counter.clone();
15746 async move {
15747 counter.fetch_add(1, atomic::Ordering::Release);
15748 Ok(Some(lsp::CompletionResponse::Array(vec![
15749 lsp::CompletionItem {
15750 label: "main".into(),
15751 kind: Some(lsp::CompletionItemKind::FUNCTION),
15752 detail: Some("() -> ()".to_string()),
15753 ..Default::default()
15754 },
15755 lsp::CompletionItem {
15756 label: "TestStruct".into(),
15757 kind: Some(lsp::CompletionItemKind::STRUCT),
15758 detail: Some("struct TestStruct".to_string()),
15759 ..Default::default()
15760 },
15761 ])))
15762 }
15763 });
15764 cx.update_editor(|editor, window, cx| {
15765 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15766 });
15767 completion_requests.next().await;
15768 cx.condition(|editor, _| editor.context_menu_visible())
15769 .await;
15770 cx.update_editor(|editor, _, _| {
15771 assert!(
15772 !editor.hover_state.visible(),
15773 "Hover popover should be hidden when completion menu is shown"
15774 );
15775 });
15776}
15777
15778#[gpui::test]
15779async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
15780 init_test(cx, |_| {});
15781
15782 let mut cx = EditorLspTestContext::new_rust(
15783 lsp::ServerCapabilities {
15784 completion_provider: Some(lsp::CompletionOptions {
15785 trigger_characters: Some(vec![".".to_string()]),
15786 resolve_provider: Some(true),
15787 ..Default::default()
15788 }),
15789 ..Default::default()
15790 },
15791 cx,
15792 )
15793 .await;
15794
15795 cx.set_state("fn main() { let a = 2ˇ; }");
15796 cx.simulate_keystroke(".");
15797
15798 let unresolved_item_1 = lsp::CompletionItem {
15799 label: "id".to_string(),
15800 filter_text: Some("id".to_string()),
15801 detail: None,
15802 documentation: None,
15803 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15804 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15805 new_text: ".id".to_string(),
15806 })),
15807 ..lsp::CompletionItem::default()
15808 };
15809 let resolved_item_1 = lsp::CompletionItem {
15810 additional_text_edits: Some(vec![lsp::TextEdit {
15811 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
15812 new_text: "!!".to_string(),
15813 }]),
15814 ..unresolved_item_1.clone()
15815 };
15816 let unresolved_item_2 = lsp::CompletionItem {
15817 label: "other".to_string(),
15818 filter_text: Some("other".to_string()),
15819 detail: None,
15820 documentation: None,
15821 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15822 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15823 new_text: ".other".to_string(),
15824 })),
15825 ..lsp::CompletionItem::default()
15826 };
15827 let resolved_item_2 = lsp::CompletionItem {
15828 additional_text_edits: Some(vec![lsp::TextEdit {
15829 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
15830 new_text: "??".to_string(),
15831 }]),
15832 ..unresolved_item_2.clone()
15833 };
15834
15835 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
15836 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
15837 cx.lsp
15838 .server
15839 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
15840 let unresolved_item_1 = unresolved_item_1.clone();
15841 let resolved_item_1 = resolved_item_1.clone();
15842 let unresolved_item_2 = unresolved_item_2.clone();
15843 let resolved_item_2 = resolved_item_2.clone();
15844 let resolve_requests_1 = resolve_requests_1.clone();
15845 let resolve_requests_2 = resolve_requests_2.clone();
15846 move |unresolved_request, _| {
15847 let unresolved_item_1 = unresolved_item_1.clone();
15848 let resolved_item_1 = resolved_item_1.clone();
15849 let unresolved_item_2 = unresolved_item_2.clone();
15850 let resolved_item_2 = resolved_item_2.clone();
15851 let resolve_requests_1 = resolve_requests_1.clone();
15852 let resolve_requests_2 = resolve_requests_2.clone();
15853 async move {
15854 if unresolved_request == unresolved_item_1 {
15855 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
15856 Ok(resolved_item_1.clone())
15857 } else if unresolved_request == unresolved_item_2 {
15858 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
15859 Ok(resolved_item_2.clone())
15860 } else {
15861 panic!("Unexpected completion item {unresolved_request:?}")
15862 }
15863 }
15864 }
15865 })
15866 .detach();
15867
15868 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15869 let unresolved_item_1 = unresolved_item_1.clone();
15870 let unresolved_item_2 = unresolved_item_2.clone();
15871 async move {
15872 Ok(Some(lsp::CompletionResponse::Array(vec![
15873 unresolved_item_1,
15874 unresolved_item_2,
15875 ])))
15876 }
15877 })
15878 .next()
15879 .await;
15880
15881 cx.condition(|editor, _| editor.context_menu_visible())
15882 .await;
15883 cx.update_editor(|editor, _, _| {
15884 let context_menu = editor.context_menu.borrow_mut();
15885 let context_menu = context_menu
15886 .as_ref()
15887 .expect("Should have the context menu deployed");
15888 match context_menu {
15889 CodeContextMenu::Completions(completions_menu) => {
15890 let completions = completions_menu.completions.borrow_mut();
15891 assert_eq!(
15892 completions
15893 .iter()
15894 .map(|completion| &completion.label.text)
15895 .collect::<Vec<_>>(),
15896 vec!["id", "other"]
15897 )
15898 }
15899 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15900 }
15901 });
15902 cx.run_until_parked();
15903
15904 cx.update_editor(|editor, window, cx| {
15905 editor.context_menu_next(&ContextMenuNext, window, cx);
15906 });
15907 cx.run_until_parked();
15908 cx.update_editor(|editor, window, cx| {
15909 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
15910 });
15911 cx.run_until_parked();
15912 cx.update_editor(|editor, window, cx| {
15913 editor.context_menu_next(&ContextMenuNext, window, cx);
15914 });
15915 cx.run_until_parked();
15916 cx.update_editor(|editor, window, cx| {
15917 editor
15918 .compose_completion(&ComposeCompletion::default(), window, cx)
15919 .expect("No task returned")
15920 })
15921 .await
15922 .expect("Completion failed");
15923 cx.run_until_parked();
15924
15925 cx.update_editor(|editor, _, cx| {
15926 assert_eq!(
15927 resolve_requests_1.load(atomic::Ordering::Acquire),
15928 1,
15929 "Should always resolve once despite multiple selections"
15930 );
15931 assert_eq!(
15932 resolve_requests_2.load(atomic::Ordering::Acquire),
15933 1,
15934 "Should always resolve once after multiple selections and applying the completion"
15935 );
15936 assert_eq!(
15937 editor.text(cx),
15938 "fn main() { let a = ??.other; }",
15939 "Should use resolved data when applying the completion"
15940 );
15941 });
15942}
15943
15944#[gpui::test]
15945async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
15946 init_test(cx, |_| {});
15947
15948 let item_0 = lsp::CompletionItem {
15949 label: "abs".into(),
15950 insert_text: Some("abs".into()),
15951 data: Some(json!({ "very": "special"})),
15952 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
15953 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
15954 lsp::InsertReplaceEdit {
15955 new_text: "abs".to_string(),
15956 insert: lsp::Range::default(),
15957 replace: lsp::Range::default(),
15958 },
15959 )),
15960 ..lsp::CompletionItem::default()
15961 };
15962 let items = iter::once(item_0.clone())
15963 .chain((11..51).map(|i| lsp::CompletionItem {
15964 label: format!("item_{}", i),
15965 insert_text: Some(format!("item_{}", i)),
15966 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15967 ..lsp::CompletionItem::default()
15968 }))
15969 .collect::<Vec<_>>();
15970
15971 let default_commit_characters = vec!["?".to_string()];
15972 let default_data = json!({ "default": "data"});
15973 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
15974 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
15975 let default_edit_range = lsp::Range {
15976 start: lsp::Position {
15977 line: 0,
15978 character: 5,
15979 },
15980 end: lsp::Position {
15981 line: 0,
15982 character: 5,
15983 },
15984 };
15985
15986 let mut cx = EditorLspTestContext::new_rust(
15987 lsp::ServerCapabilities {
15988 completion_provider: Some(lsp::CompletionOptions {
15989 trigger_characters: Some(vec![".".to_string()]),
15990 resolve_provider: Some(true),
15991 ..Default::default()
15992 }),
15993 ..Default::default()
15994 },
15995 cx,
15996 )
15997 .await;
15998
15999 cx.set_state("fn main() { let a = 2ˇ; }");
16000 cx.simulate_keystroke(".");
16001
16002 let completion_data = default_data.clone();
16003 let completion_characters = default_commit_characters.clone();
16004 let completion_items = items.clone();
16005 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
16006 let default_data = completion_data.clone();
16007 let default_commit_characters = completion_characters.clone();
16008 let items = completion_items.clone();
16009 async move {
16010 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
16011 items,
16012 item_defaults: Some(lsp::CompletionListItemDefaults {
16013 data: Some(default_data.clone()),
16014 commit_characters: Some(default_commit_characters.clone()),
16015 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
16016 default_edit_range,
16017 )),
16018 insert_text_format: Some(default_insert_text_format),
16019 insert_text_mode: Some(default_insert_text_mode),
16020 }),
16021 ..lsp::CompletionList::default()
16022 })))
16023 }
16024 })
16025 .next()
16026 .await;
16027
16028 let resolved_items = Arc::new(Mutex::new(Vec::new()));
16029 cx.lsp
16030 .server
16031 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
16032 let closure_resolved_items = resolved_items.clone();
16033 move |item_to_resolve, _| {
16034 let closure_resolved_items = closure_resolved_items.clone();
16035 async move {
16036 closure_resolved_items.lock().push(item_to_resolve.clone());
16037 Ok(item_to_resolve)
16038 }
16039 }
16040 })
16041 .detach();
16042
16043 cx.condition(|editor, _| editor.context_menu_visible())
16044 .await;
16045 cx.run_until_parked();
16046 cx.update_editor(|editor, _, _| {
16047 let menu = editor.context_menu.borrow_mut();
16048 match menu.as_ref().expect("should have the completions menu") {
16049 CodeContextMenu::Completions(completions_menu) => {
16050 assert_eq!(
16051 completions_menu
16052 .entries
16053 .borrow()
16054 .iter()
16055 .map(|mat| mat.string.clone())
16056 .collect::<Vec<String>>(),
16057 items
16058 .iter()
16059 .map(|completion| completion.label.clone())
16060 .collect::<Vec<String>>()
16061 );
16062 }
16063 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
16064 }
16065 });
16066 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
16067 // with 4 from the end.
16068 assert_eq!(
16069 *resolved_items.lock(),
16070 [&items[0..16], &items[items.len() - 4..items.len()]]
16071 .concat()
16072 .iter()
16073 .cloned()
16074 .map(|mut item| {
16075 if item.data.is_none() {
16076 item.data = Some(default_data.clone());
16077 }
16078 item
16079 })
16080 .collect::<Vec<lsp::CompletionItem>>(),
16081 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
16082 );
16083 resolved_items.lock().clear();
16084
16085 cx.update_editor(|editor, window, cx| {
16086 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
16087 });
16088 cx.run_until_parked();
16089 // Completions that have already been resolved are skipped.
16090 assert_eq!(
16091 *resolved_items.lock(),
16092 items[items.len() - 17..items.len() - 4]
16093 .iter()
16094 .cloned()
16095 .map(|mut item| {
16096 if item.data.is_none() {
16097 item.data = Some(default_data.clone());
16098 }
16099 item
16100 })
16101 .collect::<Vec<lsp::CompletionItem>>()
16102 );
16103 resolved_items.lock().clear();
16104}
16105
16106#[gpui::test]
16107async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
16108 init_test(cx, |_| {});
16109
16110 let mut cx = EditorLspTestContext::new(
16111 Language::new(
16112 LanguageConfig {
16113 matcher: LanguageMatcher {
16114 path_suffixes: vec!["jsx".into()],
16115 ..Default::default()
16116 },
16117 overrides: [(
16118 "element".into(),
16119 LanguageConfigOverride {
16120 completion_query_characters: Override::Set(['-'].into_iter().collect()),
16121 ..Default::default()
16122 },
16123 )]
16124 .into_iter()
16125 .collect(),
16126 ..Default::default()
16127 },
16128 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
16129 )
16130 .with_override_query("(jsx_self_closing_element) @element")
16131 .unwrap(),
16132 lsp::ServerCapabilities {
16133 completion_provider: Some(lsp::CompletionOptions {
16134 trigger_characters: Some(vec![":".to_string()]),
16135 ..Default::default()
16136 }),
16137 ..Default::default()
16138 },
16139 cx,
16140 )
16141 .await;
16142
16143 cx.lsp
16144 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
16145 Ok(Some(lsp::CompletionResponse::Array(vec![
16146 lsp::CompletionItem {
16147 label: "bg-blue".into(),
16148 ..Default::default()
16149 },
16150 lsp::CompletionItem {
16151 label: "bg-red".into(),
16152 ..Default::default()
16153 },
16154 lsp::CompletionItem {
16155 label: "bg-yellow".into(),
16156 ..Default::default()
16157 },
16158 ])))
16159 });
16160
16161 cx.set_state(r#"<p class="bgˇ" />"#);
16162
16163 // Trigger completion when typing a dash, because the dash is an extra
16164 // word character in the 'element' scope, which contains the cursor.
16165 cx.simulate_keystroke("-");
16166 cx.executor().run_until_parked();
16167 cx.update_editor(|editor, _, _| {
16168 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16169 {
16170 assert_eq!(
16171 completion_menu_entries(&menu),
16172 &["bg-blue", "bg-red", "bg-yellow"]
16173 );
16174 } else {
16175 panic!("expected completion menu to be open");
16176 }
16177 });
16178
16179 cx.simulate_keystroke("l");
16180 cx.executor().run_until_parked();
16181 cx.update_editor(|editor, _, _| {
16182 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16183 {
16184 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
16185 } else {
16186 panic!("expected completion menu to be open");
16187 }
16188 });
16189
16190 // When filtering completions, consider the character after the '-' to
16191 // be the start of a subword.
16192 cx.set_state(r#"<p class="yelˇ" />"#);
16193 cx.simulate_keystroke("l");
16194 cx.executor().run_until_parked();
16195 cx.update_editor(|editor, _, _| {
16196 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16197 {
16198 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
16199 } else {
16200 panic!("expected completion menu to be open");
16201 }
16202 });
16203}
16204
16205fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
16206 let entries = menu.entries.borrow();
16207 entries.iter().map(|mat| mat.string.clone()).collect()
16208}
16209
16210#[gpui::test]
16211async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
16212 init_test(cx, |settings| {
16213 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
16214 Formatter::Prettier,
16215 )))
16216 });
16217
16218 let fs = FakeFs::new(cx.executor());
16219 fs.insert_file(path!("/file.ts"), Default::default()).await;
16220
16221 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
16222 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
16223
16224 language_registry.add(Arc::new(Language::new(
16225 LanguageConfig {
16226 name: "TypeScript".into(),
16227 matcher: LanguageMatcher {
16228 path_suffixes: vec!["ts".to_string()],
16229 ..Default::default()
16230 },
16231 ..Default::default()
16232 },
16233 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
16234 )));
16235 update_test_language_settings(cx, |settings| {
16236 settings.defaults.prettier = Some(PrettierSettings {
16237 allowed: true,
16238 ..PrettierSettings::default()
16239 });
16240 });
16241
16242 let test_plugin = "test_plugin";
16243 let _ = language_registry.register_fake_lsp(
16244 "TypeScript",
16245 FakeLspAdapter {
16246 prettier_plugins: vec![test_plugin],
16247 ..Default::default()
16248 },
16249 );
16250
16251 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
16252 let buffer = project
16253 .update(cx, |project, cx| {
16254 project.open_local_buffer(path!("/file.ts"), cx)
16255 })
16256 .await
16257 .unwrap();
16258
16259 let buffer_text = "one\ntwo\nthree\n";
16260 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16261 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16262 editor.update_in(cx, |editor, window, cx| {
16263 editor.set_text(buffer_text, window, cx)
16264 });
16265
16266 editor
16267 .update_in(cx, |editor, window, cx| {
16268 editor.perform_format(
16269 project.clone(),
16270 FormatTrigger::Manual,
16271 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
16272 window,
16273 cx,
16274 )
16275 })
16276 .unwrap()
16277 .await;
16278 assert_eq!(
16279 editor.update(cx, |editor, cx| editor.text(cx)),
16280 buffer_text.to_string() + prettier_format_suffix,
16281 "Test prettier formatting was not applied to the original buffer text",
16282 );
16283
16284 update_test_language_settings(cx, |settings| {
16285 settings.defaults.formatter = Some(SelectedFormatter::Auto)
16286 });
16287 let format = editor.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 format.await.unwrap();
16297 assert_eq!(
16298 editor.update(cx, |editor, cx| editor.text(cx)),
16299 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
16300 "Autoformatting (via test prettier) was not applied to the original buffer text",
16301 );
16302}
16303
16304#[gpui::test]
16305async fn test_addition_reverts(cx: &mut TestAppContext) {
16306 init_test(cx, |_| {});
16307 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16308 let base_text = indoc! {r#"
16309 struct Row;
16310 struct Row1;
16311 struct Row2;
16312
16313 struct Row4;
16314 struct Row5;
16315 struct Row6;
16316
16317 struct Row8;
16318 struct Row9;
16319 struct Row10;"#};
16320
16321 // When addition hunks are not adjacent to carets, no hunk revert is performed
16322 assert_hunk_revert(
16323 indoc! {r#"struct Row;
16324 struct Row1;
16325 struct Row1.1;
16326 struct Row1.2;
16327 struct Row2;ˇ
16328
16329 struct Row4;
16330 struct Row5;
16331 struct Row6;
16332
16333 struct Row8;
16334 ˇstruct Row9;
16335 struct Row9.1;
16336 struct Row9.2;
16337 struct Row9.3;
16338 struct Row10;"#},
16339 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
16340 indoc! {r#"struct Row;
16341 struct Row1;
16342 struct Row1.1;
16343 struct Row1.2;
16344 struct Row2;ˇ
16345
16346 struct Row4;
16347 struct Row5;
16348 struct Row6;
16349
16350 struct Row8;
16351 ˇstruct Row9;
16352 struct Row9.1;
16353 struct Row9.2;
16354 struct Row9.3;
16355 struct Row10;"#},
16356 base_text,
16357 &mut cx,
16358 );
16359 // Same for selections
16360 assert_hunk_revert(
16361 indoc! {r#"struct Row;
16362 struct Row1;
16363 struct Row2;
16364 struct Row2.1;
16365 struct Row2.2;
16366 «ˇ
16367 struct Row4;
16368 struct» Row5;
16369 «struct Row6;
16370 ˇ»
16371 struct Row9.1;
16372 struct Row9.2;
16373 struct Row9.3;
16374 struct Row8;
16375 struct Row9;
16376 struct Row10;"#},
16377 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
16378 indoc! {r#"struct Row;
16379 struct Row1;
16380 struct Row2;
16381 struct Row2.1;
16382 struct Row2.2;
16383 «ˇ
16384 struct Row4;
16385 struct» Row5;
16386 «struct Row6;
16387 ˇ»
16388 struct Row9.1;
16389 struct Row9.2;
16390 struct Row9.3;
16391 struct Row8;
16392 struct Row9;
16393 struct Row10;"#},
16394 base_text,
16395 &mut cx,
16396 );
16397
16398 // When carets and selections intersect the addition hunks, those are reverted.
16399 // Adjacent carets got merged.
16400 assert_hunk_revert(
16401 indoc! {r#"struct Row;
16402 ˇ// something on the top
16403 struct Row1;
16404 struct Row2;
16405 struct Roˇw3.1;
16406 struct Row2.2;
16407 struct Row2.3;ˇ
16408
16409 struct Row4;
16410 struct ˇRow5.1;
16411 struct Row5.2;
16412 struct «Rowˇ»5.3;
16413 struct Row5;
16414 struct Row6;
16415 ˇ
16416 struct Row9.1;
16417 struct «Rowˇ»9.2;
16418 struct «ˇRow»9.3;
16419 struct Row8;
16420 struct Row9;
16421 «ˇ// something on bottom»
16422 struct Row10;"#},
16423 vec![
16424 DiffHunkStatusKind::Added,
16425 DiffHunkStatusKind::Added,
16426 DiffHunkStatusKind::Added,
16427 DiffHunkStatusKind::Added,
16428 DiffHunkStatusKind::Added,
16429 ],
16430 indoc! {r#"struct Row;
16431 ˇstruct Row1;
16432 struct Row2;
16433 ˇ
16434 struct Row4;
16435 ˇstruct Row5;
16436 struct Row6;
16437 ˇ
16438 ˇstruct Row8;
16439 struct Row9;
16440 ˇstruct Row10;"#},
16441 base_text,
16442 &mut cx,
16443 );
16444}
16445
16446#[gpui::test]
16447async fn test_modification_reverts(cx: &mut TestAppContext) {
16448 init_test(cx, |_| {});
16449 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16450 let base_text = indoc! {r#"
16451 struct Row;
16452 struct Row1;
16453 struct Row2;
16454
16455 struct Row4;
16456 struct Row5;
16457 struct Row6;
16458
16459 struct Row8;
16460 struct Row9;
16461 struct Row10;"#};
16462
16463 // Modification hunks behave the same as the addition ones.
16464 assert_hunk_revert(
16465 indoc! {r#"struct Row;
16466 struct Row1;
16467 struct Row33;
16468 ˇ
16469 struct Row4;
16470 struct Row5;
16471 struct Row6;
16472 ˇ
16473 struct Row99;
16474 struct Row9;
16475 struct Row10;"#},
16476 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
16477 indoc! {r#"struct Row;
16478 struct Row1;
16479 struct Row33;
16480 ˇ
16481 struct Row4;
16482 struct Row5;
16483 struct Row6;
16484 ˇ
16485 struct Row99;
16486 struct Row9;
16487 struct Row10;"#},
16488 base_text,
16489 &mut cx,
16490 );
16491 assert_hunk_revert(
16492 indoc! {r#"struct Row;
16493 struct Row1;
16494 struct Row33;
16495 «ˇ
16496 struct Row4;
16497 struct» Row5;
16498 «struct Row6;
16499 ˇ»
16500 struct Row99;
16501 struct Row9;
16502 struct Row10;"#},
16503 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
16504 indoc! {r#"struct Row;
16505 struct Row1;
16506 struct Row33;
16507 «ˇ
16508 struct Row4;
16509 struct» Row5;
16510 «struct Row6;
16511 ˇ»
16512 struct Row99;
16513 struct Row9;
16514 struct Row10;"#},
16515 base_text,
16516 &mut cx,
16517 );
16518
16519 assert_hunk_revert(
16520 indoc! {r#"ˇstruct Row1.1;
16521 struct Row1;
16522 «ˇstr»uct Row22;
16523
16524 struct ˇRow44;
16525 struct Row5;
16526 struct «Rˇ»ow66;ˇ
16527
16528 «struˇ»ct Row88;
16529 struct Row9;
16530 struct Row1011;ˇ"#},
16531 vec![
16532 DiffHunkStatusKind::Modified,
16533 DiffHunkStatusKind::Modified,
16534 DiffHunkStatusKind::Modified,
16535 DiffHunkStatusKind::Modified,
16536 DiffHunkStatusKind::Modified,
16537 DiffHunkStatusKind::Modified,
16538 ],
16539 indoc! {r#"struct Row;
16540 ˇstruct Row1;
16541 struct Row2;
16542 ˇ
16543 struct Row4;
16544 ˇstruct Row5;
16545 struct Row6;
16546 ˇ
16547 struct Row8;
16548 ˇstruct Row9;
16549 struct Row10;ˇ"#},
16550 base_text,
16551 &mut cx,
16552 );
16553}
16554
16555#[gpui::test]
16556async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
16557 init_test(cx, |_| {});
16558 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16559 let base_text = indoc! {r#"
16560 one
16561
16562 two
16563 three
16564 "#};
16565
16566 cx.set_head_text(base_text);
16567 cx.set_state("\nˇ\n");
16568 cx.executor().run_until_parked();
16569 cx.update_editor(|editor, _window, cx| {
16570 editor.expand_selected_diff_hunks(cx);
16571 });
16572 cx.executor().run_until_parked();
16573 cx.update_editor(|editor, window, cx| {
16574 editor.backspace(&Default::default(), window, cx);
16575 });
16576 cx.run_until_parked();
16577 cx.assert_state_with_diff(
16578 indoc! {r#"
16579
16580 - two
16581 - threeˇ
16582 +
16583 "#}
16584 .to_string(),
16585 );
16586}
16587
16588#[gpui::test]
16589async fn test_deletion_reverts(cx: &mut TestAppContext) {
16590 init_test(cx, |_| {});
16591 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16592 let base_text = indoc! {r#"struct Row;
16593struct Row1;
16594struct Row2;
16595
16596struct Row4;
16597struct Row5;
16598struct Row6;
16599
16600struct Row8;
16601struct Row9;
16602struct Row10;"#};
16603
16604 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
16605 assert_hunk_revert(
16606 indoc! {r#"struct Row;
16607 struct Row2;
16608
16609 ˇstruct Row4;
16610 struct Row5;
16611 struct Row6;
16612 ˇ
16613 struct Row8;
16614 struct Row10;"#},
16615 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16616 indoc! {r#"struct Row;
16617 struct Row2;
16618
16619 ˇstruct Row4;
16620 struct Row5;
16621 struct Row6;
16622 ˇ
16623 struct Row8;
16624 struct Row10;"#},
16625 base_text,
16626 &mut cx,
16627 );
16628 assert_hunk_revert(
16629 indoc! {r#"struct Row;
16630 struct Row2;
16631
16632 «ˇstruct Row4;
16633 struct» Row5;
16634 «struct Row6;
16635 ˇ»
16636 struct Row8;
16637 struct Row10;"#},
16638 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16639 indoc! {r#"struct Row;
16640 struct Row2;
16641
16642 «ˇstruct Row4;
16643 struct» Row5;
16644 «struct Row6;
16645 ˇ»
16646 struct Row8;
16647 struct Row10;"#},
16648 base_text,
16649 &mut cx,
16650 );
16651
16652 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
16653 assert_hunk_revert(
16654 indoc! {r#"struct Row;
16655 ˇstruct Row2;
16656
16657 struct Row4;
16658 struct Row5;
16659 struct Row6;
16660
16661 struct Row8;ˇ
16662 struct Row10;"#},
16663 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16664 indoc! {r#"struct Row;
16665 struct Row1;
16666 ˇstruct Row2;
16667
16668 struct Row4;
16669 struct Row5;
16670 struct Row6;
16671
16672 struct Row8;ˇ
16673 struct Row9;
16674 struct Row10;"#},
16675 base_text,
16676 &mut cx,
16677 );
16678 assert_hunk_revert(
16679 indoc! {r#"struct Row;
16680 struct Row2«ˇ;
16681 struct Row4;
16682 struct» Row5;
16683 «struct Row6;
16684
16685 struct Row8;ˇ»
16686 struct Row10;"#},
16687 vec![
16688 DiffHunkStatusKind::Deleted,
16689 DiffHunkStatusKind::Deleted,
16690 DiffHunkStatusKind::Deleted,
16691 ],
16692 indoc! {r#"struct Row;
16693 struct Row1;
16694 struct Row2«ˇ;
16695
16696 struct Row4;
16697 struct» Row5;
16698 «struct Row6;
16699
16700 struct Row8;ˇ»
16701 struct Row9;
16702 struct Row10;"#},
16703 base_text,
16704 &mut cx,
16705 );
16706}
16707
16708#[gpui::test]
16709async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
16710 init_test(cx, |_| {});
16711
16712 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
16713 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
16714 let base_text_3 =
16715 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
16716
16717 let text_1 = edit_first_char_of_every_line(base_text_1);
16718 let text_2 = edit_first_char_of_every_line(base_text_2);
16719 let text_3 = edit_first_char_of_every_line(base_text_3);
16720
16721 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
16722 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
16723 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
16724
16725 let multibuffer = cx.new(|cx| {
16726 let mut multibuffer = MultiBuffer::new(ReadWrite);
16727 multibuffer.push_excerpts(
16728 buffer_1.clone(),
16729 [
16730 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16731 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16732 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16733 ],
16734 cx,
16735 );
16736 multibuffer.push_excerpts(
16737 buffer_2.clone(),
16738 [
16739 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16740 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16741 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16742 ],
16743 cx,
16744 );
16745 multibuffer.push_excerpts(
16746 buffer_3.clone(),
16747 [
16748 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16749 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16750 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16751 ],
16752 cx,
16753 );
16754 multibuffer
16755 });
16756
16757 let fs = FakeFs::new(cx.executor());
16758 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
16759 let (editor, cx) = cx
16760 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
16761 editor.update_in(cx, |editor, _window, cx| {
16762 for (buffer, diff_base) in [
16763 (buffer_1.clone(), base_text_1),
16764 (buffer_2.clone(), base_text_2),
16765 (buffer_3.clone(), base_text_3),
16766 ] {
16767 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
16768 editor
16769 .buffer
16770 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
16771 }
16772 });
16773 cx.executor().run_until_parked();
16774
16775 editor.update_in(cx, |editor, window, cx| {
16776 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}");
16777 editor.select_all(&SelectAll, window, cx);
16778 editor.git_restore(&Default::default(), window, cx);
16779 });
16780 cx.executor().run_until_parked();
16781
16782 // When all ranges are selected, all buffer hunks are reverted.
16783 editor.update(cx, |editor, cx| {
16784 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");
16785 });
16786 buffer_1.update(cx, |buffer, _| {
16787 assert_eq!(buffer.text(), base_text_1);
16788 });
16789 buffer_2.update(cx, |buffer, _| {
16790 assert_eq!(buffer.text(), base_text_2);
16791 });
16792 buffer_3.update(cx, |buffer, _| {
16793 assert_eq!(buffer.text(), base_text_3);
16794 });
16795
16796 editor.update_in(cx, |editor, window, cx| {
16797 editor.undo(&Default::default(), window, cx);
16798 });
16799
16800 editor.update_in(cx, |editor, window, cx| {
16801 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16802 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
16803 });
16804 editor.git_restore(&Default::default(), window, cx);
16805 });
16806
16807 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
16808 // but not affect buffer_2 and its related excerpts.
16809 editor.update(cx, |editor, cx| {
16810 assert_eq!(
16811 editor.text(cx),
16812 "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}"
16813 );
16814 });
16815 buffer_1.update(cx, |buffer, _| {
16816 assert_eq!(buffer.text(), base_text_1);
16817 });
16818 buffer_2.update(cx, |buffer, _| {
16819 assert_eq!(
16820 buffer.text(),
16821 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
16822 );
16823 });
16824 buffer_3.update(cx, |buffer, _| {
16825 assert_eq!(
16826 buffer.text(),
16827 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
16828 );
16829 });
16830
16831 fn edit_first_char_of_every_line(text: &str) -> String {
16832 text.split('\n')
16833 .map(|line| format!("X{}", &line[1..]))
16834 .collect::<Vec<_>>()
16835 .join("\n")
16836 }
16837}
16838
16839#[gpui::test]
16840async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
16841 init_test(cx, |_| {});
16842
16843 let cols = 4;
16844 let rows = 10;
16845 let sample_text_1 = sample_text(rows, cols, 'a');
16846 assert_eq!(
16847 sample_text_1,
16848 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
16849 );
16850 let sample_text_2 = sample_text(rows, cols, 'l');
16851 assert_eq!(
16852 sample_text_2,
16853 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
16854 );
16855 let sample_text_3 = sample_text(rows, cols, 'v');
16856 assert_eq!(
16857 sample_text_3,
16858 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
16859 );
16860
16861 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
16862 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
16863 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
16864
16865 let multi_buffer = cx.new(|cx| {
16866 let mut multibuffer = MultiBuffer::new(ReadWrite);
16867 multibuffer.push_excerpts(
16868 buffer_1.clone(),
16869 [
16870 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16871 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16872 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16873 ],
16874 cx,
16875 );
16876 multibuffer.push_excerpts(
16877 buffer_2.clone(),
16878 [
16879 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16880 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16881 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16882 ],
16883 cx,
16884 );
16885 multibuffer.push_excerpts(
16886 buffer_3.clone(),
16887 [
16888 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16889 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16890 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16891 ],
16892 cx,
16893 );
16894 multibuffer
16895 });
16896
16897 let fs = FakeFs::new(cx.executor());
16898 fs.insert_tree(
16899 "/a",
16900 json!({
16901 "main.rs": sample_text_1,
16902 "other.rs": sample_text_2,
16903 "lib.rs": sample_text_3,
16904 }),
16905 )
16906 .await;
16907 let project = Project::test(fs, ["/a".as_ref()], cx).await;
16908 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16909 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16910 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
16911 Editor::new(
16912 EditorMode::full(),
16913 multi_buffer,
16914 Some(project.clone()),
16915 window,
16916 cx,
16917 )
16918 });
16919 let multibuffer_item_id = workspace
16920 .update(cx, |workspace, window, cx| {
16921 assert!(
16922 workspace.active_item(cx).is_none(),
16923 "active item should be None before the first item is added"
16924 );
16925 workspace.add_item_to_active_pane(
16926 Box::new(multi_buffer_editor.clone()),
16927 None,
16928 true,
16929 window,
16930 cx,
16931 );
16932 let active_item = workspace
16933 .active_item(cx)
16934 .expect("should have an active item after adding the multi buffer");
16935 assert!(
16936 !active_item.is_singleton(cx),
16937 "A multi buffer was expected to active after adding"
16938 );
16939 active_item.item_id()
16940 })
16941 .unwrap();
16942 cx.executor().run_until_parked();
16943
16944 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16945 editor.change_selections(
16946 SelectionEffects::scroll(Autoscroll::Next),
16947 window,
16948 cx,
16949 |s| s.select_ranges(Some(1..2)),
16950 );
16951 editor.open_excerpts(&OpenExcerpts, window, cx);
16952 });
16953 cx.executor().run_until_parked();
16954 let first_item_id = workspace
16955 .update(cx, |workspace, window, cx| {
16956 let active_item = workspace
16957 .active_item(cx)
16958 .expect("should have an active item after navigating into the 1st buffer");
16959 let first_item_id = active_item.item_id();
16960 assert_ne!(
16961 first_item_id, multibuffer_item_id,
16962 "Should navigate into the 1st buffer and activate it"
16963 );
16964 assert!(
16965 active_item.is_singleton(cx),
16966 "New active item should be a singleton buffer"
16967 );
16968 assert_eq!(
16969 active_item
16970 .act_as::<Editor>(cx)
16971 .expect("should have navigated into an editor for the 1st buffer")
16972 .read(cx)
16973 .text(cx),
16974 sample_text_1
16975 );
16976
16977 workspace
16978 .go_back(workspace.active_pane().downgrade(), window, cx)
16979 .detach_and_log_err(cx);
16980
16981 first_item_id
16982 })
16983 .unwrap();
16984 cx.executor().run_until_parked();
16985 workspace
16986 .update(cx, |workspace, _, cx| {
16987 let active_item = workspace
16988 .active_item(cx)
16989 .expect("should have an active item after navigating back");
16990 assert_eq!(
16991 active_item.item_id(),
16992 multibuffer_item_id,
16993 "Should navigate back to the multi buffer"
16994 );
16995 assert!(!active_item.is_singleton(cx));
16996 })
16997 .unwrap();
16998
16999 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17000 editor.change_selections(
17001 SelectionEffects::scroll(Autoscroll::Next),
17002 window,
17003 cx,
17004 |s| s.select_ranges(Some(39..40)),
17005 );
17006 editor.open_excerpts(&OpenExcerpts, window, cx);
17007 });
17008 cx.executor().run_until_parked();
17009 let second_item_id = workspace
17010 .update(cx, |workspace, window, cx| {
17011 let active_item = workspace
17012 .active_item(cx)
17013 .expect("should have an active item after navigating into the 2nd buffer");
17014 let second_item_id = active_item.item_id();
17015 assert_ne!(
17016 second_item_id, multibuffer_item_id,
17017 "Should navigate away from the multibuffer"
17018 );
17019 assert_ne!(
17020 second_item_id, first_item_id,
17021 "Should navigate into the 2nd buffer and activate it"
17022 );
17023 assert!(
17024 active_item.is_singleton(cx),
17025 "New active item should be a singleton buffer"
17026 );
17027 assert_eq!(
17028 active_item
17029 .act_as::<Editor>(cx)
17030 .expect("should have navigated into an editor")
17031 .read(cx)
17032 .text(cx),
17033 sample_text_2
17034 );
17035
17036 workspace
17037 .go_back(workspace.active_pane().downgrade(), window, cx)
17038 .detach_and_log_err(cx);
17039
17040 second_item_id
17041 })
17042 .unwrap();
17043 cx.executor().run_until_parked();
17044 workspace
17045 .update(cx, |workspace, _, cx| {
17046 let active_item = workspace
17047 .active_item(cx)
17048 .expect("should have an active item after navigating back from the 2nd buffer");
17049 assert_eq!(
17050 active_item.item_id(),
17051 multibuffer_item_id,
17052 "Should navigate back from the 2nd buffer to the multi buffer"
17053 );
17054 assert!(!active_item.is_singleton(cx));
17055 })
17056 .unwrap();
17057
17058 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17059 editor.change_selections(
17060 SelectionEffects::scroll(Autoscroll::Next),
17061 window,
17062 cx,
17063 |s| s.select_ranges(Some(70..70)),
17064 );
17065 editor.open_excerpts(&OpenExcerpts, window, cx);
17066 });
17067 cx.executor().run_until_parked();
17068 workspace
17069 .update(cx, |workspace, window, cx| {
17070 let active_item = workspace
17071 .active_item(cx)
17072 .expect("should have an active item after navigating into the 3rd buffer");
17073 let third_item_id = active_item.item_id();
17074 assert_ne!(
17075 third_item_id, multibuffer_item_id,
17076 "Should navigate into the 3rd buffer and activate it"
17077 );
17078 assert_ne!(third_item_id, first_item_id);
17079 assert_ne!(third_item_id, second_item_id);
17080 assert!(
17081 active_item.is_singleton(cx),
17082 "New active item should be a singleton buffer"
17083 );
17084 assert_eq!(
17085 active_item
17086 .act_as::<Editor>(cx)
17087 .expect("should have navigated into an editor")
17088 .read(cx)
17089 .text(cx),
17090 sample_text_3
17091 );
17092
17093 workspace
17094 .go_back(workspace.active_pane().downgrade(), window, cx)
17095 .detach_and_log_err(cx);
17096 })
17097 .unwrap();
17098 cx.executor().run_until_parked();
17099 workspace
17100 .update(cx, |workspace, _, cx| {
17101 let active_item = workspace
17102 .active_item(cx)
17103 .expect("should have an active item after navigating back from the 3rd buffer");
17104 assert_eq!(
17105 active_item.item_id(),
17106 multibuffer_item_id,
17107 "Should navigate back from the 3rd buffer to the multi buffer"
17108 );
17109 assert!(!active_item.is_singleton(cx));
17110 })
17111 .unwrap();
17112}
17113
17114#[gpui::test]
17115async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17116 init_test(cx, |_| {});
17117
17118 let mut cx = EditorTestContext::new(cx).await;
17119
17120 let diff_base = r#"
17121 use some::mod;
17122
17123 const A: u32 = 42;
17124
17125 fn main() {
17126 println!("hello");
17127
17128 println!("world");
17129 }
17130 "#
17131 .unindent();
17132
17133 cx.set_state(
17134 &r#"
17135 use some::modified;
17136
17137 ˇ
17138 fn main() {
17139 println!("hello there");
17140
17141 println!("around the");
17142 println!("world");
17143 }
17144 "#
17145 .unindent(),
17146 );
17147
17148 cx.set_head_text(&diff_base);
17149 executor.run_until_parked();
17150
17151 cx.update_editor(|editor, window, cx| {
17152 editor.go_to_next_hunk(&GoToHunk, window, cx);
17153 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17154 });
17155 executor.run_until_parked();
17156 cx.assert_state_with_diff(
17157 r#"
17158 use some::modified;
17159
17160
17161 fn main() {
17162 - println!("hello");
17163 + ˇ println!("hello there");
17164
17165 println!("around the");
17166 println!("world");
17167 }
17168 "#
17169 .unindent(),
17170 );
17171
17172 cx.update_editor(|editor, window, cx| {
17173 for _ in 0..2 {
17174 editor.go_to_next_hunk(&GoToHunk, window, cx);
17175 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17176 }
17177 });
17178 executor.run_until_parked();
17179 cx.assert_state_with_diff(
17180 r#"
17181 - use some::mod;
17182 + ˇuse some::modified;
17183
17184
17185 fn main() {
17186 - println!("hello");
17187 + println!("hello there");
17188
17189 + println!("around the");
17190 println!("world");
17191 }
17192 "#
17193 .unindent(),
17194 );
17195
17196 cx.update_editor(|editor, window, cx| {
17197 editor.go_to_next_hunk(&GoToHunk, window, cx);
17198 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17199 });
17200 executor.run_until_parked();
17201 cx.assert_state_with_diff(
17202 r#"
17203 - use some::mod;
17204 + use some::modified;
17205
17206 - const A: u32 = 42;
17207 ˇ
17208 fn main() {
17209 - println!("hello");
17210 + println!("hello there");
17211
17212 + println!("around the");
17213 println!("world");
17214 }
17215 "#
17216 .unindent(),
17217 );
17218
17219 cx.update_editor(|editor, window, cx| {
17220 editor.cancel(&Cancel, window, cx);
17221 });
17222
17223 cx.assert_state_with_diff(
17224 r#"
17225 use some::modified;
17226
17227 ˇ
17228 fn main() {
17229 println!("hello there");
17230
17231 println!("around the");
17232 println!("world");
17233 }
17234 "#
17235 .unindent(),
17236 );
17237}
17238
17239#[gpui::test]
17240async fn test_diff_base_change_with_expanded_diff_hunks(
17241 executor: BackgroundExecutor,
17242 cx: &mut TestAppContext,
17243) {
17244 init_test(cx, |_| {});
17245
17246 let mut cx = EditorTestContext::new(cx).await;
17247
17248 let diff_base = r#"
17249 use some::mod1;
17250 use some::mod2;
17251
17252 const A: u32 = 42;
17253 const B: u32 = 42;
17254 const C: u32 = 42;
17255
17256 fn main() {
17257 println!("hello");
17258
17259 println!("world");
17260 }
17261 "#
17262 .unindent();
17263
17264 cx.set_state(
17265 &r#"
17266 use some::mod2;
17267
17268 const A: u32 = 42;
17269 const C: u32 = 42;
17270
17271 fn main(ˇ) {
17272 //println!("hello");
17273
17274 println!("world");
17275 //
17276 //
17277 }
17278 "#
17279 .unindent(),
17280 );
17281
17282 cx.set_head_text(&diff_base);
17283 executor.run_until_parked();
17284
17285 cx.update_editor(|editor, window, cx| {
17286 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17287 });
17288 executor.run_until_parked();
17289 cx.assert_state_with_diff(
17290 r#"
17291 - use some::mod1;
17292 use some::mod2;
17293
17294 const A: u32 = 42;
17295 - const B: u32 = 42;
17296 const C: u32 = 42;
17297
17298 fn main(ˇ) {
17299 - println!("hello");
17300 + //println!("hello");
17301
17302 println!("world");
17303 + //
17304 + //
17305 }
17306 "#
17307 .unindent(),
17308 );
17309
17310 cx.set_head_text("new diff base!");
17311 executor.run_until_parked();
17312 cx.assert_state_with_diff(
17313 r#"
17314 - new diff base!
17315 + use some::mod2;
17316 +
17317 + const A: u32 = 42;
17318 + const C: u32 = 42;
17319 +
17320 + fn main(ˇ) {
17321 + //println!("hello");
17322 +
17323 + println!("world");
17324 + //
17325 + //
17326 + }
17327 "#
17328 .unindent(),
17329 );
17330}
17331
17332#[gpui::test]
17333async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
17334 init_test(cx, |_| {});
17335
17336 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
17337 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
17338 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
17339 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
17340 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
17341 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
17342
17343 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
17344 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
17345 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
17346
17347 let multi_buffer = cx.new(|cx| {
17348 let mut multibuffer = MultiBuffer::new(ReadWrite);
17349 multibuffer.push_excerpts(
17350 buffer_1.clone(),
17351 [
17352 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17353 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17354 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17355 ],
17356 cx,
17357 );
17358 multibuffer.push_excerpts(
17359 buffer_2.clone(),
17360 [
17361 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17362 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17363 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17364 ],
17365 cx,
17366 );
17367 multibuffer.push_excerpts(
17368 buffer_3.clone(),
17369 [
17370 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17371 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17372 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17373 ],
17374 cx,
17375 );
17376 multibuffer
17377 });
17378
17379 let editor =
17380 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
17381 editor
17382 .update(cx, |editor, _window, cx| {
17383 for (buffer, diff_base) in [
17384 (buffer_1.clone(), file_1_old),
17385 (buffer_2.clone(), file_2_old),
17386 (buffer_3.clone(), file_3_old),
17387 ] {
17388 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
17389 editor
17390 .buffer
17391 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
17392 }
17393 })
17394 .unwrap();
17395
17396 let mut cx = EditorTestContext::for_editor(editor, cx).await;
17397 cx.run_until_parked();
17398
17399 cx.assert_editor_state(
17400 &"
17401 ˇaaa
17402 ccc
17403 ddd
17404
17405 ggg
17406 hhh
17407
17408
17409 lll
17410 mmm
17411 NNN
17412
17413 qqq
17414 rrr
17415
17416 uuu
17417 111
17418 222
17419 333
17420
17421 666
17422 777
17423
17424 000
17425 !!!"
17426 .unindent(),
17427 );
17428
17429 cx.update_editor(|editor, window, cx| {
17430 editor.select_all(&SelectAll, window, cx);
17431 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17432 });
17433 cx.executor().run_until_parked();
17434
17435 cx.assert_state_with_diff(
17436 "
17437 «aaa
17438 - bbb
17439 ccc
17440 ddd
17441
17442 ggg
17443 hhh
17444
17445
17446 lll
17447 mmm
17448 - nnn
17449 + NNN
17450
17451 qqq
17452 rrr
17453
17454 uuu
17455 111
17456 222
17457 333
17458
17459 + 666
17460 777
17461
17462 000
17463 !!!ˇ»"
17464 .unindent(),
17465 );
17466}
17467
17468#[gpui::test]
17469async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
17470 init_test(cx, |_| {});
17471
17472 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
17473 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
17474
17475 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
17476 let multi_buffer = cx.new(|cx| {
17477 let mut multibuffer = MultiBuffer::new(ReadWrite);
17478 multibuffer.push_excerpts(
17479 buffer.clone(),
17480 [
17481 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
17482 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
17483 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
17484 ],
17485 cx,
17486 );
17487 multibuffer
17488 });
17489
17490 let editor =
17491 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
17492 editor
17493 .update(cx, |editor, _window, cx| {
17494 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
17495 editor
17496 .buffer
17497 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
17498 })
17499 .unwrap();
17500
17501 let mut cx = EditorTestContext::for_editor(editor, cx).await;
17502 cx.run_until_parked();
17503
17504 cx.update_editor(|editor, window, cx| {
17505 editor.expand_all_diff_hunks(&Default::default(), window, cx)
17506 });
17507 cx.executor().run_until_parked();
17508
17509 // When the start of a hunk coincides with the start of its excerpt,
17510 // the hunk is expanded. When the start of a a hunk is earlier than
17511 // the start of its excerpt, the hunk is not expanded.
17512 cx.assert_state_with_diff(
17513 "
17514 ˇaaa
17515 - bbb
17516 + BBB
17517
17518 - ddd
17519 - eee
17520 + DDD
17521 + EEE
17522 fff
17523
17524 iii
17525 "
17526 .unindent(),
17527 );
17528}
17529
17530#[gpui::test]
17531async fn test_edits_around_expanded_insertion_hunks(
17532 executor: BackgroundExecutor,
17533 cx: &mut TestAppContext,
17534) {
17535 init_test(cx, |_| {});
17536
17537 let mut cx = EditorTestContext::new(cx).await;
17538
17539 let diff_base = r#"
17540 use some::mod1;
17541 use some::mod2;
17542
17543 const A: u32 = 42;
17544
17545 fn main() {
17546 println!("hello");
17547
17548 println!("world");
17549 }
17550 "#
17551 .unindent();
17552 executor.run_until_parked();
17553 cx.set_state(
17554 &r#"
17555 use some::mod1;
17556 use some::mod2;
17557
17558 const A: u32 = 42;
17559 const B: u32 = 42;
17560 const C: u32 = 42;
17561 ˇ
17562
17563 fn main() {
17564 println!("hello");
17565
17566 println!("world");
17567 }
17568 "#
17569 .unindent(),
17570 );
17571
17572 cx.set_head_text(&diff_base);
17573 executor.run_until_parked();
17574
17575 cx.update_editor(|editor, window, cx| {
17576 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17577 });
17578 executor.run_until_parked();
17579
17580 cx.assert_state_with_diff(
17581 r#"
17582 use some::mod1;
17583 use some::mod2;
17584
17585 const A: u32 = 42;
17586 + const B: u32 = 42;
17587 + const C: u32 = 42;
17588 + ˇ
17589
17590 fn main() {
17591 println!("hello");
17592
17593 println!("world");
17594 }
17595 "#
17596 .unindent(),
17597 );
17598
17599 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
17600 executor.run_until_parked();
17601
17602 cx.assert_state_with_diff(
17603 r#"
17604 use some::mod1;
17605 use some::mod2;
17606
17607 const A: u32 = 42;
17608 + const B: u32 = 42;
17609 + const C: u32 = 42;
17610 + const D: u32 = 42;
17611 + ˇ
17612
17613 fn main() {
17614 println!("hello");
17615
17616 println!("world");
17617 }
17618 "#
17619 .unindent(),
17620 );
17621
17622 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
17623 executor.run_until_parked();
17624
17625 cx.assert_state_with_diff(
17626 r#"
17627 use some::mod1;
17628 use some::mod2;
17629
17630 const A: u32 = 42;
17631 + const B: u32 = 42;
17632 + const C: u32 = 42;
17633 + const D: u32 = 42;
17634 + const E: u32 = 42;
17635 + ˇ
17636
17637 fn main() {
17638 println!("hello");
17639
17640 println!("world");
17641 }
17642 "#
17643 .unindent(),
17644 );
17645
17646 cx.update_editor(|editor, window, cx| {
17647 editor.delete_line(&DeleteLine, window, cx);
17648 });
17649 executor.run_until_parked();
17650
17651 cx.assert_state_with_diff(
17652 r#"
17653 use some::mod1;
17654 use some::mod2;
17655
17656 const A: u32 = 42;
17657 + const B: u32 = 42;
17658 + const C: u32 = 42;
17659 + const D: u32 = 42;
17660 + const E: u32 = 42;
17661 ˇ
17662 fn main() {
17663 println!("hello");
17664
17665 println!("world");
17666 }
17667 "#
17668 .unindent(),
17669 );
17670
17671 cx.update_editor(|editor, window, cx| {
17672 editor.move_up(&MoveUp, window, cx);
17673 editor.delete_line(&DeleteLine, window, cx);
17674 editor.move_up(&MoveUp, window, cx);
17675 editor.delete_line(&DeleteLine, window, cx);
17676 editor.move_up(&MoveUp, window, cx);
17677 editor.delete_line(&DeleteLine, window, cx);
17678 });
17679 executor.run_until_parked();
17680 cx.assert_state_with_diff(
17681 r#"
17682 use some::mod1;
17683 use some::mod2;
17684
17685 const A: u32 = 42;
17686 + const B: u32 = 42;
17687 ˇ
17688 fn main() {
17689 println!("hello");
17690
17691 println!("world");
17692 }
17693 "#
17694 .unindent(),
17695 );
17696
17697 cx.update_editor(|editor, window, cx| {
17698 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
17699 editor.delete_line(&DeleteLine, window, cx);
17700 });
17701 executor.run_until_parked();
17702 cx.assert_state_with_diff(
17703 r#"
17704 ˇ
17705 fn main() {
17706 println!("hello");
17707
17708 println!("world");
17709 }
17710 "#
17711 .unindent(),
17712 );
17713}
17714
17715#[gpui::test]
17716async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
17717 init_test(cx, |_| {});
17718
17719 let mut cx = EditorTestContext::new(cx).await;
17720 cx.set_head_text(indoc! { "
17721 one
17722 two
17723 three
17724 four
17725 five
17726 "
17727 });
17728 cx.set_state(indoc! { "
17729 one
17730 ˇthree
17731 five
17732 "});
17733 cx.run_until_parked();
17734 cx.update_editor(|editor, window, cx| {
17735 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17736 });
17737 cx.assert_state_with_diff(
17738 indoc! { "
17739 one
17740 - two
17741 ˇthree
17742 - four
17743 five
17744 "}
17745 .to_string(),
17746 );
17747 cx.update_editor(|editor, window, cx| {
17748 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17749 });
17750
17751 cx.assert_state_with_diff(
17752 indoc! { "
17753 one
17754 ˇthree
17755 five
17756 "}
17757 .to_string(),
17758 );
17759
17760 cx.set_state(indoc! { "
17761 one
17762 ˇTWO
17763 three
17764 four
17765 five
17766 "});
17767 cx.run_until_parked();
17768 cx.update_editor(|editor, window, cx| {
17769 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17770 });
17771
17772 cx.assert_state_with_diff(
17773 indoc! { "
17774 one
17775 - two
17776 + ˇTWO
17777 three
17778 four
17779 five
17780 "}
17781 .to_string(),
17782 );
17783 cx.update_editor(|editor, window, cx| {
17784 editor.move_up(&Default::default(), window, cx);
17785 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17786 });
17787 cx.assert_state_with_diff(
17788 indoc! { "
17789 one
17790 ˇTWO
17791 three
17792 four
17793 five
17794 "}
17795 .to_string(),
17796 );
17797}
17798
17799#[gpui::test]
17800async fn test_edits_around_expanded_deletion_hunks(
17801 executor: BackgroundExecutor,
17802 cx: &mut TestAppContext,
17803) {
17804 init_test(cx, |_| {});
17805
17806 let mut cx = EditorTestContext::new(cx).await;
17807
17808 let diff_base = r#"
17809 use some::mod1;
17810 use some::mod2;
17811
17812 const A: u32 = 42;
17813 const B: u32 = 42;
17814 const C: u32 = 42;
17815
17816
17817 fn main() {
17818 println!("hello");
17819
17820 println!("world");
17821 }
17822 "#
17823 .unindent();
17824 executor.run_until_parked();
17825 cx.set_state(
17826 &r#"
17827 use some::mod1;
17828 use some::mod2;
17829
17830 ˇconst B: u32 = 42;
17831 const C: u32 = 42;
17832
17833
17834 fn main() {
17835 println!("hello");
17836
17837 println!("world");
17838 }
17839 "#
17840 .unindent(),
17841 );
17842
17843 cx.set_head_text(&diff_base);
17844 executor.run_until_parked();
17845
17846 cx.update_editor(|editor, window, cx| {
17847 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17848 });
17849 executor.run_until_parked();
17850
17851 cx.assert_state_with_diff(
17852 r#"
17853 use some::mod1;
17854 use some::mod2;
17855
17856 - const A: u32 = 42;
17857 ˇconst B: u32 = 42;
17858 const C: u32 = 42;
17859
17860
17861 fn main() {
17862 println!("hello");
17863
17864 println!("world");
17865 }
17866 "#
17867 .unindent(),
17868 );
17869
17870 cx.update_editor(|editor, window, cx| {
17871 editor.delete_line(&DeleteLine, window, cx);
17872 });
17873 executor.run_until_parked();
17874 cx.assert_state_with_diff(
17875 r#"
17876 use some::mod1;
17877 use some::mod2;
17878
17879 - const A: u32 = 42;
17880 - const B: u32 = 42;
17881 ˇconst C: u32 = 42;
17882
17883
17884 fn main() {
17885 println!("hello");
17886
17887 println!("world");
17888 }
17889 "#
17890 .unindent(),
17891 );
17892
17893 cx.update_editor(|editor, window, cx| {
17894 editor.delete_line(&DeleteLine, window, cx);
17895 });
17896 executor.run_until_parked();
17897 cx.assert_state_with_diff(
17898 r#"
17899 use some::mod1;
17900 use some::mod2;
17901
17902 - const A: u32 = 42;
17903 - const B: u32 = 42;
17904 - const C: u32 = 42;
17905 ˇ
17906
17907 fn main() {
17908 println!("hello");
17909
17910 println!("world");
17911 }
17912 "#
17913 .unindent(),
17914 );
17915
17916 cx.update_editor(|editor, window, cx| {
17917 editor.handle_input("replacement", window, cx);
17918 });
17919 executor.run_until_parked();
17920 cx.assert_state_with_diff(
17921 r#"
17922 use some::mod1;
17923 use some::mod2;
17924
17925 - const A: u32 = 42;
17926 - const B: u32 = 42;
17927 - const C: u32 = 42;
17928 -
17929 + replacementˇ
17930
17931 fn main() {
17932 println!("hello");
17933
17934 println!("world");
17935 }
17936 "#
17937 .unindent(),
17938 );
17939}
17940
17941#[gpui::test]
17942async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17943 init_test(cx, |_| {});
17944
17945 let mut cx = EditorTestContext::new(cx).await;
17946
17947 let base_text = r#"
17948 one
17949 two
17950 three
17951 four
17952 five
17953 "#
17954 .unindent();
17955 executor.run_until_parked();
17956 cx.set_state(
17957 &r#"
17958 one
17959 two
17960 fˇour
17961 five
17962 "#
17963 .unindent(),
17964 );
17965
17966 cx.set_head_text(&base_text);
17967 executor.run_until_parked();
17968
17969 cx.update_editor(|editor, window, cx| {
17970 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17971 });
17972 executor.run_until_parked();
17973
17974 cx.assert_state_with_diff(
17975 r#"
17976 one
17977 two
17978 - three
17979 fˇour
17980 five
17981 "#
17982 .unindent(),
17983 );
17984
17985 cx.update_editor(|editor, window, cx| {
17986 editor.backspace(&Backspace, window, cx);
17987 editor.backspace(&Backspace, window, cx);
17988 });
17989 executor.run_until_parked();
17990 cx.assert_state_with_diff(
17991 r#"
17992 one
17993 two
17994 - threeˇ
17995 - four
17996 + our
17997 five
17998 "#
17999 .unindent(),
18000 );
18001}
18002
18003#[gpui::test]
18004async fn test_edit_after_expanded_modification_hunk(
18005 executor: BackgroundExecutor,
18006 cx: &mut TestAppContext,
18007) {
18008 init_test(cx, |_| {});
18009
18010 let mut cx = EditorTestContext::new(cx).await;
18011
18012 let diff_base = r#"
18013 use some::mod1;
18014 use some::mod2;
18015
18016 const A: u32 = 42;
18017 const B: u32 = 42;
18018 const C: u32 = 42;
18019 const D: u32 = 42;
18020
18021
18022 fn main() {
18023 println!("hello");
18024
18025 println!("world");
18026 }"#
18027 .unindent();
18028
18029 cx.set_state(
18030 &r#"
18031 use some::mod1;
18032 use some::mod2;
18033
18034 const A: u32 = 42;
18035 const B: u32 = 42;
18036 const C: u32 = 43ˇ
18037 const D: u32 = 42;
18038
18039
18040 fn main() {
18041 println!("hello");
18042
18043 println!("world");
18044 }"#
18045 .unindent(),
18046 );
18047
18048 cx.set_head_text(&diff_base);
18049 executor.run_until_parked();
18050 cx.update_editor(|editor, window, cx| {
18051 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18052 });
18053 executor.run_until_parked();
18054
18055 cx.assert_state_with_diff(
18056 r#"
18057 use some::mod1;
18058 use some::mod2;
18059
18060 const A: u32 = 42;
18061 const B: u32 = 42;
18062 - const C: u32 = 42;
18063 + const C: u32 = 43ˇ
18064 const D: u32 = 42;
18065
18066
18067 fn main() {
18068 println!("hello");
18069
18070 println!("world");
18071 }"#
18072 .unindent(),
18073 );
18074
18075 cx.update_editor(|editor, window, cx| {
18076 editor.handle_input("\nnew_line\n", window, cx);
18077 });
18078 executor.run_until_parked();
18079
18080 cx.assert_state_with_diff(
18081 r#"
18082 use some::mod1;
18083 use some::mod2;
18084
18085 const A: u32 = 42;
18086 const B: u32 = 42;
18087 - const C: u32 = 42;
18088 + const C: u32 = 43
18089 + new_line
18090 + ˇ
18091 const D: u32 = 42;
18092
18093
18094 fn main() {
18095 println!("hello");
18096
18097 println!("world");
18098 }"#
18099 .unindent(),
18100 );
18101}
18102
18103#[gpui::test]
18104async fn test_stage_and_unstage_added_file_hunk(
18105 executor: BackgroundExecutor,
18106 cx: &mut TestAppContext,
18107) {
18108 init_test(cx, |_| {});
18109
18110 let mut cx = EditorTestContext::new(cx).await;
18111 cx.update_editor(|editor, _, cx| {
18112 editor.set_expand_all_diff_hunks(cx);
18113 });
18114
18115 let working_copy = r#"
18116 ˇfn main() {
18117 println!("hello, world!");
18118 }
18119 "#
18120 .unindent();
18121
18122 cx.set_state(&working_copy);
18123 executor.run_until_parked();
18124
18125 cx.assert_state_with_diff(
18126 r#"
18127 + ˇfn main() {
18128 + println!("hello, world!");
18129 + }
18130 "#
18131 .unindent(),
18132 );
18133 cx.assert_index_text(None);
18134
18135 cx.update_editor(|editor, window, cx| {
18136 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18137 });
18138 executor.run_until_parked();
18139 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
18140 cx.assert_state_with_diff(
18141 r#"
18142 + ˇfn main() {
18143 + println!("hello, world!");
18144 + }
18145 "#
18146 .unindent(),
18147 );
18148
18149 cx.update_editor(|editor, window, cx| {
18150 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18151 });
18152 executor.run_until_parked();
18153 cx.assert_index_text(None);
18154}
18155
18156async fn setup_indent_guides_editor(
18157 text: &str,
18158 cx: &mut TestAppContext,
18159) -> (BufferId, EditorTestContext) {
18160 init_test(cx, |_| {});
18161
18162 let mut cx = EditorTestContext::new(cx).await;
18163
18164 let buffer_id = cx.update_editor(|editor, window, cx| {
18165 editor.set_text(text, window, cx);
18166 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
18167
18168 buffer_ids[0]
18169 });
18170
18171 (buffer_id, cx)
18172}
18173
18174fn assert_indent_guides(
18175 range: Range<u32>,
18176 expected: Vec<IndentGuide>,
18177 active_indices: Option<Vec<usize>>,
18178 cx: &mut EditorTestContext,
18179) {
18180 let indent_guides = cx.update_editor(|editor, window, cx| {
18181 let snapshot = editor.snapshot(window, cx).display_snapshot;
18182 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
18183 editor,
18184 MultiBufferRow(range.start)..MultiBufferRow(range.end),
18185 true,
18186 &snapshot,
18187 cx,
18188 );
18189
18190 indent_guides.sort_by(|a, b| {
18191 a.depth.cmp(&b.depth).then(
18192 a.start_row
18193 .cmp(&b.start_row)
18194 .then(a.end_row.cmp(&b.end_row)),
18195 )
18196 });
18197 indent_guides
18198 });
18199
18200 if let Some(expected) = active_indices {
18201 let active_indices = cx.update_editor(|editor, window, cx| {
18202 let snapshot = editor.snapshot(window, cx).display_snapshot;
18203 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
18204 });
18205
18206 assert_eq!(
18207 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
18208 expected,
18209 "Active indent guide indices do not match"
18210 );
18211 }
18212
18213 assert_eq!(indent_guides, expected, "Indent guides do not match");
18214}
18215
18216fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
18217 IndentGuide {
18218 buffer_id,
18219 start_row: MultiBufferRow(start_row),
18220 end_row: MultiBufferRow(end_row),
18221 depth,
18222 tab_size: 4,
18223 settings: IndentGuideSettings {
18224 enabled: true,
18225 line_width: 1,
18226 active_line_width: 1,
18227 ..Default::default()
18228 },
18229 }
18230}
18231
18232#[gpui::test]
18233async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
18234 let (buffer_id, mut cx) = setup_indent_guides_editor(
18235 &"
18236 fn main() {
18237 let a = 1;
18238 }"
18239 .unindent(),
18240 cx,
18241 )
18242 .await;
18243
18244 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
18245}
18246
18247#[gpui::test]
18248async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
18249 let (buffer_id, mut cx) = setup_indent_guides_editor(
18250 &"
18251 fn main() {
18252 let a = 1;
18253 let b = 2;
18254 }"
18255 .unindent(),
18256 cx,
18257 )
18258 .await;
18259
18260 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
18261}
18262
18263#[gpui::test]
18264async fn test_indent_guide_nested(cx: &mut TestAppContext) {
18265 let (buffer_id, mut cx) = setup_indent_guides_editor(
18266 &"
18267 fn main() {
18268 let a = 1;
18269 if a == 3 {
18270 let b = 2;
18271 } else {
18272 let c = 3;
18273 }
18274 }"
18275 .unindent(),
18276 cx,
18277 )
18278 .await;
18279
18280 assert_indent_guides(
18281 0..8,
18282 vec![
18283 indent_guide(buffer_id, 1, 6, 0),
18284 indent_guide(buffer_id, 3, 3, 1),
18285 indent_guide(buffer_id, 5, 5, 1),
18286 ],
18287 None,
18288 &mut cx,
18289 );
18290}
18291
18292#[gpui::test]
18293async fn test_indent_guide_tab(cx: &mut TestAppContext) {
18294 let (buffer_id, mut cx) = setup_indent_guides_editor(
18295 &"
18296 fn main() {
18297 let a = 1;
18298 let b = 2;
18299 let c = 3;
18300 }"
18301 .unindent(),
18302 cx,
18303 )
18304 .await;
18305
18306 assert_indent_guides(
18307 0..5,
18308 vec![
18309 indent_guide(buffer_id, 1, 3, 0),
18310 indent_guide(buffer_id, 2, 2, 1),
18311 ],
18312 None,
18313 &mut cx,
18314 );
18315}
18316
18317#[gpui::test]
18318async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
18319 let (buffer_id, mut cx) = setup_indent_guides_editor(
18320 &"
18321 fn main() {
18322 let a = 1;
18323
18324 let c = 3;
18325 }"
18326 .unindent(),
18327 cx,
18328 )
18329 .await;
18330
18331 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
18332}
18333
18334#[gpui::test]
18335async fn test_indent_guide_complex(cx: &mut TestAppContext) {
18336 let (buffer_id, mut cx) = setup_indent_guides_editor(
18337 &"
18338 fn main() {
18339 let a = 1;
18340
18341 let c = 3;
18342
18343 if a == 3 {
18344 let b = 2;
18345 } else {
18346 let c = 3;
18347 }
18348 }"
18349 .unindent(),
18350 cx,
18351 )
18352 .await;
18353
18354 assert_indent_guides(
18355 0..11,
18356 vec![
18357 indent_guide(buffer_id, 1, 9, 0),
18358 indent_guide(buffer_id, 6, 6, 1),
18359 indent_guide(buffer_id, 8, 8, 1),
18360 ],
18361 None,
18362 &mut cx,
18363 );
18364}
18365
18366#[gpui::test]
18367async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
18368 let (buffer_id, mut cx) = setup_indent_guides_editor(
18369 &"
18370 fn main() {
18371 let a = 1;
18372
18373 let c = 3;
18374
18375 if a == 3 {
18376 let b = 2;
18377 } else {
18378 let c = 3;
18379 }
18380 }"
18381 .unindent(),
18382 cx,
18383 )
18384 .await;
18385
18386 assert_indent_guides(
18387 1..11,
18388 vec![
18389 indent_guide(buffer_id, 1, 9, 0),
18390 indent_guide(buffer_id, 6, 6, 1),
18391 indent_guide(buffer_id, 8, 8, 1),
18392 ],
18393 None,
18394 &mut cx,
18395 );
18396}
18397
18398#[gpui::test]
18399async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
18400 let (buffer_id, mut cx) = setup_indent_guides_editor(
18401 &"
18402 fn main() {
18403 let a = 1;
18404
18405 let c = 3;
18406
18407 if a == 3 {
18408 let b = 2;
18409 } else {
18410 let c = 3;
18411 }
18412 }"
18413 .unindent(),
18414 cx,
18415 )
18416 .await;
18417
18418 assert_indent_guides(
18419 1..10,
18420 vec![
18421 indent_guide(buffer_id, 1, 9, 0),
18422 indent_guide(buffer_id, 6, 6, 1),
18423 indent_guide(buffer_id, 8, 8, 1),
18424 ],
18425 None,
18426 &mut cx,
18427 );
18428}
18429
18430#[gpui::test]
18431async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
18432 let (buffer_id, mut cx) = setup_indent_guides_editor(
18433 &"
18434 fn main() {
18435 if a {
18436 b(
18437 c,
18438 d,
18439 )
18440 } else {
18441 e(
18442 f
18443 )
18444 }
18445 }"
18446 .unindent(),
18447 cx,
18448 )
18449 .await;
18450
18451 assert_indent_guides(
18452 0..11,
18453 vec![
18454 indent_guide(buffer_id, 1, 10, 0),
18455 indent_guide(buffer_id, 2, 5, 1),
18456 indent_guide(buffer_id, 7, 9, 1),
18457 indent_guide(buffer_id, 3, 4, 2),
18458 indent_guide(buffer_id, 8, 8, 2),
18459 ],
18460 None,
18461 &mut cx,
18462 );
18463
18464 cx.update_editor(|editor, window, cx| {
18465 editor.fold_at(MultiBufferRow(2), window, cx);
18466 assert_eq!(
18467 editor.display_text(cx),
18468 "
18469 fn main() {
18470 if a {
18471 b(⋯
18472 )
18473 } else {
18474 e(
18475 f
18476 )
18477 }
18478 }"
18479 .unindent()
18480 );
18481 });
18482
18483 assert_indent_guides(
18484 0..11,
18485 vec![
18486 indent_guide(buffer_id, 1, 10, 0),
18487 indent_guide(buffer_id, 2, 5, 1),
18488 indent_guide(buffer_id, 7, 9, 1),
18489 indent_guide(buffer_id, 8, 8, 2),
18490 ],
18491 None,
18492 &mut cx,
18493 );
18494}
18495
18496#[gpui::test]
18497async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
18498 let (buffer_id, mut cx) = setup_indent_guides_editor(
18499 &"
18500 block1
18501 block2
18502 block3
18503 block4
18504 block2
18505 block1
18506 block1"
18507 .unindent(),
18508 cx,
18509 )
18510 .await;
18511
18512 assert_indent_guides(
18513 1..10,
18514 vec![
18515 indent_guide(buffer_id, 1, 4, 0),
18516 indent_guide(buffer_id, 2, 3, 1),
18517 indent_guide(buffer_id, 3, 3, 2),
18518 ],
18519 None,
18520 &mut cx,
18521 );
18522}
18523
18524#[gpui::test]
18525async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
18526 let (buffer_id, mut cx) = setup_indent_guides_editor(
18527 &"
18528 block1
18529 block2
18530 block3
18531
18532 block1
18533 block1"
18534 .unindent(),
18535 cx,
18536 )
18537 .await;
18538
18539 assert_indent_guides(
18540 0..6,
18541 vec![
18542 indent_guide(buffer_id, 1, 2, 0),
18543 indent_guide(buffer_id, 2, 2, 1),
18544 ],
18545 None,
18546 &mut cx,
18547 );
18548}
18549
18550#[gpui::test]
18551async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
18552 let (buffer_id, mut cx) = setup_indent_guides_editor(
18553 &"
18554 function component() {
18555 \treturn (
18556 \t\t\t
18557 \t\t<div>
18558 \t\t\t<abc></abc>
18559 \t\t</div>
18560 \t)
18561 }"
18562 .unindent(),
18563 cx,
18564 )
18565 .await;
18566
18567 assert_indent_guides(
18568 0..8,
18569 vec![
18570 indent_guide(buffer_id, 1, 6, 0),
18571 indent_guide(buffer_id, 2, 5, 1),
18572 indent_guide(buffer_id, 4, 4, 2),
18573 ],
18574 None,
18575 &mut cx,
18576 );
18577}
18578
18579#[gpui::test]
18580async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
18581 let (buffer_id, mut cx) = setup_indent_guides_editor(
18582 &"
18583 function component() {
18584 \treturn (
18585 \t
18586 \t\t<div>
18587 \t\t\t<abc></abc>
18588 \t\t</div>
18589 \t)
18590 }"
18591 .unindent(),
18592 cx,
18593 )
18594 .await;
18595
18596 assert_indent_guides(
18597 0..8,
18598 vec![
18599 indent_guide(buffer_id, 1, 6, 0),
18600 indent_guide(buffer_id, 2, 5, 1),
18601 indent_guide(buffer_id, 4, 4, 2),
18602 ],
18603 None,
18604 &mut cx,
18605 );
18606}
18607
18608#[gpui::test]
18609async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
18610 let (buffer_id, mut cx) = setup_indent_guides_editor(
18611 &"
18612 block1
18613
18614
18615
18616 block2
18617 "
18618 .unindent(),
18619 cx,
18620 )
18621 .await;
18622
18623 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
18624}
18625
18626#[gpui::test]
18627async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
18628 let (buffer_id, mut cx) = setup_indent_guides_editor(
18629 &"
18630 def a:
18631 \tb = 3
18632 \tif True:
18633 \t\tc = 4
18634 \t\td = 5
18635 \tprint(b)
18636 "
18637 .unindent(),
18638 cx,
18639 )
18640 .await;
18641
18642 assert_indent_guides(
18643 0..6,
18644 vec![
18645 indent_guide(buffer_id, 1, 5, 0),
18646 indent_guide(buffer_id, 3, 4, 1),
18647 ],
18648 None,
18649 &mut cx,
18650 );
18651}
18652
18653#[gpui::test]
18654async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
18655 let (buffer_id, mut cx) = setup_indent_guides_editor(
18656 &"
18657 fn main() {
18658 let a = 1;
18659 }"
18660 .unindent(),
18661 cx,
18662 )
18663 .await;
18664
18665 cx.update_editor(|editor, window, cx| {
18666 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18667 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18668 });
18669 });
18670
18671 assert_indent_guides(
18672 0..3,
18673 vec![indent_guide(buffer_id, 1, 1, 0)],
18674 Some(vec![0]),
18675 &mut cx,
18676 );
18677}
18678
18679#[gpui::test]
18680async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
18681 let (buffer_id, mut cx) = setup_indent_guides_editor(
18682 &"
18683 fn main() {
18684 if 1 == 2 {
18685 let a = 1;
18686 }
18687 }"
18688 .unindent(),
18689 cx,
18690 )
18691 .await;
18692
18693 cx.update_editor(|editor, window, cx| {
18694 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18695 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18696 });
18697 });
18698
18699 assert_indent_guides(
18700 0..4,
18701 vec![
18702 indent_guide(buffer_id, 1, 3, 0),
18703 indent_guide(buffer_id, 2, 2, 1),
18704 ],
18705 Some(vec![1]),
18706 &mut cx,
18707 );
18708
18709 cx.update_editor(|editor, window, cx| {
18710 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18711 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
18712 });
18713 });
18714
18715 assert_indent_guides(
18716 0..4,
18717 vec![
18718 indent_guide(buffer_id, 1, 3, 0),
18719 indent_guide(buffer_id, 2, 2, 1),
18720 ],
18721 Some(vec![1]),
18722 &mut cx,
18723 );
18724
18725 cx.update_editor(|editor, window, cx| {
18726 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18727 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
18728 });
18729 });
18730
18731 assert_indent_guides(
18732 0..4,
18733 vec![
18734 indent_guide(buffer_id, 1, 3, 0),
18735 indent_guide(buffer_id, 2, 2, 1),
18736 ],
18737 Some(vec![0]),
18738 &mut cx,
18739 );
18740}
18741
18742#[gpui::test]
18743async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
18744 let (buffer_id, mut cx) = setup_indent_guides_editor(
18745 &"
18746 fn main() {
18747 let a = 1;
18748
18749 let b = 2;
18750 }"
18751 .unindent(),
18752 cx,
18753 )
18754 .await;
18755
18756 cx.update_editor(|editor, window, cx| {
18757 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18758 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
18759 });
18760 });
18761
18762 assert_indent_guides(
18763 0..5,
18764 vec![indent_guide(buffer_id, 1, 3, 0)],
18765 Some(vec![0]),
18766 &mut cx,
18767 );
18768}
18769
18770#[gpui::test]
18771async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
18772 let (buffer_id, mut cx) = setup_indent_guides_editor(
18773 &"
18774 def m:
18775 a = 1
18776 pass"
18777 .unindent(),
18778 cx,
18779 )
18780 .await;
18781
18782 cx.update_editor(|editor, window, cx| {
18783 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18784 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18785 });
18786 });
18787
18788 assert_indent_guides(
18789 0..3,
18790 vec![indent_guide(buffer_id, 1, 2, 0)],
18791 Some(vec![0]),
18792 &mut cx,
18793 );
18794}
18795
18796#[gpui::test]
18797async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
18798 init_test(cx, |_| {});
18799 let mut cx = EditorTestContext::new(cx).await;
18800 let text = indoc! {
18801 "
18802 impl A {
18803 fn b() {
18804 0;
18805 3;
18806 5;
18807 6;
18808 7;
18809 }
18810 }
18811 "
18812 };
18813 let base_text = indoc! {
18814 "
18815 impl A {
18816 fn b() {
18817 0;
18818 1;
18819 2;
18820 3;
18821 4;
18822 }
18823 fn c() {
18824 5;
18825 6;
18826 7;
18827 }
18828 }
18829 "
18830 };
18831
18832 cx.update_editor(|editor, window, cx| {
18833 editor.set_text(text, window, cx);
18834
18835 editor.buffer().update(cx, |multibuffer, cx| {
18836 let buffer = multibuffer.as_singleton().unwrap();
18837 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
18838
18839 multibuffer.set_all_diff_hunks_expanded(cx);
18840 multibuffer.add_diff(diff, cx);
18841
18842 buffer.read(cx).remote_id()
18843 })
18844 });
18845 cx.run_until_parked();
18846
18847 cx.assert_state_with_diff(
18848 indoc! { "
18849 impl A {
18850 fn b() {
18851 0;
18852 - 1;
18853 - 2;
18854 3;
18855 - 4;
18856 - }
18857 - fn c() {
18858 5;
18859 6;
18860 7;
18861 }
18862 }
18863 ˇ"
18864 }
18865 .to_string(),
18866 );
18867
18868 let mut actual_guides = cx.update_editor(|editor, window, cx| {
18869 editor
18870 .snapshot(window, cx)
18871 .buffer_snapshot
18872 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
18873 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
18874 .collect::<Vec<_>>()
18875 });
18876 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
18877 assert_eq!(
18878 actual_guides,
18879 vec![
18880 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
18881 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
18882 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
18883 ]
18884 );
18885}
18886
18887#[gpui::test]
18888async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
18889 init_test(cx, |_| {});
18890 let mut cx = EditorTestContext::new(cx).await;
18891
18892 let diff_base = r#"
18893 a
18894 b
18895 c
18896 "#
18897 .unindent();
18898
18899 cx.set_state(
18900 &r#"
18901 ˇA
18902 b
18903 C
18904 "#
18905 .unindent(),
18906 );
18907 cx.set_head_text(&diff_base);
18908 cx.update_editor(|editor, window, cx| {
18909 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18910 });
18911 executor.run_until_parked();
18912
18913 let both_hunks_expanded = r#"
18914 - a
18915 + ˇA
18916 b
18917 - c
18918 + C
18919 "#
18920 .unindent();
18921
18922 cx.assert_state_with_diff(both_hunks_expanded.clone());
18923
18924 let hunk_ranges = cx.update_editor(|editor, window, cx| {
18925 let snapshot = editor.snapshot(window, cx);
18926 let hunks = editor
18927 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18928 .collect::<Vec<_>>();
18929 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
18930 let buffer_id = hunks[0].buffer_id;
18931 hunks
18932 .into_iter()
18933 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
18934 .collect::<Vec<_>>()
18935 });
18936 assert_eq!(hunk_ranges.len(), 2);
18937
18938 cx.update_editor(|editor, _, cx| {
18939 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18940 });
18941 executor.run_until_parked();
18942
18943 let second_hunk_expanded = r#"
18944 ˇA
18945 b
18946 - c
18947 + C
18948 "#
18949 .unindent();
18950
18951 cx.assert_state_with_diff(second_hunk_expanded);
18952
18953 cx.update_editor(|editor, _, cx| {
18954 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18955 });
18956 executor.run_until_parked();
18957
18958 cx.assert_state_with_diff(both_hunks_expanded.clone());
18959
18960 cx.update_editor(|editor, _, cx| {
18961 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
18962 });
18963 executor.run_until_parked();
18964
18965 let first_hunk_expanded = r#"
18966 - a
18967 + ˇA
18968 b
18969 C
18970 "#
18971 .unindent();
18972
18973 cx.assert_state_with_diff(first_hunk_expanded);
18974
18975 cx.update_editor(|editor, _, cx| {
18976 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
18977 });
18978 executor.run_until_parked();
18979
18980 cx.assert_state_with_diff(both_hunks_expanded);
18981
18982 cx.set_state(
18983 &r#"
18984 ˇA
18985 b
18986 "#
18987 .unindent(),
18988 );
18989 cx.run_until_parked();
18990
18991 // TODO this cursor position seems bad
18992 cx.assert_state_with_diff(
18993 r#"
18994 - ˇa
18995 + A
18996 b
18997 "#
18998 .unindent(),
18999 );
19000
19001 cx.update_editor(|editor, window, cx| {
19002 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19003 });
19004
19005 cx.assert_state_with_diff(
19006 r#"
19007 - ˇa
19008 + A
19009 b
19010 - c
19011 "#
19012 .unindent(),
19013 );
19014
19015 let hunk_ranges = cx.update_editor(|editor, window, cx| {
19016 let snapshot = editor.snapshot(window, cx);
19017 let hunks = editor
19018 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19019 .collect::<Vec<_>>();
19020 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
19021 let buffer_id = hunks[0].buffer_id;
19022 hunks
19023 .into_iter()
19024 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
19025 .collect::<Vec<_>>()
19026 });
19027 assert_eq!(hunk_ranges.len(), 2);
19028
19029 cx.update_editor(|editor, _, cx| {
19030 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
19031 });
19032 executor.run_until_parked();
19033
19034 cx.assert_state_with_diff(
19035 r#"
19036 - ˇa
19037 + A
19038 b
19039 "#
19040 .unindent(),
19041 );
19042}
19043
19044#[gpui::test]
19045async fn test_toggle_deletion_hunk_at_start_of_file(
19046 executor: BackgroundExecutor,
19047 cx: &mut TestAppContext,
19048) {
19049 init_test(cx, |_| {});
19050 let mut cx = EditorTestContext::new(cx).await;
19051
19052 let diff_base = r#"
19053 a
19054 b
19055 c
19056 "#
19057 .unindent();
19058
19059 cx.set_state(
19060 &r#"
19061 ˇb
19062 c
19063 "#
19064 .unindent(),
19065 );
19066 cx.set_head_text(&diff_base);
19067 cx.update_editor(|editor, window, cx| {
19068 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19069 });
19070 executor.run_until_parked();
19071
19072 let hunk_expanded = r#"
19073 - a
19074 ˇb
19075 c
19076 "#
19077 .unindent();
19078
19079 cx.assert_state_with_diff(hunk_expanded.clone());
19080
19081 let hunk_ranges = cx.update_editor(|editor, window, cx| {
19082 let snapshot = editor.snapshot(window, cx);
19083 let hunks = editor
19084 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19085 .collect::<Vec<_>>();
19086 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
19087 let buffer_id = hunks[0].buffer_id;
19088 hunks
19089 .into_iter()
19090 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
19091 .collect::<Vec<_>>()
19092 });
19093 assert_eq!(hunk_ranges.len(), 1);
19094
19095 cx.update_editor(|editor, _, cx| {
19096 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
19097 });
19098 executor.run_until_parked();
19099
19100 let hunk_collapsed = r#"
19101 ˇb
19102 c
19103 "#
19104 .unindent();
19105
19106 cx.assert_state_with_diff(hunk_collapsed);
19107
19108 cx.update_editor(|editor, _, cx| {
19109 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
19110 });
19111 executor.run_until_parked();
19112
19113 cx.assert_state_with_diff(hunk_expanded.clone());
19114}
19115
19116#[gpui::test]
19117async fn test_display_diff_hunks(cx: &mut TestAppContext) {
19118 init_test(cx, |_| {});
19119
19120 let fs = FakeFs::new(cx.executor());
19121 fs.insert_tree(
19122 path!("/test"),
19123 json!({
19124 ".git": {},
19125 "file-1": "ONE\n",
19126 "file-2": "TWO\n",
19127 "file-3": "THREE\n",
19128 }),
19129 )
19130 .await;
19131
19132 fs.set_head_for_repo(
19133 path!("/test/.git").as_ref(),
19134 &[
19135 ("file-1".into(), "one\n".into()),
19136 ("file-2".into(), "two\n".into()),
19137 ("file-3".into(), "three\n".into()),
19138 ],
19139 "deadbeef",
19140 );
19141
19142 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
19143 let mut buffers = vec![];
19144 for i in 1..=3 {
19145 let buffer = project
19146 .update(cx, |project, cx| {
19147 let path = format!(path!("/test/file-{}"), i);
19148 project.open_local_buffer(path, cx)
19149 })
19150 .await
19151 .unwrap();
19152 buffers.push(buffer);
19153 }
19154
19155 let multibuffer = cx.new(|cx| {
19156 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
19157 multibuffer.set_all_diff_hunks_expanded(cx);
19158 for buffer in &buffers {
19159 let snapshot = buffer.read(cx).snapshot();
19160 multibuffer.set_excerpts_for_path(
19161 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
19162 buffer.clone(),
19163 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
19164 DEFAULT_MULTIBUFFER_CONTEXT,
19165 cx,
19166 );
19167 }
19168 multibuffer
19169 });
19170
19171 let editor = cx.add_window(|window, cx| {
19172 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
19173 });
19174 cx.run_until_parked();
19175
19176 let snapshot = editor
19177 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19178 .unwrap();
19179 let hunks = snapshot
19180 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
19181 .map(|hunk| match hunk {
19182 DisplayDiffHunk::Unfolded {
19183 display_row_range, ..
19184 } => display_row_range,
19185 DisplayDiffHunk::Folded { .. } => unreachable!(),
19186 })
19187 .collect::<Vec<_>>();
19188 assert_eq!(
19189 hunks,
19190 [
19191 DisplayRow(2)..DisplayRow(4),
19192 DisplayRow(7)..DisplayRow(9),
19193 DisplayRow(12)..DisplayRow(14),
19194 ]
19195 );
19196}
19197
19198#[gpui::test]
19199async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
19200 init_test(cx, |_| {});
19201
19202 let mut cx = EditorTestContext::new(cx).await;
19203 cx.set_head_text(indoc! { "
19204 one
19205 two
19206 three
19207 four
19208 five
19209 "
19210 });
19211 cx.set_index_text(indoc! { "
19212 one
19213 two
19214 three
19215 four
19216 five
19217 "
19218 });
19219 cx.set_state(indoc! {"
19220 one
19221 TWO
19222 ˇTHREE
19223 FOUR
19224 five
19225 "});
19226 cx.run_until_parked();
19227 cx.update_editor(|editor, window, cx| {
19228 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19229 });
19230 cx.run_until_parked();
19231 cx.assert_index_text(Some(indoc! {"
19232 one
19233 TWO
19234 THREE
19235 FOUR
19236 five
19237 "}));
19238 cx.set_state(indoc! { "
19239 one
19240 TWO
19241 ˇTHREE-HUNDRED
19242 FOUR
19243 five
19244 "});
19245 cx.run_until_parked();
19246 cx.update_editor(|editor, window, cx| {
19247 let snapshot = editor.snapshot(window, cx);
19248 let hunks = editor
19249 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19250 .collect::<Vec<_>>();
19251 assert_eq!(hunks.len(), 1);
19252 assert_eq!(
19253 hunks[0].status(),
19254 DiffHunkStatus {
19255 kind: DiffHunkStatusKind::Modified,
19256 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
19257 }
19258 );
19259
19260 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19261 });
19262 cx.run_until_parked();
19263 cx.assert_index_text(Some(indoc! {"
19264 one
19265 TWO
19266 THREE-HUNDRED
19267 FOUR
19268 five
19269 "}));
19270}
19271
19272#[gpui::test]
19273fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
19274 init_test(cx, |_| {});
19275
19276 let editor = cx.add_window(|window, cx| {
19277 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
19278 build_editor(buffer, window, cx)
19279 });
19280
19281 let render_args = Arc::new(Mutex::new(None));
19282 let snapshot = editor
19283 .update(cx, |editor, window, cx| {
19284 let snapshot = editor.buffer().read(cx).snapshot(cx);
19285 let range =
19286 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
19287
19288 struct RenderArgs {
19289 row: MultiBufferRow,
19290 folded: bool,
19291 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
19292 }
19293
19294 let crease = Crease::inline(
19295 range,
19296 FoldPlaceholder::test(),
19297 {
19298 let toggle_callback = render_args.clone();
19299 move |row, folded, callback, _window, _cx| {
19300 *toggle_callback.lock() = Some(RenderArgs {
19301 row,
19302 folded,
19303 callback,
19304 });
19305 div()
19306 }
19307 },
19308 |_row, _folded, _window, _cx| div(),
19309 );
19310
19311 editor.insert_creases(Some(crease), cx);
19312 let snapshot = editor.snapshot(window, cx);
19313 let _div = snapshot.render_crease_toggle(
19314 MultiBufferRow(1),
19315 false,
19316 cx.entity().clone(),
19317 window,
19318 cx,
19319 );
19320 snapshot
19321 })
19322 .unwrap();
19323
19324 let render_args = render_args.lock().take().unwrap();
19325 assert_eq!(render_args.row, MultiBufferRow(1));
19326 assert!(!render_args.folded);
19327 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
19328
19329 cx.update_window(*editor, |_, window, cx| {
19330 (render_args.callback)(true, window, cx)
19331 })
19332 .unwrap();
19333 let snapshot = editor
19334 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19335 .unwrap();
19336 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
19337
19338 cx.update_window(*editor, |_, window, cx| {
19339 (render_args.callback)(false, window, cx)
19340 })
19341 .unwrap();
19342 let snapshot = editor
19343 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19344 .unwrap();
19345 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
19346}
19347
19348#[gpui::test]
19349async fn test_input_text(cx: &mut TestAppContext) {
19350 init_test(cx, |_| {});
19351 let mut cx = EditorTestContext::new(cx).await;
19352
19353 cx.set_state(
19354 &r#"ˇone
19355 two
19356
19357 three
19358 fourˇ
19359 five
19360
19361 siˇx"#
19362 .unindent(),
19363 );
19364
19365 cx.dispatch_action(HandleInput(String::new()));
19366 cx.assert_editor_state(
19367 &r#"ˇone
19368 two
19369
19370 three
19371 fourˇ
19372 five
19373
19374 siˇx"#
19375 .unindent(),
19376 );
19377
19378 cx.dispatch_action(HandleInput("AAAA".to_string()));
19379 cx.assert_editor_state(
19380 &r#"AAAAˇone
19381 two
19382
19383 three
19384 fourAAAAˇ
19385 five
19386
19387 siAAAAˇx"#
19388 .unindent(),
19389 );
19390}
19391
19392#[gpui::test]
19393async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
19394 init_test(cx, |_| {});
19395
19396 let mut cx = EditorTestContext::new(cx).await;
19397 cx.set_state(
19398 r#"let foo = 1;
19399let foo = 2;
19400let foo = 3;
19401let fooˇ = 4;
19402let foo = 5;
19403let foo = 6;
19404let foo = 7;
19405let foo = 8;
19406let foo = 9;
19407let foo = 10;
19408let foo = 11;
19409let foo = 12;
19410let foo = 13;
19411let foo = 14;
19412let foo = 15;"#,
19413 );
19414
19415 cx.update_editor(|e, window, cx| {
19416 assert_eq!(
19417 e.next_scroll_position,
19418 NextScrollCursorCenterTopBottom::Center,
19419 "Default next scroll direction is center",
19420 );
19421
19422 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19423 assert_eq!(
19424 e.next_scroll_position,
19425 NextScrollCursorCenterTopBottom::Top,
19426 "After center, next scroll direction should be top",
19427 );
19428
19429 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19430 assert_eq!(
19431 e.next_scroll_position,
19432 NextScrollCursorCenterTopBottom::Bottom,
19433 "After top, next scroll direction should be bottom",
19434 );
19435
19436 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19437 assert_eq!(
19438 e.next_scroll_position,
19439 NextScrollCursorCenterTopBottom::Center,
19440 "After bottom, scrolling should start over",
19441 );
19442
19443 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19444 assert_eq!(
19445 e.next_scroll_position,
19446 NextScrollCursorCenterTopBottom::Top,
19447 "Scrolling continues if retriggered fast enough"
19448 );
19449 });
19450
19451 cx.executor()
19452 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
19453 cx.executor().run_until_parked();
19454 cx.update_editor(|e, _, _| {
19455 assert_eq!(
19456 e.next_scroll_position,
19457 NextScrollCursorCenterTopBottom::Center,
19458 "If scrolling is not triggered fast enough, it should reset"
19459 );
19460 });
19461}
19462
19463#[gpui::test]
19464async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
19465 init_test(cx, |_| {});
19466 let mut cx = EditorLspTestContext::new_rust(
19467 lsp::ServerCapabilities {
19468 definition_provider: Some(lsp::OneOf::Left(true)),
19469 references_provider: Some(lsp::OneOf::Left(true)),
19470 ..lsp::ServerCapabilities::default()
19471 },
19472 cx,
19473 )
19474 .await;
19475
19476 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
19477 let go_to_definition = cx
19478 .lsp
19479 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
19480 move |params, _| async move {
19481 if empty_go_to_definition {
19482 Ok(None)
19483 } else {
19484 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
19485 uri: params.text_document_position_params.text_document.uri,
19486 range: lsp::Range::new(
19487 lsp::Position::new(4, 3),
19488 lsp::Position::new(4, 6),
19489 ),
19490 })))
19491 }
19492 },
19493 );
19494 let references = cx
19495 .lsp
19496 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
19497 Ok(Some(vec![lsp::Location {
19498 uri: params.text_document_position.text_document.uri,
19499 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
19500 }]))
19501 });
19502 (go_to_definition, references)
19503 };
19504
19505 cx.set_state(
19506 &r#"fn one() {
19507 let mut a = ˇtwo();
19508 }
19509
19510 fn two() {}"#
19511 .unindent(),
19512 );
19513 set_up_lsp_handlers(false, &mut cx);
19514 let navigated = cx
19515 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19516 .await
19517 .expect("Failed to navigate to definition");
19518 assert_eq!(
19519 navigated,
19520 Navigated::Yes,
19521 "Should have navigated to definition from the GetDefinition response"
19522 );
19523 cx.assert_editor_state(
19524 &r#"fn one() {
19525 let mut a = two();
19526 }
19527
19528 fn «twoˇ»() {}"#
19529 .unindent(),
19530 );
19531
19532 let editors = cx.update_workspace(|workspace, _, cx| {
19533 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19534 });
19535 cx.update_editor(|_, _, test_editor_cx| {
19536 assert_eq!(
19537 editors.len(),
19538 1,
19539 "Initially, only one, test, editor should be open in the workspace"
19540 );
19541 assert_eq!(
19542 test_editor_cx.entity(),
19543 editors.last().expect("Asserted len is 1").clone()
19544 );
19545 });
19546
19547 set_up_lsp_handlers(true, &mut cx);
19548 let navigated = cx
19549 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19550 .await
19551 .expect("Failed to navigate to lookup references");
19552 assert_eq!(
19553 navigated,
19554 Navigated::Yes,
19555 "Should have navigated to references as a fallback after empty GoToDefinition response"
19556 );
19557 // We should not change the selections in the existing file,
19558 // if opening another milti buffer with the references
19559 cx.assert_editor_state(
19560 &r#"fn one() {
19561 let mut a = two();
19562 }
19563
19564 fn «twoˇ»() {}"#
19565 .unindent(),
19566 );
19567 let editors = cx.update_workspace(|workspace, _, cx| {
19568 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19569 });
19570 cx.update_editor(|_, _, test_editor_cx| {
19571 assert_eq!(
19572 editors.len(),
19573 2,
19574 "After falling back to references search, we open a new editor with the results"
19575 );
19576 let references_fallback_text = editors
19577 .into_iter()
19578 .find(|new_editor| *new_editor != test_editor_cx.entity())
19579 .expect("Should have one non-test editor now")
19580 .read(test_editor_cx)
19581 .text(test_editor_cx);
19582 assert_eq!(
19583 references_fallback_text, "fn one() {\n let mut a = two();\n}",
19584 "Should use the range from the references response and not the GoToDefinition one"
19585 );
19586 });
19587}
19588
19589#[gpui::test]
19590async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
19591 init_test(cx, |_| {});
19592 cx.update(|cx| {
19593 let mut editor_settings = EditorSettings::get_global(cx).clone();
19594 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
19595 EditorSettings::override_global(editor_settings, cx);
19596 });
19597 let mut cx = EditorLspTestContext::new_rust(
19598 lsp::ServerCapabilities {
19599 definition_provider: Some(lsp::OneOf::Left(true)),
19600 references_provider: Some(lsp::OneOf::Left(true)),
19601 ..lsp::ServerCapabilities::default()
19602 },
19603 cx,
19604 )
19605 .await;
19606 let original_state = r#"fn one() {
19607 let mut a = ˇtwo();
19608 }
19609
19610 fn two() {}"#
19611 .unindent();
19612 cx.set_state(&original_state);
19613
19614 let mut go_to_definition = cx
19615 .lsp
19616 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
19617 move |_, _| async move { Ok(None) },
19618 );
19619 let _references = cx
19620 .lsp
19621 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
19622 panic!("Should not call for references with no go to definition fallback")
19623 });
19624
19625 let navigated = cx
19626 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19627 .await
19628 .expect("Failed to navigate to lookup references");
19629 go_to_definition
19630 .next()
19631 .await
19632 .expect("Should have called the go_to_definition handler");
19633
19634 assert_eq!(
19635 navigated,
19636 Navigated::No,
19637 "Should have navigated to references as a fallback after empty GoToDefinition response"
19638 );
19639 cx.assert_editor_state(&original_state);
19640 let editors = cx.update_workspace(|workspace, _, cx| {
19641 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19642 });
19643 cx.update_editor(|_, _, _| {
19644 assert_eq!(
19645 editors.len(),
19646 1,
19647 "After unsuccessful fallback, no other editor should have been opened"
19648 );
19649 });
19650}
19651
19652#[gpui::test]
19653async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
19654 init_test(cx, |_| {});
19655
19656 let language = Arc::new(Language::new(
19657 LanguageConfig::default(),
19658 Some(tree_sitter_rust::LANGUAGE.into()),
19659 ));
19660
19661 let text = r#"
19662 #[cfg(test)]
19663 mod tests() {
19664 #[test]
19665 fn runnable_1() {
19666 let a = 1;
19667 }
19668
19669 #[test]
19670 fn runnable_2() {
19671 let a = 1;
19672 let b = 2;
19673 }
19674 }
19675 "#
19676 .unindent();
19677
19678 let fs = FakeFs::new(cx.executor());
19679 fs.insert_file("/file.rs", Default::default()).await;
19680
19681 let project = Project::test(fs, ["/a".as_ref()], cx).await;
19682 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19683 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19684 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
19685 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
19686
19687 let editor = cx.new_window_entity(|window, cx| {
19688 Editor::new(
19689 EditorMode::full(),
19690 multi_buffer,
19691 Some(project.clone()),
19692 window,
19693 cx,
19694 )
19695 });
19696
19697 editor.update_in(cx, |editor, window, cx| {
19698 let snapshot = editor.buffer().read(cx).snapshot(cx);
19699 editor.tasks.insert(
19700 (buffer.read(cx).remote_id(), 3),
19701 RunnableTasks {
19702 templates: vec![],
19703 offset: snapshot.anchor_before(43),
19704 column: 0,
19705 extra_variables: HashMap::default(),
19706 context_range: BufferOffset(43)..BufferOffset(85),
19707 },
19708 );
19709 editor.tasks.insert(
19710 (buffer.read(cx).remote_id(), 8),
19711 RunnableTasks {
19712 templates: vec![],
19713 offset: snapshot.anchor_before(86),
19714 column: 0,
19715 extra_variables: HashMap::default(),
19716 context_range: BufferOffset(86)..BufferOffset(191),
19717 },
19718 );
19719
19720 // Test finding task when cursor is inside function body
19721 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19722 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
19723 });
19724 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
19725 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
19726
19727 // Test finding task when cursor is on function name
19728 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19729 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
19730 });
19731 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
19732 assert_eq!(row, 8, "Should find task when cursor is on function name");
19733 });
19734}
19735
19736#[gpui::test]
19737async fn test_folding_buffers(cx: &mut TestAppContext) {
19738 init_test(cx, |_| {});
19739
19740 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
19741 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
19742 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
19743
19744 let fs = FakeFs::new(cx.executor());
19745 fs.insert_tree(
19746 path!("/a"),
19747 json!({
19748 "first.rs": sample_text_1,
19749 "second.rs": sample_text_2,
19750 "third.rs": sample_text_3,
19751 }),
19752 )
19753 .await;
19754 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19755 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19756 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19757 let worktree = project.update(cx, |project, cx| {
19758 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
19759 assert_eq!(worktrees.len(), 1);
19760 worktrees.pop().unwrap()
19761 });
19762 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
19763
19764 let buffer_1 = project
19765 .update(cx, |project, cx| {
19766 project.open_buffer((worktree_id, "first.rs"), cx)
19767 })
19768 .await
19769 .unwrap();
19770 let buffer_2 = project
19771 .update(cx, |project, cx| {
19772 project.open_buffer((worktree_id, "second.rs"), cx)
19773 })
19774 .await
19775 .unwrap();
19776 let buffer_3 = project
19777 .update(cx, |project, cx| {
19778 project.open_buffer((worktree_id, "third.rs"), cx)
19779 })
19780 .await
19781 .unwrap();
19782
19783 let multi_buffer = cx.new(|cx| {
19784 let mut multi_buffer = MultiBuffer::new(ReadWrite);
19785 multi_buffer.push_excerpts(
19786 buffer_1.clone(),
19787 [
19788 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19789 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19790 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19791 ],
19792 cx,
19793 );
19794 multi_buffer.push_excerpts(
19795 buffer_2.clone(),
19796 [
19797 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19798 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19799 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19800 ],
19801 cx,
19802 );
19803 multi_buffer.push_excerpts(
19804 buffer_3.clone(),
19805 [
19806 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19807 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19808 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19809 ],
19810 cx,
19811 );
19812 multi_buffer
19813 });
19814 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19815 Editor::new(
19816 EditorMode::full(),
19817 multi_buffer.clone(),
19818 Some(project.clone()),
19819 window,
19820 cx,
19821 )
19822 });
19823
19824 assert_eq!(
19825 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19826 "\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",
19827 );
19828
19829 multi_buffer_editor.update(cx, |editor, cx| {
19830 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
19831 });
19832 assert_eq!(
19833 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19834 "\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",
19835 "After folding the first buffer, its text should not be displayed"
19836 );
19837
19838 multi_buffer_editor.update(cx, |editor, cx| {
19839 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
19840 });
19841 assert_eq!(
19842 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19843 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
19844 "After folding the second buffer, its text should not be displayed"
19845 );
19846
19847 multi_buffer_editor.update(cx, |editor, cx| {
19848 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
19849 });
19850 assert_eq!(
19851 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19852 "\n\n\n\n\n",
19853 "After folding the third buffer, its text should not be displayed"
19854 );
19855
19856 // Emulate selection inside the fold logic, that should work
19857 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19858 editor
19859 .snapshot(window, cx)
19860 .next_line_boundary(Point::new(0, 4));
19861 });
19862
19863 multi_buffer_editor.update(cx, |editor, cx| {
19864 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
19865 });
19866 assert_eq!(
19867 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19868 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
19869 "After unfolding the second buffer, its text should be displayed"
19870 );
19871
19872 // Typing inside of buffer 1 causes that buffer to be unfolded.
19873 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19874 assert_eq!(
19875 multi_buffer
19876 .read(cx)
19877 .snapshot(cx)
19878 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
19879 .collect::<String>(),
19880 "bbbb"
19881 );
19882 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
19883 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
19884 });
19885 editor.handle_input("B", window, cx);
19886 });
19887
19888 assert_eq!(
19889 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19890 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
19891 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
19892 );
19893
19894 multi_buffer_editor.update(cx, |editor, cx| {
19895 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
19896 });
19897 assert_eq!(
19898 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19899 "\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",
19900 "After unfolding the all buffers, all original text should be displayed"
19901 );
19902}
19903
19904#[gpui::test]
19905async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
19906 init_test(cx, |_| {});
19907
19908 let sample_text_1 = "1111\n2222\n3333".to_string();
19909 let sample_text_2 = "4444\n5555\n6666".to_string();
19910 let sample_text_3 = "7777\n8888\n9999".to_string();
19911
19912 let fs = FakeFs::new(cx.executor());
19913 fs.insert_tree(
19914 path!("/a"),
19915 json!({
19916 "first.rs": sample_text_1,
19917 "second.rs": sample_text_2,
19918 "third.rs": sample_text_3,
19919 }),
19920 )
19921 .await;
19922 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19923 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19924 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19925 let worktree = project.update(cx, |project, cx| {
19926 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
19927 assert_eq!(worktrees.len(), 1);
19928 worktrees.pop().unwrap()
19929 });
19930 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
19931
19932 let buffer_1 = project
19933 .update(cx, |project, cx| {
19934 project.open_buffer((worktree_id, "first.rs"), cx)
19935 })
19936 .await
19937 .unwrap();
19938 let buffer_2 = project
19939 .update(cx, |project, cx| {
19940 project.open_buffer((worktree_id, "second.rs"), cx)
19941 })
19942 .await
19943 .unwrap();
19944 let buffer_3 = project
19945 .update(cx, |project, cx| {
19946 project.open_buffer((worktree_id, "third.rs"), cx)
19947 })
19948 .await
19949 .unwrap();
19950
19951 let multi_buffer = cx.new(|cx| {
19952 let mut multi_buffer = MultiBuffer::new(ReadWrite);
19953 multi_buffer.push_excerpts(
19954 buffer_1.clone(),
19955 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19956 cx,
19957 );
19958 multi_buffer.push_excerpts(
19959 buffer_2.clone(),
19960 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19961 cx,
19962 );
19963 multi_buffer.push_excerpts(
19964 buffer_3.clone(),
19965 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19966 cx,
19967 );
19968 multi_buffer
19969 });
19970
19971 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19972 Editor::new(
19973 EditorMode::full(),
19974 multi_buffer,
19975 Some(project.clone()),
19976 window,
19977 cx,
19978 )
19979 });
19980
19981 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
19982 assert_eq!(
19983 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19984 full_text,
19985 );
19986
19987 multi_buffer_editor.update(cx, |editor, cx| {
19988 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
19989 });
19990 assert_eq!(
19991 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19992 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
19993 "After folding the first buffer, its text should not be displayed"
19994 );
19995
19996 multi_buffer_editor.update(cx, |editor, cx| {
19997 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
19998 });
19999
20000 assert_eq!(
20001 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20002 "\n\n\n\n\n\n7777\n8888\n9999",
20003 "After folding the second buffer, its text should not be displayed"
20004 );
20005
20006 multi_buffer_editor.update(cx, |editor, cx| {
20007 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
20008 });
20009 assert_eq!(
20010 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20011 "\n\n\n\n\n",
20012 "After folding the third buffer, its text should not be displayed"
20013 );
20014
20015 multi_buffer_editor.update(cx, |editor, cx| {
20016 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
20017 });
20018 assert_eq!(
20019 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20020 "\n\n\n\n4444\n5555\n6666\n\n",
20021 "After unfolding the second buffer, its text should be displayed"
20022 );
20023
20024 multi_buffer_editor.update(cx, |editor, cx| {
20025 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
20026 });
20027 assert_eq!(
20028 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20029 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
20030 "After unfolding the first buffer, its text should be displayed"
20031 );
20032
20033 multi_buffer_editor.update(cx, |editor, cx| {
20034 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
20035 });
20036 assert_eq!(
20037 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20038 full_text,
20039 "After unfolding all buffers, all original text should be displayed"
20040 );
20041}
20042
20043#[gpui::test]
20044async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
20045 init_test(cx, |_| {});
20046
20047 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
20048
20049 let fs = FakeFs::new(cx.executor());
20050 fs.insert_tree(
20051 path!("/a"),
20052 json!({
20053 "main.rs": sample_text,
20054 }),
20055 )
20056 .await;
20057 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20058 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20059 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20060 let worktree = project.update(cx, |project, cx| {
20061 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
20062 assert_eq!(worktrees.len(), 1);
20063 worktrees.pop().unwrap()
20064 });
20065 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
20066
20067 let buffer_1 = project
20068 .update(cx, |project, cx| {
20069 project.open_buffer((worktree_id, "main.rs"), cx)
20070 })
20071 .await
20072 .unwrap();
20073
20074 let multi_buffer = cx.new(|cx| {
20075 let mut multi_buffer = MultiBuffer::new(ReadWrite);
20076 multi_buffer.push_excerpts(
20077 buffer_1.clone(),
20078 [ExcerptRange::new(
20079 Point::new(0, 0)
20080 ..Point::new(
20081 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
20082 0,
20083 ),
20084 )],
20085 cx,
20086 );
20087 multi_buffer
20088 });
20089 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
20090 Editor::new(
20091 EditorMode::full(),
20092 multi_buffer,
20093 Some(project.clone()),
20094 window,
20095 cx,
20096 )
20097 });
20098
20099 let selection_range = Point::new(1, 0)..Point::new(2, 0);
20100 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20101 enum TestHighlight {}
20102 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
20103 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
20104 editor.highlight_text::<TestHighlight>(
20105 vec![highlight_range.clone()],
20106 HighlightStyle::color(Hsla::green()),
20107 cx,
20108 );
20109 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20110 s.select_ranges(Some(highlight_range))
20111 });
20112 });
20113
20114 let full_text = format!("\n\n{sample_text}");
20115 assert_eq!(
20116 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20117 full_text,
20118 );
20119}
20120
20121#[gpui::test]
20122async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
20123 init_test(cx, |_| {});
20124 cx.update(|cx| {
20125 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
20126 "keymaps/default-linux.json",
20127 cx,
20128 )
20129 .unwrap();
20130 cx.bind_keys(default_key_bindings);
20131 });
20132
20133 let (editor, cx) = cx.add_window_view(|window, cx| {
20134 let multi_buffer = MultiBuffer::build_multi(
20135 [
20136 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
20137 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
20138 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
20139 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
20140 ],
20141 cx,
20142 );
20143 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
20144
20145 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
20146 // fold all but the second buffer, so that we test navigating between two
20147 // adjacent folded buffers, as well as folded buffers at the start and
20148 // end the multibuffer
20149 editor.fold_buffer(buffer_ids[0], cx);
20150 editor.fold_buffer(buffer_ids[2], cx);
20151 editor.fold_buffer(buffer_ids[3], cx);
20152
20153 editor
20154 });
20155 cx.simulate_resize(size(px(1000.), px(1000.)));
20156
20157 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
20158 cx.assert_excerpts_with_selections(indoc! {"
20159 [EXCERPT]
20160 ˇ[FOLDED]
20161 [EXCERPT]
20162 a1
20163 b1
20164 [EXCERPT]
20165 [FOLDED]
20166 [EXCERPT]
20167 [FOLDED]
20168 "
20169 });
20170 cx.simulate_keystroke("down");
20171 cx.assert_excerpts_with_selections(indoc! {"
20172 [EXCERPT]
20173 [FOLDED]
20174 [EXCERPT]
20175 ˇa1
20176 b1
20177 [EXCERPT]
20178 [FOLDED]
20179 [EXCERPT]
20180 [FOLDED]
20181 "
20182 });
20183 cx.simulate_keystroke("down");
20184 cx.assert_excerpts_with_selections(indoc! {"
20185 [EXCERPT]
20186 [FOLDED]
20187 [EXCERPT]
20188 a1
20189 ˇb1
20190 [EXCERPT]
20191 [FOLDED]
20192 [EXCERPT]
20193 [FOLDED]
20194 "
20195 });
20196 cx.simulate_keystroke("down");
20197 cx.assert_excerpts_with_selections(indoc! {"
20198 [EXCERPT]
20199 [FOLDED]
20200 [EXCERPT]
20201 a1
20202 b1
20203 ˇ[EXCERPT]
20204 [FOLDED]
20205 [EXCERPT]
20206 [FOLDED]
20207 "
20208 });
20209 cx.simulate_keystroke("down");
20210 cx.assert_excerpts_with_selections(indoc! {"
20211 [EXCERPT]
20212 [FOLDED]
20213 [EXCERPT]
20214 a1
20215 b1
20216 [EXCERPT]
20217 ˇ[FOLDED]
20218 [EXCERPT]
20219 [FOLDED]
20220 "
20221 });
20222 for _ in 0..5 {
20223 cx.simulate_keystroke("down");
20224 cx.assert_excerpts_with_selections(indoc! {"
20225 [EXCERPT]
20226 [FOLDED]
20227 [EXCERPT]
20228 a1
20229 b1
20230 [EXCERPT]
20231 [FOLDED]
20232 [EXCERPT]
20233 ˇ[FOLDED]
20234 "
20235 });
20236 }
20237
20238 cx.simulate_keystroke("up");
20239 cx.assert_excerpts_with_selections(indoc! {"
20240 [EXCERPT]
20241 [FOLDED]
20242 [EXCERPT]
20243 a1
20244 b1
20245 [EXCERPT]
20246 ˇ[FOLDED]
20247 [EXCERPT]
20248 [FOLDED]
20249 "
20250 });
20251 cx.simulate_keystroke("up");
20252 cx.assert_excerpts_with_selections(indoc! {"
20253 [EXCERPT]
20254 [FOLDED]
20255 [EXCERPT]
20256 a1
20257 b1
20258 ˇ[EXCERPT]
20259 [FOLDED]
20260 [EXCERPT]
20261 [FOLDED]
20262 "
20263 });
20264 cx.simulate_keystroke("up");
20265 cx.assert_excerpts_with_selections(indoc! {"
20266 [EXCERPT]
20267 [FOLDED]
20268 [EXCERPT]
20269 a1
20270 ˇb1
20271 [EXCERPT]
20272 [FOLDED]
20273 [EXCERPT]
20274 [FOLDED]
20275 "
20276 });
20277 cx.simulate_keystroke("up");
20278 cx.assert_excerpts_with_selections(indoc! {"
20279 [EXCERPT]
20280 [FOLDED]
20281 [EXCERPT]
20282 ˇa1
20283 b1
20284 [EXCERPT]
20285 [FOLDED]
20286 [EXCERPT]
20287 [FOLDED]
20288 "
20289 });
20290 for _ in 0..5 {
20291 cx.simulate_keystroke("up");
20292 cx.assert_excerpts_with_selections(indoc! {"
20293 [EXCERPT]
20294 ˇ[FOLDED]
20295 [EXCERPT]
20296 a1
20297 b1
20298 [EXCERPT]
20299 [FOLDED]
20300 [EXCERPT]
20301 [FOLDED]
20302 "
20303 });
20304 }
20305}
20306
20307#[gpui::test]
20308async fn test_inline_completion_text(cx: &mut TestAppContext) {
20309 init_test(cx, |_| {});
20310
20311 // Simple insertion
20312 assert_highlighted_edits(
20313 "Hello, world!",
20314 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
20315 true,
20316 cx,
20317 |highlighted_edits, cx| {
20318 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
20319 assert_eq!(highlighted_edits.highlights.len(), 1);
20320 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
20321 assert_eq!(
20322 highlighted_edits.highlights[0].1.background_color,
20323 Some(cx.theme().status().created_background)
20324 );
20325 },
20326 )
20327 .await;
20328
20329 // Replacement
20330 assert_highlighted_edits(
20331 "This is a test.",
20332 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
20333 false,
20334 cx,
20335 |highlighted_edits, cx| {
20336 assert_eq!(highlighted_edits.text, "That is a test.");
20337 assert_eq!(highlighted_edits.highlights.len(), 1);
20338 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
20339 assert_eq!(
20340 highlighted_edits.highlights[0].1.background_color,
20341 Some(cx.theme().status().created_background)
20342 );
20343 },
20344 )
20345 .await;
20346
20347 // Multiple edits
20348 assert_highlighted_edits(
20349 "Hello, world!",
20350 vec![
20351 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
20352 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
20353 ],
20354 false,
20355 cx,
20356 |highlighted_edits, cx| {
20357 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
20358 assert_eq!(highlighted_edits.highlights.len(), 2);
20359 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
20360 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
20361 assert_eq!(
20362 highlighted_edits.highlights[0].1.background_color,
20363 Some(cx.theme().status().created_background)
20364 );
20365 assert_eq!(
20366 highlighted_edits.highlights[1].1.background_color,
20367 Some(cx.theme().status().created_background)
20368 );
20369 },
20370 )
20371 .await;
20372
20373 // Multiple lines with edits
20374 assert_highlighted_edits(
20375 "First line\nSecond line\nThird line\nFourth line",
20376 vec![
20377 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
20378 (
20379 Point::new(2, 0)..Point::new(2, 10),
20380 "New third line".to_string(),
20381 ),
20382 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
20383 ],
20384 false,
20385 cx,
20386 |highlighted_edits, cx| {
20387 assert_eq!(
20388 highlighted_edits.text,
20389 "Second modified\nNew third line\nFourth updated line"
20390 );
20391 assert_eq!(highlighted_edits.highlights.len(), 3);
20392 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
20393 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
20394 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
20395 for highlight in &highlighted_edits.highlights {
20396 assert_eq!(
20397 highlight.1.background_color,
20398 Some(cx.theme().status().created_background)
20399 );
20400 }
20401 },
20402 )
20403 .await;
20404}
20405
20406#[gpui::test]
20407async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
20408 init_test(cx, |_| {});
20409
20410 // Deletion
20411 assert_highlighted_edits(
20412 "Hello, world!",
20413 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
20414 true,
20415 cx,
20416 |highlighted_edits, cx| {
20417 assert_eq!(highlighted_edits.text, "Hello, world!");
20418 assert_eq!(highlighted_edits.highlights.len(), 1);
20419 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
20420 assert_eq!(
20421 highlighted_edits.highlights[0].1.background_color,
20422 Some(cx.theme().status().deleted_background)
20423 );
20424 },
20425 )
20426 .await;
20427
20428 // Insertion
20429 assert_highlighted_edits(
20430 "Hello, world!",
20431 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
20432 true,
20433 cx,
20434 |highlighted_edits, cx| {
20435 assert_eq!(highlighted_edits.highlights.len(), 1);
20436 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
20437 assert_eq!(
20438 highlighted_edits.highlights[0].1.background_color,
20439 Some(cx.theme().status().created_background)
20440 );
20441 },
20442 )
20443 .await;
20444}
20445
20446async fn assert_highlighted_edits(
20447 text: &str,
20448 edits: Vec<(Range<Point>, String)>,
20449 include_deletions: bool,
20450 cx: &mut TestAppContext,
20451 assertion_fn: impl Fn(HighlightedText, &App),
20452) {
20453 let window = cx.add_window(|window, cx| {
20454 let buffer = MultiBuffer::build_simple(text, cx);
20455 Editor::new(EditorMode::full(), buffer, None, window, cx)
20456 });
20457 let cx = &mut VisualTestContext::from_window(*window, cx);
20458
20459 let (buffer, snapshot) = window
20460 .update(cx, |editor, _window, cx| {
20461 (
20462 editor.buffer().clone(),
20463 editor.buffer().read(cx).snapshot(cx),
20464 )
20465 })
20466 .unwrap();
20467
20468 let edits = edits
20469 .into_iter()
20470 .map(|(range, edit)| {
20471 (
20472 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
20473 edit,
20474 )
20475 })
20476 .collect::<Vec<_>>();
20477
20478 let text_anchor_edits = edits
20479 .clone()
20480 .into_iter()
20481 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
20482 .collect::<Vec<_>>();
20483
20484 let edit_preview = window
20485 .update(cx, |_, _window, cx| {
20486 buffer
20487 .read(cx)
20488 .as_singleton()
20489 .unwrap()
20490 .read(cx)
20491 .preview_edits(text_anchor_edits.into(), cx)
20492 })
20493 .unwrap()
20494 .await;
20495
20496 cx.update(|_window, cx| {
20497 let highlighted_edits = inline_completion_edit_text(
20498 &snapshot.as_singleton().unwrap().2,
20499 &edits,
20500 &edit_preview,
20501 include_deletions,
20502 cx,
20503 );
20504 assertion_fn(highlighted_edits, cx)
20505 });
20506}
20507
20508#[track_caller]
20509fn assert_breakpoint(
20510 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
20511 path: &Arc<Path>,
20512 expected: Vec<(u32, Breakpoint)>,
20513) {
20514 if expected.len() == 0usize {
20515 assert!(!breakpoints.contains_key(path), "{}", path.display());
20516 } else {
20517 let mut breakpoint = breakpoints
20518 .get(path)
20519 .unwrap()
20520 .into_iter()
20521 .map(|breakpoint| {
20522 (
20523 breakpoint.row,
20524 Breakpoint {
20525 message: breakpoint.message.clone(),
20526 state: breakpoint.state,
20527 condition: breakpoint.condition.clone(),
20528 hit_condition: breakpoint.hit_condition.clone(),
20529 },
20530 )
20531 })
20532 .collect::<Vec<_>>();
20533
20534 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
20535
20536 assert_eq!(expected, breakpoint);
20537 }
20538}
20539
20540fn add_log_breakpoint_at_cursor(
20541 editor: &mut Editor,
20542 log_message: &str,
20543 window: &mut Window,
20544 cx: &mut Context<Editor>,
20545) {
20546 let (anchor, bp) = editor
20547 .breakpoints_at_cursors(window, cx)
20548 .first()
20549 .and_then(|(anchor, bp)| {
20550 if let Some(bp) = bp {
20551 Some((*anchor, bp.clone()))
20552 } else {
20553 None
20554 }
20555 })
20556 .unwrap_or_else(|| {
20557 let cursor_position: Point = editor.selections.newest(cx).head();
20558
20559 let breakpoint_position = editor
20560 .snapshot(window, cx)
20561 .display_snapshot
20562 .buffer_snapshot
20563 .anchor_before(Point::new(cursor_position.row, 0));
20564
20565 (breakpoint_position, Breakpoint::new_log(&log_message))
20566 });
20567
20568 editor.edit_breakpoint_at_anchor(
20569 anchor,
20570 bp,
20571 BreakpointEditAction::EditLogMessage(log_message.into()),
20572 cx,
20573 );
20574}
20575
20576#[gpui::test]
20577async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
20578 init_test(cx, |_| {});
20579
20580 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20581 let fs = FakeFs::new(cx.executor());
20582 fs.insert_tree(
20583 path!("/a"),
20584 json!({
20585 "main.rs": sample_text,
20586 }),
20587 )
20588 .await;
20589 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20590 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20591 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20592
20593 let fs = FakeFs::new(cx.executor());
20594 fs.insert_tree(
20595 path!("/a"),
20596 json!({
20597 "main.rs": sample_text,
20598 }),
20599 )
20600 .await;
20601 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20602 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20603 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20604 let worktree_id = workspace
20605 .update(cx, |workspace, _window, cx| {
20606 workspace.project().update(cx, |project, cx| {
20607 project.worktrees(cx).next().unwrap().read(cx).id()
20608 })
20609 })
20610 .unwrap();
20611
20612 let buffer = project
20613 .update(cx, |project, cx| {
20614 project.open_buffer((worktree_id, "main.rs"), cx)
20615 })
20616 .await
20617 .unwrap();
20618
20619 let (editor, cx) = cx.add_window_view(|window, cx| {
20620 Editor::new(
20621 EditorMode::full(),
20622 MultiBuffer::build_from_buffer(buffer, cx),
20623 Some(project.clone()),
20624 window,
20625 cx,
20626 )
20627 });
20628
20629 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20630 let abs_path = project.read_with(cx, |project, cx| {
20631 project
20632 .absolute_path(&project_path, cx)
20633 .map(|path_buf| Arc::from(path_buf.to_owned()))
20634 .unwrap()
20635 });
20636
20637 // assert we can add breakpoint on the first line
20638 editor.update_in(cx, |editor, window, cx| {
20639 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20640 editor.move_to_end(&MoveToEnd, window, cx);
20641 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20642 });
20643
20644 let breakpoints = editor.update(cx, |editor, cx| {
20645 editor
20646 .breakpoint_store()
20647 .as_ref()
20648 .unwrap()
20649 .read(cx)
20650 .all_source_breakpoints(cx)
20651 .clone()
20652 });
20653
20654 assert_eq!(1, breakpoints.len());
20655 assert_breakpoint(
20656 &breakpoints,
20657 &abs_path,
20658 vec![
20659 (0, Breakpoint::new_standard()),
20660 (3, Breakpoint::new_standard()),
20661 ],
20662 );
20663
20664 editor.update_in(cx, |editor, window, cx| {
20665 editor.move_to_beginning(&MoveToBeginning, window, cx);
20666 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20667 });
20668
20669 let breakpoints = editor.update(cx, |editor, cx| {
20670 editor
20671 .breakpoint_store()
20672 .as_ref()
20673 .unwrap()
20674 .read(cx)
20675 .all_source_breakpoints(cx)
20676 .clone()
20677 });
20678
20679 assert_eq!(1, breakpoints.len());
20680 assert_breakpoint(
20681 &breakpoints,
20682 &abs_path,
20683 vec![(3, Breakpoint::new_standard())],
20684 );
20685
20686 editor.update_in(cx, |editor, window, cx| {
20687 editor.move_to_end(&MoveToEnd, window, cx);
20688 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20689 });
20690
20691 let breakpoints = editor.update(cx, |editor, cx| {
20692 editor
20693 .breakpoint_store()
20694 .as_ref()
20695 .unwrap()
20696 .read(cx)
20697 .all_source_breakpoints(cx)
20698 .clone()
20699 });
20700
20701 assert_eq!(0, breakpoints.len());
20702 assert_breakpoint(&breakpoints, &abs_path, vec![]);
20703}
20704
20705#[gpui::test]
20706async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
20707 init_test(cx, |_| {});
20708
20709 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20710
20711 let fs = FakeFs::new(cx.executor());
20712 fs.insert_tree(
20713 path!("/a"),
20714 json!({
20715 "main.rs": sample_text,
20716 }),
20717 )
20718 .await;
20719 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20720 let (workspace, cx) =
20721 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20722
20723 let worktree_id = workspace.update(cx, |workspace, cx| {
20724 workspace.project().update(cx, |project, cx| {
20725 project.worktrees(cx).next().unwrap().read(cx).id()
20726 })
20727 });
20728
20729 let buffer = project
20730 .update(cx, |project, cx| {
20731 project.open_buffer((worktree_id, "main.rs"), cx)
20732 })
20733 .await
20734 .unwrap();
20735
20736 let (editor, cx) = cx.add_window_view(|window, cx| {
20737 Editor::new(
20738 EditorMode::full(),
20739 MultiBuffer::build_from_buffer(buffer, cx),
20740 Some(project.clone()),
20741 window,
20742 cx,
20743 )
20744 });
20745
20746 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20747 let abs_path = project.read_with(cx, |project, cx| {
20748 project
20749 .absolute_path(&project_path, cx)
20750 .map(|path_buf| Arc::from(path_buf.to_owned()))
20751 .unwrap()
20752 });
20753
20754 editor.update_in(cx, |editor, window, cx| {
20755 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
20756 });
20757
20758 let breakpoints = editor.update(cx, |editor, cx| {
20759 editor
20760 .breakpoint_store()
20761 .as_ref()
20762 .unwrap()
20763 .read(cx)
20764 .all_source_breakpoints(cx)
20765 .clone()
20766 });
20767
20768 assert_breakpoint(
20769 &breakpoints,
20770 &abs_path,
20771 vec![(0, Breakpoint::new_log("hello world"))],
20772 );
20773
20774 // Removing a log message from a log breakpoint should remove it
20775 editor.update_in(cx, |editor, window, cx| {
20776 add_log_breakpoint_at_cursor(editor, "", window, cx);
20777 });
20778
20779 let breakpoints = editor.update(cx, |editor, cx| {
20780 editor
20781 .breakpoint_store()
20782 .as_ref()
20783 .unwrap()
20784 .read(cx)
20785 .all_source_breakpoints(cx)
20786 .clone()
20787 });
20788
20789 assert_breakpoint(&breakpoints, &abs_path, vec![]);
20790
20791 editor.update_in(cx, |editor, window, cx| {
20792 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20793 editor.move_to_end(&MoveToEnd, window, cx);
20794 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20795 // Not adding a log message to a standard breakpoint shouldn't remove it
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(
20810 &breakpoints,
20811 &abs_path,
20812 vec![
20813 (0, Breakpoint::new_standard()),
20814 (3, Breakpoint::new_standard()),
20815 ],
20816 );
20817
20818 editor.update_in(cx, |editor, window, cx| {
20819 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
20820 });
20821
20822 let breakpoints = editor.update(cx, |editor, cx| {
20823 editor
20824 .breakpoint_store()
20825 .as_ref()
20826 .unwrap()
20827 .read(cx)
20828 .all_source_breakpoints(cx)
20829 .clone()
20830 });
20831
20832 assert_breakpoint(
20833 &breakpoints,
20834 &abs_path,
20835 vec![
20836 (0, Breakpoint::new_standard()),
20837 (3, Breakpoint::new_log("hello world")),
20838 ],
20839 );
20840
20841 editor.update_in(cx, |editor, window, cx| {
20842 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
20843 });
20844
20845 let breakpoints = editor.update(cx, |editor, cx| {
20846 editor
20847 .breakpoint_store()
20848 .as_ref()
20849 .unwrap()
20850 .read(cx)
20851 .all_source_breakpoints(cx)
20852 .clone()
20853 });
20854
20855 assert_breakpoint(
20856 &breakpoints,
20857 &abs_path,
20858 vec![
20859 (0, Breakpoint::new_standard()),
20860 (3, Breakpoint::new_log("hello Earth!!")),
20861 ],
20862 );
20863}
20864
20865/// This also tests that Editor::breakpoint_at_cursor_head is working properly
20866/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
20867/// or when breakpoints were placed out of order. This tests for a regression too
20868#[gpui::test]
20869async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
20870 init_test(cx, |_| {});
20871
20872 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20873 let fs = FakeFs::new(cx.executor());
20874 fs.insert_tree(
20875 path!("/a"),
20876 json!({
20877 "main.rs": sample_text,
20878 }),
20879 )
20880 .await;
20881 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20882 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20883 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20884
20885 let fs = FakeFs::new(cx.executor());
20886 fs.insert_tree(
20887 path!("/a"),
20888 json!({
20889 "main.rs": sample_text,
20890 }),
20891 )
20892 .await;
20893 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20894 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20895 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20896 let worktree_id = workspace
20897 .update(cx, |workspace, _window, cx| {
20898 workspace.project().update(cx, |project, cx| {
20899 project.worktrees(cx).next().unwrap().read(cx).id()
20900 })
20901 })
20902 .unwrap();
20903
20904 let buffer = project
20905 .update(cx, |project, cx| {
20906 project.open_buffer((worktree_id, "main.rs"), cx)
20907 })
20908 .await
20909 .unwrap();
20910
20911 let (editor, cx) = cx.add_window_view(|window, cx| {
20912 Editor::new(
20913 EditorMode::full(),
20914 MultiBuffer::build_from_buffer(buffer, cx),
20915 Some(project.clone()),
20916 window,
20917 cx,
20918 )
20919 });
20920
20921 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20922 let abs_path = project.read_with(cx, |project, cx| {
20923 project
20924 .absolute_path(&project_path, cx)
20925 .map(|path_buf| Arc::from(path_buf.to_owned()))
20926 .unwrap()
20927 });
20928
20929 // assert we can add breakpoint on the first line
20930 editor.update_in(cx, |editor, window, cx| {
20931 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20932 editor.move_to_end(&MoveToEnd, window, cx);
20933 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20934 editor.move_up(&MoveUp, window, cx);
20935 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20936 });
20937
20938 let breakpoints = editor.update(cx, |editor, cx| {
20939 editor
20940 .breakpoint_store()
20941 .as_ref()
20942 .unwrap()
20943 .read(cx)
20944 .all_source_breakpoints(cx)
20945 .clone()
20946 });
20947
20948 assert_eq!(1, breakpoints.len());
20949 assert_breakpoint(
20950 &breakpoints,
20951 &abs_path,
20952 vec![
20953 (0, Breakpoint::new_standard()),
20954 (2, Breakpoint::new_standard()),
20955 (3, Breakpoint::new_standard()),
20956 ],
20957 );
20958
20959 editor.update_in(cx, |editor, window, cx| {
20960 editor.move_to_beginning(&MoveToBeginning, window, cx);
20961 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20962 editor.move_to_end(&MoveToEnd, window, cx);
20963 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20964 // Disabling a breakpoint that doesn't exist should do nothing
20965 editor.move_up(&MoveUp, window, cx);
20966 editor.move_up(&MoveUp, window, cx);
20967 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20968 });
20969
20970 let breakpoints = editor.update(cx, |editor, cx| {
20971 editor
20972 .breakpoint_store()
20973 .as_ref()
20974 .unwrap()
20975 .read(cx)
20976 .all_source_breakpoints(cx)
20977 .clone()
20978 });
20979
20980 let disable_breakpoint = {
20981 let mut bp = Breakpoint::new_standard();
20982 bp.state = BreakpointState::Disabled;
20983 bp
20984 };
20985
20986 assert_eq!(1, breakpoints.len());
20987 assert_breakpoint(
20988 &breakpoints,
20989 &abs_path,
20990 vec![
20991 (0, disable_breakpoint.clone()),
20992 (2, Breakpoint::new_standard()),
20993 (3, disable_breakpoint.clone()),
20994 ],
20995 );
20996
20997 editor.update_in(cx, |editor, window, cx| {
20998 editor.move_to_beginning(&MoveToBeginning, window, cx);
20999 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
21000 editor.move_to_end(&MoveToEnd, window, cx);
21001 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
21002 editor.move_up(&MoveUp, window, cx);
21003 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
21004 });
21005
21006 let breakpoints = editor.update(cx, |editor, cx| {
21007 editor
21008 .breakpoint_store()
21009 .as_ref()
21010 .unwrap()
21011 .read(cx)
21012 .all_source_breakpoints(cx)
21013 .clone()
21014 });
21015
21016 assert_eq!(1, breakpoints.len());
21017 assert_breakpoint(
21018 &breakpoints,
21019 &abs_path,
21020 vec![
21021 (0, Breakpoint::new_standard()),
21022 (2, disable_breakpoint),
21023 (3, Breakpoint::new_standard()),
21024 ],
21025 );
21026}
21027
21028#[gpui::test]
21029async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
21030 init_test(cx, |_| {});
21031 let capabilities = lsp::ServerCapabilities {
21032 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
21033 prepare_provider: Some(true),
21034 work_done_progress_options: Default::default(),
21035 })),
21036 ..Default::default()
21037 };
21038 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
21039
21040 cx.set_state(indoc! {"
21041 struct Fˇoo {}
21042 "});
21043
21044 cx.update_editor(|editor, _, cx| {
21045 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
21046 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
21047 editor.highlight_background::<DocumentHighlightRead>(
21048 &[highlight_range],
21049 |theme| theme.colors().editor_document_highlight_read_background,
21050 cx,
21051 );
21052 });
21053
21054 let mut prepare_rename_handler = cx
21055 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
21056 move |_, _, _| async move {
21057 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
21058 start: lsp::Position {
21059 line: 0,
21060 character: 7,
21061 },
21062 end: lsp::Position {
21063 line: 0,
21064 character: 10,
21065 },
21066 })))
21067 },
21068 );
21069 let prepare_rename_task = cx
21070 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
21071 .expect("Prepare rename was not started");
21072 prepare_rename_handler.next().await.unwrap();
21073 prepare_rename_task.await.expect("Prepare rename failed");
21074
21075 let mut rename_handler =
21076 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
21077 let edit = lsp::TextEdit {
21078 range: lsp::Range {
21079 start: lsp::Position {
21080 line: 0,
21081 character: 7,
21082 },
21083 end: lsp::Position {
21084 line: 0,
21085 character: 10,
21086 },
21087 },
21088 new_text: "FooRenamed".to_string(),
21089 };
21090 Ok(Some(lsp::WorkspaceEdit::new(
21091 // Specify the same edit twice
21092 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
21093 )))
21094 });
21095 let rename_task = cx
21096 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
21097 .expect("Confirm rename was not started");
21098 rename_handler.next().await.unwrap();
21099 rename_task.await.expect("Confirm rename failed");
21100 cx.run_until_parked();
21101
21102 // Despite two edits, only one is actually applied as those are identical
21103 cx.assert_editor_state(indoc! {"
21104 struct FooRenamedˇ {}
21105 "});
21106}
21107
21108#[gpui::test]
21109async fn test_rename_without_prepare(cx: &mut TestAppContext) {
21110 init_test(cx, |_| {});
21111 // These capabilities indicate that the server does not support prepare rename.
21112 let capabilities = lsp::ServerCapabilities {
21113 rename_provider: Some(lsp::OneOf::Left(true)),
21114 ..Default::default()
21115 };
21116 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
21117
21118 cx.set_state(indoc! {"
21119 struct Fˇoo {}
21120 "});
21121
21122 cx.update_editor(|editor, _window, cx| {
21123 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
21124 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
21125 editor.highlight_background::<DocumentHighlightRead>(
21126 &[highlight_range],
21127 |theme| theme.colors().editor_document_highlight_read_background,
21128 cx,
21129 );
21130 });
21131
21132 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
21133 .expect("Prepare rename was not started")
21134 .await
21135 .expect("Prepare rename failed");
21136
21137 let mut rename_handler =
21138 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
21139 let edit = lsp::TextEdit {
21140 range: lsp::Range {
21141 start: lsp::Position {
21142 line: 0,
21143 character: 7,
21144 },
21145 end: lsp::Position {
21146 line: 0,
21147 character: 10,
21148 },
21149 },
21150 new_text: "FooRenamed".to_string(),
21151 };
21152 Ok(Some(lsp::WorkspaceEdit::new(
21153 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
21154 )))
21155 });
21156 let rename_task = cx
21157 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
21158 .expect("Confirm rename was not started");
21159 rename_handler.next().await.unwrap();
21160 rename_task.await.expect("Confirm rename failed");
21161 cx.run_until_parked();
21162
21163 // Correct range is renamed, as `surrounding_word` is used to find it.
21164 cx.assert_editor_state(indoc! {"
21165 struct FooRenamedˇ {}
21166 "});
21167}
21168
21169#[gpui::test]
21170async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
21171 init_test(cx, |_| {});
21172 let mut cx = EditorTestContext::new(cx).await;
21173
21174 let language = Arc::new(
21175 Language::new(
21176 LanguageConfig::default(),
21177 Some(tree_sitter_html::LANGUAGE.into()),
21178 )
21179 .with_brackets_query(
21180 r#"
21181 ("<" @open "/>" @close)
21182 ("</" @open ">" @close)
21183 ("<" @open ">" @close)
21184 ("\"" @open "\"" @close)
21185 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
21186 "#,
21187 )
21188 .unwrap(),
21189 );
21190 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
21191
21192 cx.set_state(indoc! {"
21193 <span>ˇ</span>
21194 "});
21195 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21196 cx.assert_editor_state(indoc! {"
21197 <span>
21198 ˇ
21199 </span>
21200 "});
21201
21202 cx.set_state(indoc! {"
21203 <span><span></span>ˇ</span>
21204 "});
21205 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21206 cx.assert_editor_state(indoc! {"
21207 <span><span></span>
21208 ˇ</span>
21209 "});
21210
21211 cx.set_state(indoc! {"
21212 <span>ˇ
21213 </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
21223#[gpui::test(iterations = 10)]
21224async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
21225 init_test(cx, |_| {});
21226
21227 let fs = FakeFs::new(cx.executor());
21228 fs.insert_tree(
21229 path!("/dir"),
21230 json!({
21231 "a.ts": "a",
21232 }),
21233 )
21234 .await;
21235
21236 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
21237 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21238 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21239
21240 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21241 language_registry.add(Arc::new(Language::new(
21242 LanguageConfig {
21243 name: "TypeScript".into(),
21244 matcher: LanguageMatcher {
21245 path_suffixes: vec!["ts".to_string()],
21246 ..Default::default()
21247 },
21248 ..Default::default()
21249 },
21250 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
21251 )));
21252 let mut fake_language_servers = language_registry.register_fake_lsp(
21253 "TypeScript",
21254 FakeLspAdapter {
21255 capabilities: lsp::ServerCapabilities {
21256 code_lens_provider: Some(lsp::CodeLensOptions {
21257 resolve_provider: Some(true),
21258 }),
21259 execute_command_provider: Some(lsp::ExecuteCommandOptions {
21260 commands: vec!["_the/command".to_string()],
21261 ..lsp::ExecuteCommandOptions::default()
21262 }),
21263 ..lsp::ServerCapabilities::default()
21264 },
21265 ..FakeLspAdapter::default()
21266 },
21267 );
21268
21269 let (buffer, _handle) = project
21270 .update(cx, |p, cx| {
21271 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
21272 })
21273 .await
21274 .unwrap();
21275 cx.executor().run_until_parked();
21276
21277 let fake_server = fake_language_servers.next().await.unwrap();
21278
21279 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
21280 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
21281 drop(buffer_snapshot);
21282 let actions = cx
21283 .update_window(*workspace, |_, window, cx| {
21284 project.code_actions(&buffer, anchor..anchor, window, cx)
21285 })
21286 .unwrap();
21287
21288 fake_server
21289 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
21290 Ok(Some(vec![
21291 lsp::CodeLens {
21292 range: lsp::Range::default(),
21293 command: Some(lsp::Command {
21294 title: "Code lens command".to_owned(),
21295 command: "_the/command".to_owned(),
21296 arguments: None,
21297 }),
21298 data: None,
21299 },
21300 lsp::CodeLens {
21301 range: lsp::Range::default(),
21302 command: Some(lsp::Command {
21303 title: "Command not in capabilities".to_owned(),
21304 command: "not in capabilities".to_owned(),
21305 arguments: None,
21306 }),
21307 data: None,
21308 },
21309 lsp::CodeLens {
21310 range: lsp::Range {
21311 start: lsp::Position {
21312 line: 1,
21313 character: 1,
21314 },
21315 end: lsp::Position {
21316 line: 1,
21317 character: 1,
21318 },
21319 },
21320 command: Some(lsp::Command {
21321 title: "Command not in range".to_owned(),
21322 command: "_the/command".to_owned(),
21323 arguments: None,
21324 }),
21325 data: None,
21326 },
21327 ]))
21328 })
21329 .next()
21330 .await;
21331
21332 let actions = actions.await.unwrap();
21333 assert_eq!(
21334 actions.len(),
21335 1,
21336 "Should have only one valid action for the 0..0 range"
21337 );
21338 let action = actions[0].clone();
21339 let apply = project.update(cx, |project, cx| {
21340 project.apply_code_action(buffer.clone(), action, true, cx)
21341 });
21342
21343 // Resolving the code action does not populate its edits. In absence of
21344 // edits, we must execute the given command.
21345 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
21346 |mut lens, _| async move {
21347 let lens_command = lens.command.as_mut().expect("should have a command");
21348 assert_eq!(lens_command.title, "Code lens command");
21349 lens_command.arguments = Some(vec![json!("the-argument")]);
21350 Ok(lens)
21351 },
21352 );
21353
21354 // While executing the command, the language server sends the editor
21355 // a `workspaceEdit` request.
21356 fake_server
21357 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
21358 let fake = fake_server.clone();
21359 move |params, _| {
21360 assert_eq!(params.command, "_the/command");
21361 let fake = fake.clone();
21362 async move {
21363 fake.server
21364 .request::<lsp::request::ApplyWorkspaceEdit>(
21365 lsp::ApplyWorkspaceEditParams {
21366 label: None,
21367 edit: lsp::WorkspaceEdit {
21368 changes: Some(
21369 [(
21370 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
21371 vec![lsp::TextEdit {
21372 range: lsp::Range::new(
21373 lsp::Position::new(0, 0),
21374 lsp::Position::new(0, 0),
21375 ),
21376 new_text: "X".into(),
21377 }],
21378 )]
21379 .into_iter()
21380 .collect(),
21381 ),
21382 ..Default::default()
21383 },
21384 },
21385 )
21386 .await
21387 .into_response()
21388 .unwrap();
21389 Ok(Some(json!(null)))
21390 }
21391 }
21392 })
21393 .next()
21394 .await;
21395
21396 // Applying the code lens command returns a project transaction containing the edits
21397 // sent by the language server in its `workspaceEdit` request.
21398 let transaction = apply.await.unwrap();
21399 assert!(transaction.0.contains_key(&buffer));
21400 buffer.update(cx, |buffer, cx| {
21401 assert_eq!(buffer.text(), "Xa");
21402 buffer.undo(cx);
21403 assert_eq!(buffer.text(), "a");
21404 });
21405}
21406
21407#[gpui::test]
21408async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
21409 init_test(cx, |_| {});
21410
21411 let fs = FakeFs::new(cx.executor());
21412 let main_text = r#"fn main() {
21413println!("1");
21414println!("2");
21415println!("3");
21416println!("4");
21417println!("5");
21418}"#;
21419 let lib_text = "mod foo {}";
21420 fs.insert_tree(
21421 path!("/a"),
21422 json!({
21423 "lib.rs": lib_text,
21424 "main.rs": main_text,
21425 }),
21426 )
21427 .await;
21428
21429 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21430 let (workspace, cx) =
21431 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21432 let worktree_id = workspace.update(cx, |workspace, cx| {
21433 workspace.project().update(cx, |project, cx| {
21434 project.worktrees(cx).next().unwrap().read(cx).id()
21435 })
21436 });
21437
21438 let expected_ranges = vec![
21439 Point::new(0, 0)..Point::new(0, 0),
21440 Point::new(1, 0)..Point::new(1, 1),
21441 Point::new(2, 0)..Point::new(2, 2),
21442 Point::new(3, 0)..Point::new(3, 3),
21443 ];
21444
21445 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
21446 let editor_1 = workspace
21447 .update_in(cx, |workspace, window, cx| {
21448 workspace.open_path(
21449 (worktree_id, "main.rs"),
21450 Some(pane_1.downgrade()),
21451 true,
21452 window,
21453 cx,
21454 )
21455 })
21456 .unwrap()
21457 .await
21458 .downcast::<Editor>()
21459 .unwrap();
21460 pane_1.update(cx, |pane, cx| {
21461 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21462 open_editor.update(cx, |editor, cx| {
21463 assert_eq!(
21464 editor.display_text(cx),
21465 main_text,
21466 "Original main.rs text on initial open",
21467 );
21468 assert_eq!(
21469 editor
21470 .selections
21471 .all::<Point>(cx)
21472 .into_iter()
21473 .map(|s| s.range())
21474 .collect::<Vec<_>>(),
21475 vec![Point::zero()..Point::zero()],
21476 "Default selections on initial open",
21477 );
21478 })
21479 });
21480 editor_1.update_in(cx, |editor, window, cx| {
21481 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21482 s.select_ranges(expected_ranges.clone());
21483 });
21484 });
21485
21486 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
21487 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
21488 });
21489 let editor_2 = workspace
21490 .update_in(cx, |workspace, window, cx| {
21491 workspace.open_path(
21492 (worktree_id, "main.rs"),
21493 Some(pane_2.downgrade()),
21494 true,
21495 window,
21496 cx,
21497 )
21498 })
21499 .unwrap()
21500 .await
21501 .downcast::<Editor>()
21502 .unwrap();
21503 pane_2.update(cx, |pane, cx| {
21504 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21505 open_editor.update(cx, |editor, cx| {
21506 assert_eq!(
21507 editor.display_text(cx),
21508 main_text,
21509 "Original main.rs text on initial open in another panel",
21510 );
21511 assert_eq!(
21512 editor
21513 .selections
21514 .all::<Point>(cx)
21515 .into_iter()
21516 .map(|s| s.range())
21517 .collect::<Vec<_>>(),
21518 vec![Point::zero()..Point::zero()],
21519 "Default selections on initial open in another panel",
21520 );
21521 })
21522 });
21523
21524 editor_2.update_in(cx, |editor, window, cx| {
21525 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
21526 });
21527
21528 let _other_editor_1 = workspace
21529 .update_in(cx, |workspace, window, cx| {
21530 workspace.open_path(
21531 (worktree_id, "lib.rs"),
21532 Some(pane_1.downgrade()),
21533 true,
21534 window,
21535 cx,
21536 )
21537 })
21538 .unwrap()
21539 .await
21540 .downcast::<Editor>()
21541 .unwrap();
21542 pane_1
21543 .update_in(cx, |pane, window, cx| {
21544 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
21545 })
21546 .await
21547 .unwrap();
21548 drop(editor_1);
21549 pane_1.update(cx, |pane, cx| {
21550 pane.active_item()
21551 .unwrap()
21552 .downcast::<Editor>()
21553 .unwrap()
21554 .update(cx, |editor, cx| {
21555 assert_eq!(
21556 editor.display_text(cx),
21557 lib_text,
21558 "Other file should be open and active",
21559 );
21560 });
21561 assert_eq!(pane.items().count(), 1, "No other editors should be open");
21562 });
21563
21564 let _other_editor_2 = workspace
21565 .update_in(cx, |workspace, window, cx| {
21566 workspace.open_path(
21567 (worktree_id, "lib.rs"),
21568 Some(pane_2.downgrade()),
21569 true,
21570 window,
21571 cx,
21572 )
21573 })
21574 .unwrap()
21575 .await
21576 .downcast::<Editor>()
21577 .unwrap();
21578 pane_2
21579 .update_in(cx, |pane, window, cx| {
21580 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
21581 })
21582 .await
21583 .unwrap();
21584 drop(editor_2);
21585 pane_2.update(cx, |pane, cx| {
21586 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21587 open_editor.update(cx, |editor, cx| {
21588 assert_eq!(
21589 editor.display_text(cx),
21590 lib_text,
21591 "Other file should be open and active in another panel too",
21592 );
21593 });
21594 assert_eq!(
21595 pane.items().count(),
21596 1,
21597 "No other editors should be open in another pane",
21598 );
21599 });
21600
21601 let _editor_1_reopened = workspace
21602 .update_in(cx, |workspace, window, cx| {
21603 workspace.open_path(
21604 (worktree_id, "main.rs"),
21605 Some(pane_1.downgrade()),
21606 true,
21607 window,
21608 cx,
21609 )
21610 })
21611 .unwrap()
21612 .await
21613 .downcast::<Editor>()
21614 .unwrap();
21615 let _editor_2_reopened = workspace
21616 .update_in(cx, |workspace, window, cx| {
21617 workspace.open_path(
21618 (worktree_id, "main.rs"),
21619 Some(pane_2.downgrade()),
21620 true,
21621 window,
21622 cx,
21623 )
21624 })
21625 .unwrap()
21626 .await
21627 .downcast::<Editor>()
21628 .unwrap();
21629 pane_1.update(cx, |pane, cx| {
21630 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21631 open_editor.update(cx, |editor, cx| {
21632 assert_eq!(
21633 editor.display_text(cx),
21634 main_text,
21635 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
21636 );
21637 assert_eq!(
21638 editor
21639 .selections
21640 .all::<Point>(cx)
21641 .into_iter()
21642 .map(|s| s.range())
21643 .collect::<Vec<_>>(),
21644 expected_ranges,
21645 "Previous editor in the 1st panel had selections and should get them restored on reopen",
21646 );
21647 })
21648 });
21649 pane_2.update(cx, |pane, cx| {
21650 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21651 open_editor.update(cx, |editor, cx| {
21652 assert_eq!(
21653 editor.display_text(cx),
21654 r#"fn main() {
21655⋯rintln!("1");
21656⋯intln!("2");
21657⋯ntln!("3");
21658println!("4");
21659println!("5");
21660}"#,
21661 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
21662 );
21663 assert_eq!(
21664 editor
21665 .selections
21666 .all::<Point>(cx)
21667 .into_iter()
21668 .map(|s| s.range())
21669 .collect::<Vec<_>>(),
21670 vec![Point::zero()..Point::zero()],
21671 "Previous editor in the 2nd pane had no selections changed hence should restore none",
21672 );
21673 })
21674 });
21675}
21676
21677#[gpui::test]
21678async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
21679 init_test(cx, |_| {});
21680
21681 let fs = FakeFs::new(cx.executor());
21682 let main_text = r#"fn main() {
21683println!("1");
21684println!("2");
21685println!("3");
21686println!("4");
21687println!("5");
21688}"#;
21689 let lib_text = "mod foo {}";
21690 fs.insert_tree(
21691 path!("/a"),
21692 json!({
21693 "lib.rs": lib_text,
21694 "main.rs": main_text,
21695 }),
21696 )
21697 .await;
21698
21699 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21700 let (workspace, cx) =
21701 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21702 let worktree_id = workspace.update(cx, |workspace, cx| {
21703 workspace.project().update(cx, |project, cx| {
21704 project.worktrees(cx).next().unwrap().read(cx).id()
21705 })
21706 });
21707
21708 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
21709 let editor = workspace
21710 .update_in(cx, |workspace, window, cx| {
21711 workspace.open_path(
21712 (worktree_id, "main.rs"),
21713 Some(pane.downgrade()),
21714 true,
21715 window,
21716 cx,
21717 )
21718 })
21719 .unwrap()
21720 .await
21721 .downcast::<Editor>()
21722 .unwrap();
21723 pane.update(cx, |pane, cx| {
21724 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21725 open_editor.update(cx, |editor, cx| {
21726 assert_eq!(
21727 editor.display_text(cx),
21728 main_text,
21729 "Original main.rs text on initial open",
21730 );
21731 })
21732 });
21733 editor.update_in(cx, |editor, window, cx| {
21734 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
21735 });
21736
21737 cx.update_global(|store: &mut SettingsStore, cx| {
21738 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
21739 s.restore_on_file_reopen = Some(false);
21740 });
21741 });
21742 editor.update_in(cx, |editor, window, cx| {
21743 editor.fold_ranges(
21744 vec![
21745 Point::new(1, 0)..Point::new(1, 1),
21746 Point::new(2, 0)..Point::new(2, 2),
21747 Point::new(3, 0)..Point::new(3, 3),
21748 ],
21749 false,
21750 window,
21751 cx,
21752 );
21753 });
21754 pane.update_in(cx, |pane, window, cx| {
21755 pane.close_all_items(&CloseAllItems::default(), window, cx)
21756 })
21757 .await
21758 .unwrap();
21759 pane.update(cx, |pane, _| {
21760 assert!(pane.active_item().is_none());
21761 });
21762 cx.update_global(|store: &mut SettingsStore, cx| {
21763 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
21764 s.restore_on_file_reopen = Some(true);
21765 });
21766 });
21767
21768 let _editor_reopened = workspace
21769 .update_in(cx, |workspace, window, cx| {
21770 workspace.open_path(
21771 (worktree_id, "main.rs"),
21772 Some(pane.downgrade()),
21773 true,
21774 window,
21775 cx,
21776 )
21777 })
21778 .unwrap()
21779 .await
21780 .downcast::<Editor>()
21781 .unwrap();
21782 pane.update(cx, |pane, cx| {
21783 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21784 open_editor.update(cx, |editor, cx| {
21785 assert_eq!(
21786 editor.display_text(cx),
21787 main_text,
21788 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
21789 );
21790 })
21791 });
21792}
21793
21794#[gpui::test]
21795async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
21796 struct EmptyModalView {
21797 focus_handle: gpui::FocusHandle,
21798 }
21799 impl EventEmitter<DismissEvent> for EmptyModalView {}
21800 impl Render for EmptyModalView {
21801 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
21802 div()
21803 }
21804 }
21805 impl Focusable for EmptyModalView {
21806 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
21807 self.focus_handle.clone()
21808 }
21809 }
21810 impl workspace::ModalView for EmptyModalView {}
21811 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
21812 EmptyModalView {
21813 focus_handle: cx.focus_handle(),
21814 }
21815 }
21816
21817 init_test(cx, |_| {});
21818
21819 let fs = FakeFs::new(cx.executor());
21820 let project = Project::test(fs, [], cx).await;
21821 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21822 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
21823 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21824 let editor = cx.new_window_entity(|window, cx| {
21825 Editor::new(
21826 EditorMode::full(),
21827 buffer,
21828 Some(project.clone()),
21829 window,
21830 cx,
21831 )
21832 });
21833 workspace
21834 .update(cx, |workspace, window, cx| {
21835 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
21836 })
21837 .unwrap();
21838 editor.update_in(cx, |editor, window, cx| {
21839 editor.open_context_menu(&OpenContextMenu, window, cx);
21840 assert!(editor.mouse_context_menu.is_some());
21841 });
21842 workspace
21843 .update(cx, |workspace, window, cx| {
21844 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
21845 })
21846 .unwrap();
21847 cx.read(|cx| {
21848 assert!(editor.read(cx).mouse_context_menu.is_none());
21849 });
21850}
21851
21852#[gpui::test]
21853async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
21854 init_test(cx, |_| {});
21855
21856 let fs = FakeFs::new(cx.executor());
21857 fs.insert_file(path!("/file.html"), Default::default())
21858 .await;
21859
21860 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
21861
21862 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21863 let html_language = Arc::new(Language::new(
21864 LanguageConfig {
21865 name: "HTML".into(),
21866 matcher: LanguageMatcher {
21867 path_suffixes: vec!["html".to_string()],
21868 ..LanguageMatcher::default()
21869 },
21870 brackets: BracketPairConfig {
21871 pairs: vec![BracketPair {
21872 start: "<".into(),
21873 end: ">".into(),
21874 close: true,
21875 ..Default::default()
21876 }],
21877 ..Default::default()
21878 },
21879 ..Default::default()
21880 },
21881 Some(tree_sitter_html::LANGUAGE.into()),
21882 ));
21883 language_registry.add(html_language);
21884 let mut fake_servers = language_registry.register_fake_lsp(
21885 "HTML",
21886 FakeLspAdapter {
21887 capabilities: lsp::ServerCapabilities {
21888 completion_provider: Some(lsp::CompletionOptions {
21889 resolve_provider: Some(true),
21890 ..Default::default()
21891 }),
21892 ..Default::default()
21893 },
21894 ..Default::default()
21895 },
21896 );
21897
21898 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21899 let cx = &mut VisualTestContext::from_window(*workspace, cx);
21900
21901 let worktree_id = workspace
21902 .update(cx, |workspace, _window, cx| {
21903 workspace.project().update(cx, |project, cx| {
21904 project.worktrees(cx).next().unwrap().read(cx).id()
21905 })
21906 })
21907 .unwrap();
21908 project
21909 .update(cx, |project, cx| {
21910 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
21911 })
21912 .await
21913 .unwrap();
21914 let editor = workspace
21915 .update(cx, |workspace, window, cx| {
21916 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
21917 })
21918 .unwrap()
21919 .await
21920 .unwrap()
21921 .downcast::<Editor>()
21922 .unwrap();
21923
21924 let fake_server = fake_servers.next().await.unwrap();
21925 editor.update_in(cx, |editor, window, cx| {
21926 editor.set_text("<ad></ad>", window, cx);
21927 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
21928 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
21929 });
21930 let Some((buffer, _)) = editor
21931 .buffer
21932 .read(cx)
21933 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
21934 else {
21935 panic!("Failed to get buffer for selection position");
21936 };
21937 let buffer = buffer.read(cx);
21938 let buffer_id = buffer.remote_id();
21939 let opening_range =
21940 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
21941 let closing_range =
21942 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
21943 let mut linked_ranges = HashMap::default();
21944 linked_ranges.insert(
21945 buffer_id,
21946 vec![(opening_range.clone(), vec![closing_range.clone()])],
21947 );
21948 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
21949 });
21950 let mut completion_handle =
21951 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
21952 Ok(Some(lsp::CompletionResponse::Array(vec![
21953 lsp::CompletionItem {
21954 label: "head".to_string(),
21955 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
21956 lsp::InsertReplaceEdit {
21957 new_text: "head".to_string(),
21958 insert: lsp::Range::new(
21959 lsp::Position::new(0, 1),
21960 lsp::Position::new(0, 3),
21961 ),
21962 replace: lsp::Range::new(
21963 lsp::Position::new(0, 1),
21964 lsp::Position::new(0, 3),
21965 ),
21966 },
21967 )),
21968 ..Default::default()
21969 },
21970 ])))
21971 });
21972 editor.update_in(cx, |editor, window, cx| {
21973 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
21974 });
21975 cx.run_until_parked();
21976 completion_handle.next().await.unwrap();
21977 editor.update(cx, |editor, _| {
21978 assert!(
21979 editor.context_menu_visible(),
21980 "Completion menu should be visible"
21981 );
21982 });
21983 editor.update_in(cx, |editor, window, cx| {
21984 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
21985 });
21986 cx.executor().run_until_parked();
21987 editor.update(cx, |editor, cx| {
21988 assert_eq!(editor.text(cx), "<head></head>");
21989 });
21990}
21991
21992#[gpui::test]
21993async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
21994 init_test(cx, |_| {});
21995
21996 let fs = FakeFs::new(cx.executor());
21997 fs.insert_tree(
21998 path!("/root"),
21999 json!({
22000 "a": {
22001 "main.rs": "fn main() {}",
22002 },
22003 "foo": {
22004 "bar": {
22005 "external_file.rs": "pub mod external {}",
22006 }
22007 }
22008 }),
22009 )
22010 .await;
22011
22012 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
22013 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22014 language_registry.add(rust_lang());
22015 let _fake_servers = language_registry.register_fake_lsp(
22016 "Rust",
22017 FakeLspAdapter {
22018 ..FakeLspAdapter::default()
22019 },
22020 );
22021 let (workspace, cx) =
22022 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22023 let worktree_id = workspace.update(cx, |workspace, cx| {
22024 workspace.project().update(cx, |project, cx| {
22025 project.worktrees(cx).next().unwrap().read(cx).id()
22026 })
22027 });
22028
22029 let assert_language_servers_count =
22030 |expected: usize, context: &str, cx: &mut VisualTestContext| {
22031 project.update(cx, |project, cx| {
22032 let current = project
22033 .lsp_store()
22034 .read(cx)
22035 .as_local()
22036 .unwrap()
22037 .language_servers
22038 .len();
22039 assert_eq!(expected, current, "{context}");
22040 });
22041 };
22042
22043 assert_language_servers_count(
22044 0,
22045 "No servers should be running before any file is open",
22046 cx,
22047 );
22048 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
22049 let main_editor = workspace
22050 .update_in(cx, |workspace, window, cx| {
22051 workspace.open_path(
22052 (worktree_id, "main.rs"),
22053 Some(pane.downgrade()),
22054 true,
22055 window,
22056 cx,
22057 )
22058 })
22059 .unwrap()
22060 .await
22061 .downcast::<Editor>()
22062 .unwrap();
22063 pane.update(cx, |pane, cx| {
22064 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22065 open_editor.update(cx, |editor, cx| {
22066 assert_eq!(
22067 editor.display_text(cx),
22068 "fn main() {}",
22069 "Original main.rs text on initial open",
22070 );
22071 });
22072 assert_eq!(open_editor, main_editor);
22073 });
22074 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
22075
22076 let external_editor = workspace
22077 .update_in(cx, |workspace, window, cx| {
22078 workspace.open_abs_path(
22079 PathBuf::from("/root/foo/bar/external_file.rs"),
22080 OpenOptions::default(),
22081 window,
22082 cx,
22083 )
22084 })
22085 .await
22086 .expect("opening external file")
22087 .downcast::<Editor>()
22088 .expect("downcasted external file's open element to editor");
22089 pane.update(cx, |pane, cx| {
22090 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22091 open_editor.update(cx, |editor, cx| {
22092 assert_eq!(
22093 editor.display_text(cx),
22094 "pub mod external {}",
22095 "External file is open now",
22096 );
22097 });
22098 assert_eq!(open_editor, external_editor);
22099 });
22100 assert_language_servers_count(
22101 1,
22102 "Second, external, *.rs file should join the existing server",
22103 cx,
22104 );
22105
22106 pane.update_in(cx, |pane, window, cx| {
22107 pane.close_active_item(&CloseActiveItem::default(), window, cx)
22108 })
22109 .await
22110 .unwrap();
22111 pane.update_in(cx, |pane, window, cx| {
22112 pane.navigate_backward(window, cx);
22113 });
22114 cx.run_until_parked();
22115 pane.update(cx, |pane, cx| {
22116 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22117 open_editor.update(cx, |editor, cx| {
22118 assert_eq!(
22119 editor.display_text(cx),
22120 "pub mod external {}",
22121 "External file is open now",
22122 );
22123 });
22124 });
22125 assert_language_servers_count(
22126 1,
22127 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
22128 cx,
22129 );
22130
22131 cx.update(|_, cx| {
22132 workspace::reload(&workspace::Reload::default(), cx);
22133 });
22134 assert_language_servers_count(
22135 1,
22136 "After reloading the worktree with local and external files opened, only one project should be started",
22137 cx,
22138 );
22139}
22140
22141#[gpui::test]
22142async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
22143 init_test(cx, |_| {});
22144
22145 let mut cx = EditorTestContext::new(cx).await;
22146 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22147 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22148
22149 // test cursor move to start of each line on tab
22150 // for `if`, `elif`, `else`, `while`, `with` and `for`
22151 cx.set_state(indoc! {"
22152 def main():
22153 ˇ for item in items:
22154 ˇ while item.active:
22155 ˇ if item.value > 10:
22156 ˇ continue
22157 ˇ elif item.value < 0:
22158 ˇ break
22159 ˇ else:
22160 ˇ with item.context() as ctx:
22161 ˇ yield count
22162 ˇ else:
22163 ˇ log('while else')
22164 ˇ else:
22165 ˇ log('for else')
22166 "});
22167 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22168 cx.assert_editor_state(indoc! {"
22169 def main():
22170 ˇfor item in items:
22171 ˇwhile item.active:
22172 ˇif item.value > 10:
22173 ˇcontinue
22174 ˇelif item.value < 0:
22175 ˇbreak
22176 ˇelse:
22177 ˇwith item.context() as ctx:
22178 ˇyield count
22179 ˇelse:
22180 ˇlog('while else')
22181 ˇelse:
22182 ˇlog('for else')
22183 "});
22184 // test relative indent is preserved when tab
22185 // for `if`, `elif`, `else`, `while`, `with` and `for`
22186 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22187 cx.assert_editor_state(indoc! {"
22188 def main():
22189 ˇfor item in items:
22190 ˇwhile item.active:
22191 ˇif item.value > 10:
22192 ˇcontinue
22193 ˇelif item.value < 0:
22194 ˇbreak
22195 ˇelse:
22196 ˇwith item.context() as ctx:
22197 ˇyield count
22198 ˇelse:
22199 ˇlog('while else')
22200 ˇelse:
22201 ˇlog('for else')
22202 "});
22203
22204 // test cursor move to start of each line on tab
22205 // for `try`, `except`, `else`, `finally`, `match` and `def`
22206 cx.set_state(indoc! {"
22207 def main():
22208 ˇ try:
22209 ˇ fetch()
22210 ˇ except ValueError:
22211 ˇ handle_error()
22212 ˇ else:
22213 ˇ match value:
22214 ˇ case _:
22215 ˇ finally:
22216 ˇ def status():
22217 ˇ return 0
22218 "});
22219 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22220 cx.assert_editor_state(indoc! {"
22221 def main():
22222 ˇtry:
22223 ˇfetch()
22224 ˇexcept ValueError:
22225 ˇhandle_error()
22226 ˇelse:
22227 ˇmatch value:
22228 ˇcase _:
22229 ˇfinally:
22230 ˇdef status():
22231 ˇreturn 0
22232 "});
22233 // test relative indent is preserved when tab
22234 // for `try`, `except`, `else`, `finally`, `match` and `def`
22235 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22236 cx.assert_editor_state(indoc! {"
22237 def main():
22238 ˇtry:
22239 ˇfetch()
22240 ˇexcept ValueError:
22241 ˇhandle_error()
22242 ˇelse:
22243 ˇmatch value:
22244 ˇcase _:
22245 ˇfinally:
22246 ˇdef status():
22247 ˇreturn 0
22248 "});
22249}
22250
22251#[gpui::test]
22252async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
22253 init_test(cx, |_| {});
22254
22255 let mut cx = EditorTestContext::new(cx).await;
22256 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22257 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22258
22259 // test `else` auto outdents when typed inside `if` block
22260 cx.set_state(indoc! {"
22261 def main():
22262 if i == 2:
22263 return
22264 ˇ
22265 "});
22266 cx.update_editor(|editor, window, cx| {
22267 editor.handle_input("else:", window, cx);
22268 });
22269 cx.assert_editor_state(indoc! {"
22270 def main():
22271 if i == 2:
22272 return
22273 else:ˇ
22274 "});
22275
22276 // test `except` auto outdents when typed inside `try` block
22277 cx.set_state(indoc! {"
22278 def main():
22279 try:
22280 i = 2
22281 ˇ
22282 "});
22283 cx.update_editor(|editor, window, cx| {
22284 editor.handle_input("except:", window, cx);
22285 });
22286 cx.assert_editor_state(indoc! {"
22287 def main():
22288 try:
22289 i = 2
22290 except:ˇ
22291 "});
22292
22293 // test `else` auto outdents when typed inside `except` block
22294 cx.set_state(indoc! {"
22295 def main():
22296 try:
22297 i = 2
22298 except:
22299 j = 2
22300 ˇ
22301 "});
22302 cx.update_editor(|editor, window, cx| {
22303 editor.handle_input("else:", window, cx);
22304 });
22305 cx.assert_editor_state(indoc! {"
22306 def main():
22307 try:
22308 i = 2
22309 except:
22310 j = 2
22311 else:ˇ
22312 "});
22313
22314 // test `finally` auto outdents when typed inside `else` block
22315 cx.set_state(indoc! {"
22316 def main():
22317 try:
22318 i = 2
22319 except:
22320 j = 2
22321 else:
22322 k = 2
22323 ˇ
22324 "});
22325 cx.update_editor(|editor, window, cx| {
22326 editor.handle_input("finally:", window, cx);
22327 });
22328 cx.assert_editor_state(indoc! {"
22329 def main():
22330 try:
22331 i = 2
22332 except:
22333 j = 2
22334 else:
22335 k = 2
22336 finally:ˇ
22337 "});
22338
22339 // test `else` does not outdents when typed inside `except` block right after for block
22340 cx.set_state(indoc! {"
22341 def main():
22342 try:
22343 i = 2
22344 except:
22345 for i in range(n):
22346 pass
22347 ˇ
22348 "});
22349 cx.update_editor(|editor, window, cx| {
22350 editor.handle_input("else:", window, cx);
22351 });
22352 cx.assert_editor_state(indoc! {"
22353 def main():
22354 try:
22355 i = 2
22356 except:
22357 for i in range(n):
22358 pass
22359 else:ˇ
22360 "});
22361
22362 // test `finally` auto outdents when typed inside `else` block right after for block
22363 cx.set_state(indoc! {"
22364 def main():
22365 try:
22366 i = 2
22367 except:
22368 j = 2
22369 else:
22370 for i in range(n):
22371 pass
22372 ˇ
22373 "});
22374 cx.update_editor(|editor, window, cx| {
22375 editor.handle_input("finally:", window, cx);
22376 });
22377 cx.assert_editor_state(indoc! {"
22378 def main():
22379 try:
22380 i = 2
22381 except:
22382 j = 2
22383 else:
22384 for i in range(n):
22385 pass
22386 finally:ˇ
22387 "});
22388
22389 // test `except` outdents to inner "try" block
22390 cx.set_state(indoc! {"
22391 def main():
22392 try:
22393 i = 2
22394 if i == 2:
22395 try:
22396 i = 3
22397 ˇ
22398 "});
22399 cx.update_editor(|editor, window, cx| {
22400 editor.handle_input("except:", window, cx);
22401 });
22402 cx.assert_editor_state(indoc! {"
22403 def main():
22404 try:
22405 i = 2
22406 if i == 2:
22407 try:
22408 i = 3
22409 except:ˇ
22410 "});
22411
22412 // test `except` outdents to outer "try" block
22413 cx.set_state(indoc! {"
22414 def main():
22415 try:
22416 i = 2
22417 if i == 2:
22418 try:
22419 i = 3
22420 ˇ
22421 "});
22422 cx.update_editor(|editor, window, cx| {
22423 editor.handle_input("except:", window, cx);
22424 });
22425 cx.assert_editor_state(indoc! {"
22426 def main():
22427 try:
22428 i = 2
22429 if i == 2:
22430 try:
22431 i = 3
22432 except:ˇ
22433 "});
22434
22435 // test `else` stays at correct indent when typed after `for` block
22436 cx.set_state(indoc! {"
22437 def main():
22438 for i in range(10):
22439 if i == 3:
22440 break
22441 ˇ
22442 "});
22443 cx.update_editor(|editor, window, cx| {
22444 editor.handle_input("else:", window, cx);
22445 });
22446 cx.assert_editor_state(indoc! {"
22447 def main():
22448 for i in range(10):
22449 if i == 3:
22450 break
22451 else:ˇ
22452 "});
22453
22454 // test does not outdent on typing after line with square brackets
22455 cx.set_state(indoc! {"
22456 def f() -> list[str]:
22457 ˇ
22458 "});
22459 cx.update_editor(|editor, window, cx| {
22460 editor.handle_input("a", window, cx);
22461 });
22462 cx.assert_editor_state(indoc! {"
22463 def f() -> list[str]:
22464 aˇ
22465 "});
22466
22467 // test does not outdent on typing : after case keyword
22468 cx.set_state(indoc! {"
22469 match 1:
22470 caseˇ
22471 "});
22472 cx.update_editor(|editor, window, cx| {
22473 editor.handle_input(":", window, cx);
22474 });
22475 cx.assert_editor_state(indoc! {"
22476 match 1:
22477 case:ˇ
22478 "});
22479}
22480
22481#[gpui::test]
22482async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
22483 init_test(cx, |_| {});
22484 update_test_language_settings(cx, |settings| {
22485 settings.defaults.extend_comment_on_newline = Some(false);
22486 });
22487 let mut cx = EditorTestContext::new(cx).await;
22488 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22489 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22490
22491 // test correct indent after newline on comment
22492 cx.set_state(indoc! {"
22493 # COMMENT:ˇ
22494 "});
22495 cx.update_editor(|editor, window, cx| {
22496 editor.newline(&Newline, window, cx);
22497 });
22498 cx.assert_editor_state(indoc! {"
22499 # COMMENT:
22500 ˇ
22501 "});
22502
22503 // test correct indent after newline in brackets
22504 cx.set_state(indoc! {"
22505 {ˇ}
22506 "});
22507 cx.update_editor(|editor, window, cx| {
22508 editor.newline(&Newline, window, cx);
22509 });
22510 cx.run_until_parked();
22511 cx.assert_editor_state(indoc! {"
22512 {
22513 ˇ
22514 }
22515 "});
22516
22517 cx.set_state(indoc! {"
22518 (ˇ)
22519 "});
22520 cx.update_editor(|editor, window, cx| {
22521 editor.newline(&Newline, window, cx);
22522 });
22523 cx.run_until_parked();
22524 cx.assert_editor_state(indoc! {"
22525 (
22526 ˇ
22527 )
22528 "});
22529
22530 // do not indent after empty lists or dictionaries
22531 cx.set_state(indoc! {"
22532 a = []ˇ
22533 "});
22534 cx.update_editor(|editor, window, cx| {
22535 editor.newline(&Newline, window, cx);
22536 });
22537 cx.run_until_parked();
22538 cx.assert_editor_state(indoc! {"
22539 a = []
22540 ˇ
22541 "});
22542}
22543
22544fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
22545 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
22546 point..point
22547}
22548
22549#[track_caller]
22550fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
22551 let (text, ranges) = marked_text_ranges(marked_text, true);
22552 assert_eq!(editor.text(cx), text);
22553 assert_eq!(
22554 editor.selections.ranges(cx),
22555 ranges,
22556 "Assert selections are {}",
22557 marked_text
22558 );
22559}
22560
22561pub fn handle_signature_help_request(
22562 cx: &mut EditorLspTestContext,
22563 mocked_response: lsp::SignatureHelp,
22564) -> impl Future<Output = ()> + use<> {
22565 let mut request =
22566 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
22567 let mocked_response = mocked_response.clone();
22568 async move { Ok(Some(mocked_response)) }
22569 });
22570
22571 async move {
22572 request.next().await;
22573 }
22574}
22575
22576#[track_caller]
22577pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
22578 cx.update_editor(|editor, _, _| {
22579 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
22580 let entries = menu.entries.borrow();
22581 let entries = entries
22582 .iter()
22583 .map(|entry| entry.string.as_str())
22584 .collect::<Vec<_>>();
22585 assert_eq!(entries, expected);
22586 } else {
22587 panic!("Expected completions menu");
22588 }
22589 });
22590}
22591
22592/// Handle completion request passing a marked string specifying where the completion
22593/// should be triggered from using '|' character, what range should be replaced, and what completions
22594/// should be returned using '<' and '>' to delimit the range.
22595///
22596/// Also see `handle_completion_request_with_insert_and_replace`.
22597#[track_caller]
22598pub fn handle_completion_request(
22599 marked_string: &str,
22600 completions: Vec<&'static str>,
22601 is_incomplete: bool,
22602 counter: Arc<AtomicUsize>,
22603 cx: &mut EditorLspTestContext,
22604) -> impl Future<Output = ()> {
22605 let complete_from_marker: TextRangeMarker = '|'.into();
22606 let replace_range_marker: TextRangeMarker = ('<', '>').into();
22607 let (_, mut marked_ranges) = marked_text_ranges_by(
22608 marked_string,
22609 vec![complete_from_marker.clone(), replace_range_marker.clone()],
22610 );
22611
22612 let complete_from_position =
22613 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
22614 let replace_range =
22615 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
22616
22617 let mut request =
22618 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
22619 let completions = completions.clone();
22620 counter.fetch_add(1, atomic::Ordering::Release);
22621 async move {
22622 assert_eq!(params.text_document_position.text_document.uri, url.clone());
22623 assert_eq!(
22624 params.text_document_position.position,
22625 complete_from_position
22626 );
22627 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
22628 is_incomplete: is_incomplete,
22629 item_defaults: None,
22630 items: completions
22631 .iter()
22632 .map(|completion_text| lsp::CompletionItem {
22633 label: completion_text.to_string(),
22634 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
22635 range: replace_range,
22636 new_text: completion_text.to_string(),
22637 })),
22638 ..Default::default()
22639 })
22640 .collect(),
22641 })))
22642 }
22643 });
22644
22645 async move {
22646 request.next().await;
22647 }
22648}
22649
22650/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
22651/// given instead, which also contains an `insert` range.
22652///
22653/// This function uses markers to define ranges:
22654/// - `|` marks the cursor position
22655/// - `<>` marks the replace range
22656/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
22657pub fn handle_completion_request_with_insert_and_replace(
22658 cx: &mut EditorLspTestContext,
22659 marked_string: &str,
22660 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
22661 counter: Arc<AtomicUsize>,
22662) -> impl Future<Output = ()> {
22663 let complete_from_marker: TextRangeMarker = '|'.into();
22664 let replace_range_marker: TextRangeMarker = ('<', '>').into();
22665 let insert_range_marker: TextRangeMarker = ('{', '}').into();
22666
22667 let (_, mut marked_ranges) = marked_text_ranges_by(
22668 marked_string,
22669 vec![
22670 complete_from_marker.clone(),
22671 replace_range_marker.clone(),
22672 insert_range_marker.clone(),
22673 ],
22674 );
22675
22676 let complete_from_position =
22677 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
22678 let replace_range =
22679 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
22680
22681 let insert_range = match marked_ranges.remove(&insert_range_marker) {
22682 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
22683 _ => lsp::Range {
22684 start: replace_range.start,
22685 end: complete_from_position,
22686 },
22687 };
22688
22689 let mut request =
22690 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
22691 let completions = completions.clone();
22692 counter.fetch_add(1, atomic::Ordering::Release);
22693 async move {
22694 assert_eq!(params.text_document_position.text_document.uri, url.clone());
22695 assert_eq!(
22696 params.text_document_position.position, complete_from_position,
22697 "marker `|` position doesn't match",
22698 );
22699 Ok(Some(lsp::CompletionResponse::Array(
22700 completions
22701 .iter()
22702 .map(|(label, new_text)| lsp::CompletionItem {
22703 label: label.to_string(),
22704 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
22705 lsp::InsertReplaceEdit {
22706 insert: insert_range,
22707 replace: replace_range,
22708 new_text: new_text.to_string(),
22709 },
22710 )),
22711 ..Default::default()
22712 })
22713 .collect(),
22714 )))
22715 }
22716 });
22717
22718 async move {
22719 request.next().await;
22720 }
22721}
22722
22723fn handle_resolve_completion_request(
22724 cx: &mut EditorLspTestContext,
22725 edits: Option<Vec<(&'static str, &'static str)>>,
22726) -> impl Future<Output = ()> {
22727 let edits = edits.map(|edits| {
22728 edits
22729 .iter()
22730 .map(|(marked_string, new_text)| {
22731 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
22732 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
22733 lsp::TextEdit::new(replace_range, new_text.to_string())
22734 })
22735 .collect::<Vec<_>>()
22736 });
22737
22738 let mut request =
22739 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
22740 let edits = edits.clone();
22741 async move {
22742 Ok(lsp::CompletionItem {
22743 additional_text_edits: edits,
22744 ..Default::default()
22745 })
22746 }
22747 });
22748
22749 async move {
22750 request.next().await;
22751 }
22752}
22753
22754pub(crate) fn update_test_language_settings(
22755 cx: &mut TestAppContext,
22756 f: impl Fn(&mut AllLanguageSettingsContent),
22757) {
22758 cx.update(|cx| {
22759 SettingsStore::update_global(cx, |store, cx| {
22760 store.update_user_settings::<AllLanguageSettings>(cx, f);
22761 });
22762 });
22763}
22764
22765pub(crate) fn update_test_project_settings(
22766 cx: &mut TestAppContext,
22767 f: impl Fn(&mut ProjectSettings),
22768) {
22769 cx.update(|cx| {
22770 SettingsStore::update_global(cx, |store, cx| {
22771 store.update_user_settings::<ProjectSettings>(cx, f);
22772 });
22773 });
22774}
22775
22776pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
22777 cx.update(|cx| {
22778 assets::Assets.load_test_fonts(cx);
22779 let store = SettingsStore::test(cx);
22780 cx.set_global(store);
22781 theme::init(theme::LoadThemes::JustBase, cx);
22782 release_channel::init(SemanticVersion::default(), cx);
22783 client::init_settings(cx);
22784 language::init(cx);
22785 Project::init_settings(cx);
22786 workspace::init_settings(cx);
22787 crate::init(cx);
22788 });
22789 zlog::init_test();
22790 update_test_language_settings(cx, f);
22791}
22792
22793#[track_caller]
22794fn assert_hunk_revert(
22795 not_reverted_text_with_selections: &str,
22796 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
22797 expected_reverted_text_with_selections: &str,
22798 base_text: &str,
22799 cx: &mut EditorLspTestContext,
22800) {
22801 cx.set_state(not_reverted_text_with_selections);
22802 cx.set_head_text(base_text);
22803 cx.executor().run_until_parked();
22804
22805 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
22806 let snapshot = editor.snapshot(window, cx);
22807 let reverted_hunk_statuses = snapshot
22808 .buffer_snapshot
22809 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
22810 .map(|hunk| hunk.status().kind)
22811 .collect::<Vec<_>>();
22812
22813 editor.git_restore(&Default::default(), window, cx);
22814 reverted_hunk_statuses
22815 });
22816 cx.executor().run_until_parked();
22817 cx.assert_editor_state(expected_reverted_text_with_selections);
22818 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
22819}
22820
22821#[gpui::test(iterations = 10)]
22822async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
22823 init_test(cx, |_| {});
22824
22825 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
22826 let counter = diagnostic_requests.clone();
22827
22828 let fs = FakeFs::new(cx.executor());
22829 fs.insert_tree(
22830 path!("/a"),
22831 json!({
22832 "first.rs": "fn main() { let a = 5; }",
22833 "second.rs": "// Test file",
22834 }),
22835 )
22836 .await;
22837
22838 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22839 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22840 let cx = &mut VisualTestContext::from_window(*workspace, cx);
22841
22842 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22843 language_registry.add(rust_lang());
22844 let mut fake_servers = language_registry.register_fake_lsp(
22845 "Rust",
22846 FakeLspAdapter {
22847 capabilities: lsp::ServerCapabilities {
22848 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
22849 lsp::DiagnosticOptions {
22850 identifier: None,
22851 inter_file_dependencies: true,
22852 workspace_diagnostics: true,
22853 work_done_progress_options: Default::default(),
22854 },
22855 )),
22856 ..Default::default()
22857 },
22858 ..Default::default()
22859 },
22860 );
22861
22862 let editor = workspace
22863 .update(cx, |workspace, window, cx| {
22864 workspace.open_abs_path(
22865 PathBuf::from(path!("/a/first.rs")),
22866 OpenOptions::default(),
22867 window,
22868 cx,
22869 )
22870 })
22871 .unwrap()
22872 .await
22873 .unwrap()
22874 .downcast::<Editor>()
22875 .unwrap();
22876 let fake_server = fake_servers.next().await.unwrap();
22877 let server_id = fake_server.server.server_id();
22878 let mut first_request = fake_server
22879 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
22880 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
22881 let result_id = Some(new_result_id.to_string());
22882 assert_eq!(
22883 params.text_document.uri,
22884 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
22885 );
22886 async move {
22887 Ok(lsp::DocumentDiagnosticReportResult::Report(
22888 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
22889 related_documents: None,
22890 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
22891 items: Vec::new(),
22892 result_id,
22893 },
22894 }),
22895 ))
22896 }
22897 });
22898
22899 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
22900 project.update(cx, |project, cx| {
22901 let buffer_id = editor
22902 .read(cx)
22903 .buffer()
22904 .read(cx)
22905 .as_singleton()
22906 .expect("created a singleton buffer")
22907 .read(cx)
22908 .remote_id();
22909 let buffer_result_id = project
22910 .lsp_store()
22911 .read(cx)
22912 .result_id(server_id, buffer_id, cx);
22913 assert_eq!(expected, buffer_result_id);
22914 });
22915 };
22916
22917 ensure_result_id(None, cx);
22918 cx.executor().advance_clock(Duration::from_millis(60));
22919 cx.executor().run_until_parked();
22920 assert_eq!(
22921 diagnostic_requests.load(atomic::Ordering::Acquire),
22922 1,
22923 "Opening file should trigger diagnostic request"
22924 );
22925 first_request
22926 .next()
22927 .await
22928 .expect("should have sent the first diagnostics pull request");
22929 ensure_result_id(Some("1".to_string()), cx);
22930
22931 // Editing should trigger diagnostics
22932 editor.update_in(cx, |editor, window, cx| {
22933 editor.handle_input("2", window, cx)
22934 });
22935 cx.executor().advance_clock(Duration::from_millis(60));
22936 cx.executor().run_until_parked();
22937 assert_eq!(
22938 diagnostic_requests.load(atomic::Ordering::Acquire),
22939 2,
22940 "Editing should trigger diagnostic request"
22941 );
22942 ensure_result_id(Some("2".to_string()), cx);
22943
22944 // Moving cursor should not trigger diagnostic request
22945 editor.update_in(cx, |editor, window, cx| {
22946 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22947 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
22948 });
22949 });
22950 cx.executor().advance_clock(Duration::from_millis(60));
22951 cx.executor().run_until_parked();
22952 assert_eq!(
22953 diagnostic_requests.load(atomic::Ordering::Acquire),
22954 2,
22955 "Cursor movement should not trigger diagnostic request"
22956 );
22957 ensure_result_id(Some("2".to_string()), cx);
22958 // Multiple rapid edits should be debounced
22959 for _ in 0..5 {
22960 editor.update_in(cx, |editor, window, cx| {
22961 editor.handle_input("x", window, cx)
22962 });
22963 }
22964 cx.executor().advance_clock(Duration::from_millis(60));
22965 cx.executor().run_until_parked();
22966
22967 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
22968 assert!(
22969 final_requests <= 4,
22970 "Multiple rapid edits should be debounced (got {final_requests} requests)",
22971 );
22972 ensure_result_id(Some(final_requests.to_string()), cx);
22973}
22974
22975#[gpui::test]
22976async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
22977 // Regression test for issue #11671
22978 // Previously, adding a cursor after moving multiple cursors would reset
22979 // the cursor count instead of adding to the existing cursors.
22980 init_test(cx, |_| {});
22981 let mut cx = EditorTestContext::new(cx).await;
22982
22983 // Create a simple buffer with cursor at start
22984 cx.set_state(indoc! {"
22985 ˇaaaa
22986 bbbb
22987 cccc
22988 dddd
22989 eeee
22990 ffff
22991 gggg
22992 hhhh"});
22993
22994 // Add 2 cursors below (so we have 3 total)
22995 cx.update_editor(|editor, window, cx| {
22996 editor.add_selection_below(&Default::default(), window, cx);
22997 editor.add_selection_below(&Default::default(), window, cx);
22998 });
22999
23000 // Verify we have 3 cursors
23001 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
23002 assert_eq!(
23003 initial_count, 3,
23004 "Should have 3 cursors after adding 2 below"
23005 );
23006
23007 // Move down one line
23008 cx.update_editor(|editor, window, cx| {
23009 editor.move_down(&MoveDown, window, cx);
23010 });
23011
23012 // Add another cursor below
23013 cx.update_editor(|editor, window, cx| {
23014 editor.add_selection_below(&Default::default(), window, cx);
23015 });
23016
23017 // Should now have 4 cursors (3 original + 1 new)
23018 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
23019 assert_eq!(
23020 final_count, 4,
23021 "Should have 4 cursors after moving and adding another"
23022 );
23023}
23024
23025#[gpui::test(iterations = 10)]
23026async fn test_document_colors(cx: &mut TestAppContext) {
23027 let expected_color = Rgba {
23028 r: 0.33,
23029 g: 0.33,
23030 b: 0.33,
23031 a: 0.33,
23032 };
23033
23034 init_test(cx, |_| {});
23035
23036 let fs = FakeFs::new(cx.executor());
23037 fs.insert_tree(
23038 path!("/a"),
23039 json!({
23040 "first.rs": "fn main() { let a = 5; }",
23041 }),
23042 )
23043 .await;
23044
23045 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23046 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23047 let cx = &mut VisualTestContext::from_window(*workspace, cx);
23048
23049 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23050 language_registry.add(rust_lang());
23051 let mut fake_servers = language_registry.register_fake_lsp(
23052 "Rust",
23053 FakeLspAdapter {
23054 capabilities: lsp::ServerCapabilities {
23055 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
23056 ..lsp::ServerCapabilities::default()
23057 },
23058 name: "rust-analyzer",
23059 ..FakeLspAdapter::default()
23060 },
23061 );
23062 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
23063 "Rust",
23064 FakeLspAdapter {
23065 capabilities: lsp::ServerCapabilities {
23066 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
23067 ..lsp::ServerCapabilities::default()
23068 },
23069 name: "not-rust-analyzer",
23070 ..FakeLspAdapter::default()
23071 },
23072 );
23073
23074 let editor = workspace
23075 .update(cx, |workspace, window, cx| {
23076 workspace.open_abs_path(
23077 PathBuf::from(path!("/a/first.rs")),
23078 OpenOptions::default(),
23079 window,
23080 cx,
23081 )
23082 })
23083 .unwrap()
23084 .await
23085 .unwrap()
23086 .downcast::<Editor>()
23087 .unwrap();
23088 let fake_language_server = fake_servers.next().await.unwrap();
23089 let fake_language_server_without_capabilities =
23090 fake_servers_without_capabilities.next().await.unwrap();
23091 let requests_made = Arc::new(AtomicUsize::new(0));
23092 let closure_requests_made = Arc::clone(&requests_made);
23093 let mut color_request_handle = fake_language_server
23094 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
23095 let requests_made = Arc::clone(&closure_requests_made);
23096 async move {
23097 assert_eq!(
23098 params.text_document.uri,
23099 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
23100 );
23101 requests_made.fetch_add(1, atomic::Ordering::Release);
23102 Ok(vec![
23103 lsp::ColorInformation {
23104 range: lsp::Range {
23105 start: lsp::Position {
23106 line: 0,
23107 character: 0,
23108 },
23109 end: lsp::Position {
23110 line: 0,
23111 character: 1,
23112 },
23113 },
23114 color: lsp::Color {
23115 red: 0.33,
23116 green: 0.33,
23117 blue: 0.33,
23118 alpha: 0.33,
23119 },
23120 },
23121 lsp::ColorInformation {
23122 range: lsp::Range {
23123 start: lsp::Position {
23124 line: 0,
23125 character: 0,
23126 },
23127 end: lsp::Position {
23128 line: 0,
23129 character: 1,
23130 },
23131 },
23132 color: lsp::Color {
23133 red: 0.33,
23134 green: 0.33,
23135 blue: 0.33,
23136 alpha: 0.33,
23137 },
23138 },
23139 ])
23140 }
23141 });
23142
23143 let _handle = fake_language_server_without_capabilities
23144 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
23145 panic!("Should not be called");
23146 });
23147 cx.executor().advance_clock(Duration::from_millis(100));
23148 color_request_handle.next().await.unwrap();
23149 cx.run_until_parked();
23150 assert_eq!(
23151 1,
23152 requests_made.load(atomic::Ordering::Acquire),
23153 "Should query for colors once per editor open"
23154 );
23155 editor.update_in(cx, |editor, _, cx| {
23156 assert_eq!(
23157 vec![expected_color],
23158 extract_color_inlays(editor, cx),
23159 "Should have an initial inlay"
23160 );
23161 });
23162
23163 // opening another file in a split should not influence the LSP query counter
23164 workspace
23165 .update(cx, |workspace, window, cx| {
23166 assert_eq!(
23167 workspace.panes().len(),
23168 1,
23169 "Should have one pane with one editor"
23170 );
23171 workspace.move_item_to_pane_in_direction(
23172 &MoveItemToPaneInDirection {
23173 direction: SplitDirection::Right,
23174 focus: false,
23175 clone: true,
23176 },
23177 window,
23178 cx,
23179 );
23180 })
23181 .unwrap();
23182 cx.run_until_parked();
23183 workspace
23184 .update(cx, |workspace, _, cx| {
23185 let panes = workspace.panes();
23186 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
23187 for pane in panes {
23188 let editor = pane
23189 .read(cx)
23190 .active_item()
23191 .and_then(|item| item.downcast::<Editor>())
23192 .expect("Should have opened an editor in each split");
23193 let editor_file = editor
23194 .read(cx)
23195 .buffer()
23196 .read(cx)
23197 .as_singleton()
23198 .expect("test deals with singleton buffers")
23199 .read(cx)
23200 .file()
23201 .expect("test buffese should have a file")
23202 .path();
23203 assert_eq!(
23204 editor_file.as_ref(),
23205 Path::new("first.rs"),
23206 "Both editors should be opened for the same file"
23207 )
23208 }
23209 })
23210 .unwrap();
23211
23212 cx.executor().advance_clock(Duration::from_millis(500));
23213 let save = editor.update_in(cx, |editor, window, cx| {
23214 editor.move_to_end(&MoveToEnd, window, cx);
23215 editor.handle_input("dirty", window, cx);
23216 editor.save(
23217 SaveOptions {
23218 format: true,
23219 autosave: true,
23220 },
23221 project.clone(),
23222 window,
23223 cx,
23224 )
23225 });
23226 save.await.unwrap();
23227
23228 color_request_handle.next().await.unwrap();
23229 cx.run_until_parked();
23230 assert_eq!(
23231 3,
23232 requests_made.load(atomic::Ordering::Acquire),
23233 "Should query for colors once per save and once per formatting after save"
23234 );
23235
23236 drop(editor);
23237 let close = workspace
23238 .update(cx, |workspace, window, cx| {
23239 workspace.active_pane().update(cx, |pane, cx| {
23240 pane.close_active_item(&CloseActiveItem::default(), window, cx)
23241 })
23242 })
23243 .unwrap();
23244 close.await.unwrap();
23245 let close = workspace
23246 .update(cx, |workspace, window, cx| {
23247 workspace.active_pane().update(cx, |pane, cx| {
23248 pane.close_active_item(&CloseActiveItem::default(), window, cx)
23249 })
23250 })
23251 .unwrap();
23252 close.await.unwrap();
23253 assert_eq!(
23254 3,
23255 requests_made.load(atomic::Ordering::Acquire),
23256 "After saving and closing all editors, no extra requests should be made"
23257 );
23258 workspace
23259 .update(cx, |workspace, _, cx| {
23260 assert!(
23261 workspace.active_item(cx).is_none(),
23262 "Should close all editors"
23263 )
23264 })
23265 .unwrap();
23266
23267 workspace
23268 .update(cx, |workspace, window, cx| {
23269 workspace.active_pane().update(cx, |pane, cx| {
23270 pane.navigate_backward(window, cx);
23271 })
23272 })
23273 .unwrap();
23274 cx.executor().advance_clock(Duration::from_millis(100));
23275 cx.run_until_parked();
23276 let editor = workspace
23277 .update(cx, |workspace, _, cx| {
23278 workspace
23279 .active_item(cx)
23280 .expect("Should have reopened the editor again after navigating back")
23281 .downcast::<Editor>()
23282 .expect("Should be an editor")
23283 })
23284 .unwrap();
23285 color_request_handle.next().await.unwrap();
23286 assert_eq!(
23287 3,
23288 requests_made.load(atomic::Ordering::Acquire),
23289 "Cache should be reused on buffer close and reopen"
23290 );
23291 editor.update(cx, |editor, cx| {
23292 assert_eq!(
23293 vec![expected_color],
23294 extract_color_inlays(editor, cx),
23295 "Should have an initial inlay"
23296 );
23297 });
23298}
23299
23300#[gpui::test]
23301async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
23302 init_test(cx, |_| {});
23303 let (editor, cx) = cx.add_window_view(Editor::single_line);
23304 editor.update_in(cx, |editor, window, cx| {
23305 editor.set_text("oops\n\nwow\n", window, cx)
23306 });
23307 cx.run_until_parked();
23308 editor.update(cx, |editor, cx| {
23309 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
23310 });
23311 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
23312 cx.run_until_parked();
23313 editor.update(cx, |editor, cx| {
23314 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
23315 });
23316}
23317
23318#[track_caller]
23319fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
23320 editor
23321 .all_inlays(cx)
23322 .into_iter()
23323 .filter_map(|inlay| inlay.get_color())
23324 .map(Rgba::from)
23325 .collect()
23326}