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_selection_at_end_of_fold(cx: &mut TestAppContext) {
5074 init_test(cx, |_| {});
5075 let editor = cx.add_window(|window, cx| {
5076 let buffer = MultiBuffer::build_simple("\n\n\n\n\n\naaaa\nbbbb\ncccc", cx);
5077 build_editor(buffer, window, cx)
5078 });
5079 _ = editor.update(cx, |editor, window, cx| {
5080 editor.fold_creases(
5081 vec![Crease::simple(
5082 Point::new(6, 4)..Point::new(7, 4),
5083 FoldPlaceholder::test(),
5084 )],
5085 true,
5086 window,
5087 cx,
5088 );
5089 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5090 s.select_ranges([Point::new(7, 4)..Point::new(7, 4)])
5091 });
5092 assert_eq!(editor.display_text(cx), "\n\n\n\n\n\naaaa⋯\ncccc");
5093 editor.move_line_up(&MoveLineUp, window, cx);
5094 let buffer_text = editor.buffer.read(cx).snapshot(cx).text();
5095 assert_eq!(buffer_text, "\n\n\n\n\naaaa\nbbbb\n\ncccc");
5096 });
5097}
5098
5099#[gpui::test]
5100fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
5101 init_test(cx, |_| {});
5102
5103 let editor = cx.add_window(|window, cx| {
5104 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5105 build_editor(buffer, window, cx)
5106 });
5107 _ = editor.update(cx, |editor, window, cx| {
5108 let snapshot = editor.buffer.read(cx).snapshot(cx);
5109 editor.insert_blocks(
5110 [BlockProperties {
5111 style: BlockStyle::Fixed,
5112 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
5113 height: Some(1),
5114 render: Arc::new(|_| div().into_any()),
5115 priority: 0,
5116 }],
5117 Some(Autoscroll::fit()),
5118 cx,
5119 );
5120 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5121 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
5122 });
5123 editor.move_line_down(&MoveLineDown, window, cx);
5124 });
5125}
5126
5127#[gpui::test]
5128async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
5129 init_test(cx, |_| {});
5130
5131 let mut cx = EditorTestContext::new(cx).await;
5132 cx.set_state(
5133 &"
5134 ˇzero
5135 one
5136 two
5137 three
5138 four
5139 five
5140 "
5141 .unindent(),
5142 );
5143
5144 // Create a four-line block that replaces three lines of text.
5145 cx.update_editor(|editor, window, cx| {
5146 let snapshot = editor.snapshot(window, cx);
5147 let snapshot = &snapshot.buffer_snapshot;
5148 let placement = BlockPlacement::Replace(
5149 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
5150 );
5151 editor.insert_blocks(
5152 [BlockProperties {
5153 placement,
5154 height: Some(4),
5155 style: BlockStyle::Sticky,
5156 render: Arc::new(|_| gpui::div().into_any_element()),
5157 priority: 0,
5158 }],
5159 None,
5160 cx,
5161 );
5162 });
5163
5164 // Move down so that the cursor touches the block.
5165 cx.update_editor(|editor, window, cx| {
5166 editor.move_down(&Default::default(), window, cx);
5167 });
5168 cx.assert_editor_state(
5169 &"
5170 zero
5171 «one
5172 two
5173 threeˇ»
5174 four
5175 five
5176 "
5177 .unindent(),
5178 );
5179
5180 // Move down past the block.
5181 cx.update_editor(|editor, window, cx| {
5182 editor.move_down(&Default::default(), window, cx);
5183 });
5184 cx.assert_editor_state(
5185 &"
5186 zero
5187 one
5188 two
5189 three
5190 ˇfour
5191 five
5192 "
5193 .unindent(),
5194 );
5195}
5196
5197#[gpui::test]
5198fn test_transpose(cx: &mut TestAppContext) {
5199 init_test(cx, |_| {});
5200
5201 _ = cx.add_window(|window, cx| {
5202 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
5203 editor.set_style(EditorStyle::default(), window, cx);
5204 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5205 s.select_ranges([1..1])
5206 });
5207 editor.transpose(&Default::default(), window, cx);
5208 assert_eq!(editor.text(cx), "bac");
5209 assert_eq!(editor.selections.ranges(cx), [2..2]);
5210
5211 editor.transpose(&Default::default(), window, cx);
5212 assert_eq!(editor.text(cx), "bca");
5213 assert_eq!(editor.selections.ranges(cx), [3..3]);
5214
5215 editor.transpose(&Default::default(), window, cx);
5216 assert_eq!(editor.text(cx), "bac");
5217 assert_eq!(editor.selections.ranges(cx), [3..3]);
5218
5219 editor
5220 });
5221
5222 _ = cx.add_window(|window, cx| {
5223 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5224 editor.set_style(EditorStyle::default(), window, cx);
5225 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5226 s.select_ranges([3..3])
5227 });
5228 editor.transpose(&Default::default(), window, cx);
5229 assert_eq!(editor.text(cx), "acb\nde");
5230 assert_eq!(editor.selections.ranges(cx), [3..3]);
5231
5232 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5233 s.select_ranges([4..4])
5234 });
5235 editor.transpose(&Default::default(), window, cx);
5236 assert_eq!(editor.text(cx), "acbd\ne");
5237 assert_eq!(editor.selections.ranges(cx), [5..5]);
5238
5239 editor.transpose(&Default::default(), window, cx);
5240 assert_eq!(editor.text(cx), "acbde\n");
5241 assert_eq!(editor.selections.ranges(cx), [6..6]);
5242
5243 editor.transpose(&Default::default(), window, cx);
5244 assert_eq!(editor.text(cx), "acbd\ne");
5245 assert_eq!(editor.selections.ranges(cx), [6..6]);
5246
5247 editor
5248 });
5249
5250 _ = cx.add_window(|window, cx| {
5251 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5252 editor.set_style(EditorStyle::default(), window, cx);
5253 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5254 s.select_ranges([1..1, 2..2, 4..4])
5255 });
5256 editor.transpose(&Default::default(), window, cx);
5257 assert_eq!(editor.text(cx), "bacd\ne");
5258 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
5259
5260 editor.transpose(&Default::default(), window, cx);
5261 assert_eq!(editor.text(cx), "bcade\n");
5262 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
5263
5264 editor.transpose(&Default::default(), window, cx);
5265 assert_eq!(editor.text(cx), "bcda\ne");
5266 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5267
5268 editor.transpose(&Default::default(), window, cx);
5269 assert_eq!(editor.text(cx), "bcade\n");
5270 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5271
5272 editor.transpose(&Default::default(), window, cx);
5273 assert_eq!(editor.text(cx), "bcaed\n");
5274 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
5275
5276 editor
5277 });
5278
5279 _ = cx.add_window(|window, cx| {
5280 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
5281 editor.set_style(EditorStyle::default(), window, cx);
5282 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5283 s.select_ranges([4..4])
5284 });
5285 editor.transpose(&Default::default(), window, cx);
5286 assert_eq!(editor.text(cx), "🏀🍐✋");
5287 assert_eq!(editor.selections.ranges(cx), [8..8]);
5288
5289 editor.transpose(&Default::default(), window, cx);
5290 assert_eq!(editor.text(cx), "🏀✋🍐");
5291 assert_eq!(editor.selections.ranges(cx), [11..11]);
5292
5293 editor.transpose(&Default::default(), window, cx);
5294 assert_eq!(editor.text(cx), "🏀🍐✋");
5295 assert_eq!(editor.selections.ranges(cx), [11..11]);
5296
5297 editor
5298 });
5299}
5300
5301#[gpui::test]
5302async fn test_rewrap(cx: &mut TestAppContext) {
5303 init_test(cx, |settings| {
5304 settings.languages.0.extend([
5305 (
5306 "Markdown".into(),
5307 LanguageSettingsContent {
5308 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5309 preferred_line_length: Some(40),
5310 ..Default::default()
5311 },
5312 ),
5313 (
5314 "Plain Text".into(),
5315 LanguageSettingsContent {
5316 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5317 preferred_line_length: Some(40),
5318 ..Default::default()
5319 },
5320 ),
5321 (
5322 "C++".into(),
5323 LanguageSettingsContent {
5324 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5325 preferred_line_length: Some(40),
5326 ..Default::default()
5327 },
5328 ),
5329 (
5330 "Python".into(),
5331 LanguageSettingsContent {
5332 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5333 preferred_line_length: Some(40),
5334 ..Default::default()
5335 },
5336 ),
5337 (
5338 "Rust".into(),
5339 LanguageSettingsContent {
5340 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5341 preferred_line_length: Some(40),
5342 ..Default::default()
5343 },
5344 ),
5345 ])
5346 });
5347
5348 let mut cx = EditorTestContext::new(cx).await;
5349
5350 let cpp_language = Arc::new(Language::new(
5351 LanguageConfig {
5352 name: "C++".into(),
5353 line_comments: vec!["// ".into()],
5354 ..LanguageConfig::default()
5355 },
5356 None,
5357 ));
5358 let python_language = Arc::new(Language::new(
5359 LanguageConfig {
5360 name: "Python".into(),
5361 line_comments: vec!["# ".into()],
5362 ..LanguageConfig::default()
5363 },
5364 None,
5365 ));
5366 let markdown_language = Arc::new(Language::new(
5367 LanguageConfig {
5368 name: "Markdown".into(),
5369 rewrap_prefixes: vec![
5370 regex::Regex::new("\\d+\\.\\s+").unwrap(),
5371 regex::Regex::new("[-*+]\\s+").unwrap(),
5372 ],
5373 ..LanguageConfig::default()
5374 },
5375 None,
5376 ));
5377 let rust_language = Arc::new(Language::new(
5378 LanguageConfig {
5379 name: "Rust".into(),
5380 line_comments: vec!["// ".into(), "/// ".into()],
5381 ..LanguageConfig::default()
5382 },
5383 Some(tree_sitter_rust::LANGUAGE.into()),
5384 ));
5385
5386 let plaintext_language = Arc::new(Language::new(
5387 LanguageConfig {
5388 name: "Plain Text".into(),
5389 ..LanguageConfig::default()
5390 },
5391 None,
5392 ));
5393
5394 // Test basic rewrapping of a long line with a cursor
5395 assert_rewrap(
5396 indoc! {"
5397 // ˇThis is a long comment that needs to be wrapped.
5398 "},
5399 indoc! {"
5400 // ˇThis is a long comment that needs to
5401 // be wrapped.
5402 "},
5403 cpp_language.clone(),
5404 &mut cx,
5405 );
5406
5407 // Test rewrapping a full selection
5408 assert_rewrap(
5409 indoc! {"
5410 «// This selected long comment needs to be wrapped.ˇ»"
5411 },
5412 indoc! {"
5413 «// This selected long comment needs to
5414 // be wrapped.ˇ»"
5415 },
5416 cpp_language.clone(),
5417 &mut cx,
5418 );
5419
5420 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
5421 assert_rewrap(
5422 indoc! {"
5423 // ˇThis is the first line.
5424 // Thisˇ is the second line.
5425 // This is the thirdˇ line, all part of one paragraph.
5426 "},
5427 indoc! {"
5428 // ˇThis is the first line. Thisˇ is the
5429 // second line. This is the thirdˇ line,
5430 // all part of one paragraph.
5431 "},
5432 cpp_language.clone(),
5433 &mut cx,
5434 );
5435
5436 // Test multiple cursors in different paragraphs trigger separate rewraps
5437 assert_rewrap(
5438 indoc! {"
5439 // ˇThis is the first paragraph, first line.
5440 // ˇThis is the first paragraph, second line.
5441
5442 // ˇThis is the second paragraph, first line.
5443 // ˇThis is the second paragraph, second line.
5444 "},
5445 indoc! {"
5446 // ˇThis is the first paragraph, first
5447 // line. ˇThis is the first paragraph,
5448 // second line.
5449
5450 // ˇThis is the second paragraph, first
5451 // line. ˇThis is the second paragraph,
5452 // second line.
5453 "},
5454 cpp_language.clone(),
5455 &mut cx,
5456 );
5457
5458 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
5459 assert_rewrap(
5460 indoc! {"
5461 «// A regular long long comment to be wrapped.
5462 /// A documentation long comment to be wrapped.ˇ»
5463 "},
5464 indoc! {"
5465 «// A regular long long comment to be
5466 // wrapped.
5467 /// A documentation long comment to be
5468 /// wrapped.ˇ»
5469 "},
5470 rust_language.clone(),
5471 &mut cx,
5472 );
5473
5474 // Test that change in indentation level trigger seperate rewraps
5475 assert_rewrap(
5476 indoc! {"
5477 fn foo() {
5478 «// This is a long comment at the base indent.
5479 // This is a long comment at the next indent.ˇ»
5480 }
5481 "},
5482 indoc! {"
5483 fn foo() {
5484 «// This is a long comment at the
5485 // base indent.
5486 // This is a long comment at the
5487 // next indent.ˇ»
5488 }
5489 "},
5490 rust_language.clone(),
5491 &mut cx,
5492 );
5493
5494 // Test that different comment prefix characters (e.g., '#') are handled correctly
5495 assert_rewrap(
5496 indoc! {"
5497 # ˇThis is a long comment using a pound sign.
5498 "},
5499 indoc! {"
5500 # ˇThis is a long comment using a pound
5501 # sign.
5502 "},
5503 python_language.clone(),
5504 &mut cx,
5505 );
5506
5507 // Test rewrapping only affects comments, not code even when selected
5508 assert_rewrap(
5509 indoc! {"
5510 «/// This doc comment is long and should be wrapped.
5511 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
5512 "},
5513 indoc! {"
5514 «/// This doc comment is long and should
5515 /// be wrapped.
5516 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
5517 "},
5518 rust_language.clone(),
5519 &mut cx,
5520 );
5521
5522 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
5523 assert_rewrap(
5524 indoc! {"
5525 # Header
5526
5527 A long long long line of markdown text to wrap.ˇ
5528 "},
5529 indoc! {"
5530 # Header
5531
5532 A long long long line of markdown text
5533 to wrap.ˇ
5534 "},
5535 markdown_language.clone(),
5536 &mut cx,
5537 );
5538
5539 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
5540 assert_rewrap(
5541 indoc! {"
5542 «1. This is a numbered list item that is very long and needs to be wrapped properly.
5543 2. This is a numbered list item that is very long and needs to be wrapped properly.
5544 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
5545 "},
5546 indoc! {"
5547 «1. This is a numbered list item that is
5548 very long and needs to be wrapped
5549 properly.
5550 2. This is a numbered list item that is
5551 very long and needs to be wrapped
5552 properly.
5553 - This is an unordered list item that is
5554 also very long and should not merge
5555 with the numbered item.ˇ»
5556 "},
5557 markdown_language.clone(),
5558 &mut cx,
5559 );
5560
5561 // Test that rewrapping add indents for rewrapping boundary if not exists already.
5562 assert_rewrap(
5563 indoc! {"
5564 «1. This is a numbered list item that is
5565 very long and needs to be wrapped
5566 properly.
5567 2. This is a numbered list item that is
5568 very long and needs to be wrapped
5569 properly.
5570 - This is an unordered list item that is
5571 also very long and should not merge with
5572 the numbered item.ˇ»
5573 "},
5574 indoc! {"
5575 «1. This is a numbered list item that is
5576 very long and needs to be wrapped
5577 properly.
5578 2. This is a numbered list item that is
5579 very long and needs to be wrapped
5580 properly.
5581 - This is an unordered list item that is
5582 also very long and should not merge
5583 with the numbered item.ˇ»
5584 "},
5585 markdown_language.clone(),
5586 &mut cx,
5587 );
5588
5589 // Test that rewrapping maintain indents even when they already exists.
5590 assert_rewrap(
5591 indoc! {"
5592 «1. This is a numbered list
5593 item that is very long and needs to be wrapped properly.
5594 2. This is a numbered list
5595 item that is very long and needs to be wrapped properly.
5596 - This is an unordered list item that is also very long and
5597 should not merge with the numbered item.ˇ»
5598 "},
5599 indoc! {"
5600 «1. This is a numbered list item that is
5601 very long and needs to be wrapped
5602 properly.
5603 2. This is a numbered list item that is
5604 very long and needs to be wrapped
5605 properly.
5606 - This is an unordered list item that is
5607 also very long and should not merge
5608 with the numbered item.ˇ»
5609 "},
5610 markdown_language.clone(),
5611 &mut cx,
5612 );
5613
5614 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
5615 assert_rewrap(
5616 indoc! {"
5617 ˇThis is a very long line of plain text that will be wrapped.
5618 "},
5619 indoc! {"
5620 ˇThis is a very long line of plain text
5621 that will be wrapped.
5622 "},
5623 plaintext_language.clone(),
5624 &mut cx,
5625 );
5626
5627 // Test that non-commented code acts as a paragraph boundary within a selection
5628 assert_rewrap(
5629 indoc! {"
5630 «// This is the first long comment block to be wrapped.
5631 fn my_func(a: u32);
5632 // This is the second long comment block to be wrapped.ˇ»
5633 "},
5634 indoc! {"
5635 «// This is the first long comment block
5636 // to be wrapped.
5637 fn my_func(a: u32);
5638 // This is the second long comment block
5639 // to be wrapped.ˇ»
5640 "},
5641 rust_language.clone(),
5642 &mut cx,
5643 );
5644
5645 // Test rewrapping multiple selections, including ones with blank lines or tabs
5646 assert_rewrap(
5647 indoc! {"
5648 «ˇThis is a very long line that will be wrapped.
5649
5650 This is another paragraph in the same selection.»
5651
5652 «\tThis is a very long indented line that will be wrapped.ˇ»
5653 "},
5654 indoc! {"
5655 «ˇThis is a very long line that will be
5656 wrapped.
5657
5658 This is another paragraph in the same
5659 selection.»
5660
5661 «\tThis is a very long indented line
5662 \tthat will be wrapped.ˇ»
5663 "},
5664 plaintext_language.clone(),
5665 &mut cx,
5666 );
5667
5668 // Test that an empty comment line acts as a paragraph boundary
5669 assert_rewrap(
5670 indoc! {"
5671 // ˇThis is a long comment that will be wrapped.
5672 //
5673 // And this is another long comment that will also be wrapped.ˇ
5674 "},
5675 indoc! {"
5676 // ˇThis is a long comment that will be
5677 // wrapped.
5678 //
5679 // And this is another long comment that
5680 // will also be wrapped.ˇ
5681 "},
5682 cpp_language,
5683 &mut cx,
5684 );
5685
5686 #[track_caller]
5687 fn assert_rewrap(
5688 unwrapped_text: &str,
5689 wrapped_text: &str,
5690 language: Arc<Language>,
5691 cx: &mut EditorTestContext,
5692 ) {
5693 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
5694 cx.set_state(unwrapped_text);
5695 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
5696 cx.assert_editor_state(wrapped_text);
5697 }
5698}
5699
5700#[gpui::test]
5701async fn test_hard_wrap(cx: &mut TestAppContext) {
5702 init_test(cx, |_| {});
5703 let mut cx = EditorTestContext::new(cx).await;
5704
5705 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
5706 cx.update_editor(|editor, _, cx| {
5707 editor.set_hard_wrap(Some(14), cx);
5708 });
5709
5710 cx.set_state(indoc!(
5711 "
5712 one two three ˇ
5713 "
5714 ));
5715 cx.simulate_input("four");
5716 cx.run_until_parked();
5717
5718 cx.assert_editor_state(indoc!(
5719 "
5720 one two three
5721 fourˇ
5722 "
5723 ));
5724
5725 cx.update_editor(|editor, window, cx| {
5726 editor.newline(&Default::default(), window, cx);
5727 });
5728 cx.run_until_parked();
5729 cx.assert_editor_state(indoc!(
5730 "
5731 one two three
5732 four
5733 ˇ
5734 "
5735 ));
5736
5737 cx.simulate_input("five");
5738 cx.run_until_parked();
5739 cx.assert_editor_state(indoc!(
5740 "
5741 one two three
5742 four
5743 fiveˇ
5744 "
5745 ));
5746
5747 cx.update_editor(|editor, window, cx| {
5748 editor.newline(&Default::default(), window, cx);
5749 });
5750 cx.run_until_parked();
5751 cx.simulate_input("# ");
5752 cx.run_until_parked();
5753 cx.assert_editor_state(indoc!(
5754 "
5755 one two three
5756 four
5757 five
5758 # ˇ
5759 "
5760 ));
5761
5762 cx.update_editor(|editor, window, cx| {
5763 editor.newline(&Default::default(), window, cx);
5764 });
5765 cx.run_until_parked();
5766 cx.assert_editor_state(indoc!(
5767 "
5768 one two three
5769 four
5770 five
5771 #\x20
5772 #ˇ
5773 "
5774 ));
5775
5776 cx.simulate_input(" 6");
5777 cx.run_until_parked();
5778 cx.assert_editor_state(indoc!(
5779 "
5780 one two three
5781 four
5782 five
5783 #
5784 # 6ˇ
5785 "
5786 ));
5787}
5788
5789#[gpui::test]
5790async fn test_clipboard(cx: &mut TestAppContext) {
5791 init_test(cx, |_| {});
5792
5793 let mut cx = EditorTestContext::new(cx).await;
5794
5795 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
5796 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5797 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
5798
5799 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
5800 cx.set_state("two ˇfour ˇsix ˇ");
5801 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5802 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
5803
5804 // Paste again but with only two cursors. Since the number of cursors doesn't
5805 // match the number of slices in the clipboard, the entire clipboard text
5806 // is pasted at each cursor.
5807 cx.set_state("ˇtwo one✅ four three six five ˇ");
5808 cx.update_editor(|e, window, cx| {
5809 e.handle_input("( ", window, cx);
5810 e.paste(&Paste, window, cx);
5811 e.handle_input(") ", window, cx);
5812 });
5813 cx.assert_editor_state(
5814 &([
5815 "( one✅ ",
5816 "three ",
5817 "five ) ˇtwo one✅ four three six five ( one✅ ",
5818 "three ",
5819 "five ) ˇ",
5820 ]
5821 .join("\n")),
5822 );
5823
5824 // Cut with three selections, one of which is full-line.
5825 cx.set_state(indoc! {"
5826 1«2ˇ»3
5827 4ˇ567
5828 «8ˇ»9"});
5829 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5830 cx.assert_editor_state(indoc! {"
5831 1ˇ3
5832 ˇ9"});
5833
5834 // Paste with three selections, noticing how the copied selection that was full-line
5835 // gets inserted before the second cursor.
5836 cx.set_state(indoc! {"
5837 1ˇ3
5838 9ˇ
5839 «oˇ»ne"});
5840 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5841 cx.assert_editor_state(indoc! {"
5842 12ˇ3
5843 4567
5844 9ˇ
5845 8ˇne"});
5846
5847 // Copy with a single cursor only, which writes the whole line into the clipboard.
5848 cx.set_state(indoc! {"
5849 The quick brown
5850 fox juˇmps over
5851 the lazy dog"});
5852 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5853 assert_eq!(
5854 cx.read_from_clipboard()
5855 .and_then(|item| item.text().as_deref().map(str::to_string)),
5856 Some("fox jumps over\n".to_string())
5857 );
5858
5859 // Paste with three selections, noticing how the copied full-line selection is inserted
5860 // before the empty selections but replaces the selection that is non-empty.
5861 cx.set_state(indoc! {"
5862 Tˇhe quick brown
5863 «foˇ»x jumps over
5864 tˇhe lazy dog"});
5865 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5866 cx.assert_editor_state(indoc! {"
5867 fox jumps over
5868 Tˇhe quick brown
5869 fox jumps over
5870 ˇx jumps over
5871 fox jumps over
5872 tˇhe lazy dog"});
5873}
5874
5875#[gpui::test]
5876async fn test_copy_trim(cx: &mut TestAppContext) {
5877 init_test(cx, |_| {});
5878
5879 let mut cx = EditorTestContext::new(cx).await;
5880 cx.set_state(
5881 r#" «for selection in selections.iter() {
5882 let mut start = selection.start;
5883 let mut end = selection.end;
5884 let is_entire_line = selection.is_empty();
5885 if is_entire_line {
5886 start = Point::new(start.row, 0);ˇ»
5887 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5888 }
5889 "#,
5890 );
5891 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5892 assert_eq!(
5893 cx.read_from_clipboard()
5894 .and_then(|item| item.text().as_deref().map(str::to_string)),
5895 Some(
5896 "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 .to_string()
5903 ),
5904 "Regular copying preserves all indentation selected",
5905 );
5906 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, 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() {
5912let mut start = selection.start;
5913let mut end = selection.end;
5914let is_entire_line = selection.is_empty();
5915if is_entire_line {
5916 start = Point::new(start.row, 0);"
5917 .to_string()
5918 ),
5919 "Copying with stripping should strip all leading whitespaces"
5920 );
5921
5922 cx.set_state(
5923 r#" « for selection in selections.iter() {
5924 let mut start = selection.start;
5925 let mut end = selection.end;
5926 let is_entire_line = selection.is_empty();
5927 if is_entire_line {
5928 start = Point::new(start.row, 0);ˇ»
5929 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5930 }
5931 "#,
5932 );
5933 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5934 assert_eq!(
5935 cx.read_from_clipboard()
5936 .and_then(|item| item.text().as_deref().map(str::to_string)),
5937 Some(
5938 " 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 .to_string()
5945 ),
5946 "Regular copying preserves all indentation selected",
5947 );
5948 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, 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() {
5954let mut start = selection.start;
5955let mut end = selection.end;
5956let is_entire_line = selection.is_empty();
5957if is_entire_line {
5958 start = Point::new(start.row, 0);"
5959 .to_string()
5960 ),
5961 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
5962 );
5963
5964 cx.set_state(
5965 r#" «ˇ for selection in selections.iter() {
5966 let mut start = selection.start;
5967 let mut end = selection.end;
5968 let is_entire_line = selection.is_empty();
5969 if is_entire_line {
5970 start = Point::new(start.row, 0);»
5971 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5972 }
5973 "#,
5974 );
5975 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5976 assert_eq!(
5977 cx.read_from_clipboard()
5978 .and_then(|item| item.text().as_deref().map(str::to_string)),
5979 Some(
5980 " 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 .to_string()
5987 ),
5988 "Regular copying for reverse selection works the same",
5989 );
5990 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, 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 "for selection in selections.iter() {
5996let mut start = selection.start;
5997let mut end = selection.end;
5998let is_entire_line = selection.is_empty();
5999if is_entire_line {
6000 start = Point::new(start.row, 0);"
6001 .to_string()
6002 ),
6003 "Copying with stripping for reverse selection works the same"
6004 );
6005
6006 cx.set_state(
6007 r#" for selection «in selections.iter() {
6008 let mut start = selection.start;
6009 let mut end = selection.end;
6010 let is_entire_line = selection.is_empty();
6011 if is_entire_line {
6012 start = Point::new(start.row, 0);ˇ»
6013 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6014 }
6015 "#,
6016 );
6017 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6018 assert_eq!(
6019 cx.read_from_clipboard()
6020 .and_then(|item| item.text().as_deref().map(str::to_string)),
6021 Some(
6022 "in selections.iter() {
6023 let mut start = selection.start;
6024 let mut end = selection.end;
6025 let is_entire_line = selection.is_empty();
6026 if is_entire_line {
6027 start = Point::new(start.row, 0);"
6028 .to_string()
6029 ),
6030 "When selecting past the indent, the copying works as usual",
6031 );
6032 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6033 assert_eq!(
6034 cx.read_from_clipboard()
6035 .and_then(|item| item.text().as_deref().map(str::to_string)),
6036 Some(
6037 "in selections.iter() {
6038 let mut start = selection.start;
6039 let mut end = selection.end;
6040 let is_entire_line = selection.is_empty();
6041 if is_entire_line {
6042 start = Point::new(start.row, 0);"
6043 .to_string()
6044 ),
6045 "When selecting past the indent, nothing is trimmed"
6046 );
6047
6048 cx.set_state(
6049 r#" «for selection in selections.iter() {
6050 let mut start = selection.start;
6051
6052 let mut end = selection.end;
6053 let is_entire_line = selection.is_empty();
6054 if is_entire_line {
6055 start = Point::new(start.row, 0);
6056ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
6057 }
6058 "#,
6059 );
6060 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6061 assert_eq!(
6062 cx.read_from_clipboard()
6063 .and_then(|item| item.text().as_deref().map(str::to_string)),
6064 Some(
6065 "for selection in selections.iter() {
6066let mut start = selection.start;
6067
6068let mut end = selection.end;
6069let is_entire_line = selection.is_empty();
6070if is_entire_line {
6071 start = Point::new(start.row, 0);
6072"
6073 .to_string()
6074 ),
6075 "Copying with stripping should ignore empty lines"
6076 );
6077}
6078
6079#[gpui::test]
6080async fn test_copy_entire_line(cx: &mut TestAppContext) {
6081 init_test(cx, |_| {});
6082
6083 let mut cx = EditorTestContext::new(cx).await;
6084
6085 cx.set_state("line1\nline2\nlastˇ line");
6086 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6087 assert_eq!(
6088 cx.read_from_clipboard()
6089 .and_then(|item| item.text().as_deref().map(str::to_string)),
6090 Some("last line\n".to_string()),
6091 "Copying last line of file without newline should include trailing newline"
6092 );
6093
6094 cx.set_state("line1\nˇline2\nlast line");
6095 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6096 assert_eq!(
6097 cx.read_from_clipboard()
6098 .and_then(|item| item.text().as_deref().map(str::to_string)),
6099 Some("line2\n".to_string()),
6100 "Copying a line without a selection should copy that line with a trailing newline"
6101 );
6102
6103 cx.set_state("ˇ");
6104 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6105 assert_eq!(
6106 cx.read_from_clipboard()
6107 .and_then(|item| item.text().as_deref().map(str::to_string)),
6108 Some("\n".to_string()),
6109 "Copying empty line should be a newline"
6110 );
6111
6112 cx.set_state("line1\nline2\nlast line\nˇ");
6113 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6114 assert_eq!(
6115 cx.read_from_clipboard()
6116 .and_then(|item| item.text().as_deref().map(str::to_string)),
6117 Some("\n".to_string()),
6118 "Copying empty line at end of file should be a newline"
6119 );
6120}
6121
6122#[gpui::test]
6123async fn test_cut_entire_line(cx: &mut TestAppContext) {
6124 init_test(cx, |_| {});
6125
6126 let mut cx = EditorTestContext::new(cx).await;
6127
6128 cx.set_state("line1\nline2\nlastˇ line");
6129 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6130 assert_eq!(
6131 cx.read_from_clipboard()
6132 .and_then(|item| item.text().as_deref().map(str::to_string)),
6133 Some("last line\n".to_string()),
6134 "Cutting last line of file without newline should include trailing newline"
6135 );
6136 cx.assert_editor_state("line1\nline2\nˇ");
6137
6138 cx.set_state("line1\nˇline2\nlast line");
6139 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6140 assert_eq!(
6141 cx.read_from_clipboard()
6142 .and_then(|item| item.text().as_deref().map(str::to_string)),
6143 Some("line2\n".to_string()),
6144 "Cutting a line without a selection should cut that line with a trailing newline"
6145 );
6146 cx.assert_editor_state("line1\nˇlast line");
6147
6148 cx.set_state("ˇ");
6149 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6150 assert_eq!(
6151 cx.read_from_clipboard()
6152 .and_then(|item| item.text().as_deref().map(str::to_string)),
6153 Some("\n".to_string()),
6154 "Cutting empty line should be a newline"
6155 );
6156 cx.assert_editor_state("ˇ");
6157
6158 cx.set_state("line1\nline2\nlast line\nˇ");
6159 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6160 assert_eq!(
6161 cx.read_from_clipboard()
6162 .and_then(|item| item.text().as_deref().map(str::to_string)),
6163 Some("\n".to_string()),
6164 "Cutting empty line at end of file should be a newline"
6165 );
6166 cx.assert_editor_state("line1\nline2\nlast lineˇ");
6167}
6168
6169#[gpui::test]
6170async fn test_paste_multiline(cx: &mut TestAppContext) {
6171 init_test(cx, |_| {});
6172
6173 let mut cx = EditorTestContext::new(cx).await;
6174 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
6175
6176 // Cut an indented block, without the leading whitespace.
6177 cx.set_state(indoc! {"
6178 const a: B = (
6179 c(),
6180 «d(
6181 e,
6182 f
6183 )ˇ»
6184 );
6185 "});
6186 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6187 cx.assert_editor_state(indoc! {"
6188 const a: B = (
6189 c(),
6190 ˇ
6191 );
6192 "});
6193
6194 // Paste it at the same position.
6195 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6196 cx.assert_editor_state(indoc! {"
6197 const a: B = (
6198 c(),
6199 d(
6200 e,
6201 f
6202 )ˇ
6203 );
6204 "});
6205
6206 // Paste it at a line with a lower indent level.
6207 cx.set_state(indoc! {"
6208 ˇ
6209 const a: B = (
6210 c(),
6211 );
6212 "});
6213 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6214 cx.assert_editor_state(indoc! {"
6215 d(
6216 e,
6217 f
6218 )ˇ
6219 const a: B = (
6220 c(),
6221 );
6222 "});
6223
6224 // Cut an indented block, with the leading whitespace.
6225 cx.set_state(indoc! {"
6226 const a: B = (
6227 c(),
6228 « d(
6229 e,
6230 f
6231 )
6232 ˇ»);
6233 "});
6234 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6235 cx.assert_editor_state(indoc! {"
6236 const a: B = (
6237 c(),
6238 ˇ);
6239 "});
6240
6241 // Paste it at the same position.
6242 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6243 cx.assert_editor_state(indoc! {"
6244 const a: B = (
6245 c(),
6246 d(
6247 e,
6248 f
6249 )
6250 ˇ);
6251 "});
6252
6253 // Paste it at a line with a higher indent level.
6254 cx.set_state(indoc! {"
6255 const a: B = (
6256 c(),
6257 d(
6258 e,
6259 fˇ
6260 )
6261 );
6262 "});
6263 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6264 cx.assert_editor_state(indoc! {"
6265 const a: B = (
6266 c(),
6267 d(
6268 e,
6269 f d(
6270 e,
6271 f
6272 )
6273 ˇ
6274 )
6275 );
6276 "});
6277
6278 // Copy an indented block, starting mid-line
6279 cx.set_state(indoc! {"
6280 const a: B = (
6281 c(),
6282 somethin«g(
6283 e,
6284 f
6285 )ˇ»
6286 );
6287 "});
6288 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6289
6290 // Paste it on a line with a lower indent level
6291 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
6292 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6293 cx.assert_editor_state(indoc! {"
6294 const a: B = (
6295 c(),
6296 something(
6297 e,
6298 f
6299 )
6300 );
6301 g(
6302 e,
6303 f
6304 )ˇ"});
6305}
6306
6307#[gpui::test]
6308async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
6309 init_test(cx, |_| {});
6310
6311 cx.write_to_clipboard(ClipboardItem::new_string(
6312 " d(\n e\n );\n".into(),
6313 ));
6314
6315 let mut cx = EditorTestContext::new(cx).await;
6316 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
6317
6318 cx.set_state(indoc! {"
6319 fn a() {
6320 b();
6321 if c() {
6322 ˇ
6323 }
6324 }
6325 "});
6326
6327 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6328 cx.assert_editor_state(indoc! {"
6329 fn a() {
6330 b();
6331 if c() {
6332 d(
6333 e
6334 );
6335 ˇ
6336 }
6337 }
6338 "});
6339
6340 cx.set_state(indoc! {"
6341 fn a() {
6342 b();
6343 ˇ
6344 }
6345 "});
6346
6347 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6348 cx.assert_editor_state(indoc! {"
6349 fn a() {
6350 b();
6351 d(
6352 e
6353 );
6354 ˇ
6355 }
6356 "});
6357}
6358
6359#[gpui::test]
6360fn test_select_all(cx: &mut TestAppContext) {
6361 init_test(cx, |_| {});
6362
6363 let editor = cx.add_window(|window, cx| {
6364 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
6365 build_editor(buffer, window, cx)
6366 });
6367 _ = editor.update(cx, |editor, window, cx| {
6368 editor.select_all(&SelectAll, window, cx);
6369 assert_eq!(
6370 editor.selections.display_ranges(cx),
6371 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
6372 );
6373 });
6374}
6375
6376#[gpui::test]
6377fn test_select_line(cx: &mut TestAppContext) {
6378 init_test(cx, |_| {});
6379
6380 let editor = cx.add_window(|window, cx| {
6381 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
6382 build_editor(buffer, window, cx)
6383 });
6384 _ = editor.update(cx, |editor, window, cx| {
6385 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6386 s.select_display_ranges([
6387 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6388 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
6389 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
6390 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
6391 ])
6392 });
6393 editor.select_line(&SelectLine, window, cx);
6394 assert_eq!(
6395 editor.selections.display_ranges(cx),
6396 vec![
6397 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
6398 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
6399 ]
6400 );
6401 });
6402
6403 _ = editor.update(cx, |editor, window, cx| {
6404 editor.select_line(&SelectLine, window, cx);
6405 assert_eq!(
6406 editor.selections.display_ranges(cx),
6407 vec![
6408 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
6409 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
6410 ]
6411 );
6412 });
6413
6414 _ = editor.update(cx, |editor, window, cx| {
6415 editor.select_line(&SelectLine, window, cx);
6416 assert_eq!(
6417 editor.selections.display_ranges(cx),
6418 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
6419 );
6420 });
6421}
6422
6423#[gpui::test]
6424async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
6425 init_test(cx, |_| {});
6426 let mut cx = EditorTestContext::new(cx).await;
6427
6428 #[track_caller]
6429 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
6430 cx.set_state(initial_state);
6431 cx.update_editor(|e, window, cx| {
6432 e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
6433 });
6434 cx.assert_editor_state(expected_state);
6435 }
6436
6437 // Selection starts and ends at the middle of lines, left-to-right
6438 test(
6439 &mut cx,
6440 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
6441 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
6442 );
6443 // Same thing, right-to-left
6444 test(
6445 &mut cx,
6446 "aa\nb«b\ncc\ndd\neˇ»e\nff",
6447 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
6448 );
6449
6450 // Whole buffer, left-to-right, last line *doesn't* end with newline
6451 test(
6452 &mut cx,
6453 "«ˇaa\nbb\ncc\ndd\nee\nff»",
6454 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
6455 );
6456 // Same thing, right-to-left
6457 test(
6458 &mut cx,
6459 "«aa\nbb\ncc\ndd\nee\nffˇ»",
6460 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
6461 );
6462
6463 // Whole buffer, left-to-right, last line ends with newline
6464 test(
6465 &mut cx,
6466 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
6467 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
6468 );
6469 // Same thing, right-to-left
6470 test(
6471 &mut cx,
6472 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
6473 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
6474 );
6475
6476 // Starts at the end of a line, ends at the start of another
6477 test(
6478 &mut cx,
6479 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
6480 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
6481 );
6482}
6483
6484#[gpui::test]
6485async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
6486 init_test(cx, |_| {});
6487
6488 let editor = cx.add_window(|window, cx| {
6489 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
6490 build_editor(buffer, window, cx)
6491 });
6492
6493 // setup
6494 _ = editor.update(cx, |editor, window, cx| {
6495 editor.fold_creases(
6496 vec![
6497 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
6498 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
6499 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
6500 ],
6501 true,
6502 window,
6503 cx,
6504 );
6505 assert_eq!(
6506 editor.display_text(cx),
6507 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
6508 );
6509 });
6510
6511 _ = editor.update(cx, |editor, window, cx| {
6512 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6513 s.select_display_ranges([
6514 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6515 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
6516 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
6517 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
6518 ])
6519 });
6520 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
6521 assert_eq!(
6522 editor.display_text(cx),
6523 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
6524 );
6525 });
6526 EditorTestContext::for_editor(editor, cx)
6527 .await
6528 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
6529
6530 _ = editor.update(cx, |editor, window, cx| {
6531 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6532 s.select_display_ranges([
6533 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
6534 ])
6535 });
6536 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
6537 assert_eq!(
6538 editor.display_text(cx),
6539 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
6540 );
6541 assert_eq!(
6542 editor.selections.display_ranges(cx),
6543 [
6544 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
6545 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
6546 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
6547 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
6548 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
6549 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
6550 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
6551 ]
6552 );
6553 });
6554 EditorTestContext::for_editor(editor, cx)
6555 .await
6556 .assert_editor_state(
6557 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
6558 );
6559}
6560
6561#[gpui::test]
6562async fn test_add_selection_above_below(cx: &mut TestAppContext) {
6563 init_test(cx, |_| {});
6564
6565 let mut cx = EditorTestContext::new(cx).await;
6566
6567 cx.set_state(indoc!(
6568 r#"abc
6569 defˇghi
6570
6571 jk
6572 nlmo
6573 "#
6574 ));
6575
6576 cx.update_editor(|editor, window, cx| {
6577 editor.add_selection_above(&Default::default(), window, cx);
6578 });
6579
6580 cx.assert_editor_state(indoc!(
6581 r#"abcˇ
6582 defˇghi
6583
6584 jk
6585 nlmo
6586 "#
6587 ));
6588
6589 cx.update_editor(|editor, window, cx| {
6590 editor.add_selection_above(&Default::default(), window, cx);
6591 });
6592
6593 cx.assert_editor_state(indoc!(
6594 r#"abcˇ
6595 defˇghi
6596
6597 jk
6598 nlmo
6599 "#
6600 ));
6601
6602 cx.update_editor(|editor, window, cx| {
6603 editor.add_selection_below(&Default::default(), window, cx);
6604 });
6605
6606 cx.assert_editor_state(indoc!(
6607 r#"abc
6608 defˇghi
6609
6610 jk
6611 nlmo
6612 "#
6613 ));
6614
6615 cx.update_editor(|editor, window, cx| {
6616 editor.undo_selection(&Default::default(), window, cx);
6617 });
6618
6619 cx.assert_editor_state(indoc!(
6620 r#"abcˇ
6621 defˇghi
6622
6623 jk
6624 nlmo
6625 "#
6626 ));
6627
6628 cx.update_editor(|editor, window, cx| {
6629 editor.redo_selection(&Default::default(), window, cx);
6630 });
6631
6632 cx.assert_editor_state(indoc!(
6633 r#"abc
6634 defˇghi
6635
6636 jk
6637 nlmo
6638 "#
6639 ));
6640
6641 cx.update_editor(|editor, window, cx| {
6642 editor.add_selection_below(&Default::default(), window, cx);
6643 });
6644
6645 cx.assert_editor_state(indoc!(
6646 r#"abc
6647 defˇghi
6648 ˇ
6649 jk
6650 nlmo
6651 "#
6652 ));
6653
6654 cx.update_editor(|editor, window, cx| {
6655 editor.add_selection_below(&Default::default(), window, cx);
6656 });
6657
6658 cx.assert_editor_state(indoc!(
6659 r#"abc
6660 defˇghi
6661 ˇ
6662 jkˇ
6663 nlmo
6664 "#
6665 ));
6666
6667 cx.update_editor(|editor, window, cx| {
6668 editor.add_selection_below(&Default::default(), window, cx);
6669 });
6670
6671 cx.assert_editor_state(indoc!(
6672 r#"abc
6673 defˇghi
6674 ˇ
6675 jkˇ
6676 nlmˇo
6677 "#
6678 ));
6679
6680 cx.update_editor(|editor, window, cx| {
6681 editor.add_selection_below(&Default::default(), window, cx);
6682 });
6683
6684 cx.assert_editor_state(indoc!(
6685 r#"abc
6686 defˇghi
6687 ˇ
6688 jkˇ
6689 nlmˇo
6690 ˇ"#
6691 ));
6692
6693 // change selections
6694 cx.set_state(indoc!(
6695 r#"abc
6696 def«ˇg»hi
6697
6698 jk
6699 nlmo
6700 "#
6701 ));
6702
6703 cx.update_editor(|editor, window, cx| {
6704 editor.add_selection_below(&Default::default(), window, cx);
6705 });
6706
6707 cx.assert_editor_state(indoc!(
6708 r#"abc
6709 def«ˇg»hi
6710
6711 jk
6712 nlm«ˇo»
6713 "#
6714 ));
6715
6716 cx.update_editor(|editor, window, cx| {
6717 editor.add_selection_below(&Default::default(), window, cx);
6718 });
6719
6720 cx.assert_editor_state(indoc!(
6721 r#"abc
6722 def«ˇg»hi
6723
6724 jk
6725 nlm«ˇo»
6726 "#
6727 ));
6728
6729 cx.update_editor(|editor, window, cx| {
6730 editor.add_selection_above(&Default::default(), window, cx);
6731 });
6732
6733 cx.assert_editor_state(indoc!(
6734 r#"abc
6735 def«ˇg»hi
6736
6737 jk
6738 nlmo
6739 "#
6740 ));
6741
6742 cx.update_editor(|editor, window, cx| {
6743 editor.add_selection_above(&Default::default(), window, cx);
6744 });
6745
6746 cx.assert_editor_state(indoc!(
6747 r#"abc
6748 def«ˇg»hi
6749
6750 jk
6751 nlmo
6752 "#
6753 ));
6754
6755 // Change selections again
6756 cx.set_state(indoc!(
6757 r#"a«bc
6758 defgˇ»hi
6759
6760 jk
6761 nlmo
6762 "#
6763 ));
6764
6765 cx.update_editor(|editor, window, cx| {
6766 editor.add_selection_below(&Default::default(), window, cx);
6767 });
6768
6769 cx.assert_editor_state(indoc!(
6770 r#"a«bcˇ»
6771 d«efgˇ»hi
6772
6773 j«kˇ»
6774 nlmo
6775 "#
6776 ));
6777
6778 cx.update_editor(|editor, window, cx| {
6779 editor.add_selection_below(&Default::default(), window, cx);
6780 });
6781 cx.assert_editor_state(indoc!(
6782 r#"a«bcˇ»
6783 d«efgˇ»hi
6784
6785 j«kˇ»
6786 n«lmoˇ»
6787 "#
6788 ));
6789 cx.update_editor(|editor, window, cx| {
6790 editor.add_selection_above(&Default::default(), window, cx);
6791 });
6792
6793 cx.assert_editor_state(indoc!(
6794 r#"a«bcˇ»
6795 d«efgˇ»hi
6796
6797 j«kˇ»
6798 nlmo
6799 "#
6800 ));
6801
6802 // Change selections again
6803 cx.set_state(indoc!(
6804 r#"abc
6805 d«ˇefghi
6806
6807 jk
6808 nlm»o
6809 "#
6810 ));
6811
6812 cx.update_editor(|editor, window, cx| {
6813 editor.add_selection_above(&Default::default(), window, cx);
6814 });
6815
6816 cx.assert_editor_state(indoc!(
6817 r#"a«ˇbc»
6818 d«ˇef»ghi
6819
6820 j«ˇk»
6821 n«ˇlm»o
6822 "#
6823 ));
6824
6825 cx.update_editor(|editor, window, cx| {
6826 editor.add_selection_below(&Default::default(), window, cx);
6827 });
6828
6829 cx.assert_editor_state(indoc!(
6830 r#"abc
6831 d«ˇef»ghi
6832
6833 j«ˇk»
6834 n«ˇlm»o
6835 "#
6836 ));
6837}
6838
6839#[gpui::test]
6840async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
6841 init_test(cx, |_| {});
6842 let mut cx = EditorTestContext::new(cx).await;
6843
6844 cx.set_state(indoc!(
6845 r#"line onˇe
6846 liˇne two
6847 line three
6848 line four"#
6849 ));
6850
6851 cx.update_editor(|editor, window, cx| {
6852 editor.add_selection_below(&Default::default(), window, cx);
6853 });
6854
6855 // test multiple cursors expand in the same direction
6856 cx.assert_editor_state(indoc!(
6857 r#"line onˇe
6858 liˇne twˇo
6859 liˇne three
6860 line four"#
6861 ));
6862
6863 cx.update_editor(|editor, window, cx| {
6864 editor.add_selection_below(&Default::default(), window, cx);
6865 });
6866
6867 cx.update_editor(|editor, window, cx| {
6868 editor.add_selection_below(&Default::default(), window, cx);
6869 });
6870
6871 // test multiple cursors expand below overflow
6872 cx.assert_editor_state(indoc!(
6873 r#"line onˇe
6874 liˇne twˇo
6875 liˇne thˇree
6876 liˇne foˇur"#
6877 ));
6878
6879 cx.update_editor(|editor, window, cx| {
6880 editor.add_selection_above(&Default::default(), window, cx);
6881 });
6882
6883 // test multiple cursors retrieves back correctly
6884 cx.assert_editor_state(indoc!(
6885 r#"line onˇe
6886 liˇne twˇo
6887 liˇne thˇree
6888 line four"#
6889 ));
6890
6891 cx.update_editor(|editor, window, cx| {
6892 editor.add_selection_above(&Default::default(), window, cx);
6893 });
6894
6895 cx.update_editor(|editor, window, cx| {
6896 editor.add_selection_above(&Default::default(), window, cx);
6897 });
6898
6899 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
6900 cx.assert_editor_state(indoc!(
6901 r#"liˇne onˇe
6902 liˇne two
6903 line three
6904 line four"#
6905 ));
6906
6907 cx.update_editor(|editor, window, cx| {
6908 editor.undo_selection(&Default::default(), window, cx);
6909 });
6910
6911 // test undo
6912 cx.assert_editor_state(indoc!(
6913 r#"line onˇe
6914 liˇne twˇo
6915 line three
6916 line four"#
6917 ));
6918
6919 cx.update_editor(|editor, window, cx| {
6920 editor.redo_selection(&Default::default(), window, cx);
6921 });
6922
6923 // test redo
6924 cx.assert_editor_state(indoc!(
6925 r#"liˇne onˇe
6926 liˇne two
6927 line three
6928 line four"#
6929 ));
6930
6931 cx.set_state(indoc!(
6932 r#"abcd
6933 ef«ghˇ»
6934 ijkl
6935 «mˇ»nop"#
6936 ));
6937
6938 cx.update_editor(|editor, window, cx| {
6939 editor.add_selection_above(&Default::default(), window, cx);
6940 });
6941
6942 // test multiple selections expand in the same direction
6943 cx.assert_editor_state(indoc!(
6944 r#"ab«cdˇ»
6945 ef«ghˇ»
6946 «iˇ»jkl
6947 «mˇ»nop"#
6948 ));
6949
6950 cx.update_editor(|editor, window, cx| {
6951 editor.add_selection_above(&Default::default(), window, cx);
6952 });
6953
6954 // test multiple selection upward overflow
6955 cx.assert_editor_state(indoc!(
6956 r#"ab«cdˇ»
6957 «eˇ»f«ghˇ»
6958 «iˇ»jkl
6959 «mˇ»nop"#
6960 ));
6961
6962 cx.update_editor(|editor, window, cx| {
6963 editor.add_selection_below(&Default::default(), window, cx);
6964 });
6965
6966 // test multiple selection retrieves back correctly
6967 cx.assert_editor_state(indoc!(
6968 r#"abcd
6969 ef«ghˇ»
6970 «iˇ»jkl
6971 «mˇ»nop"#
6972 ));
6973
6974 cx.update_editor(|editor, window, cx| {
6975 editor.add_selection_below(&Default::default(), window, cx);
6976 });
6977
6978 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
6979 cx.assert_editor_state(indoc!(
6980 r#"abcd
6981 ef«ghˇ»
6982 ij«klˇ»
6983 «mˇ»nop"#
6984 ));
6985
6986 cx.update_editor(|editor, window, cx| {
6987 editor.undo_selection(&Default::default(), window, cx);
6988 });
6989
6990 // test undo
6991 cx.assert_editor_state(indoc!(
6992 r#"abcd
6993 ef«ghˇ»
6994 «iˇ»jkl
6995 «mˇ»nop"#
6996 ));
6997
6998 cx.update_editor(|editor, window, cx| {
6999 editor.redo_selection(&Default::default(), window, cx);
7000 });
7001
7002 // test redo
7003 cx.assert_editor_state(indoc!(
7004 r#"abcd
7005 ef«ghˇ»
7006 ij«klˇ»
7007 «mˇ»nop"#
7008 ));
7009}
7010
7011#[gpui::test]
7012async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
7013 init_test(cx, |_| {});
7014 let mut cx = EditorTestContext::new(cx).await;
7015
7016 cx.set_state(indoc!(
7017 r#"line onˇe
7018 liˇne two
7019 line three
7020 line four"#
7021 ));
7022
7023 cx.update_editor(|editor, window, cx| {
7024 editor.add_selection_below(&Default::default(), window, cx);
7025 editor.add_selection_below(&Default::default(), window, cx);
7026 editor.add_selection_below(&Default::default(), window, cx);
7027 });
7028
7029 // initial state with two multi cursor groups
7030 cx.assert_editor_state(indoc!(
7031 r#"line onˇe
7032 liˇne twˇo
7033 liˇne thˇree
7034 liˇne foˇur"#
7035 ));
7036
7037 // add single cursor in middle - simulate opt click
7038 cx.update_editor(|editor, window, cx| {
7039 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
7040 editor.begin_selection(new_cursor_point, true, 1, window, cx);
7041 editor.end_selection(window, cx);
7042 });
7043
7044 cx.assert_editor_state(indoc!(
7045 r#"line onˇe
7046 liˇne twˇo
7047 liˇneˇ thˇree
7048 liˇne foˇur"#
7049 ));
7050
7051 cx.update_editor(|editor, window, cx| {
7052 editor.add_selection_above(&Default::default(), window, cx);
7053 });
7054
7055 // test new added selection expands above and existing selection shrinks
7056 cx.assert_editor_state(indoc!(
7057 r#"line onˇe
7058 liˇneˇ twˇo
7059 liˇneˇ thˇree
7060 line four"#
7061 ));
7062
7063 cx.update_editor(|editor, window, cx| {
7064 editor.add_selection_above(&Default::default(), window, cx);
7065 });
7066
7067 // test new added selection expands above and existing selection shrinks
7068 cx.assert_editor_state(indoc!(
7069 r#"lineˇ onˇe
7070 liˇneˇ twˇo
7071 lineˇ three
7072 line four"#
7073 ));
7074
7075 // intial state with two selection groups
7076 cx.set_state(indoc!(
7077 r#"abcd
7078 ef«ghˇ»
7079 ijkl
7080 «mˇ»nop"#
7081 ));
7082
7083 cx.update_editor(|editor, window, cx| {
7084 editor.add_selection_above(&Default::default(), window, cx);
7085 editor.add_selection_above(&Default::default(), window, cx);
7086 });
7087
7088 cx.assert_editor_state(indoc!(
7089 r#"ab«cdˇ»
7090 «eˇ»f«ghˇ»
7091 «iˇ»jkl
7092 «mˇ»nop"#
7093 ));
7094
7095 // add single selection in middle - simulate opt drag
7096 cx.update_editor(|editor, window, cx| {
7097 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
7098 editor.begin_selection(new_cursor_point, true, 1, window, cx);
7099 editor.update_selection(
7100 DisplayPoint::new(DisplayRow(2), 4),
7101 0,
7102 gpui::Point::<f32>::default(),
7103 window,
7104 cx,
7105 );
7106 editor.end_selection(window, cx);
7107 });
7108
7109 cx.assert_editor_state(indoc!(
7110 r#"ab«cdˇ»
7111 «eˇ»f«ghˇ»
7112 «iˇ»jk«lˇ»
7113 «mˇ»nop"#
7114 ));
7115
7116 cx.update_editor(|editor, window, cx| {
7117 editor.add_selection_below(&Default::default(), window, cx);
7118 });
7119
7120 // test new added selection expands below, others shrinks from above
7121 cx.assert_editor_state(indoc!(
7122 r#"abcd
7123 ef«ghˇ»
7124 «iˇ»jk«lˇ»
7125 «mˇ»no«pˇ»"#
7126 ));
7127}
7128
7129#[gpui::test]
7130async fn test_select_next(cx: &mut TestAppContext) {
7131 init_test(cx, |_| {});
7132
7133 let mut cx = EditorTestContext::new(cx).await;
7134 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
7135
7136 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7137 .unwrap();
7138 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7139
7140 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7141 .unwrap();
7142 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
7143
7144 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7145 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7146
7147 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7148 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
7149
7150 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7151 .unwrap();
7152 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7153
7154 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7155 .unwrap();
7156 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7157
7158 // Test selection direction should be preserved
7159 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
7160
7161 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7162 .unwrap();
7163 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
7164}
7165
7166#[gpui::test]
7167async fn test_select_all_matches(cx: &mut TestAppContext) {
7168 init_test(cx, |_| {});
7169
7170 let mut cx = EditorTestContext::new(cx).await;
7171
7172 // Test caret-only selections
7173 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
7174 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7175 .unwrap();
7176 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7177
7178 // Test left-to-right selections
7179 cx.set_state("abc\n«abcˇ»\nabc");
7180 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7181 .unwrap();
7182 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
7183
7184 // Test right-to-left selections
7185 cx.set_state("abc\n«ˇabc»\nabc");
7186 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7187 .unwrap();
7188 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
7189
7190 // Test selecting whitespace with caret selection
7191 cx.set_state("abc\nˇ abc\nabc");
7192 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7193 .unwrap();
7194 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
7195
7196 // Test selecting whitespace with left-to-right selection
7197 cx.set_state("abc\n«ˇ »abc\nabc");
7198 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7199 .unwrap();
7200 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
7201
7202 // Test no matches with right-to-left selection
7203 cx.set_state("abc\n« ˇ»abc\nabc");
7204 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7205 .unwrap();
7206 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
7207
7208 // Test with a single word and clip_at_line_ends=true (#29823)
7209 cx.set_state("aˇbc");
7210 cx.update_editor(|e, window, cx| {
7211 e.set_clip_at_line_ends(true, cx);
7212 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
7213 e.set_clip_at_line_ends(false, cx);
7214 });
7215 cx.assert_editor_state("«abcˇ»");
7216}
7217
7218#[gpui::test]
7219async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
7220 init_test(cx, |_| {});
7221
7222 let mut cx = EditorTestContext::new(cx).await;
7223
7224 let large_body_1 = "\nd".repeat(200);
7225 let large_body_2 = "\ne".repeat(200);
7226
7227 cx.set_state(&format!(
7228 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
7229 ));
7230 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
7231 let scroll_position = editor.scroll_position(cx);
7232 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
7233 scroll_position
7234 });
7235
7236 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7237 .unwrap();
7238 cx.assert_editor_state(&format!(
7239 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
7240 ));
7241 let scroll_position_after_selection =
7242 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
7243 assert_eq!(
7244 initial_scroll_position, scroll_position_after_selection,
7245 "Scroll position should not change after selecting all matches"
7246 );
7247}
7248
7249#[gpui::test]
7250async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
7251 init_test(cx, |_| {});
7252
7253 let mut cx = EditorLspTestContext::new_rust(
7254 lsp::ServerCapabilities {
7255 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7256 ..Default::default()
7257 },
7258 cx,
7259 )
7260 .await;
7261
7262 cx.set_state(indoc! {"
7263 line 1
7264 line 2
7265 linˇe 3
7266 line 4
7267 line 5
7268 "});
7269
7270 // Make an edit
7271 cx.update_editor(|editor, window, cx| {
7272 editor.handle_input("X", window, cx);
7273 });
7274
7275 // Move cursor to a different position
7276 cx.update_editor(|editor, window, cx| {
7277 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7278 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
7279 });
7280 });
7281
7282 cx.assert_editor_state(indoc! {"
7283 line 1
7284 line 2
7285 linXe 3
7286 line 4
7287 liˇne 5
7288 "});
7289
7290 cx.lsp
7291 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
7292 Ok(Some(vec![lsp::TextEdit::new(
7293 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
7294 "PREFIX ".to_string(),
7295 )]))
7296 });
7297
7298 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
7299 .unwrap()
7300 .await
7301 .unwrap();
7302
7303 cx.assert_editor_state(indoc! {"
7304 PREFIX line 1
7305 line 2
7306 linXe 3
7307 line 4
7308 liˇne 5
7309 "});
7310
7311 // Undo formatting
7312 cx.update_editor(|editor, window, cx| {
7313 editor.undo(&Default::default(), window, cx);
7314 });
7315
7316 // Verify cursor moved back to position after edit
7317 cx.assert_editor_state(indoc! {"
7318 line 1
7319 line 2
7320 linXˇe 3
7321 line 4
7322 line 5
7323 "});
7324}
7325
7326#[gpui::test]
7327async fn test_undo_inline_completion_scrolls_to_edit_pos(cx: &mut TestAppContext) {
7328 init_test(cx, |_| {});
7329
7330 let mut cx = EditorTestContext::new(cx).await;
7331
7332 let provider = cx.new(|_| FakeInlineCompletionProvider::default());
7333 cx.update_editor(|editor, window, cx| {
7334 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
7335 });
7336
7337 cx.set_state(indoc! {"
7338 line 1
7339 line 2
7340 linˇe 3
7341 line 4
7342 line 5
7343 line 6
7344 line 7
7345 line 8
7346 line 9
7347 line 10
7348 "});
7349
7350 let snapshot = cx.buffer_snapshot();
7351 let edit_position = snapshot.anchor_after(Point::new(2, 4));
7352
7353 cx.update(|_, cx| {
7354 provider.update(cx, |provider, _| {
7355 provider.set_inline_completion(Some(inline_completion::InlineCompletion {
7356 id: None,
7357 edits: vec![(edit_position..edit_position, "X".into())],
7358 edit_preview: None,
7359 }))
7360 })
7361 });
7362
7363 cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx));
7364 cx.update_editor(|editor, window, cx| {
7365 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
7366 });
7367
7368 cx.assert_editor_state(indoc! {"
7369 line 1
7370 line 2
7371 lineXˇ 3
7372 line 4
7373 line 5
7374 line 6
7375 line 7
7376 line 8
7377 line 9
7378 line 10
7379 "});
7380
7381 cx.update_editor(|editor, window, cx| {
7382 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7383 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
7384 });
7385 });
7386
7387 cx.assert_editor_state(indoc! {"
7388 line 1
7389 line 2
7390 lineX 3
7391 line 4
7392 line 5
7393 line 6
7394 line 7
7395 line 8
7396 line 9
7397 liˇne 10
7398 "});
7399
7400 cx.update_editor(|editor, window, cx| {
7401 editor.undo(&Default::default(), window, cx);
7402 });
7403
7404 cx.assert_editor_state(indoc! {"
7405 line 1
7406 line 2
7407 lineˇ 3
7408 line 4
7409 line 5
7410 line 6
7411 line 7
7412 line 8
7413 line 9
7414 line 10
7415 "});
7416}
7417
7418#[gpui::test]
7419async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
7420 init_test(cx, |_| {});
7421
7422 let mut cx = EditorTestContext::new(cx).await;
7423 cx.set_state(
7424 r#"let foo = 2;
7425lˇet foo = 2;
7426let fooˇ = 2;
7427let foo = 2;
7428let foo = ˇ2;"#,
7429 );
7430
7431 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7432 .unwrap();
7433 cx.assert_editor_state(
7434 r#"let foo = 2;
7435«letˇ» foo = 2;
7436let «fooˇ» = 2;
7437let foo = 2;
7438let foo = «2ˇ»;"#,
7439 );
7440
7441 // noop for multiple selections with different contents
7442 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7443 .unwrap();
7444 cx.assert_editor_state(
7445 r#"let foo = 2;
7446«letˇ» foo = 2;
7447let «fooˇ» = 2;
7448let foo = 2;
7449let foo = «2ˇ»;"#,
7450 );
7451
7452 // Test last selection direction should be preserved
7453 cx.set_state(
7454 r#"let foo = 2;
7455let foo = 2;
7456let «fooˇ» = 2;
7457let «ˇfoo» = 2;
7458let foo = 2;"#,
7459 );
7460
7461 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7462 .unwrap();
7463 cx.assert_editor_state(
7464 r#"let foo = 2;
7465let foo = 2;
7466let «fooˇ» = 2;
7467let «ˇfoo» = 2;
7468let «ˇfoo» = 2;"#,
7469 );
7470}
7471
7472#[gpui::test]
7473async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
7474 init_test(cx, |_| {});
7475
7476 let mut cx =
7477 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
7478
7479 cx.assert_editor_state(indoc! {"
7480 ˇbbb
7481 ccc
7482
7483 bbb
7484 ccc
7485 "});
7486 cx.dispatch_action(SelectPrevious::default());
7487 cx.assert_editor_state(indoc! {"
7488 «bbbˇ»
7489 ccc
7490
7491 bbb
7492 ccc
7493 "});
7494 cx.dispatch_action(SelectPrevious::default());
7495 cx.assert_editor_state(indoc! {"
7496 «bbbˇ»
7497 ccc
7498
7499 «bbbˇ»
7500 ccc
7501 "});
7502}
7503
7504#[gpui::test]
7505async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
7506 init_test(cx, |_| {});
7507
7508 let mut cx = EditorTestContext::new(cx).await;
7509 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
7510
7511 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7512 .unwrap();
7513 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7514
7515 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7516 .unwrap();
7517 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
7518
7519 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7520 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7521
7522 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7523 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
7524
7525 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7526 .unwrap();
7527 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
7528
7529 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7530 .unwrap();
7531 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7532}
7533
7534#[gpui::test]
7535async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
7536 init_test(cx, |_| {});
7537
7538 let mut cx = EditorTestContext::new(cx).await;
7539 cx.set_state("aˇ");
7540
7541 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7542 .unwrap();
7543 cx.assert_editor_state("«aˇ»");
7544 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7545 .unwrap();
7546 cx.assert_editor_state("«aˇ»");
7547}
7548
7549#[gpui::test]
7550async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
7551 init_test(cx, |_| {});
7552
7553 let mut cx = EditorTestContext::new(cx).await;
7554 cx.set_state(
7555 r#"let foo = 2;
7556lˇet foo = 2;
7557let fooˇ = 2;
7558let foo = 2;
7559let foo = ˇ2;"#,
7560 );
7561
7562 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7563 .unwrap();
7564 cx.assert_editor_state(
7565 r#"let foo = 2;
7566«letˇ» foo = 2;
7567let «fooˇ» = 2;
7568let foo = 2;
7569let foo = «2ˇ»;"#,
7570 );
7571
7572 // noop for multiple selections with different contents
7573 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7574 .unwrap();
7575 cx.assert_editor_state(
7576 r#"let foo = 2;
7577«letˇ» foo = 2;
7578let «fooˇ» = 2;
7579let foo = 2;
7580let foo = «2ˇ»;"#,
7581 );
7582}
7583
7584#[gpui::test]
7585async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
7586 init_test(cx, |_| {});
7587
7588 let mut cx = EditorTestContext::new(cx).await;
7589 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
7590
7591 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7592 .unwrap();
7593 // selection direction is preserved
7594 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
7595
7596 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7597 .unwrap();
7598 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
7599
7600 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7601 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
7602
7603 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7604 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
7605
7606 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7607 .unwrap();
7608 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
7609
7610 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7611 .unwrap();
7612 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
7613}
7614
7615#[gpui::test]
7616async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
7617 init_test(cx, |_| {});
7618
7619 let language = Arc::new(Language::new(
7620 LanguageConfig::default(),
7621 Some(tree_sitter_rust::LANGUAGE.into()),
7622 ));
7623
7624 let text = r#"
7625 use mod1::mod2::{mod3, mod4};
7626
7627 fn fn_1(param1: bool, param2: &str) {
7628 let var1 = "text";
7629 }
7630 "#
7631 .unindent();
7632
7633 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7634 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7635 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7636
7637 editor
7638 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7639 .await;
7640
7641 editor.update_in(cx, |editor, window, cx| {
7642 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7643 s.select_display_ranges([
7644 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
7645 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
7646 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
7647 ]);
7648 });
7649 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7650 });
7651 editor.update(cx, |editor, cx| {
7652 assert_text_with_selections(
7653 editor,
7654 indoc! {r#"
7655 use mod1::mod2::{mod3, «mod4ˇ»};
7656
7657 fn fn_1«ˇ(param1: bool, param2: &str)» {
7658 let var1 = "«ˇtext»";
7659 }
7660 "#},
7661 cx,
7662 );
7663 });
7664
7665 editor.update_in(cx, |editor, window, cx| {
7666 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7667 });
7668 editor.update(cx, |editor, cx| {
7669 assert_text_with_selections(
7670 editor,
7671 indoc! {r#"
7672 use mod1::mod2::«{mod3, mod4}ˇ»;
7673
7674 «ˇfn fn_1(param1: bool, param2: &str) {
7675 let var1 = "text";
7676 }»
7677 "#},
7678 cx,
7679 );
7680 });
7681
7682 editor.update_in(cx, |editor, window, cx| {
7683 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7684 });
7685 assert_eq!(
7686 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
7687 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
7688 );
7689
7690 // Trying to expand the selected syntax node one more time has no effect.
7691 editor.update_in(cx, |editor, window, cx| {
7692 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7693 });
7694 assert_eq!(
7695 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
7696 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
7697 );
7698
7699 editor.update_in(cx, |editor, window, cx| {
7700 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7701 });
7702 editor.update(cx, |editor, cx| {
7703 assert_text_with_selections(
7704 editor,
7705 indoc! {r#"
7706 use mod1::mod2::«{mod3, mod4}ˇ»;
7707
7708 «ˇfn fn_1(param1: bool, param2: &str) {
7709 let var1 = "text";
7710 }»
7711 "#},
7712 cx,
7713 );
7714 });
7715
7716 editor.update_in(cx, |editor, window, cx| {
7717 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7718 });
7719 editor.update(cx, |editor, cx| {
7720 assert_text_with_selections(
7721 editor,
7722 indoc! {r#"
7723 use mod1::mod2::{mod3, «mod4ˇ»};
7724
7725 fn fn_1«ˇ(param1: bool, param2: &str)» {
7726 let var1 = "«ˇtext»";
7727 }
7728 "#},
7729 cx,
7730 );
7731 });
7732
7733 editor.update_in(cx, |editor, window, cx| {
7734 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7735 });
7736 editor.update(cx, |editor, cx| {
7737 assert_text_with_selections(
7738 editor,
7739 indoc! {r#"
7740 use mod1::mod2::{mod3, mo«ˇ»d4};
7741
7742 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
7743 let var1 = "te«ˇ»xt";
7744 }
7745 "#},
7746 cx,
7747 );
7748 });
7749
7750 // Trying to shrink the selected syntax node one more time has no effect.
7751 editor.update_in(cx, |editor, window, cx| {
7752 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7753 });
7754 editor.update_in(cx, |editor, _, cx| {
7755 assert_text_with_selections(
7756 editor,
7757 indoc! {r#"
7758 use mod1::mod2::{mod3, mo«ˇ»d4};
7759
7760 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
7761 let var1 = "te«ˇ»xt";
7762 }
7763 "#},
7764 cx,
7765 );
7766 });
7767
7768 // Ensure that we keep expanding the selection if the larger selection starts or ends within
7769 // a fold.
7770 editor.update_in(cx, |editor, window, cx| {
7771 editor.fold_creases(
7772 vec![
7773 Crease::simple(
7774 Point::new(0, 21)..Point::new(0, 24),
7775 FoldPlaceholder::test(),
7776 ),
7777 Crease::simple(
7778 Point::new(3, 20)..Point::new(3, 22),
7779 FoldPlaceholder::test(),
7780 ),
7781 ],
7782 true,
7783 window,
7784 cx,
7785 );
7786 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7787 });
7788 editor.update(cx, |editor, cx| {
7789 assert_text_with_selections(
7790 editor,
7791 indoc! {r#"
7792 use mod1::mod2::«{mod3, mod4}ˇ»;
7793
7794 fn fn_1«ˇ(param1: bool, param2: &str)» {
7795 let var1 = "«ˇtext»";
7796 }
7797 "#},
7798 cx,
7799 );
7800 });
7801}
7802
7803#[gpui::test]
7804async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
7805 init_test(cx, |_| {});
7806
7807 let language = Arc::new(Language::new(
7808 LanguageConfig::default(),
7809 Some(tree_sitter_rust::LANGUAGE.into()),
7810 ));
7811
7812 let text = "let a = 2;";
7813
7814 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7815 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7816 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7817
7818 editor
7819 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7820 .await;
7821
7822 // Test case 1: Cursor at end of word
7823 editor.update_in(cx, |editor, window, cx| {
7824 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7825 s.select_display_ranges([
7826 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
7827 ]);
7828 });
7829 });
7830 editor.update(cx, |editor, cx| {
7831 assert_text_with_selections(editor, "let aˇ = 2;", cx);
7832 });
7833 editor.update_in(cx, |editor, window, cx| {
7834 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7835 });
7836 editor.update(cx, |editor, cx| {
7837 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
7838 });
7839 editor.update_in(cx, |editor, window, cx| {
7840 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7841 });
7842 editor.update(cx, |editor, cx| {
7843 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
7844 });
7845
7846 // Test case 2: Cursor at end of statement
7847 editor.update_in(cx, |editor, window, cx| {
7848 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7849 s.select_display_ranges([
7850 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
7851 ]);
7852 });
7853 });
7854 editor.update(cx, |editor, cx| {
7855 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
7856 });
7857 editor.update_in(cx, |editor, window, cx| {
7858 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7859 });
7860 editor.update(cx, |editor, cx| {
7861 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
7862 });
7863}
7864
7865#[gpui::test]
7866async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
7867 init_test(cx, |_| {});
7868
7869 let language = Arc::new(Language::new(
7870 LanguageConfig::default(),
7871 Some(tree_sitter_rust::LANGUAGE.into()),
7872 ));
7873
7874 let text = r#"
7875 use mod1::mod2::{mod3, mod4};
7876
7877 fn fn_1(param1: bool, param2: &str) {
7878 let var1 = "hello world";
7879 }
7880 "#
7881 .unindent();
7882
7883 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7884 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7885 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7886
7887 editor
7888 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7889 .await;
7890
7891 // Test 1: Cursor on a letter of a string word
7892 editor.update_in(cx, |editor, window, cx| {
7893 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7894 s.select_display_ranges([
7895 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
7896 ]);
7897 });
7898 });
7899 editor.update_in(cx, |editor, window, cx| {
7900 assert_text_with_selections(
7901 editor,
7902 indoc! {r#"
7903 use mod1::mod2::{mod3, mod4};
7904
7905 fn fn_1(param1: bool, param2: &str) {
7906 let var1 = "hˇello world";
7907 }
7908 "#},
7909 cx,
7910 );
7911 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7912 assert_text_with_selections(
7913 editor,
7914 indoc! {r#"
7915 use mod1::mod2::{mod3, mod4};
7916
7917 fn fn_1(param1: bool, param2: &str) {
7918 let var1 = "«ˇhello» world";
7919 }
7920 "#},
7921 cx,
7922 );
7923 });
7924
7925 // Test 2: Partial selection within a word
7926 editor.update_in(cx, |editor, window, cx| {
7927 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7928 s.select_display_ranges([
7929 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
7930 ]);
7931 });
7932 });
7933 editor.update_in(cx, |editor, window, cx| {
7934 assert_text_with_selections(
7935 editor,
7936 indoc! {r#"
7937 use mod1::mod2::{mod3, mod4};
7938
7939 fn fn_1(param1: bool, param2: &str) {
7940 let var1 = "h«elˇ»lo world";
7941 }
7942 "#},
7943 cx,
7944 );
7945 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7946 assert_text_with_selections(
7947 editor,
7948 indoc! {r#"
7949 use mod1::mod2::{mod3, mod4};
7950
7951 fn fn_1(param1: bool, param2: &str) {
7952 let var1 = "«ˇhello» world";
7953 }
7954 "#},
7955 cx,
7956 );
7957 });
7958
7959 // Test 3: Complete word already selected
7960 editor.update_in(cx, |editor, window, cx| {
7961 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7962 s.select_display_ranges([
7963 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
7964 ]);
7965 });
7966 });
7967 editor.update_in(cx, |editor, window, cx| {
7968 assert_text_with_selections(
7969 editor,
7970 indoc! {r#"
7971 use mod1::mod2::{mod3, mod4};
7972
7973 fn fn_1(param1: bool, param2: &str) {
7974 let var1 = "«helloˇ» world";
7975 }
7976 "#},
7977 cx,
7978 );
7979 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7980 assert_text_with_selections(
7981 editor,
7982 indoc! {r#"
7983 use mod1::mod2::{mod3, mod4};
7984
7985 fn fn_1(param1: bool, param2: &str) {
7986 let var1 = "«hello worldˇ»";
7987 }
7988 "#},
7989 cx,
7990 );
7991 });
7992
7993 // Test 4: Selection spanning across words
7994 editor.update_in(cx, |editor, window, cx| {
7995 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7996 s.select_display_ranges([
7997 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
7998 ]);
7999 });
8000 });
8001 editor.update_in(cx, |editor, window, cx| {
8002 assert_text_with_selections(
8003 editor,
8004 indoc! {r#"
8005 use mod1::mod2::{mod3, mod4};
8006
8007 fn fn_1(param1: bool, param2: &str) {
8008 let var1 = "hel«lo woˇ»rld";
8009 }
8010 "#},
8011 cx,
8012 );
8013 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8014 assert_text_with_selections(
8015 editor,
8016 indoc! {r#"
8017 use mod1::mod2::{mod3, mod4};
8018
8019 fn fn_1(param1: bool, param2: &str) {
8020 let var1 = "«ˇhello world»";
8021 }
8022 "#},
8023 cx,
8024 );
8025 });
8026
8027 // Test 5: Expansion beyond string
8028 editor.update_in(cx, |editor, window, cx| {
8029 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8030 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8031 assert_text_with_selections(
8032 editor,
8033 indoc! {r#"
8034 use mod1::mod2::{mod3, mod4};
8035
8036 fn fn_1(param1: bool, param2: &str) {
8037 «ˇlet var1 = "hello world";»
8038 }
8039 "#},
8040 cx,
8041 );
8042 });
8043}
8044
8045#[gpui::test]
8046async fn test_fold_function_bodies(cx: &mut TestAppContext) {
8047 init_test(cx, |_| {});
8048
8049 let base_text = r#"
8050 impl A {
8051 // this is an uncommitted comment
8052
8053 fn b() {
8054 c();
8055 }
8056
8057 // this is another uncommitted comment
8058
8059 fn d() {
8060 // e
8061 // f
8062 }
8063 }
8064
8065 fn g() {
8066 // h
8067 }
8068 "#
8069 .unindent();
8070
8071 let text = r#"
8072 ˇimpl A {
8073
8074 fn b() {
8075 c();
8076 }
8077
8078 fn d() {
8079 // e
8080 // f
8081 }
8082 }
8083
8084 fn g() {
8085 // h
8086 }
8087 "#
8088 .unindent();
8089
8090 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
8091 cx.set_state(&text);
8092 cx.set_head_text(&base_text);
8093 cx.update_editor(|editor, window, cx| {
8094 editor.expand_all_diff_hunks(&Default::default(), window, cx);
8095 });
8096
8097 cx.assert_state_with_diff(
8098 "
8099 ˇimpl A {
8100 - // this is an uncommitted comment
8101
8102 fn b() {
8103 c();
8104 }
8105
8106 - // this is another uncommitted comment
8107 -
8108 fn d() {
8109 // e
8110 // f
8111 }
8112 }
8113
8114 fn g() {
8115 // h
8116 }
8117 "
8118 .unindent(),
8119 );
8120
8121 let expected_display_text = "
8122 impl A {
8123 // this is an uncommitted comment
8124
8125 fn b() {
8126 ⋯
8127 }
8128
8129 // this is another uncommitted comment
8130
8131 fn d() {
8132 ⋯
8133 }
8134 }
8135
8136 fn g() {
8137 ⋯
8138 }
8139 "
8140 .unindent();
8141
8142 cx.update_editor(|editor, window, cx| {
8143 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
8144 assert_eq!(editor.display_text(cx), expected_display_text);
8145 });
8146}
8147
8148#[gpui::test]
8149async fn test_autoindent(cx: &mut TestAppContext) {
8150 init_test(cx, |_| {});
8151
8152 let language = Arc::new(
8153 Language::new(
8154 LanguageConfig {
8155 brackets: BracketPairConfig {
8156 pairs: vec![
8157 BracketPair {
8158 start: "{".to_string(),
8159 end: "}".to_string(),
8160 close: false,
8161 surround: false,
8162 newline: true,
8163 },
8164 BracketPair {
8165 start: "(".to_string(),
8166 end: ")".to_string(),
8167 close: false,
8168 surround: false,
8169 newline: true,
8170 },
8171 ],
8172 ..Default::default()
8173 },
8174 ..Default::default()
8175 },
8176 Some(tree_sitter_rust::LANGUAGE.into()),
8177 )
8178 .with_indents_query(
8179 r#"
8180 (_ "(" ")" @end) @indent
8181 (_ "{" "}" @end) @indent
8182 "#,
8183 )
8184 .unwrap(),
8185 );
8186
8187 let text = "fn a() {}";
8188
8189 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8190 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8191 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8192 editor
8193 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8194 .await;
8195
8196 editor.update_in(cx, |editor, window, cx| {
8197 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8198 s.select_ranges([5..5, 8..8, 9..9])
8199 });
8200 editor.newline(&Newline, window, cx);
8201 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
8202 assert_eq!(
8203 editor.selections.ranges(cx),
8204 &[
8205 Point::new(1, 4)..Point::new(1, 4),
8206 Point::new(3, 4)..Point::new(3, 4),
8207 Point::new(5, 0)..Point::new(5, 0)
8208 ]
8209 );
8210 });
8211}
8212
8213#[gpui::test]
8214async fn test_autoindent_selections(cx: &mut TestAppContext) {
8215 init_test(cx, |_| {});
8216
8217 {
8218 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
8219 cx.set_state(indoc! {"
8220 impl A {
8221
8222 fn b() {}
8223
8224 «fn c() {
8225
8226 }ˇ»
8227 }
8228 "});
8229
8230 cx.update_editor(|editor, window, cx| {
8231 editor.autoindent(&Default::default(), window, cx);
8232 });
8233
8234 cx.assert_editor_state(indoc! {"
8235 impl A {
8236
8237 fn b() {}
8238
8239 «fn c() {
8240
8241 }ˇ»
8242 }
8243 "});
8244 }
8245
8246 {
8247 let mut cx = EditorTestContext::new_multibuffer(
8248 cx,
8249 [indoc! { "
8250 impl A {
8251 «
8252 // a
8253 fn b(){}
8254 »
8255 «
8256 }
8257 fn c(){}
8258 »
8259 "}],
8260 );
8261
8262 let buffer = cx.update_editor(|editor, _, cx| {
8263 let buffer = editor.buffer().update(cx, |buffer, _| {
8264 buffer.all_buffers().iter().next().unwrap().clone()
8265 });
8266 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
8267 buffer
8268 });
8269
8270 cx.run_until_parked();
8271 cx.update_editor(|editor, window, cx| {
8272 editor.select_all(&Default::default(), window, cx);
8273 editor.autoindent(&Default::default(), window, cx)
8274 });
8275 cx.run_until_parked();
8276
8277 cx.update(|_, cx| {
8278 assert_eq!(
8279 buffer.read(cx).text(),
8280 indoc! { "
8281 impl A {
8282
8283 // a
8284 fn b(){}
8285
8286
8287 }
8288 fn c(){}
8289
8290 " }
8291 )
8292 });
8293 }
8294}
8295
8296#[gpui::test]
8297async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
8298 init_test(cx, |_| {});
8299
8300 let mut cx = EditorTestContext::new(cx).await;
8301
8302 let language = Arc::new(Language::new(
8303 LanguageConfig {
8304 brackets: BracketPairConfig {
8305 pairs: vec![
8306 BracketPair {
8307 start: "{".to_string(),
8308 end: "}".to_string(),
8309 close: true,
8310 surround: true,
8311 newline: true,
8312 },
8313 BracketPair {
8314 start: "(".to_string(),
8315 end: ")".to_string(),
8316 close: true,
8317 surround: true,
8318 newline: true,
8319 },
8320 BracketPair {
8321 start: "/*".to_string(),
8322 end: " */".to_string(),
8323 close: true,
8324 surround: true,
8325 newline: true,
8326 },
8327 BracketPair {
8328 start: "[".to_string(),
8329 end: "]".to_string(),
8330 close: false,
8331 surround: false,
8332 newline: true,
8333 },
8334 BracketPair {
8335 start: "\"".to_string(),
8336 end: "\"".to_string(),
8337 close: true,
8338 surround: true,
8339 newline: false,
8340 },
8341 BracketPair {
8342 start: "<".to_string(),
8343 end: ">".to_string(),
8344 close: false,
8345 surround: true,
8346 newline: true,
8347 },
8348 ],
8349 ..Default::default()
8350 },
8351 autoclose_before: "})]".to_string(),
8352 ..Default::default()
8353 },
8354 Some(tree_sitter_rust::LANGUAGE.into()),
8355 ));
8356
8357 cx.language_registry().add(language.clone());
8358 cx.update_buffer(|buffer, cx| {
8359 buffer.set_language(Some(language), cx);
8360 });
8361
8362 cx.set_state(
8363 &r#"
8364 🏀ˇ
8365 εˇ
8366 ❤️ˇ
8367 "#
8368 .unindent(),
8369 );
8370
8371 // autoclose multiple nested brackets at multiple cursors
8372 cx.update_editor(|editor, window, cx| {
8373 editor.handle_input("{", window, cx);
8374 editor.handle_input("{", window, cx);
8375 editor.handle_input("{", window, cx);
8376 });
8377 cx.assert_editor_state(
8378 &"
8379 🏀{{{ˇ}}}
8380 ε{{{ˇ}}}
8381 ❤️{{{ˇ}}}
8382 "
8383 .unindent(),
8384 );
8385
8386 // insert a different closing bracket
8387 cx.update_editor(|editor, window, cx| {
8388 editor.handle_input(")", window, cx);
8389 });
8390 cx.assert_editor_state(
8391 &"
8392 🏀{{{)ˇ}}}
8393 ε{{{)ˇ}}}
8394 ❤️{{{)ˇ}}}
8395 "
8396 .unindent(),
8397 );
8398
8399 // skip over the auto-closed brackets when typing a closing bracket
8400 cx.update_editor(|editor, window, cx| {
8401 editor.move_right(&MoveRight, window, cx);
8402 editor.handle_input("}", window, cx);
8403 editor.handle_input("}", window, cx);
8404 editor.handle_input("}", window, cx);
8405 });
8406 cx.assert_editor_state(
8407 &"
8408 🏀{{{)}}}}ˇ
8409 ε{{{)}}}}ˇ
8410 ❤️{{{)}}}}ˇ
8411 "
8412 .unindent(),
8413 );
8414
8415 // autoclose multi-character pairs
8416 cx.set_state(
8417 &"
8418 ˇ
8419 ˇ
8420 "
8421 .unindent(),
8422 );
8423 cx.update_editor(|editor, window, cx| {
8424 editor.handle_input("/", window, cx);
8425 editor.handle_input("*", window, cx);
8426 });
8427 cx.assert_editor_state(
8428 &"
8429 /*ˇ */
8430 /*ˇ */
8431 "
8432 .unindent(),
8433 );
8434
8435 // one cursor autocloses a multi-character pair, one cursor
8436 // does not autoclose.
8437 cx.set_state(
8438 &"
8439 /ˇ
8440 ˇ
8441 "
8442 .unindent(),
8443 );
8444 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
8445 cx.assert_editor_state(
8446 &"
8447 /*ˇ */
8448 *ˇ
8449 "
8450 .unindent(),
8451 );
8452
8453 // Don't autoclose if the next character isn't whitespace and isn't
8454 // listed in the language's "autoclose_before" section.
8455 cx.set_state("ˇa b");
8456 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8457 cx.assert_editor_state("{ˇa b");
8458
8459 // Don't autoclose if `close` is false for the bracket pair
8460 cx.set_state("ˇ");
8461 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
8462 cx.assert_editor_state("[ˇ");
8463
8464 // Surround with brackets if text is selected
8465 cx.set_state("«aˇ» b");
8466 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8467 cx.assert_editor_state("{«aˇ»} b");
8468
8469 // Autoclose when not immediately after a word character
8470 cx.set_state("a ˇ");
8471 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8472 cx.assert_editor_state("a \"ˇ\"");
8473
8474 // Autoclose pair where the start and end characters are the same
8475 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8476 cx.assert_editor_state("a \"\"ˇ");
8477
8478 // Don't autoclose when immediately after a word character
8479 cx.set_state("aˇ");
8480 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8481 cx.assert_editor_state("a\"ˇ");
8482
8483 // Do autoclose when after a non-word character
8484 cx.set_state("{ˇ");
8485 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8486 cx.assert_editor_state("{\"ˇ\"");
8487
8488 // Non identical pairs autoclose regardless of preceding character
8489 cx.set_state("aˇ");
8490 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8491 cx.assert_editor_state("a{ˇ}");
8492
8493 // Don't autoclose pair if autoclose is disabled
8494 cx.set_state("ˇ");
8495 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
8496 cx.assert_editor_state("<ˇ");
8497
8498 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
8499 cx.set_state("«aˇ» b");
8500 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
8501 cx.assert_editor_state("<«aˇ»> b");
8502}
8503
8504#[gpui::test]
8505async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
8506 init_test(cx, |settings| {
8507 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
8508 });
8509
8510 let mut cx = EditorTestContext::new(cx).await;
8511
8512 let language = Arc::new(Language::new(
8513 LanguageConfig {
8514 brackets: BracketPairConfig {
8515 pairs: vec![
8516 BracketPair {
8517 start: "{".to_string(),
8518 end: "}".to_string(),
8519 close: true,
8520 surround: true,
8521 newline: true,
8522 },
8523 BracketPair {
8524 start: "(".to_string(),
8525 end: ")".to_string(),
8526 close: true,
8527 surround: true,
8528 newline: true,
8529 },
8530 BracketPair {
8531 start: "[".to_string(),
8532 end: "]".to_string(),
8533 close: false,
8534 surround: false,
8535 newline: true,
8536 },
8537 ],
8538 ..Default::default()
8539 },
8540 autoclose_before: "})]".to_string(),
8541 ..Default::default()
8542 },
8543 Some(tree_sitter_rust::LANGUAGE.into()),
8544 ));
8545
8546 cx.language_registry().add(language.clone());
8547 cx.update_buffer(|buffer, cx| {
8548 buffer.set_language(Some(language), cx);
8549 });
8550
8551 cx.set_state(
8552 &"
8553 ˇ
8554 ˇ
8555 ˇ
8556 "
8557 .unindent(),
8558 );
8559
8560 // ensure only matching closing brackets are skipped over
8561 cx.update_editor(|editor, window, cx| {
8562 editor.handle_input("}", window, cx);
8563 editor.move_left(&MoveLeft, window, cx);
8564 editor.handle_input(")", window, cx);
8565 editor.move_left(&MoveLeft, window, cx);
8566 });
8567 cx.assert_editor_state(
8568 &"
8569 ˇ)}
8570 ˇ)}
8571 ˇ)}
8572 "
8573 .unindent(),
8574 );
8575
8576 // skip-over closing brackets at multiple cursors
8577 cx.update_editor(|editor, window, cx| {
8578 editor.handle_input(")", window, cx);
8579 editor.handle_input("}", window, cx);
8580 });
8581 cx.assert_editor_state(
8582 &"
8583 )}ˇ
8584 )}ˇ
8585 )}ˇ
8586 "
8587 .unindent(),
8588 );
8589
8590 // ignore non-close brackets
8591 cx.update_editor(|editor, window, cx| {
8592 editor.handle_input("]", window, cx);
8593 editor.move_left(&MoveLeft, window, cx);
8594 editor.handle_input("]", window, cx);
8595 });
8596 cx.assert_editor_state(
8597 &"
8598 )}]ˇ]
8599 )}]ˇ]
8600 )}]ˇ]
8601 "
8602 .unindent(),
8603 );
8604}
8605
8606#[gpui::test]
8607async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
8608 init_test(cx, |_| {});
8609
8610 let mut cx = EditorTestContext::new(cx).await;
8611
8612 let html_language = Arc::new(
8613 Language::new(
8614 LanguageConfig {
8615 name: "HTML".into(),
8616 brackets: BracketPairConfig {
8617 pairs: vec![
8618 BracketPair {
8619 start: "<".into(),
8620 end: ">".into(),
8621 close: true,
8622 ..Default::default()
8623 },
8624 BracketPair {
8625 start: "{".into(),
8626 end: "}".into(),
8627 close: true,
8628 ..Default::default()
8629 },
8630 BracketPair {
8631 start: "(".into(),
8632 end: ")".into(),
8633 close: true,
8634 ..Default::default()
8635 },
8636 ],
8637 ..Default::default()
8638 },
8639 autoclose_before: "})]>".into(),
8640 ..Default::default()
8641 },
8642 Some(tree_sitter_html::LANGUAGE.into()),
8643 )
8644 .with_injection_query(
8645 r#"
8646 (script_element
8647 (raw_text) @injection.content
8648 (#set! injection.language "javascript"))
8649 "#,
8650 )
8651 .unwrap(),
8652 );
8653
8654 let javascript_language = Arc::new(Language::new(
8655 LanguageConfig {
8656 name: "JavaScript".into(),
8657 brackets: BracketPairConfig {
8658 pairs: vec![
8659 BracketPair {
8660 start: "/*".into(),
8661 end: " */".into(),
8662 close: true,
8663 ..Default::default()
8664 },
8665 BracketPair {
8666 start: "{".into(),
8667 end: "}".into(),
8668 close: true,
8669 ..Default::default()
8670 },
8671 BracketPair {
8672 start: "(".into(),
8673 end: ")".into(),
8674 close: true,
8675 ..Default::default()
8676 },
8677 ],
8678 ..Default::default()
8679 },
8680 autoclose_before: "})]>".into(),
8681 ..Default::default()
8682 },
8683 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
8684 ));
8685
8686 cx.language_registry().add(html_language.clone());
8687 cx.language_registry().add(javascript_language.clone());
8688
8689 cx.update_buffer(|buffer, cx| {
8690 buffer.set_language(Some(html_language), cx);
8691 });
8692
8693 cx.set_state(
8694 &r#"
8695 <body>ˇ
8696 <script>
8697 var x = 1;ˇ
8698 </script>
8699 </body>ˇ
8700 "#
8701 .unindent(),
8702 );
8703
8704 // Precondition: different languages are active at different locations.
8705 cx.update_editor(|editor, window, cx| {
8706 let snapshot = editor.snapshot(window, cx);
8707 let cursors = editor.selections.ranges::<usize>(cx);
8708 let languages = cursors
8709 .iter()
8710 .map(|c| snapshot.language_at(c.start).unwrap().name())
8711 .collect::<Vec<_>>();
8712 assert_eq!(
8713 languages,
8714 &["HTML".into(), "JavaScript".into(), "HTML".into()]
8715 );
8716 });
8717
8718 // Angle brackets autoclose in HTML, but not JavaScript.
8719 cx.update_editor(|editor, window, cx| {
8720 editor.handle_input("<", window, cx);
8721 editor.handle_input("a", window, cx);
8722 });
8723 cx.assert_editor_state(
8724 &r#"
8725 <body><aˇ>
8726 <script>
8727 var x = 1;<aˇ
8728 </script>
8729 </body><aˇ>
8730 "#
8731 .unindent(),
8732 );
8733
8734 // Curly braces and parens autoclose in both HTML and JavaScript.
8735 cx.update_editor(|editor, window, cx| {
8736 editor.handle_input(" b=", window, cx);
8737 editor.handle_input("{", window, cx);
8738 editor.handle_input("c", window, cx);
8739 editor.handle_input("(", window, cx);
8740 });
8741 cx.assert_editor_state(
8742 &r#"
8743 <body><a b={c(ˇ)}>
8744 <script>
8745 var x = 1;<a b={c(ˇ)}
8746 </script>
8747 </body><a b={c(ˇ)}>
8748 "#
8749 .unindent(),
8750 );
8751
8752 // Brackets that were already autoclosed are skipped.
8753 cx.update_editor(|editor, window, cx| {
8754 editor.handle_input(")", window, cx);
8755 editor.handle_input("d", window, cx);
8756 editor.handle_input("}", window, cx);
8757 });
8758 cx.assert_editor_state(
8759 &r#"
8760 <body><a b={c()d}ˇ>
8761 <script>
8762 var x = 1;<a b={c()d}ˇ
8763 </script>
8764 </body><a b={c()d}ˇ>
8765 "#
8766 .unindent(),
8767 );
8768 cx.update_editor(|editor, window, cx| {
8769 editor.handle_input(">", window, cx);
8770 });
8771 cx.assert_editor_state(
8772 &r#"
8773 <body><a b={c()d}>ˇ
8774 <script>
8775 var x = 1;<a b={c()d}>ˇ
8776 </script>
8777 </body><a b={c()d}>ˇ
8778 "#
8779 .unindent(),
8780 );
8781
8782 // Reset
8783 cx.set_state(
8784 &r#"
8785 <body>ˇ
8786 <script>
8787 var x = 1;ˇ
8788 </script>
8789 </body>ˇ
8790 "#
8791 .unindent(),
8792 );
8793
8794 cx.update_editor(|editor, window, cx| {
8795 editor.handle_input("<", window, cx);
8796 });
8797 cx.assert_editor_state(
8798 &r#"
8799 <body><ˇ>
8800 <script>
8801 var x = 1;<ˇ
8802 </script>
8803 </body><ˇ>
8804 "#
8805 .unindent(),
8806 );
8807
8808 // When backspacing, the closing angle brackets are removed.
8809 cx.update_editor(|editor, window, cx| {
8810 editor.backspace(&Backspace, window, cx);
8811 });
8812 cx.assert_editor_state(
8813 &r#"
8814 <body>ˇ
8815 <script>
8816 var x = 1;ˇ
8817 </script>
8818 </body>ˇ
8819 "#
8820 .unindent(),
8821 );
8822
8823 // Block comments autoclose in JavaScript, but not HTML.
8824 cx.update_editor(|editor, window, cx| {
8825 editor.handle_input("/", window, cx);
8826 editor.handle_input("*", window, cx);
8827 });
8828 cx.assert_editor_state(
8829 &r#"
8830 <body>/*ˇ
8831 <script>
8832 var x = 1;/*ˇ */
8833 </script>
8834 </body>/*ˇ
8835 "#
8836 .unindent(),
8837 );
8838}
8839
8840#[gpui::test]
8841async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
8842 init_test(cx, |_| {});
8843
8844 let mut cx = EditorTestContext::new(cx).await;
8845
8846 let rust_language = Arc::new(
8847 Language::new(
8848 LanguageConfig {
8849 name: "Rust".into(),
8850 brackets: serde_json::from_value(json!([
8851 { "start": "{", "end": "}", "close": true, "newline": true },
8852 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
8853 ]))
8854 .unwrap(),
8855 autoclose_before: "})]>".into(),
8856 ..Default::default()
8857 },
8858 Some(tree_sitter_rust::LANGUAGE.into()),
8859 )
8860 .with_override_query("(string_literal) @string")
8861 .unwrap(),
8862 );
8863
8864 cx.language_registry().add(rust_language.clone());
8865 cx.update_buffer(|buffer, cx| {
8866 buffer.set_language(Some(rust_language), cx);
8867 });
8868
8869 cx.set_state(
8870 &r#"
8871 let x = ˇ
8872 "#
8873 .unindent(),
8874 );
8875
8876 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
8877 cx.update_editor(|editor, window, cx| {
8878 editor.handle_input("\"", window, cx);
8879 });
8880 cx.assert_editor_state(
8881 &r#"
8882 let x = "ˇ"
8883 "#
8884 .unindent(),
8885 );
8886
8887 // Inserting another quotation mark. The cursor moves across the existing
8888 // automatically-inserted quotation mark.
8889 cx.update_editor(|editor, window, cx| {
8890 editor.handle_input("\"", window, cx);
8891 });
8892 cx.assert_editor_state(
8893 &r#"
8894 let x = ""ˇ
8895 "#
8896 .unindent(),
8897 );
8898
8899 // Reset
8900 cx.set_state(
8901 &r#"
8902 let x = ˇ
8903 "#
8904 .unindent(),
8905 );
8906
8907 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
8908 cx.update_editor(|editor, window, cx| {
8909 editor.handle_input("\"", window, cx);
8910 editor.handle_input(" ", window, cx);
8911 editor.move_left(&Default::default(), window, cx);
8912 editor.handle_input("\\", window, cx);
8913 editor.handle_input("\"", window, cx);
8914 });
8915 cx.assert_editor_state(
8916 &r#"
8917 let x = "\"ˇ "
8918 "#
8919 .unindent(),
8920 );
8921
8922 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
8923 // mark. Nothing is inserted.
8924 cx.update_editor(|editor, window, cx| {
8925 editor.move_right(&Default::default(), window, cx);
8926 editor.handle_input("\"", window, cx);
8927 });
8928 cx.assert_editor_state(
8929 &r#"
8930 let x = "\" "ˇ
8931 "#
8932 .unindent(),
8933 );
8934}
8935
8936#[gpui::test]
8937async fn test_surround_with_pair(cx: &mut TestAppContext) {
8938 init_test(cx, |_| {});
8939
8940 let language = Arc::new(Language::new(
8941 LanguageConfig {
8942 brackets: BracketPairConfig {
8943 pairs: vec![
8944 BracketPair {
8945 start: "{".to_string(),
8946 end: "}".to_string(),
8947 close: true,
8948 surround: true,
8949 newline: true,
8950 },
8951 BracketPair {
8952 start: "/* ".to_string(),
8953 end: "*/".to_string(),
8954 close: true,
8955 surround: true,
8956 ..Default::default()
8957 },
8958 ],
8959 ..Default::default()
8960 },
8961 ..Default::default()
8962 },
8963 Some(tree_sitter_rust::LANGUAGE.into()),
8964 ));
8965
8966 let text = r#"
8967 a
8968 b
8969 c
8970 "#
8971 .unindent();
8972
8973 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8974 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8975 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8976 editor
8977 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8978 .await;
8979
8980 editor.update_in(cx, |editor, window, cx| {
8981 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8982 s.select_display_ranges([
8983 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8984 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8985 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
8986 ])
8987 });
8988
8989 editor.handle_input("{", window, cx);
8990 editor.handle_input("{", window, cx);
8991 editor.handle_input("{", window, cx);
8992 assert_eq!(
8993 editor.text(cx),
8994 "
8995 {{{a}}}
8996 {{{b}}}
8997 {{{c}}}
8998 "
8999 .unindent()
9000 );
9001 assert_eq!(
9002 editor.selections.display_ranges(cx),
9003 [
9004 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
9005 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
9006 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
9007 ]
9008 );
9009
9010 editor.undo(&Undo, window, cx);
9011 editor.undo(&Undo, window, cx);
9012 editor.undo(&Undo, window, cx);
9013 assert_eq!(
9014 editor.text(cx),
9015 "
9016 a
9017 b
9018 c
9019 "
9020 .unindent()
9021 );
9022 assert_eq!(
9023 editor.selections.display_ranges(cx),
9024 [
9025 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
9026 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
9027 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
9028 ]
9029 );
9030
9031 // Ensure inserting the first character of a multi-byte bracket pair
9032 // doesn't surround the selections with the bracket.
9033 editor.handle_input("/", window, cx);
9034 assert_eq!(
9035 editor.text(cx),
9036 "
9037 /
9038 /
9039 /
9040 "
9041 .unindent()
9042 );
9043 assert_eq!(
9044 editor.selections.display_ranges(cx),
9045 [
9046 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
9047 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
9048 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
9049 ]
9050 );
9051
9052 editor.undo(&Undo, window, cx);
9053 assert_eq!(
9054 editor.text(cx),
9055 "
9056 a
9057 b
9058 c
9059 "
9060 .unindent()
9061 );
9062 assert_eq!(
9063 editor.selections.display_ranges(cx),
9064 [
9065 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
9066 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
9067 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
9068 ]
9069 );
9070
9071 // Ensure inserting the last character of a multi-byte bracket pair
9072 // doesn't surround the selections with the bracket.
9073 editor.handle_input("*", window, cx);
9074 assert_eq!(
9075 editor.text(cx),
9076 "
9077 *
9078 *
9079 *
9080 "
9081 .unindent()
9082 );
9083 assert_eq!(
9084 editor.selections.display_ranges(cx),
9085 [
9086 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
9087 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
9088 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
9089 ]
9090 );
9091 });
9092}
9093
9094#[gpui::test]
9095async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
9096 init_test(cx, |_| {});
9097
9098 let language = Arc::new(Language::new(
9099 LanguageConfig {
9100 brackets: BracketPairConfig {
9101 pairs: vec![BracketPair {
9102 start: "{".to_string(),
9103 end: "}".to_string(),
9104 close: true,
9105 surround: true,
9106 newline: true,
9107 }],
9108 ..Default::default()
9109 },
9110 autoclose_before: "}".to_string(),
9111 ..Default::default()
9112 },
9113 Some(tree_sitter_rust::LANGUAGE.into()),
9114 ));
9115
9116 let text = r#"
9117 a
9118 b
9119 c
9120 "#
9121 .unindent();
9122
9123 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9124 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9125 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9126 editor
9127 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9128 .await;
9129
9130 editor.update_in(cx, |editor, window, cx| {
9131 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9132 s.select_ranges([
9133 Point::new(0, 1)..Point::new(0, 1),
9134 Point::new(1, 1)..Point::new(1, 1),
9135 Point::new(2, 1)..Point::new(2, 1),
9136 ])
9137 });
9138
9139 editor.handle_input("{", window, cx);
9140 editor.handle_input("{", window, cx);
9141 editor.handle_input("_", window, cx);
9142 assert_eq!(
9143 editor.text(cx),
9144 "
9145 a{{_}}
9146 b{{_}}
9147 c{{_}}
9148 "
9149 .unindent()
9150 );
9151 assert_eq!(
9152 editor.selections.ranges::<Point>(cx),
9153 [
9154 Point::new(0, 4)..Point::new(0, 4),
9155 Point::new(1, 4)..Point::new(1, 4),
9156 Point::new(2, 4)..Point::new(2, 4)
9157 ]
9158 );
9159
9160 editor.backspace(&Default::default(), window, cx);
9161 editor.backspace(&Default::default(), window, cx);
9162 assert_eq!(
9163 editor.text(cx),
9164 "
9165 a{}
9166 b{}
9167 c{}
9168 "
9169 .unindent()
9170 );
9171 assert_eq!(
9172 editor.selections.ranges::<Point>(cx),
9173 [
9174 Point::new(0, 2)..Point::new(0, 2),
9175 Point::new(1, 2)..Point::new(1, 2),
9176 Point::new(2, 2)..Point::new(2, 2)
9177 ]
9178 );
9179
9180 editor.delete_to_previous_word_start(&Default::default(), window, cx);
9181 assert_eq!(
9182 editor.text(cx),
9183 "
9184 a
9185 b
9186 c
9187 "
9188 .unindent()
9189 );
9190 assert_eq!(
9191 editor.selections.ranges::<Point>(cx),
9192 [
9193 Point::new(0, 1)..Point::new(0, 1),
9194 Point::new(1, 1)..Point::new(1, 1),
9195 Point::new(2, 1)..Point::new(2, 1)
9196 ]
9197 );
9198 });
9199}
9200
9201#[gpui::test]
9202async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
9203 init_test(cx, |settings| {
9204 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
9205 });
9206
9207 let mut cx = EditorTestContext::new(cx).await;
9208
9209 let language = Arc::new(Language::new(
9210 LanguageConfig {
9211 brackets: BracketPairConfig {
9212 pairs: vec![
9213 BracketPair {
9214 start: "{".to_string(),
9215 end: "}".to_string(),
9216 close: true,
9217 surround: true,
9218 newline: true,
9219 },
9220 BracketPair {
9221 start: "(".to_string(),
9222 end: ")".to_string(),
9223 close: true,
9224 surround: true,
9225 newline: true,
9226 },
9227 BracketPair {
9228 start: "[".to_string(),
9229 end: "]".to_string(),
9230 close: false,
9231 surround: true,
9232 newline: true,
9233 },
9234 ],
9235 ..Default::default()
9236 },
9237 autoclose_before: "})]".to_string(),
9238 ..Default::default()
9239 },
9240 Some(tree_sitter_rust::LANGUAGE.into()),
9241 ));
9242
9243 cx.language_registry().add(language.clone());
9244 cx.update_buffer(|buffer, cx| {
9245 buffer.set_language(Some(language), cx);
9246 });
9247
9248 cx.set_state(
9249 &"
9250 {(ˇ)}
9251 [[ˇ]]
9252 {(ˇ)}
9253 "
9254 .unindent(),
9255 );
9256
9257 cx.update_editor(|editor, window, cx| {
9258 editor.backspace(&Default::default(), window, cx);
9259 editor.backspace(&Default::default(), window, cx);
9260 });
9261
9262 cx.assert_editor_state(
9263 &"
9264 ˇ
9265 ˇ]]
9266 ˇ
9267 "
9268 .unindent(),
9269 );
9270
9271 cx.update_editor(|editor, window, cx| {
9272 editor.handle_input("{", window, cx);
9273 editor.handle_input("{", window, cx);
9274 editor.move_right(&MoveRight, window, cx);
9275 editor.move_right(&MoveRight, window, cx);
9276 editor.move_left(&MoveLeft, window, cx);
9277 editor.move_left(&MoveLeft, window, cx);
9278 editor.backspace(&Default::default(), window, cx);
9279 });
9280
9281 cx.assert_editor_state(
9282 &"
9283 {ˇ}
9284 {ˇ}]]
9285 {ˇ}
9286 "
9287 .unindent(),
9288 );
9289
9290 cx.update_editor(|editor, window, cx| {
9291 editor.backspace(&Default::default(), window, cx);
9292 });
9293
9294 cx.assert_editor_state(
9295 &"
9296 ˇ
9297 ˇ]]
9298 ˇ
9299 "
9300 .unindent(),
9301 );
9302}
9303
9304#[gpui::test]
9305async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
9306 init_test(cx, |_| {});
9307
9308 let language = Arc::new(Language::new(
9309 LanguageConfig::default(),
9310 Some(tree_sitter_rust::LANGUAGE.into()),
9311 ));
9312
9313 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
9314 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9315 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9316 editor
9317 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9318 .await;
9319
9320 editor.update_in(cx, |editor, window, cx| {
9321 editor.set_auto_replace_emoji_shortcode(true);
9322
9323 editor.handle_input("Hello ", window, cx);
9324 editor.handle_input(":wave", window, cx);
9325 assert_eq!(editor.text(cx), "Hello :wave".unindent());
9326
9327 editor.handle_input(":", window, cx);
9328 assert_eq!(editor.text(cx), "Hello 👋".unindent());
9329
9330 editor.handle_input(" :smile", window, cx);
9331 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
9332
9333 editor.handle_input(":", window, cx);
9334 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
9335
9336 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
9337 editor.handle_input(":wave", window, cx);
9338 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
9339
9340 editor.handle_input(":", window, cx);
9341 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
9342
9343 editor.handle_input(":1", window, cx);
9344 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
9345
9346 editor.handle_input(":", window, cx);
9347 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
9348
9349 // Ensure shortcode does not get replaced when it is part of a word
9350 editor.handle_input(" Test:wave", window, cx);
9351 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
9352
9353 editor.handle_input(":", window, cx);
9354 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
9355
9356 editor.set_auto_replace_emoji_shortcode(false);
9357
9358 // Ensure shortcode does not get replaced when auto replace is off
9359 editor.handle_input(" :wave", window, cx);
9360 assert_eq!(
9361 editor.text(cx),
9362 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
9363 );
9364
9365 editor.handle_input(":", window, cx);
9366 assert_eq!(
9367 editor.text(cx),
9368 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
9369 );
9370 });
9371}
9372
9373#[gpui::test]
9374async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
9375 init_test(cx, |_| {});
9376
9377 let (text, insertion_ranges) = marked_text_ranges(
9378 indoc! {"
9379 ˇ
9380 "},
9381 false,
9382 );
9383
9384 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
9385 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9386
9387 _ = editor.update_in(cx, |editor, window, cx| {
9388 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
9389
9390 editor
9391 .insert_snippet(&insertion_ranges, snippet, window, cx)
9392 .unwrap();
9393
9394 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
9395 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
9396 assert_eq!(editor.text(cx), expected_text);
9397 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
9398 }
9399
9400 assert(
9401 editor,
9402 cx,
9403 indoc! {"
9404 type «» =•
9405 "},
9406 );
9407
9408 assert!(editor.context_menu_visible(), "There should be a matches");
9409 });
9410}
9411
9412#[gpui::test]
9413async fn test_snippets(cx: &mut TestAppContext) {
9414 init_test(cx, |_| {});
9415
9416 let mut cx = EditorTestContext::new(cx).await;
9417
9418 cx.set_state(indoc! {"
9419 a.ˇ b
9420 a.ˇ b
9421 a.ˇ b
9422 "});
9423
9424 cx.update_editor(|editor, window, cx| {
9425 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
9426 let insertion_ranges = editor
9427 .selections
9428 .all(cx)
9429 .iter()
9430 .map(|s| s.range().clone())
9431 .collect::<Vec<_>>();
9432 editor
9433 .insert_snippet(&insertion_ranges, snippet, window, cx)
9434 .unwrap();
9435 });
9436
9437 cx.assert_editor_state(indoc! {"
9438 a.f(«oneˇ», two, «threeˇ») b
9439 a.f(«oneˇ», two, «threeˇ») b
9440 a.f(«oneˇ», two, «threeˇ») b
9441 "});
9442
9443 // Can't move earlier than the first tab stop
9444 cx.update_editor(|editor, window, cx| {
9445 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
9446 });
9447 cx.assert_editor_state(indoc! {"
9448 a.f(«oneˇ», two, «threeˇ») b
9449 a.f(«oneˇ», two, «threeˇ») b
9450 a.f(«oneˇ», two, «threeˇ») b
9451 "});
9452
9453 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9454 cx.assert_editor_state(indoc! {"
9455 a.f(one, «twoˇ», three) b
9456 a.f(one, «twoˇ», three) b
9457 a.f(one, «twoˇ», three) b
9458 "});
9459
9460 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
9461 cx.assert_editor_state(indoc! {"
9462 a.f(«oneˇ», two, «threeˇ») b
9463 a.f(«oneˇ», two, «threeˇ») b
9464 a.f(«oneˇ», two, «threeˇ») b
9465 "});
9466
9467 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9468 cx.assert_editor_state(indoc! {"
9469 a.f(one, «twoˇ», three) b
9470 a.f(one, «twoˇ», three) b
9471 a.f(one, «twoˇ», three) b
9472 "});
9473 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9474 cx.assert_editor_state(indoc! {"
9475 a.f(one, two, three)ˇ b
9476 a.f(one, two, three)ˇ b
9477 a.f(one, two, three)ˇ b
9478 "});
9479
9480 // As soon as the last tab stop is reached, snippet state is gone
9481 cx.update_editor(|editor, window, cx| {
9482 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
9483 });
9484 cx.assert_editor_state(indoc! {"
9485 a.f(one, two, three)ˇ b
9486 a.f(one, two, three)ˇ b
9487 a.f(one, two, three)ˇ b
9488 "});
9489}
9490
9491#[gpui::test]
9492async fn test_snippet_indentation(cx: &mut TestAppContext) {
9493 init_test(cx, |_| {});
9494
9495 let mut cx = EditorTestContext::new(cx).await;
9496
9497 cx.update_editor(|editor, window, cx| {
9498 let snippet = Snippet::parse(indoc! {"
9499 /*
9500 * Multiline comment with leading indentation
9501 *
9502 * $1
9503 */
9504 $0"})
9505 .unwrap();
9506 let insertion_ranges = editor
9507 .selections
9508 .all(cx)
9509 .iter()
9510 .map(|s| s.range().clone())
9511 .collect::<Vec<_>>();
9512 editor
9513 .insert_snippet(&insertion_ranges, snippet, window, cx)
9514 .unwrap();
9515 });
9516
9517 cx.assert_editor_state(indoc! {"
9518 /*
9519 * Multiline comment with leading indentation
9520 *
9521 * ˇ
9522 */
9523 "});
9524
9525 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9526 cx.assert_editor_state(indoc! {"
9527 /*
9528 * Multiline comment with leading indentation
9529 *
9530 *•
9531 */
9532 ˇ"});
9533}
9534
9535#[gpui::test]
9536async fn test_document_format_during_save(cx: &mut TestAppContext) {
9537 init_test(cx, |_| {});
9538
9539 let fs = FakeFs::new(cx.executor());
9540 fs.insert_file(path!("/file.rs"), Default::default()).await;
9541
9542 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
9543
9544 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9545 language_registry.add(rust_lang());
9546 let mut fake_servers = language_registry.register_fake_lsp(
9547 "Rust",
9548 FakeLspAdapter {
9549 capabilities: lsp::ServerCapabilities {
9550 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9551 ..Default::default()
9552 },
9553 ..Default::default()
9554 },
9555 );
9556
9557 let buffer = project
9558 .update(cx, |project, cx| {
9559 project.open_local_buffer(path!("/file.rs"), cx)
9560 })
9561 .await
9562 .unwrap();
9563
9564 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9565 let (editor, cx) = cx.add_window_view(|window, cx| {
9566 build_editor_with_project(project.clone(), buffer, window, cx)
9567 });
9568 editor.update_in(cx, |editor, window, cx| {
9569 editor.set_text("one\ntwo\nthree\n", window, cx)
9570 });
9571 assert!(cx.read(|cx| editor.is_dirty(cx)));
9572
9573 cx.executor().start_waiting();
9574 let fake_server = fake_servers.next().await.unwrap();
9575
9576 {
9577 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9578 move |params, _| async move {
9579 assert_eq!(
9580 params.text_document.uri,
9581 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9582 );
9583 assert_eq!(params.options.tab_size, 4);
9584 Ok(Some(vec![lsp::TextEdit::new(
9585 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9586 ", ".to_string(),
9587 )]))
9588 },
9589 );
9590 let save = editor
9591 .update_in(cx, |editor, window, cx| {
9592 editor.save(
9593 SaveOptions {
9594 format: true,
9595 autosave: false,
9596 },
9597 project.clone(),
9598 window,
9599 cx,
9600 )
9601 })
9602 .unwrap();
9603 cx.executor().start_waiting();
9604 save.await;
9605
9606 assert_eq!(
9607 editor.update(cx, |editor, cx| editor.text(cx)),
9608 "one, two\nthree\n"
9609 );
9610 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9611 }
9612
9613 {
9614 editor.update_in(cx, |editor, window, cx| {
9615 editor.set_text("one\ntwo\nthree\n", window, cx)
9616 });
9617 assert!(cx.read(|cx| editor.is_dirty(cx)));
9618
9619 // Ensure we can still save even if formatting hangs.
9620 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9621 move |params, _| async move {
9622 assert_eq!(
9623 params.text_document.uri,
9624 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9625 );
9626 futures::future::pending::<()>().await;
9627 unreachable!()
9628 },
9629 );
9630 let save = editor
9631 .update_in(cx, |editor, window, cx| {
9632 editor.save(
9633 SaveOptions {
9634 format: true,
9635 autosave: false,
9636 },
9637 project.clone(),
9638 window,
9639 cx,
9640 )
9641 })
9642 .unwrap();
9643 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9644 cx.executor().start_waiting();
9645 save.await;
9646 assert_eq!(
9647 editor.update(cx, |editor, cx| editor.text(cx)),
9648 "one\ntwo\nthree\n"
9649 );
9650 }
9651
9652 // Set rust language override and assert overridden tabsize is sent to language server
9653 update_test_language_settings(cx, |settings| {
9654 settings.languages.0.insert(
9655 "Rust".into(),
9656 LanguageSettingsContent {
9657 tab_size: NonZeroU32::new(8),
9658 ..Default::default()
9659 },
9660 );
9661 });
9662
9663 {
9664 editor.update_in(cx, |editor, window, cx| {
9665 editor.set_text("somehting_new\n", window, cx)
9666 });
9667 assert!(cx.read(|cx| editor.is_dirty(cx)));
9668 let _formatting_request_signal = fake_server
9669 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
9670 assert_eq!(
9671 params.text_document.uri,
9672 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9673 );
9674 assert_eq!(params.options.tab_size, 8);
9675 Ok(Some(vec![]))
9676 });
9677 let save = editor
9678 .update_in(cx, |editor, window, cx| {
9679 editor.save(
9680 SaveOptions {
9681 format: true,
9682 autosave: false,
9683 },
9684 project.clone(),
9685 window,
9686 cx,
9687 )
9688 })
9689 .unwrap();
9690 cx.executor().start_waiting();
9691 save.await;
9692 }
9693}
9694
9695#[gpui::test]
9696async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
9697 init_test(cx, |settings| {
9698 settings.defaults.ensure_final_newline_on_save = Some(false);
9699 });
9700
9701 let fs = FakeFs::new(cx.executor());
9702 fs.insert_file(path!("/file.txt"), "foo".into()).await;
9703
9704 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
9705
9706 let buffer = project
9707 .update(cx, |project, cx| {
9708 project.open_local_buffer(path!("/file.txt"), cx)
9709 })
9710 .await
9711 .unwrap();
9712
9713 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9714 let (editor, cx) = cx.add_window_view(|window, cx| {
9715 build_editor_with_project(project.clone(), buffer, window, cx)
9716 });
9717 editor.update_in(cx, |editor, window, cx| {
9718 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
9719 s.select_ranges([0..0])
9720 });
9721 });
9722 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9723
9724 editor.update_in(cx, |editor, window, cx| {
9725 editor.handle_input("\n", window, cx)
9726 });
9727 cx.run_until_parked();
9728 save(&editor, &project, cx).await;
9729 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
9730
9731 editor.update_in(cx, |editor, window, cx| {
9732 editor.undo(&Default::default(), window, cx);
9733 });
9734 save(&editor, &project, cx).await;
9735 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
9736
9737 editor.update_in(cx, |editor, window, cx| {
9738 editor.redo(&Default::default(), window, cx);
9739 });
9740 cx.run_until_parked();
9741 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
9742
9743 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
9744 let save = editor
9745 .update_in(cx, |editor, window, cx| {
9746 editor.save(
9747 SaveOptions {
9748 format: true,
9749 autosave: false,
9750 },
9751 project.clone(),
9752 window,
9753 cx,
9754 )
9755 })
9756 .unwrap();
9757 cx.executor().start_waiting();
9758 save.await;
9759 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9760 }
9761}
9762
9763#[gpui::test]
9764async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
9765 init_test(cx, |_| {});
9766
9767 let cols = 4;
9768 let rows = 10;
9769 let sample_text_1 = sample_text(rows, cols, 'a');
9770 assert_eq!(
9771 sample_text_1,
9772 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
9773 );
9774 let sample_text_2 = sample_text(rows, cols, 'l');
9775 assert_eq!(
9776 sample_text_2,
9777 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
9778 );
9779 let sample_text_3 = sample_text(rows, cols, 'v');
9780 assert_eq!(
9781 sample_text_3,
9782 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
9783 );
9784
9785 let fs = FakeFs::new(cx.executor());
9786 fs.insert_tree(
9787 path!("/a"),
9788 json!({
9789 "main.rs": sample_text_1,
9790 "other.rs": sample_text_2,
9791 "lib.rs": sample_text_3,
9792 }),
9793 )
9794 .await;
9795
9796 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
9797 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9798 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
9799
9800 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9801 language_registry.add(rust_lang());
9802 let mut fake_servers = language_registry.register_fake_lsp(
9803 "Rust",
9804 FakeLspAdapter {
9805 capabilities: lsp::ServerCapabilities {
9806 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9807 ..Default::default()
9808 },
9809 ..Default::default()
9810 },
9811 );
9812
9813 let worktree = project.update(cx, |project, cx| {
9814 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
9815 assert_eq!(worktrees.len(), 1);
9816 worktrees.pop().unwrap()
9817 });
9818 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
9819
9820 let buffer_1 = project
9821 .update(cx, |project, cx| {
9822 project.open_buffer((worktree_id, "main.rs"), cx)
9823 })
9824 .await
9825 .unwrap();
9826 let buffer_2 = project
9827 .update(cx, |project, cx| {
9828 project.open_buffer((worktree_id, "other.rs"), cx)
9829 })
9830 .await
9831 .unwrap();
9832 let buffer_3 = project
9833 .update(cx, |project, cx| {
9834 project.open_buffer((worktree_id, "lib.rs"), cx)
9835 })
9836 .await
9837 .unwrap();
9838
9839 let multi_buffer = cx.new(|cx| {
9840 let mut multi_buffer = MultiBuffer::new(ReadWrite);
9841 multi_buffer.push_excerpts(
9842 buffer_1.clone(),
9843 [
9844 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9845 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9846 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9847 ],
9848 cx,
9849 );
9850 multi_buffer.push_excerpts(
9851 buffer_2.clone(),
9852 [
9853 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9854 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9855 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9856 ],
9857 cx,
9858 );
9859 multi_buffer.push_excerpts(
9860 buffer_3.clone(),
9861 [
9862 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9863 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9864 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9865 ],
9866 cx,
9867 );
9868 multi_buffer
9869 });
9870 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
9871 Editor::new(
9872 EditorMode::full(),
9873 multi_buffer,
9874 Some(project.clone()),
9875 window,
9876 cx,
9877 )
9878 });
9879
9880 multi_buffer_editor.update_in(cx, |editor, window, cx| {
9881 editor.change_selections(
9882 SelectionEffects::scroll(Autoscroll::Next),
9883 window,
9884 cx,
9885 |s| s.select_ranges(Some(1..2)),
9886 );
9887 editor.insert("|one|two|three|", window, cx);
9888 });
9889 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
9890 multi_buffer_editor.update_in(cx, |editor, window, cx| {
9891 editor.change_selections(
9892 SelectionEffects::scroll(Autoscroll::Next),
9893 window,
9894 cx,
9895 |s| s.select_ranges(Some(60..70)),
9896 );
9897 editor.insert("|four|five|six|", window, cx);
9898 });
9899 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
9900
9901 // First two buffers should be edited, but not the third one.
9902 assert_eq!(
9903 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
9904 "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}",
9905 );
9906 buffer_1.update(cx, |buffer, _| {
9907 assert!(buffer.is_dirty());
9908 assert_eq!(
9909 buffer.text(),
9910 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
9911 )
9912 });
9913 buffer_2.update(cx, |buffer, _| {
9914 assert!(buffer.is_dirty());
9915 assert_eq!(
9916 buffer.text(),
9917 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
9918 )
9919 });
9920 buffer_3.update(cx, |buffer, _| {
9921 assert!(!buffer.is_dirty());
9922 assert_eq!(buffer.text(), sample_text_3,)
9923 });
9924 cx.executor().run_until_parked();
9925
9926 cx.executor().start_waiting();
9927 let save = multi_buffer_editor
9928 .update_in(cx, |editor, window, cx| {
9929 editor.save(
9930 SaveOptions {
9931 format: true,
9932 autosave: false,
9933 },
9934 project.clone(),
9935 window,
9936 cx,
9937 )
9938 })
9939 .unwrap();
9940
9941 let fake_server = fake_servers.next().await.unwrap();
9942 fake_server
9943 .server
9944 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
9945 Ok(Some(vec![lsp::TextEdit::new(
9946 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9947 format!("[{} formatted]", params.text_document.uri),
9948 )]))
9949 })
9950 .detach();
9951 save.await;
9952
9953 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
9954 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
9955 assert_eq!(
9956 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
9957 uri!(
9958 "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}"
9959 ),
9960 );
9961 buffer_1.update(cx, |buffer, _| {
9962 assert!(!buffer.is_dirty());
9963 assert_eq!(
9964 buffer.text(),
9965 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
9966 )
9967 });
9968 buffer_2.update(cx, |buffer, _| {
9969 assert!(!buffer.is_dirty());
9970 assert_eq!(
9971 buffer.text(),
9972 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
9973 )
9974 });
9975 buffer_3.update(cx, |buffer, _| {
9976 assert!(!buffer.is_dirty());
9977 assert_eq!(buffer.text(), sample_text_3,)
9978 });
9979}
9980
9981#[gpui::test]
9982async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
9983 init_test(cx, |_| {});
9984
9985 let fs = FakeFs::new(cx.executor());
9986 fs.insert_tree(
9987 path!("/dir"),
9988 json!({
9989 "file1.rs": "fn main() { println!(\"hello\"); }",
9990 "file2.rs": "fn test() { println!(\"test\"); }",
9991 "file3.rs": "fn other() { println!(\"other\"); }\n",
9992 }),
9993 )
9994 .await;
9995
9996 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
9997 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9998 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
9999
10000 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10001 language_registry.add(rust_lang());
10002
10003 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
10004 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
10005
10006 // Open three buffers
10007 let buffer_1 = project
10008 .update(cx, |project, cx| {
10009 project.open_buffer((worktree_id, "file1.rs"), cx)
10010 })
10011 .await
10012 .unwrap();
10013 let buffer_2 = project
10014 .update(cx, |project, cx| {
10015 project.open_buffer((worktree_id, "file2.rs"), cx)
10016 })
10017 .await
10018 .unwrap();
10019 let buffer_3 = project
10020 .update(cx, |project, cx| {
10021 project.open_buffer((worktree_id, "file3.rs"), cx)
10022 })
10023 .await
10024 .unwrap();
10025
10026 // Create a multi-buffer with all three buffers
10027 let multi_buffer = cx.new(|cx| {
10028 let mut multi_buffer = MultiBuffer::new(ReadWrite);
10029 multi_buffer.push_excerpts(
10030 buffer_1.clone(),
10031 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
10032 cx,
10033 );
10034 multi_buffer.push_excerpts(
10035 buffer_2.clone(),
10036 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
10037 cx,
10038 );
10039 multi_buffer.push_excerpts(
10040 buffer_3.clone(),
10041 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
10042 cx,
10043 );
10044 multi_buffer
10045 });
10046
10047 let editor = cx.new_window_entity(|window, cx| {
10048 Editor::new(
10049 EditorMode::full(),
10050 multi_buffer,
10051 Some(project.clone()),
10052 window,
10053 cx,
10054 )
10055 });
10056
10057 // Edit only the first buffer
10058 editor.update_in(cx, |editor, window, cx| {
10059 editor.change_selections(
10060 SelectionEffects::scroll(Autoscroll::Next),
10061 window,
10062 cx,
10063 |s| s.select_ranges(Some(10..10)),
10064 );
10065 editor.insert("// edited", window, cx);
10066 });
10067
10068 // Verify that only buffer 1 is dirty
10069 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
10070 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
10071 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
10072
10073 // Get write counts after file creation (files were created with initial content)
10074 // We expect each file to have been written once during creation
10075 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
10076 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
10077 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
10078
10079 // Perform autosave
10080 let save_task = editor.update_in(cx, |editor, window, cx| {
10081 editor.save(
10082 SaveOptions {
10083 format: true,
10084 autosave: true,
10085 },
10086 project.clone(),
10087 window,
10088 cx,
10089 )
10090 });
10091 save_task.await.unwrap();
10092
10093 // Only the dirty buffer should have been saved
10094 assert_eq!(
10095 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
10096 1,
10097 "Buffer 1 was dirty, so it should have been written once during autosave"
10098 );
10099 assert_eq!(
10100 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
10101 0,
10102 "Buffer 2 was clean, so it should not have been written during autosave"
10103 );
10104 assert_eq!(
10105 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
10106 0,
10107 "Buffer 3 was clean, so it should not have been written during autosave"
10108 );
10109
10110 // Verify buffer states after autosave
10111 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
10112 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
10113 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
10114
10115 // Now perform a manual save (format = true)
10116 let save_task = editor.update_in(cx, |editor, window, cx| {
10117 editor.save(
10118 SaveOptions {
10119 format: true,
10120 autosave: false,
10121 },
10122 project.clone(),
10123 window,
10124 cx,
10125 )
10126 });
10127 save_task.await.unwrap();
10128
10129 // During manual save, clean buffers don't get written to disk
10130 // They just get did_save called for language server notifications
10131 assert_eq!(
10132 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
10133 1,
10134 "Buffer 1 should only have been written once total (during autosave, not manual save)"
10135 );
10136 assert_eq!(
10137 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
10138 0,
10139 "Buffer 2 should not have been written at all"
10140 );
10141 assert_eq!(
10142 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
10143 0,
10144 "Buffer 3 should not have been written at all"
10145 );
10146}
10147
10148#[gpui::test]
10149async fn test_range_format_during_save(cx: &mut TestAppContext) {
10150 init_test(cx, |_| {});
10151
10152 let fs = FakeFs::new(cx.executor());
10153 fs.insert_file(path!("/file.rs"), Default::default()).await;
10154
10155 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10156
10157 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10158 language_registry.add(rust_lang());
10159 let mut fake_servers = language_registry.register_fake_lsp(
10160 "Rust",
10161 FakeLspAdapter {
10162 capabilities: lsp::ServerCapabilities {
10163 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
10164 ..Default::default()
10165 },
10166 ..Default::default()
10167 },
10168 );
10169
10170 let buffer = project
10171 .update(cx, |project, cx| {
10172 project.open_local_buffer(path!("/file.rs"), cx)
10173 })
10174 .await
10175 .unwrap();
10176
10177 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10178 let (editor, cx) = cx.add_window_view(|window, cx| {
10179 build_editor_with_project(project.clone(), buffer, window, cx)
10180 });
10181 editor.update_in(cx, |editor, window, cx| {
10182 editor.set_text("one\ntwo\nthree\n", window, cx)
10183 });
10184 assert!(cx.read(|cx| editor.is_dirty(cx)));
10185
10186 cx.executor().start_waiting();
10187 let fake_server = fake_servers.next().await.unwrap();
10188
10189 let save = editor
10190 .update_in(cx, |editor, window, cx| {
10191 editor.save(
10192 SaveOptions {
10193 format: true,
10194 autosave: false,
10195 },
10196 project.clone(),
10197 window,
10198 cx,
10199 )
10200 })
10201 .unwrap();
10202 fake_server
10203 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
10204 assert_eq!(
10205 params.text_document.uri,
10206 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10207 );
10208 assert_eq!(params.options.tab_size, 4);
10209 Ok(Some(vec![lsp::TextEdit::new(
10210 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10211 ", ".to_string(),
10212 )]))
10213 })
10214 .next()
10215 .await;
10216 cx.executor().start_waiting();
10217 save.await;
10218 assert_eq!(
10219 editor.update(cx, |editor, cx| editor.text(cx)),
10220 "one, two\nthree\n"
10221 );
10222 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10223
10224 editor.update_in(cx, |editor, window, cx| {
10225 editor.set_text("one\ntwo\nthree\n", window, cx)
10226 });
10227 assert!(cx.read(|cx| editor.is_dirty(cx)));
10228
10229 // Ensure we can still save even if formatting hangs.
10230 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
10231 move |params, _| async move {
10232 assert_eq!(
10233 params.text_document.uri,
10234 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10235 );
10236 futures::future::pending::<()>().await;
10237 unreachable!()
10238 },
10239 );
10240 let save = editor
10241 .update_in(cx, |editor, window, cx| {
10242 editor.save(
10243 SaveOptions {
10244 format: true,
10245 autosave: false,
10246 },
10247 project.clone(),
10248 window,
10249 cx,
10250 )
10251 })
10252 .unwrap();
10253 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
10254 cx.executor().start_waiting();
10255 save.await;
10256 assert_eq!(
10257 editor.update(cx, |editor, cx| editor.text(cx)),
10258 "one\ntwo\nthree\n"
10259 );
10260 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10261
10262 // For non-dirty buffer, no formatting request should be sent
10263 let save = editor
10264 .update_in(cx, |editor, window, cx| {
10265 editor.save(
10266 SaveOptions {
10267 format: false,
10268 autosave: false,
10269 },
10270 project.clone(),
10271 window,
10272 cx,
10273 )
10274 })
10275 .unwrap();
10276 let _pending_format_request = fake_server
10277 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
10278 panic!("Should not be invoked");
10279 })
10280 .next();
10281 cx.executor().start_waiting();
10282 save.await;
10283
10284 // Set Rust language override and assert overridden tabsize is sent to language server
10285 update_test_language_settings(cx, |settings| {
10286 settings.languages.0.insert(
10287 "Rust".into(),
10288 LanguageSettingsContent {
10289 tab_size: NonZeroU32::new(8),
10290 ..Default::default()
10291 },
10292 );
10293 });
10294
10295 editor.update_in(cx, |editor, window, cx| {
10296 editor.set_text("somehting_new\n", window, cx)
10297 });
10298 assert!(cx.read(|cx| editor.is_dirty(cx)));
10299 let save = editor
10300 .update_in(cx, |editor, window, cx| {
10301 editor.save(
10302 SaveOptions {
10303 format: true,
10304 autosave: false,
10305 },
10306 project.clone(),
10307 window,
10308 cx,
10309 )
10310 })
10311 .unwrap();
10312 fake_server
10313 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
10314 assert_eq!(
10315 params.text_document.uri,
10316 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10317 );
10318 assert_eq!(params.options.tab_size, 8);
10319 Ok(Some(Vec::new()))
10320 })
10321 .next()
10322 .await;
10323 save.await;
10324}
10325
10326#[gpui::test]
10327async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
10328 init_test(cx, |settings| {
10329 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
10330 Formatter::LanguageServer { name: None },
10331 )))
10332 });
10333
10334 let fs = FakeFs::new(cx.executor());
10335 fs.insert_file(path!("/file.rs"), Default::default()).await;
10336
10337 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10338
10339 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10340 language_registry.add(Arc::new(Language::new(
10341 LanguageConfig {
10342 name: "Rust".into(),
10343 matcher: LanguageMatcher {
10344 path_suffixes: vec!["rs".to_string()],
10345 ..Default::default()
10346 },
10347 ..LanguageConfig::default()
10348 },
10349 Some(tree_sitter_rust::LANGUAGE.into()),
10350 )));
10351 update_test_language_settings(cx, |settings| {
10352 // Enable Prettier formatting for the same buffer, and ensure
10353 // LSP is called instead of Prettier.
10354 settings.defaults.prettier = Some(PrettierSettings {
10355 allowed: true,
10356 ..PrettierSettings::default()
10357 });
10358 });
10359 let mut fake_servers = language_registry.register_fake_lsp(
10360 "Rust",
10361 FakeLspAdapter {
10362 capabilities: lsp::ServerCapabilities {
10363 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10364 ..Default::default()
10365 },
10366 ..Default::default()
10367 },
10368 );
10369
10370 let buffer = project
10371 .update(cx, |project, cx| {
10372 project.open_local_buffer(path!("/file.rs"), cx)
10373 })
10374 .await
10375 .unwrap();
10376
10377 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10378 let (editor, cx) = cx.add_window_view(|window, cx| {
10379 build_editor_with_project(project.clone(), buffer, window, cx)
10380 });
10381 editor.update_in(cx, |editor, window, cx| {
10382 editor.set_text("one\ntwo\nthree\n", window, cx)
10383 });
10384
10385 cx.executor().start_waiting();
10386 let fake_server = fake_servers.next().await.unwrap();
10387
10388 let format = editor
10389 .update_in(cx, |editor, window, cx| {
10390 editor.perform_format(
10391 project.clone(),
10392 FormatTrigger::Manual,
10393 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10394 window,
10395 cx,
10396 )
10397 })
10398 .unwrap();
10399 fake_server
10400 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
10401 assert_eq!(
10402 params.text_document.uri,
10403 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10404 );
10405 assert_eq!(params.options.tab_size, 4);
10406 Ok(Some(vec![lsp::TextEdit::new(
10407 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10408 ", ".to_string(),
10409 )]))
10410 })
10411 .next()
10412 .await;
10413 cx.executor().start_waiting();
10414 format.await;
10415 assert_eq!(
10416 editor.update(cx, |editor, cx| editor.text(cx)),
10417 "one, two\nthree\n"
10418 );
10419
10420 editor.update_in(cx, |editor, window, cx| {
10421 editor.set_text("one\ntwo\nthree\n", window, cx)
10422 });
10423 // Ensure we don't lock if formatting hangs.
10424 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10425 move |params, _| async move {
10426 assert_eq!(
10427 params.text_document.uri,
10428 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10429 );
10430 futures::future::pending::<()>().await;
10431 unreachable!()
10432 },
10433 );
10434 let format = editor
10435 .update_in(cx, |editor, window, cx| {
10436 editor.perform_format(
10437 project,
10438 FormatTrigger::Manual,
10439 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10440 window,
10441 cx,
10442 )
10443 })
10444 .unwrap();
10445 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
10446 cx.executor().start_waiting();
10447 format.await;
10448 assert_eq!(
10449 editor.update(cx, |editor, cx| editor.text(cx)),
10450 "one\ntwo\nthree\n"
10451 );
10452}
10453
10454#[gpui::test]
10455async fn test_multiple_formatters(cx: &mut TestAppContext) {
10456 init_test(cx, |settings| {
10457 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
10458 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
10459 Formatter::LanguageServer { name: None },
10460 Formatter::CodeActions(
10461 [
10462 ("code-action-1".into(), true),
10463 ("code-action-2".into(), true),
10464 ]
10465 .into_iter()
10466 .collect(),
10467 ),
10468 ])))
10469 });
10470
10471 let fs = FakeFs::new(cx.executor());
10472 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
10473 .await;
10474
10475 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10476 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10477 language_registry.add(rust_lang());
10478
10479 let mut fake_servers = language_registry.register_fake_lsp(
10480 "Rust",
10481 FakeLspAdapter {
10482 capabilities: lsp::ServerCapabilities {
10483 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10484 execute_command_provider: Some(lsp::ExecuteCommandOptions {
10485 commands: vec!["the-command-for-code-action-1".into()],
10486 ..Default::default()
10487 }),
10488 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
10489 ..Default::default()
10490 },
10491 ..Default::default()
10492 },
10493 );
10494
10495 let buffer = project
10496 .update(cx, |project, cx| {
10497 project.open_local_buffer(path!("/file.rs"), cx)
10498 })
10499 .await
10500 .unwrap();
10501
10502 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10503 let (editor, cx) = cx.add_window_view(|window, cx| {
10504 build_editor_with_project(project.clone(), buffer, window, cx)
10505 });
10506
10507 cx.executor().start_waiting();
10508
10509 let fake_server = fake_servers.next().await.unwrap();
10510 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10511 move |_params, _| async move {
10512 Ok(Some(vec![lsp::TextEdit::new(
10513 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10514 "applied-formatting\n".to_string(),
10515 )]))
10516 },
10517 );
10518 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
10519 move |params, _| async move {
10520 assert_eq!(
10521 params.context.only,
10522 Some(vec!["code-action-1".into(), "code-action-2".into()])
10523 );
10524 let uri = lsp::Url::from_file_path(path!("/file.rs")).unwrap();
10525 Ok(Some(vec![
10526 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
10527 kind: Some("code-action-1".into()),
10528 edit: Some(lsp::WorkspaceEdit::new(
10529 [(
10530 uri.clone(),
10531 vec![lsp::TextEdit::new(
10532 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10533 "applied-code-action-1-edit\n".to_string(),
10534 )],
10535 )]
10536 .into_iter()
10537 .collect(),
10538 )),
10539 command: Some(lsp::Command {
10540 command: "the-command-for-code-action-1".into(),
10541 ..Default::default()
10542 }),
10543 ..Default::default()
10544 }),
10545 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
10546 kind: Some("code-action-2".into()),
10547 edit: Some(lsp::WorkspaceEdit::new(
10548 [(
10549 uri.clone(),
10550 vec![lsp::TextEdit::new(
10551 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10552 "applied-code-action-2-edit\n".to_string(),
10553 )],
10554 )]
10555 .into_iter()
10556 .collect(),
10557 )),
10558 ..Default::default()
10559 }),
10560 ]))
10561 },
10562 );
10563
10564 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
10565 move |params, _| async move { Ok(params) }
10566 });
10567
10568 let command_lock = Arc::new(futures::lock::Mutex::new(()));
10569 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
10570 let fake = fake_server.clone();
10571 let lock = command_lock.clone();
10572 move |params, _| {
10573 assert_eq!(params.command, "the-command-for-code-action-1");
10574 let fake = fake.clone();
10575 let lock = lock.clone();
10576 async move {
10577 lock.lock().await;
10578 fake.server
10579 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
10580 label: None,
10581 edit: lsp::WorkspaceEdit {
10582 changes: Some(
10583 [(
10584 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
10585 vec![lsp::TextEdit {
10586 range: lsp::Range::new(
10587 lsp::Position::new(0, 0),
10588 lsp::Position::new(0, 0),
10589 ),
10590 new_text: "applied-code-action-1-command\n".into(),
10591 }],
10592 )]
10593 .into_iter()
10594 .collect(),
10595 ),
10596 ..Default::default()
10597 },
10598 })
10599 .await
10600 .into_response()
10601 .unwrap();
10602 Ok(Some(json!(null)))
10603 }
10604 }
10605 });
10606
10607 cx.executor().start_waiting();
10608 editor
10609 .update_in(cx, |editor, window, cx| {
10610 editor.perform_format(
10611 project.clone(),
10612 FormatTrigger::Manual,
10613 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10614 window,
10615 cx,
10616 )
10617 })
10618 .unwrap()
10619 .await;
10620 editor.update(cx, |editor, cx| {
10621 assert_eq!(
10622 editor.text(cx),
10623 r#"
10624 applied-code-action-2-edit
10625 applied-code-action-1-command
10626 applied-code-action-1-edit
10627 applied-formatting
10628 one
10629 two
10630 three
10631 "#
10632 .unindent()
10633 );
10634 });
10635
10636 editor.update_in(cx, |editor, window, cx| {
10637 editor.undo(&Default::default(), window, cx);
10638 assert_eq!(editor.text(cx), "one \ntwo \nthree");
10639 });
10640
10641 // Perform a manual edit while waiting for an LSP command
10642 // that's being run as part of a formatting code action.
10643 let lock_guard = command_lock.lock().await;
10644 let format = editor
10645 .update_in(cx, |editor, window, cx| {
10646 editor.perform_format(
10647 project.clone(),
10648 FormatTrigger::Manual,
10649 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10650 window,
10651 cx,
10652 )
10653 })
10654 .unwrap();
10655 cx.run_until_parked();
10656 editor.update(cx, |editor, cx| {
10657 assert_eq!(
10658 editor.text(cx),
10659 r#"
10660 applied-code-action-1-edit
10661 applied-formatting
10662 one
10663 two
10664 three
10665 "#
10666 .unindent()
10667 );
10668
10669 editor.buffer.update(cx, |buffer, cx| {
10670 let ix = buffer.len(cx);
10671 buffer.edit([(ix..ix, "edited\n")], None, cx);
10672 });
10673 });
10674
10675 // Allow the LSP command to proceed. Because the buffer was edited,
10676 // the second code action will not be run.
10677 drop(lock_guard);
10678 format.await;
10679 editor.update_in(cx, |editor, window, cx| {
10680 assert_eq!(
10681 editor.text(cx),
10682 r#"
10683 applied-code-action-1-command
10684 applied-code-action-1-edit
10685 applied-formatting
10686 one
10687 two
10688 three
10689 edited
10690 "#
10691 .unindent()
10692 );
10693
10694 // The manual edit is undone first, because it is the last thing the user did
10695 // (even though the command completed afterwards).
10696 editor.undo(&Default::default(), window, cx);
10697 assert_eq!(
10698 editor.text(cx),
10699 r#"
10700 applied-code-action-1-command
10701 applied-code-action-1-edit
10702 applied-formatting
10703 one
10704 two
10705 three
10706 "#
10707 .unindent()
10708 );
10709
10710 // All the formatting (including the command, which completed after the manual edit)
10711 // is undone together.
10712 editor.undo(&Default::default(), window, cx);
10713 assert_eq!(editor.text(cx), "one \ntwo \nthree");
10714 });
10715}
10716
10717#[gpui::test]
10718async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
10719 init_test(cx, |settings| {
10720 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
10721 Formatter::LanguageServer { name: None },
10722 ])))
10723 });
10724
10725 let fs = FakeFs::new(cx.executor());
10726 fs.insert_file(path!("/file.ts"), Default::default()).await;
10727
10728 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10729
10730 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10731 language_registry.add(Arc::new(Language::new(
10732 LanguageConfig {
10733 name: "TypeScript".into(),
10734 matcher: LanguageMatcher {
10735 path_suffixes: vec!["ts".to_string()],
10736 ..Default::default()
10737 },
10738 ..LanguageConfig::default()
10739 },
10740 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
10741 )));
10742 update_test_language_settings(cx, |settings| {
10743 settings.defaults.prettier = Some(PrettierSettings {
10744 allowed: true,
10745 ..PrettierSettings::default()
10746 });
10747 });
10748 let mut fake_servers = language_registry.register_fake_lsp(
10749 "TypeScript",
10750 FakeLspAdapter {
10751 capabilities: lsp::ServerCapabilities {
10752 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
10753 ..Default::default()
10754 },
10755 ..Default::default()
10756 },
10757 );
10758
10759 let buffer = project
10760 .update(cx, |project, cx| {
10761 project.open_local_buffer(path!("/file.ts"), cx)
10762 })
10763 .await
10764 .unwrap();
10765
10766 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10767 let (editor, cx) = cx.add_window_view(|window, cx| {
10768 build_editor_with_project(project.clone(), buffer, window, cx)
10769 });
10770 editor.update_in(cx, |editor, window, cx| {
10771 editor.set_text(
10772 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
10773 window,
10774 cx,
10775 )
10776 });
10777
10778 cx.executor().start_waiting();
10779 let fake_server = fake_servers.next().await.unwrap();
10780
10781 let format = editor
10782 .update_in(cx, |editor, window, cx| {
10783 editor.perform_code_action_kind(
10784 project.clone(),
10785 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
10786 window,
10787 cx,
10788 )
10789 })
10790 .unwrap();
10791 fake_server
10792 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
10793 assert_eq!(
10794 params.text_document.uri,
10795 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
10796 );
10797 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
10798 lsp::CodeAction {
10799 title: "Organize Imports".to_string(),
10800 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
10801 edit: Some(lsp::WorkspaceEdit {
10802 changes: Some(
10803 [(
10804 params.text_document.uri.clone(),
10805 vec![lsp::TextEdit::new(
10806 lsp::Range::new(
10807 lsp::Position::new(1, 0),
10808 lsp::Position::new(2, 0),
10809 ),
10810 "".to_string(),
10811 )],
10812 )]
10813 .into_iter()
10814 .collect(),
10815 ),
10816 ..Default::default()
10817 }),
10818 ..Default::default()
10819 },
10820 )]))
10821 })
10822 .next()
10823 .await;
10824 cx.executor().start_waiting();
10825 format.await;
10826 assert_eq!(
10827 editor.update(cx, |editor, cx| editor.text(cx)),
10828 "import { a } from 'module';\n\nconst x = a;\n"
10829 );
10830
10831 editor.update_in(cx, |editor, window, cx| {
10832 editor.set_text(
10833 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
10834 window,
10835 cx,
10836 )
10837 });
10838 // Ensure we don't lock if code action hangs.
10839 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
10840 move |params, _| async move {
10841 assert_eq!(
10842 params.text_document.uri,
10843 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
10844 );
10845 futures::future::pending::<()>().await;
10846 unreachable!()
10847 },
10848 );
10849 let format = editor
10850 .update_in(cx, |editor, window, cx| {
10851 editor.perform_code_action_kind(
10852 project,
10853 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
10854 window,
10855 cx,
10856 )
10857 })
10858 .unwrap();
10859 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
10860 cx.executor().start_waiting();
10861 format.await;
10862 assert_eq!(
10863 editor.update(cx, |editor, cx| editor.text(cx)),
10864 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
10865 );
10866}
10867
10868#[gpui::test]
10869async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
10870 init_test(cx, |_| {});
10871
10872 let mut cx = EditorLspTestContext::new_rust(
10873 lsp::ServerCapabilities {
10874 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10875 ..Default::default()
10876 },
10877 cx,
10878 )
10879 .await;
10880
10881 cx.set_state(indoc! {"
10882 one.twoˇ
10883 "});
10884
10885 // The format request takes a long time. When it completes, it inserts
10886 // a newline and an indent before the `.`
10887 cx.lsp
10888 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
10889 let executor = cx.background_executor().clone();
10890 async move {
10891 executor.timer(Duration::from_millis(100)).await;
10892 Ok(Some(vec![lsp::TextEdit {
10893 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
10894 new_text: "\n ".into(),
10895 }]))
10896 }
10897 });
10898
10899 // Submit a format request.
10900 let format_1 = cx
10901 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10902 .unwrap();
10903 cx.executor().run_until_parked();
10904
10905 // Submit a second format request.
10906 let format_2 = cx
10907 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10908 .unwrap();
10909 cx.executor().run_until_parked();
10910
10911 // Wait for both format requests to complete
10912 cx.executor().advance_clock(Duration::from_millis(200));
10913 cx.executor().start_waiting();
10914 format_1.await.unwrap();
10915 cx.executor().start_waiting();
10916 format_2.await.unwrap();
10917
10918 // The formatting edits only happens once.
10919 cx.assert_editor_state(indoc! {"
10920 one
10921 .twoˇ
10922 "});
10923}
10924
10925#[gpui::test]
10926async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
10927 init_test(cx, |settings| {
10928 settings.defaults.formatter = Some(SelectedFormatter::Auto)
10929 });
10930
10931 let mut cx = EditorLspTestContext::new_rust(
10932 lsp::ServerCapabilities {
10933 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10934 ..Default::default()
10935 },
10936 cx,
10937 )
10938 .await;
10939
10940 // Set up a buffer white some trailing whitespace and no trailing newline.
10941 cx.set_state(
10942 &[
10943 "one ", //
10944 "twoˇ", //
10945 "three ", //
10946 "four", //
10947 ]
10948 .join("\n"),
10949 );
10950
10951 // Submit a format request.
10952 let format = cx
10953 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10954 .unwrap();
10955
10956 // Record which buffer changes have been sent to the language server
10957 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
10958 cx.lsp
10959 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
10960 let buffer_changes = buffer_changes.clone();
10961 move |params, _| {
10962 buffer_changes.lock().extend(
10963 params
10964 .content_changes
10965 .into_iter()
10966 .map(|e| (e.range.unwrap(), e.text)),
10967 );
10968 }
10969 });
10970
10971 // Handle formatting requests to the language server.
10972 cx.lsp
10973 .set_request_handler::<lsp::request::Formatting, _, _>({
10974 let buffer_changes = buffer_changes.clone();
10975 move |_, _| {
10976 // When formatting is requested, trailing whitespace has already been stripped,
10977 // and the trailing newline has already been added.
10978 assert_eq!(
10979 &buffer_changes.lock()[1..],
10980 &[
10981 (
10982 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
10983 "".into()
10984 ),
10985 (
10986 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
10987 "".into()
10988 ),
10989 (
10990 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
10991 "\n".into()
10992 ),
10993 ]
10994 );
10995
10996 // Insert blank lines between each line of the buffer.
10997 async move {
10998 Ok(Some(vec![
10999 lsp::TextEdit {
11000 range: lsp::Range::new(
11001 lsp::Position::new(1, 0),
11002 lsp::Position::new(1, 0),
11003 ),
11004 new_text: "\n".into(),
11005 },
11006 lsp::TextEdit {
11007 range: lsp::Range::new(
11008 lsp::Position::new(2, 0),
11009 lsp::Position::new(2, 0),
11010 ),
11011 new_text: "\n".into(),
11012 },
11013 ]))
11014 }
11015 }
11016 });
11017
11018 // After formatting the buffer, the trailing whitespace is stripped,
11019 // a newline is appended, and the edits provided by the language server
11020 // have been applied.
11021 format.await.unwrap();
11022 cx.assert_editor_state(
11023 &[
11024 "one", //
11025 "", //
11026 "twoˇ", //
11027 "", //
11028 "three", //
11029 "four", //
11030 "", //
11031 ]
11032 .join("\n"),
11033 );
11034
11035 // Undoing the formatting undoes the trailing whitespace removal, the
11036 // trailing newline, and the LSP edits.
11037 cx.update_buffer(|buffer, cx| buffer.undo(cx));
11038 cx.assert_editor_state(
11039 &[
11040 "one ", //
11041 "twoˇ", //
11042 "three ", //
11043 "four", //
11044 ]
11045 .join("\n"),
11046 );
11047}
11048
11049#[gpui::test]
11050async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
11051 cx: &mut TestAppContext,
11052) {
11053 init_test(cx, |_| {});
11054
11055 cx.update(|cx| {
11056 cx.update_global::<SettingsStore, _>(|settings, cx| {
11057 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11058 settings.auto_signature_help = Some(true);
11059 });
11060 });
11061 });
11062
11063 let mut cx = EditorLspTestContext::new_rust(
11064 lsp::ServerCapabilities {
11065 signature_help_provider: Some(lsp::SignatureHelpOptions {
11066 ..Default::default()
11067 }),
11068 ..Default::default()
11069 },
11070 cx,
11071 )
11072 .await;
11073
11074 let language = Language::new(
11075 LanguageConfig {
11076 name: "Rust".into(),
11077 brackets: BracketPairConfig {
11078 pairs: vec![
11079 BracketPair {
11080 start: "{".to_string(),
11081 end: "}".to_string(),
11082 close: true,
11083 surround: true,
11084 newline: true,
11085 },
11086 BracketPair {
11087 start: "(".to_string(),
11088 end: ")".to_string(),
11089 close: true,
11090 surround: true,
11091 newline: true,
11092 },
11093 BracketPair {
11094 start: "/*".to_string(),
11095 end: " */".to_string(),
11096 close: true,
11097 surround: true,
11098 newline: true,
11099 },
11100 BracketPair {
11101 start: "[".to_string(),
11102 end: "]".to_string(),
11103 close: false,
11104 surround: false,
11105 newline: true,
11106 },
11107 BracketPair {
11108 start: "\"".to_string(),
11109 end: "\"".to_string(),
11110 close: true,
11111 surround: true,
11112 newline: false,
11113 },
11114 BracketPair {
11115 start: "<".to_string(),
11116 end: ">".to_string(),
11117 close: false,
11118 surround: true,
11119 newline: true,
11120 },
11121 ],
11122 ..Default::default()
11123 },
11124 autoclose_before: "})]".to_string(),
11125 ..Default::default()
11126 },
11127 Some(tree_sitter_rust::LANGUAGE.into()),
11128 );
11129 let language = Arc::new(language);
11130
11131 cx.language_registry().add(language.clone());
11132 cx.update_buffer(|buffer, cx| {
11133 buffer.set_language(Some(language), cx);
11134 });
11135
11136 cx.set_state(
11137 &r#"
11138 fn main() {
11139 sampleˇ
11140 }
11141 "#
11142 .unindent(),
11143 );
11144
11145 cx.update_editor(|editor, window, cx| {
11146 editor.handle_input("(", window, cx);
11147 });
11148 cx.assert_editor_state(
11149 &"
11150 fn main() {
11151 sample(ˇ)
11152 }
11153 "
11154 .unindent(),
11155 );
11156
11157 let mocked_response = lsp::SignatureHelp {
11158 signatures: vec![lsp::SignatureInformation {
11159 label: "fn sample(param1: u8, param2: u8)".to_string(),
11160 documentation: None,
11161 parameters: Some(vec![
11162 lsp::ParameterInformation {
11163 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11164 documentation: None,
11165 },
11166 lsp::ParameterInformation {
11167 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11168 documentation: None,
11169 },
11170 ]),
11171 active_parameter: None,
11172 }],
11173 active_signature: Some(0),
11174 active_parameter: Some(0),
11175 };
11176 handle_signature_help_request(&mut cx, mocked_response).await;
11177
11178 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11179 .await;
11180
11181 cx.editor(|editor, _, _| {
11182 let signature_help_state = editor.signature_help_state.popover().cloned();
11183 let signature = signature_help_state.unwrap();
11184 assert_eq!(
11185 signature.signatures[signature.current_signature].label,
11186 "fn sample(param1: u8, param2: u8)"
11187 );
11188 });
11189}
11190
11191#[gpui::test]
11192async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
11193 init_test(cx, |_| {});
11194
11195 cx.update(|cx| {
11196 cx.update_global::<SettingsStore, _>(|settings, cx| {
11197 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11198 settings.auto_signature_help = Some(false);
11199 settings.show_signature_help_after_edits = Some(false);
11200 });
11201 });
11202 });
11203
11204 let mut cx = EditorLspTestContext::new_rust(
11205 lsp::ServerCapabilities {
11206 signature_help_provider: Some(lsp::SignatureHelpOptions {
11207 ..Default::default()
11208 }),
11209 ..Default::default()
11210 },
11211 cx,
11212 )
11213 .await;
11214
11215 let language = Language::new(
11216 LanguageConfig {
11217 name: "Rust".into(),
11218 brackets: BracketPairConfig {
11219 pairs: vec![
11220 BracketPair {
11221 start: "{".to_string(),
11222 end: "}".to_string(),
11223 close: true,
11224 surround: true,
11225 newline: true,
11226 },
11227 BracketPair {
11228 start: "(".to_string(),
11229 end: ")".to_string(),
11230 close: true,
11231 surround: true,
11232 newline: true,
11233 },
11234 BracketPair {
11235 start: "/*".to_string(),
11236 end: " */".to_string(),
11237 close: true,
11238 surround: true,
11239 newline: true,
11240 },
11241 BracketPair {
11242 start: "[".to_string(),
11243 end: "]".to_string(),
11244 close: false,
11245 surround: false,
11246 newline: true,
11247 },
11248 BracketPair {
11249 start: "\"".to_string(),
11250 end: "\"".to_string(),
11251 close: true,
11252 surround: true,
11253 newline: false,
11254 },
11255 BracketPair {
11256 start: "<".to_string(),
11257 end: ">".to_string(),
11258 close: false,
11259 surround: true,
11260 newline: true,
11261 },
11262 ],
11263 ..Default::default()
11264 },
11265 autoclose_before: "})]".to_string(),
11266 ..Default::default()
11267 },
11268 Some(tree_sitter_rust::LANGUAGE.into()),
11269 );
11270 let language = Arc::new(language);
11271
11272 cx.language_registry().add(language.clone());
11273 cx.update_buffer(|buffer, cx| {
11274 buffer.set_language(Some(language), cx);
11275 });
11276
11277 // Ensure that signature_help is not called when no signature help is enabled.
11278 cx.set_state(
11279 &r#"
11280 fn main() {
11281 sampleˇ
11282 }
11283 "#
11284 .unindent(),
11285 );
11286 cx.update_editor(|editor, window, cx| {
11287 editor.handle_input("(", window, cx);
11288 });
11289 cx.assert_editor_state(
11290 &"
11291 fn main() {
11292 sample(ˇ)
11293 }
11294 "
11295 .unindent(),
11296 );
11297 cx.editor(|editor, _, _| {
11298 assert!(editor.signature_help_state.task().is_none());
11299 });
11300
11301 let mocked_response = lsp::SignatureHelp {
11302 signatures: vec![lsp::SignatureInformation {
11303 label: "fn sample(param1: u8, param2: u8)".to_string(),
11304 documentation: None,
11305 parameters: Some(vec![
11306 lsp::ParameterInformation {
11307 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11308 documentation: None,
11309 },
11310 lsp::ParameterInformation {
11311 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11312 documentation: None,
11313 },
11314 ]),
11315 active_parameter: None,
11316 }],
11317 active_signature: Some(0),
11318 active_parameter: Some(0),
11319 };
11320
11321 // Ensure that signature_help is called when enabled afte edits
11322 cx.update(|_, cx| {
11323 cx.update_global::<SettingsStore, _>(|settings, cx| {
11324 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11325 settings.auto_signature_help = Some(false);
11326 settings.show_signature_help_after_edits = Some(true);
11327 });
11328 });
11329 });
11330 cx.set_state(
11331 &r#"
11332 fn main() {
11333 sampleˇ
11334 }
11335 "#
11336 .unindent(),
11337 );
11338 cx.update_editor(|editor, window, cx| {
11339 editor.handle_input("(", window, cx);
11340 });
11341 cx.assert_editor_state(
11342 &"
11343 fn main() {
11344 sample(ˇ)
11345 }
11346 "
11347 .unindent(),
11348 );
11349 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11350 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11351 .await;
11352 cx.update_editor(|editor, _, _| {
11353 let signature_help_state = editor.signature_help_state.popover().cloned();
11354 assert!(signature_help_state.is_some());
11355 let signature = signature_help_state.unwrap();
11356 assert_eq!(
11357 signature.signatures[signature.current_signature].label,
11358 "fn sample(param1: u8, param2: u8)"
11359 );
11360 editor.signature_help_state = SignatureHelpState::default();
11361 });
11362
11363 // Ensure that signature_help is called when auto signature help override is enabled
11364 cx.update(|_, cx| {
11365 cx.update_global::<SettingsStore, _>(|settings, cx| {
11366 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11367 settings.auto_signature_help = Some(true);
11368 settings.show_signature_help_after_edits = Some(false);
11369 });
11370 });
11371 });
11372 cx.set_state(
11373 &r#"
11374 fn main() {
11375 sampleˇ
11376 }
11377 "#
11378 .unindent(),
11379 );
11380 cx.update_editor(|editor, window, cx| {
11381 editor.handle_input("(", window, cx);
11382 });
11383 cx.assert_editor_state(
11384 &"
11385 fn main() {
11386 sample(ˇ)
11387 }
11388 "
11389 .unindent(),
11390 );
11391 handle_signature_help_request(&mut cx, mocked_response).await;
11392 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11393 .await;
11394 cx.editor(|editor, _, _| {
11395 let signature_help_state = editor.signature_help_state.popover().cloned();
11396 assert!(signature_help_state.is_some());
11397 let signature = signature_help_state.unwrap();
11398 assert_eq!(
11399 signature.signatures[signature.current_signature].label,
11400 "fn sample(param1: u8, param2: u8)"
11401 );
11402 });
11403}
11404
11405#[gpui::test]
11406async fn test_signature_help(cx: &mut TestAppContext) {
11407 init_test(cx, |_| {});
11408 cx.update(|cx| {
11409 cx.update_global::<SettingsStore, _>(|settings, cx| {
11410 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11411 settings.auto_signature_help = Some(true);
11412 });
11413 });
11414 });
11415
11416 let mut cx = EditorLspTestContext::new_rust(
11417 lsp::ServerCapabilities {
11418 signature_help_provider: Some(lsp::SignatureHelpOptions {
11419 ..Default::default()
11420 }),
11421 ..Default::default()
11422 },
11423 cx,
11424 )
11425 .await;
11426
11427 // A test that directly calls `show_signature_help`
11428 cx.update_editor(|editor, window, cx| {
11429 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11430 });
11431
11432 let mocked_response = lsp::SignatureHelp {
11433 signatures: vec![lsp::SignatureInformation {
11434 label: "fn sample(param1: u8, param2: u8)".to_string(),
11435 documentation: None,
11436 parameters: Some(vec![
11437 lsp::ParameterInformation {
11438 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11439 documentation: None,
11440 },
11441 lsp::ParameterInformation {
11442 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11443 documentation: None,
11444 },
11445 ]),
11446 active_parameter: None,
11447 }],
11448 active_signature: Some(0),
11449 active_parameter: Some(0),
11450 };
11451 handle_signature_help_request(&mut cx, mocked_response).await;
11452
11453 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11454 .await;
11455
11456 cx.editor(|editor, _, _| {
11457 let signature_help_state = editor.signature_help_state.popover().cloned();
11458 assert!(signature_help_state.is_some());
11459 let signature = signature_help_state.unwrap();
11460 assert_eq!(
11461 signature.signatures[signature.current_signature].label,
11462 "fn sample(param1: u8, param2: u8)"
11463 );
11464 });
11465
11466 // When exiting outside from inside the brackets, `signature_help` is closed.
11467 cx.set_state(indoc! {"
11468 fn main() {
11469 sample(ˇ);
11470 }
11471
11472 fn sample(param1: u8, param2: u8) {}
11473 "});
11474
11475 cx.update_editor(|editor, window, cx| {
11476 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11477 s.select_ranges([0..0])
11478 });
11479 });
11480
11481 let mocked_response = lsp::SignatureHelp {
11482 signatures: Vec::new(),
11483 active_signature: None,
11484 active_parameter: None,
11485 };
11486 handle_signature_help_request(&mut cx, mocked_response).await;
11487
11488 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
11489 .await;
11490
11491 cx.editor(|editor, _, _| {
11492 assert!(!editor.signature_help_state.is_shown());
11493 });
11494
11495 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
11496 cx.set_state(indoc! {"
11497 fn main() {
11498 sample(ˇ);
11499 }
11500
11501 fn sample(param1: u8, param2: u8) {}
11502 "});
11503
11504 let mocked_response = lsp::SignatureHelp {
11505 signatures: vec![lsp::SignatureInformation {
11506 label: "fn sample(param1: u8, param2: u8)".to_string(),
11507 documentation: None,
11508 parameters: Some(vec![
11509 lsp::ParameterInformation {
11510 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11511 documentation: None,
11512 },
11513 lsp::ParameterInformation {
11514 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11515 documentation: None,
11516 },
11517 ]),
11518 active_parameter: None,
11519 }],
11520 active_signature: Some(0),
11521 active_parameter: Some(0),
11522 };
11523 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11524 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11525 .await;
11526 cx.editor(|editor, _, _| {
11527 assert!(editor.signature_help_state.is_shown());
11528 });
11529
11530 // Restore the popover with more parameter input
11531 cx.set_state(indoc! {"
11532 fn main() {
11533 sample(param1, param2ˇ);
11534 }
11535
11536 fn sample(param1: u8, param2: u8) {}
11537 "});
11538
11539 let mocked_response = lsp::SignatureHelp {
11540 signatures: vec![lsp::SignatureInformation {
11541 label: "fn sample(param1: u8, param2: u8)".to_string(),
11542 documentation: None,
11543 parameters: Some(vec![
11544 lsp::ParameterInformation {
11545 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11546 documentation: None,
11547 },
11548 lsp::ParameterInformation {
11549 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11550 documentation: None,
11551 },
11552 ]),
11553 active_parameter: None,
11554 }],
11555 active_signature: Some(0),
11556 active_parameter: Some(1),
11557 };
11558 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11559 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11560 .await;
11561
11562 // When selecting a range, the popover is gone.
11563 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
11564 cx.update_editor(|editor, window, cx| {
11565 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11566 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
11567 })
11568 });
11569 cx.assert_editor_state(indoc! {"
11570 fn main() {
11571 sample(param1, «ˇparam2»);
11572 }
11573
11574 fn sample(param1: u8, param2: u8) {}
11575 "});
11576 cx.editor(|editor, _, _| {
11577 assert!(!editor.signature_help_state.is_shown());
11578 });
11579
11580 // When unselecting again, the popover is back if within the brackets.
11581 cx.update_editor(|editor, window, cx| {
11582 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11583 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11584 })
11585 });
11586 cx.assert_editor_state(indoc! {"
11587 fn main() {
11588 sample(param1, ˇparam2);
11589 }
11590
11591 fn sample(param1: u8, param2: u8) {}
11592 "});
11593 handle_signature_help_request(&mut cx, mocked_response).await;
11594 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11595 .await;
11596 cx.editor(|editor, _, _| {
11597 assert!(editor.signature_help_state.is_shown());
11598 });
11599
11600 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
11601 cx.update_editor(|editor, window, cx| {
11602 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11603 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
11604 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11605 })
11606 });
11607 cx.assert_editor_state(indoc! {"
11608 fn main() {
11609 sample(param1, ˇparam2);
11610 }
11611
11612 fn sample(param1: u8, param2: u8) {}
11613 "});
11614
11615 let mocked_response = lsp::SignatureHelp {
11616 signatures: vec![lsp::SignatureInformation {
11617 label: "fn sample(param1: u8, param2: u8)".to_string(),
11618 documentation: None,
11619 parameters: Some(vec![
11620 lsp::ParameterInformation {
11621 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11622 documentation: None,
11623 },
11624 lsp::ParameterInformation {
11625 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11626 documentation: None,
11627 },
11628 ]),
11629 active_parameter: None,
11630 }],
11631 active_signature: Some(0),
11632 active_parameter: Some(1),
11633 };
11634 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11635 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11636 .await;
11637 cx.update_editor(|editor, _, cx| {
11638 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
11639 });
11640 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
11641 .await;
11642 cx.update_editor(|editor, window, cx| {
11643 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11644 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
11645 })
11646 });
11647 cx.assert_editor_state(indoc! {"
11648 fn main() {
11649 sample(param1, «ˇparam2»);
11650 }
11651
11652 fn sample(param1: u8, param2: u8) {}
11653 "});
11654 cx.update_editor(|editor, window, cx| {
11655 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11656 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11657 })
11658 });
11659 cx.assert_editor_state(indoc! {"
11660 fn main() {
11661 sample(param1, ˇparam2);
11662 }
11663
11664 fn sample(param1: u8, param2: u8) {}
11665 "});
11666 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
11667 .await;
11668}
11669
11670#[gpui::test]
11671async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
11672 init_test(cx, |_| {});
11673
11674 let mut cx = EditorLspTestContext::new_rust(
11675 lsp::ServerCapabilities {
11676 signature_help_provider: Some(lsp::SignatureHelpOptions {
11677 ..Default::default()
11678 }),
11679 ..Default::default()
11680 },
11681 cx,
11682 )
11683 .await;
11684
11685 cx.set_state(indoc! {"
11686 fn main() {
11687 overloadedˇ
11688 }
11689 "});
11690
11691 cx.update_editor(|editor, window, cx| {
11692 editor.handle_input("(", window, cx);
11693 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11694 });
11695
11696 // Mock response with 3 signatures
11697 let mocked_response = lsp::SignatureHelp {
11698 signatures: vec![
11699 lsp::SignatureInformation {
11700 label: "fn overloaded(x: i32)".to_string(),
11701 documentation: None,
11702 parameters: Some(vec![lsp::ParameterInformation {
11703 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11704 documentation: None,
11705 }]),
11706 active_parameter: None,
11707 },
11708 lsp::SignatureInformation {
11709 label: "fn overloaded(x: i32, y: i32)".to_string(),
11710 documentation: None,
11711 parameters: Some(vec![
11712 lsp::ParameterInformation {
11713 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11714 documentation: None,
11715 },
11716 lsp::ParameterInformation {
11717 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
11718 documentation: None,
11719 },
11720 ]),
11721 active_parameter: None,
11722 },
11723 lsp::SignatureInformation {
11724 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
11725 documentation: None,
11726 parameters: Some(vec![
11727 lsp::ParameterInformation {
11728 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11729 documentation: None,
11730 },
11731 lsp::ParameterInformation {
11732 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
11733 documentation: None,
11734 },
11735 lsp::ParameterInformation {
11736 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
11737 documentation: None,
11738 },
11739 ]),
11740 active_parameter: None,
11741 },
11742 ],
11743 active_signature: Some(1),
11744 active_parameter: Some(0),
11745 };
11746 handle_signature_help_request(&mut cx, mocked_response).await;
11747
11748 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11749 .await;
11750
11751 // Verify we have multiple signatures and the right one is selected
11752 cx.editor(|editor, _, _| {
11753 let popover = editor.signature_help_state.popover().cloned().unwrap();
11754 assert_eq!(popover.signatures.len(), 3);
11755 // active_signature was 1, so that should be the current
11756 assert_eq!(popover.current_signature, 1);
11757 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
11758 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
11759 assert_eq!(
11760 popover.signatures[2].label,
11761 "fn overloaded(x: i32, y: i32, z: i32)"
11762 );
11763 });
11764
11765 // Test navigation functionality
11766 cx.update_editor(|editor, window, cx| {
11767 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
11768 });
11769
11770 cx.editor(|editor, _, _| {
11771 let popover = editor.signature_help_state.popover().cloned().unwrap();
11772 assert_eq!(popover.current_signature, 2);
11773 });
11774
11775 // Test wrap around
11776 cx.update_editor(|editor, window, cx| {
11777 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
11778 });
11779
11780 cx.editor(|editor, _, _| {
11781 let popover = editor.signature_help_state.popover().cloned().unwrap();
11782 assert_eq!(popover.current_signature, 0);
11783 });
11784
11785 // Test previous navigation
11786 cx.update_editor(|editor, window, cx| {
11787 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
11788 });
11789
11790 cx.editor(|editor, _, _| {
11791 let popover = editor.signature_help_state.popover().cloned().unwrap();
11792 assert_eq!(popover.current_signature, 2);
11793 });
11794}
11795
11796#[gpui::test]
11797async fn test_completion_mode(cx: &mut TestAppContext) {
11798 init_test(cx, |_| {});
11799 let mut cx = EditorLspTestContext::new_rust(
11800 lsp::ServerCapabilities {
11801 completion_provider: Some(lsp::CompletionOptions {
11802 resolve_provider: Some(true),
11803 ..Default::default()
11804 }),
11805 ..Default::default()
11806 },
11807 cx,
11808 )
11809 .await;
11810
11811 struct Run {
11812 run_description: &'static str,
11813 initial_state: String,
11814 buffer_marked_text: String,
11815 completion_label: &'static str,
11816 completion_text: &'static str,
11817 expected_with_insert_mode: String,
11818 expected_with_replace_mode: String,
11819 expected_with_replace_subsequence_mode: String,
11820 expected_with_replace_suffix_mode: String,
11821 }
11822
11823 let runs = [
11824 Run {
11825 run_description: "Start of word matches completion text",
11826 initial_state: "before ediˇ after".into(),
11827 buffer_marked_text: "before <edi|> after".into(),
11828 completion_label: "editor",
11829 completion_text: "editor",
11830 expected_with_insert_mode: "before editorˇ after".into(),
11831 expected_with_replace_mode: "before editorˇ after".into(),
11832 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11833 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11834 },
11835 Run {
11836 run_description: "Accept same text at the middle of the word",
11837 initial_state: "before ediˇtor after".into(),
11838 buffer_marked_text: "before <edi|tor> after".into(),
11839 completion_label: "editor",
11840 completion_text: "editor",
11841 expected_with_insert_mode: "before editorˇtor after".into(),
11842 expected_with_replace_mode: "before editorˇ after".into(),
11843 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11844 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11845 },
11846 Run {
11847 run_description: "End of word matches completion text -- cursor at end",
11848 initial_state: "before torˇ after".into(),
11849 buffer_marked_text: "before <tor|> after".into(),
11850 completion_label: "editor",
11851 completion_text: "editor",
11852 expected_with_insert_mode: "before editorˇ after".into(),
11853 expected_with_replace_mode: "before editorˇ after".into(),
11854 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11855 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11856 },
11857 Run {
11858 run_description: "End of word matches completion text -- cursor at start",
11859 initial_state: "before ˇtor after".into(),
11860 buffer_marked_text: "before <|tor> after".into(),
11861 completion_label: "editor",
11862 completion_text: "editor",
11863 expected_with_insert_mode: "before editorˇtor after".into(),
11864 expected_with_replace_mode: "before editorˇ after".into(),
11865 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11866 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11867 },
11868 Run {
11869 run_description: "Prepend text containing whitespace",
11870 initial_state: "pˇfield: bool".into(),
11871 buffer_marked_text: "<p|field>: bool".into(),
11872 completion_label: "pub ",
11873 completion_text: "pub ",
11874 expected_with_insert_mode: "pub ˇfield: bool".into(),
11875 expected_with_replace_mode: "pub ˇ: bool".into(),
11876 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
11877 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
11878 },
11879 Run {
11880 run_description: "Add element to start of list",
11881 initial_state: "[element_ˇelement_2]".into(),
11882 buffer_marked_text: "[<element_|element_2>]".into(),
11883 completion_label: "element_1",
11884 completion_text: "element_1",
11885 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
11886 expected_with_replace_mode: "[element_1ˇ]".into(),
11887 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
11888 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
11889 },
11890 Run {
11891 run_description: "Add element to start of list -- first and second elements are equal",
11892 initial_state: "[elˇelement]".into(),
11893 buffer_marked_text: "[<el|element>]".into(),
11894 completion_label: "element",
11895 completion_text: "element",
11896 expected_with_insert_mode: "[elementˇelement]".into(),
11897 expected_with_replace_mode: "[elementˇ]".into(),
11898 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
11899 expected_with_replace_suffix_mode: "[elementˇ]".into(),
11900 },
11901 Run {
11902 run_description: "Ends with matching suffix",
11903 initial_state: "SubˇError".into(),
11904 buffer_marked_text: "<Sub|Error>".into(),
11905 completion_label: "SubscriptionError",
11906 completion_text: "SubscriptionError",
11907 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
11908 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11909 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11910 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
11911 },
11912 Run {
11913 run_description: "Suffix is a subsequence -- contiguous",
11914 initial_state: "SubˇErr".into(),
11915 buffer_marked_text: "<Sub|Err>".into(),
11916 completion_label: "SubscriptionError",
11917 completion_text: "SubscriptionError",
11918 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
11919 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11920 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11921 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
11922 },
11923 Run {
11924 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
11925 initial_state: "Suˇscrirr".into(),
11926 buffer_marked_text: "<Su|scrirr>".into(),
11927 completion_label: "SubscriptionError",
11928 completion_text: "SubscriptionError",
11929 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
11930 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11931 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11932 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
11933 },
11934 Run {
11935 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
11936 initial_state: "foo(indˇix)".into(),
11937 buffer_marked_text: "foo(<ind|ix>)".into(),
11938 completion_label: "node_index",
11939 completion_text: "node_index",
11940 expected_with_insert_mode: "foo(node_indexˇix)".into(),
11941 expected_with_replace_mode: "foo(node_indexˇ)".into(),
11942 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
11943 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
11944 },
11945 Run {
11946 run_description: "Replace range ends before cursor - should extend to cursor",
11947 initial_state: "before editˇo after".into(),
11948 buffer_marked_text: "before <{ed}>it|o after".into(),
11949 completion_label: "editor",
11950 completion_text: "editor",
11951 expected_with_insert_mode: "before editorˇo after".into(),
11952 expected_with_replace_mode: "before editorˇo after".into(),
11953 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
11954 expected_with_replace_suffix_mode: "before editorˇo after".into(),
11955 },
11956 Run {
11957 run_description: "Uses label for suffix matching",
11958 initial_state: "before ediˇtor after".into(),
11959 buffer_marked_text: "before <edi|tor> after".into(),
11960 completion_label: "editor",
11961 completion_text: "editor()",
11962 expected_with_insert_mode: "before editor()ˇtor after".into(),
11963 expected_with_replace_mode: "before editor()ˇ after".into(),
11964 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
11965 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
11966 },
11967 Run {
11968 run_description: "Case insensitive subsequence and suffix matching",
11969 initial_state: "before EDiˇtoR after".into(),
11970 buffer_marked_text: "before <EDi|toR> after".into(),
11971 completion_label: "editor",
11972 completion_text: "editor",
11973 expected_with_insert_mode: "before editorˇtoR after".into(),
11974 expected_with_replace_mode: "before editorˇ after".into(),
11975 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11976 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11977 },
11978 ];
11979
11980 for run in runs {
11981 let run_variations = [
11982 (LspInsertMode::Insert, run.expected_with_insert_mode),
11983 (LspInsertMode::Replace, run.expected_with_replace_mode),
11984 (
11985 LspInsertMode::ReplaceSubsequence,
11986 run.expected_with_replace_subsequence_mode,
11987 ),
11988 (
11989 LspInsertMode::ReplaceSuffix,
11990 run.expected_with_replace_suffix_mode,
11991 ),
11992 ];
11993
11994 for (lsp_insert_mode, expected_text) in run_variations {
11995 eprintln!(
11996 "run = {:?}, mode = {lsp_insert_mode:.?}",
11997 run.run_description,
11998 );
11999
12000 update_test_language_settings(&mut cx, |settings| {
12001 settings.defaults.completions = Some(CompletionSettings {
12002 lsp_insert_mode,
12003 words: WordsCompletionMode::Disabled,
12004 lsp: true,
12005 lsp_fetch_timeout_ms: 0,
12006 });
12007 });
12008
12009 cx.set_state(&run.initial_state);
12010 cx.update_editor(|editor, window, cx| {
12011 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12012 });
12013
12014 let counter = Arc::new(AtomicUsize::new(0));
12015 handle_completion_request_with_insert_and_replace(
12016 &mut cx,
12017 &run.buffer_marked_text,
12018 vec![(run.completion_label, run.completion_text)],
12019 counter.clone(),
12020 )
12021 .await;
12022 cx.condition(|editor, _| editor.context_menu_visible())
12023 .await;
12024 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12025
12026 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12027 editor
12028 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12029 .unwrap()
12030 });
12031 cx.assert_editor_state(&expected_text);
12032 handle_resolve_completion_request(&mut cx, None).await;
12033 apply_additional_edits.await.unwrap();
12034 }
12035 }
12036}
12037
12038#[gpui::test]
12039async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
12040 init_test(cx, |_| {});
12041 let mut cx = EditorLspTestContext::new_rust(
12042 lsp::ServerCapabilities {
12043 completion_provider: Some(lsp::CompletionOptions {
12044 resolve_provider: Some(true),
12045 ..Default::default()
12046 }),
12047 ..Default::default()
12048 },
12049 cx,
12050 )
12051 .await;
12052
12053 let initial_state = "SubˇError";
12054 let buffer_marked_text = "<Sub|Error>";
12055 let completion_text = "SubscriptionError";
12056 let expected_with_insert_mode = "SubscriptionErrorˇError";
12057 let expected_with_replace_mode = "SubscriptionErrorˇ";
12058
12059 update_test_language_settings(&mut cx, |settings| {
12060 settings.defaults.completions = Some(CompletionSettings {
12061 words: WordsCompletionMode::Disabled,
12062 // set the opposite here to ensure that the action is overriding the default behavior
12063 lsp_insert_mode: LspInsertMode::Insert,
12064 lsp: true,
12065 lsp_fetch_timeout_ms: 0,
12066 });
12067 });
12068
12069 cx.set_state(initial_state);
12070 cx.update_editor(|editor, window, cx| {
12071 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12072 });
12073
12074 let counter = Arc::new(AtomicUsize::new(0));
12075 handle_completion_request_with_insert_and_replace(
12076 &mut cx,
12077 &buffer_marked_text,
12078 vec![(completion_text, completion_text)],
12079 counter.clone(),
12080 )
12081 .await;
12082 cx.condition(|editor, _| editor.context_menu_visible())
12083 .await;
12084 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12085
12086 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12087 editor
12088 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12089 .unwrap()
12090 });
12091 cx.assert_editor_state(&expected_with_replace_mode);
12092 handle_resolve_completion_request(&mut cx, None).await;
12093 apply_additional_edits.await.unwrap();
12094
12095 update_test_language_settings(&mut cx, |settings| {
12096 settings.defaults.completions = Some(CompletionSettings {
12097 words: WordsCompletionMode::Disabled,
12098 // set the opposite here to ensure that the action is overriding the default behavior
12099 lsp_insert_mode: LspInsertMode::Replace,
12100 lsp: true,
12101 lsp_fetch_timeout_ms: 0,
12102 });
12103 });
12104
12105 cx.set_state(initial_state);
12106 cx.update_editor(|editor, window, cx| {
12107 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12108 });
12109 handle_completion_request_with_insert_and_replace(
12110 &mut cx,
12111 &buffer_marked_text,
12112 vec![(completion_text, completion_text)],
12113 counter.clone(),
12114 )
12115 .await;
12116 cx.condition(|editor, _| editor.context_menu_visible())
12117 .await;
12118 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12119
12120 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12121 editor
12122 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
12123 .unwrap()
12124 });
12125 cx.assert_editor_state(&expected_with_insert_mode);
12126 handle_resolve_completion_request(&mut cx, None).await;
12127 apply_additional_edits.await.unwrap();
12128}
12129
12130#[gpui::test]
12131async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
12132 init_test(cx, |_| {});
12133 let mut cx = EditorLspTestContext::new_rust(
12134 lsp::ServerCapabilities {
12135 completion_provider: Some(lsp::CompletionOptions {
12136 resolve_provider: Some(true),
12137 ..Default::default()
12138 }),
12139 ..Default::default()
12140 },
12141 cx,
12142 )
12143 .await;
12144
12145 // scenario: surrounding text matches completion text
12146 let completion_text = "to_offset";
12147 let initial_state = indoc! {"
12148 1. buf.to_offˇsuffix
12149 2. buf.to_offˇsuf
12150 3. buf.to_offˇfix
12151 4. buf.to_offˇ
12152 5. into_offˇensive
12153 6. ˇsuffix
12154 7. let ˇ //
12155 8. aaˇzz
12156 9. buf.to_off«zzzzzˇ»suffix
12157 10. buf.«ˇzzzzz»suffix
12158 11. to_off«ˇzzzzz»
12159
12160 buf.to_offˇsuffix // newest cursor
12161 "};
12162 let completion_marked_buffer = indoc! {"
12163 1. buf.to_offsuffix
12164 2. buf.to_offsuf
12165 3. buf.to_offfix
12166 4. buf.to_off
12167 5. into_offensive
12168 6. suffix
12169 7. let //
12170 8. aazz
12171 9. buf.to_offzzzzzsuffix
12172 10. buf.zzzzzsuffix
12173 11. to_offzzzzz
12174
12175 buf.<to_off|suffix> // newest cursor
12176 "};
12177 let expected = indoc! {"
12178 1. buf.to_offsetˇ
12179 2. buf.to_offsetˇsuf
12180 3. buf.to_offsetˇfix
12181 4. buf.to_offsetˇ
12182 5. into_offsetˇensive
12183 6. to_offsetˇsuffix
12184 7. let to_offsetˇ //
12185 8. aato_offsetˇzz
12186 9. buf.to_offsetˇ
12187 10. buf.to_offsetˇsuffix
12188 11. to_offsetˇ
12189
12190 buf.to_offsetˇ // newest cursor
12191 "};
12192 cx.set_state(initial_state);
12193 cx.update_editor(|editor, window, cx| {
12194 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12195 });
12196 handle_completion_request_with_insert_and_replace(
12197 &mut cx,
12198 completion_marked_buffer,
12199 vec![(completion_text, completion_text)],
12200 Arc::new(AtomicUsize::new(0)),
12201 )
12202 .await;
12203 cx.condition(|editor, _| editor.context_menu_visible())
12204 .await;
12205 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12206 editor
12207 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12208 .unwrap()
12209 });
12210 cx.assert_editor_state(expected);
12211 handle_resolve_completion_request(&mut cx, None).await;
12212 apply_additional_edits.await.unwrap();
12213
12214 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
12215 let completion_text = "foo_and_bar";
12216 let initial_state = indoc! {"
12217 1. ooanbˇ
12218 2. zooanbˇ
12219 3. ooanbˇz
12220 4. zooanbˇz
12221 5. ooanˇ
12222 6. oanbˇ
12223
12224 ooanbˇ
12225 "};
12226 let completion_marked_buffer = indoc! {"
12227 1. ooanb
12228 2. zooanb
12229 3. ooanbz
12230 4. zooanbz
12231 5. ooan
12232 6. oanb
12233
12234 <ooanb|>
12235 "};
12236 let expected = indoc! {"
12237 1. foo_and_barˇ
12238 2. zfoo_and_barˇ
12239 3. foo_and_barˇz
12240 4. zfoo_and_barˇz
12241 5. ooanfoo_and_barˇ
12242 6. oanbfoo_and_barˇ
12243
12244 foo_and_barˇ
12245 "};
12246 cx.set_state(initial_state);
12247 cx.update_editor(|editor, window, cx| {
12248 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12249 });
12250 handle_completion_request_with_insert_and_replace(
12251 &mut cx,
12252 completion_marked_buffer,
12253 vec![(completion_text, completion_text)],
12254 Arc::new(AtomicUsize::new(0)),
12255 )
12256 .await;
12257 cx.condition(|editor, _| editor.context_menu_visible())
12258 .await;
12259 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12260 editor
12261 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12262 .unwrap()
12263 });
12264 cx.assert_editor_state(expected);
12265 handle_resolve_completion_request(&mut cx, None).await;
12266 apply_additional_edits.await.unwrap();
12267
12268 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
12269 // (expects the same as if it was inserted at the end)
12270 let completion_text = "foo_and_bar";
12271 let initial_state = indoc! {"
12272 1. ooˇanb
12273 2. zooˇanb
12274 3. ooˇanbz
12275 4. zooˇanbz
12276
12277 ooˇanb
12278 "};
12279 let completion_marked_buffer = indoc! {"
12280 1. ooanb
12281 2. zooanb
12282 3. ooanbz
12283 4. zooanbz
12284
12285 <oo|anb>
12286 "};
12287 let expected = indoc! {"
12288 1. foo_and_barˇ
12289 2. zfoo_and_barˇ
12290 3. foo_and_barˇz
12291 4. zfoo_and_barˇz
12292
12293 foo_and_barˇ
12294 "};
12295 cx.set_state(initial_state);
12296 cx.update_editor(|editor, window, cx| {
12297 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12298 });
12299 handle_completion_request_with_insert_and_replace(
12300 &mut cx,
12301 completion_marked_buffer,
12302 vec![(completion_text, completion_text)],
12303 Arc::new(AtomicUsize::new(0)),
12304 )
12305 .await;
12306 cx.condition(|editor, _| editor.context_menu_visible())
12307 .await;
12308 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12309 editor
12310 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12311 .unwrap()
12312 });
12313 cx.assert_editor_state(expected);
12314 handle_resolve_completion_request(&mut cx, None).await;
12315 apply_additional_edits.await.unwrap();
12316}
12317
12318// This used to crash
12319#[gpui::test]
12320async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
12321 init_test(cx, |_| {});
12322
12323 let buffer_text = indoc! {"
12324 fn main() {
12325 10.satu;
12326
12327 //
12328 // separate cursors so they open in different excerpts (manually reproducible)
12329 //
12330
12331 10.satu20;
12332 }
12333 "};
12334 let multibuffer_text_with_selections = indoc! {"
12335 fn main() {
12336 10.satuˇ;
12337
12338 //
12339
12340 //
12341
12342 10.satuˇ20;
12343 }
12344 "};
12345 let expected_multibuffer = indoc! {"
12346 fn main() {
12347 10.saturating_sub()ˇ;
12348
12349 //
12350
12351 //
12352
12353 10.saturating_sub()ˇ;
12354 }
12355 "};
12356
12357 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
12358 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
12359
12360 let fs = FakeFs::new(cx.executor());
12361 fs.insert_tree(
12362 path!("/a"),
12363 json!({
12364 "main.rs": buffer_text,
12365 }),
12366 )
12367 .await;
12368
12369 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12370 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12371 language_registry.add(rust_lang());
12372 let mut fake_servers = language_registry.register_fake_lsp(
12373 "Rust",
12374 FakeLspAdapter {
12375 capabilities: lsp::ServerCapabilities {
12376 completion_provider: Some(lsp::CompletionOptions {
12377 resolve_provider: None,
12378 ..lsp::CompletionOptions::default()
12379 }),
12380 ..lsp::ServerCapabilities::default()
12381 },
12382 ..FakeLspAdapter::default()
12383 },
12384 );
12385 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12386 let cx = &mut VisualTestContext::from_window(*workspace, cx);
12387 let buffer = project
12388 .update(cx, |project, cx| {
12389 project.open_local_buffer(path!("/a/main.rs"), cx)
12390 })
12391 .await
12392 .unwrap();
12393
12394 let multi_buffer = cx.new(|cx| {
12395 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
12396 multi_buffer.push_excerpts(
12397 buffer.clone(),
12398 [ExcerptRange::new(0..first_excerpt_end)],
12399 cx,
12400 );
12401 multi_buffer.push_excerpts(
12402 buffer.clone(),
12403 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
12404 cx,
12405 );
12406 multi_buffer
12407 });
12408
12409 let editor = workspace
12410 .update(cx, |_, window, cx| {
12411 cx.new(|cx| {
12412 Editor::new(
12413 EditorMode::Full {
12414 scale_ui_elements_with_buffer_font_size: false,
12415 show_active_line_background: false,
12416 sized_by_content: false,
12417 },
12418 multi_buffer.clone(),
12419 Some(project.clone()),
12420 window,
12421 cx,
12422 )
12423 })
12424 })
12425 .unwrap();
12426
12427 let pane = workspace
12428 .update(cx, |workspace, _, _| workspace.active_pane().clone())
12429 .unwrap();
12430 pane.update_in(cx, |pane, window, cx| {
12431 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
12432 });
12433
12434 let fake_server = fake_servers.next().await.unwrap();
12435
12436 editor.update_in(cx, |editor, window, cx| {
12437 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12438 s.select_ranges([
12439 Point::new(1, 11)..Point::new(1, 11),
12440 Point::new(7, 11)..Point::new(7, 11),
12441 ])
12442 });
12443
12444 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
12445 });
12446
12447 editor.update_in(cx, |editor, window, cx| {
12448 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12449 });
12450
12451 fake_server
12452 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
12453 let completion_item = lsp::CompletionItem {
12454 label: "saturating_sub()".into(),
12455 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
12456 lsp::InsertReplaceEdit {
12457 new_text: "saturating_sub()".to_owned(),
12458 insert: lsp::Range::new(
12459 lsp::Position::new(7, 7),
12460 lsp::Position::new(7, 11),
12461 ),
12462 replace: lsp::Range::new(
12463 lsp::Position::new(7, 7),
12464 lsp::Position::new(7, 13),
12465 ),
12466 },
12467 )),
12468 ..lsp::CompletionItem::default()
12469 };
12470
12471 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
12472 })
12473 .next()
12474 .await
12475 .unwrap();
12476
12477 cx.condition(&editor, |editor, _| editor.context_menu_visible())
12478 .await;
12479
12480 editor
12481 .update_in(cx, |editor, window, cx| {
12482 editor
12483 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12484 .unwrap()
12485 })
12486 .await
12487 .unwrap();
12488
12489 editor.update(cx, |editor, cx| {
12490 assert_text_with_selections(editor, expected_multibuffer, cx);
12491 })
12492}
12493
12494#[gpui::test]
12495async fn test_completion(cx: &mut TestAppContext) {
12496 init_test(cx, |_| {});
12497
12498 let mut cx = EditorLspTestContext::new_rust(
12499 lsp::ServerCapabilities {
12500 completion_provider: Some(lsp::CompletionOptions {
12501 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12502 resolve_provider: Some(true),
12503 ..Default::default()
12504 }),
12505 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12506 ..Default::default()
12507 },
12508 cx,
12509 )
12510 .await;
12511 let counter = Arc::new(AtomicUsize::new(0));
12512
12513 cx.set_state(indoc! {"
12514 oneˇ
12515 two
12516 three
12517 "});
12518 cx.simulate_keystroke(".");
12519 handle_completion_request(
12520 indoc! {"
12521 one.|<>
12522 two
12523 three
12524 "},
12525 vec!["first_completion", "second_completion"],
12526 true,
12527 counter.clone(),
12528 &mut cx,
12529 )
12530 .await;
12531 cx.condition(|editor, _| editor.context_menu_visible())
12532 .await;
12533 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12534
12535 let _handler = handle_signature_help_request(
12536 &mut cx,
12537 lsp::SignatureHelp {
12538 signatures: vec![lsp::SignatureInformation {
12539 label: "test signature".to_string(),
12540 documentation: None,
12541 parameters: Some(vec![lsp::ParameterInformation {
12542 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
12543 documentation: None,
12544 }]),
12545 active_parameter: None,
12546 }],
12547 active_signature: None,
12548 active_parameter: None,
12549 },
12550 );
12551 cx.update_editor(|editor, window, cx| {
12552 assert!(
12553 !editor.signature_help_state.is_shown(),
12554 "No signature help was called for"
12555 );
12556 editor.show_signature_help(&ShowSignatureHelp, window, cx);
12557 });
12558 cx.run_until_parked();
12559 cx.update_editor(|editor, _, _| {
12560 assert!(
12561 !editor.signature_help_state.is_shown(),
12562 "No signature help should be shown when completions menu is open"
12563 );
12564 });
12565
12566 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12567 editor.context_menu_next(&Default::default(), window, cx);
12568 editor
12569 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12570 .unwrap()
12571 });
12572 cx.assert_editor_state(indoc! {"
12573 one.second_completionˇ
12574 two
12575 three
12576 "});
12577
12578 handle_resolve_completion_request(
12579 &mut cx,
12580 Some(vec![
12581 (
12582 //This overlaps with the primary completion edit which is
12583 //misbehavior from the LSP spec, test that we filter it out
12584 indoc! {"
12585 one.second_ˇcompletion
12586 two
12587 threeˇ
12588 "},
12589 "overlapping additional edit",
12590 ),
12591 (
12592 indoc! {"
12593 one.second_completion
12594 two
12595 threeˇ
12596 "},
12597 "\nadditional edit",
12598 ),
12599 ]),
12600 )
12601 .await;
12602 apply_additional_edits.await.unwrap();
12603 cx.assert_editor_state(indoc! {"
12604 one.second_completionˇ
12605 two
12606 three
12607 additional edit
12608 "});
12609
12610 cx.set_state(indoc! {"
12611 one.second_completion
12612 twoˇ
12613 threeˇ
12614 additional edit
12615 "});
12616 cx.simulate_keystroke(" ");
12617 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12618 cx.simulate_keystroke("s");
12619 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12620
12621 cx.assert_editor_state(indoc! {"
12622 one.second_completion
12623 two sˇ
12624 three sˇ
12625 additional edit
12626 "});
12627 handle_completion_request(
12628 indoc! {"
12629 one.second_completion
12630 two s
12631 three <s|>
12632 additional edit
12633 "},
12634 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
12635 true,
12636 counter.clone(),
12637 &mut cx,
12638 )
12639 .await;
12640 cx.condition(|editor, _| editor.context_menu_visible())
12641 .await;
12642 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12643
12644 cx.simulate_keystroke("i");
12645
12646 handle_completion_request(
12647 indoc! {"
12648 one.second_completion
12649 two si
12650 three <si|>
12651 additional edit
12652 "},
12653 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
12654 true,
12655 counter.clone(),
12656 &mut cx,
12657 )
12658 .await;
12659 cx.condition(|editor, _| editor.context_menu_visible())
12660 .await;
12661 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12662
12663 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12664 editor
12665 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12666 .unwrap()
12667 });
12668 cx.assert_editor_state(indoc! {"
12669 one.second_completion
12670 two sixth_completionˇ
12671 three sixth_completionˇ
12672 additional edit
12673 "});
12674
12675 apply_additional_edits.await.unwrap();
12676
12677 update_test_language_settings(&mut cx, |settings| {
12678 settings.defaults.show_completions_on_input = Some(false);
12679 });
12680 cx.set_state("editorˇ");
12681 cx.simulate_keystroke(".");
12682 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12683 cx.simulate_keystrokes("c l o");
12684 cx.assert_editor_state("editor.cloˇ");
12685 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12686 cx.update_editor(|editor, window, cx| {
12687 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12688 });
12689 handle_completion_request(
12690 "editor.<clo|>",
12691 vec!["close", "clobber"],
12692 true,
12693 counter.clone(),
12694 &mut cx,
12695 )
12696 .await;
12697 cx.condition(|editor, _| editor.context_menu_visible())
12698 .await;
12699 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
12700
12701 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12702 editor
12703 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12704 .unwrap()
12705 });
12706 cx.assert_editor_state("editor.clobberˇ");
12707 handle_resolve_completion_request(&mut cx, None).await;
12708 apply_additional_edits.await.unwrap();
12709}
12710
12711#[gpui::test]
12712async fn test_completion_reuse(cx: &mut TestAppContext) {
12713 init_test(cx, |_| {});
12714
12715 let mut cx = EditorLspTestContext::new_rust(
12716 lsp::ServerCapabilities {
12717 completion_provider: Some(lsp::CompletionOptions {
12718 trigger_characters: Some(vec![".".to_string()]),
12719 ..Default::default()
12720 }),
12721 ..Default::default()
12722 },
12723 cx,
12724 )
12725 .await;
12726
12727 let counter = Arc::new(AtomicUsize::new(0));
12728 cx.set_state("objˇ");
12729 cx.simulate_keystroke(".");
12730
12731 // Initial completion request returns complete results
12732 let is_incomplete = false;
12733 handle_completion_request(
12734 "obj.|<>",
12735 vec!["a", "ab", "abc"],
12736 is_incomplete,
12737 counter.clone(),
12738 &mut cx,
12739 )
12740 .await;
12741 cx.run_until_parked();
12742 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12743 cx.assert_editor_state("obj.ˇ");
12744 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12745
12746 // Type "a" - filters existing completions
12747 cx.simulate_keystroke("a");
12748 cx.run_until_parked();
12749 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12750 cx.assert_editor_state("obj.aˇ");
12751 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12752
12753 // Type "b" - filters existing completions
12754 cx.simulate_keystroke("b");
12755 cx.run_until_parked();
12756 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12757 cx.assert_editor_state("obj.abˇ");
12758 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12759
12760 // Type "c" - filters existing completions
12761 cx.simulate_keystroke("c");
12762 cx.run_until_parked();
12763 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12764 cx.assert_editor_state("obj.abcˇ");
12765 check_displayed_completions(vec!["abc"], &mut cx);
12766
12767 // Backspace to delete "c" - filters existing completions
12768 cx.update_editor(|editor, window, cx| {
12769 editor.backspace(&Backspace, window, cx);
12770 });
12771 cx.run_until_parked();
12772 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12773 cx.assert_editor_state("obj.abˇ");
12774 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12775
12776 // Moving cursor to the left dismisses menu.
12777 cx.update_editor(|editor, window, cx| {
12778 editor.move_left(&MoveLeft, window, cx);
12779 });
12780 cx.run_until_parked();
12781 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12782 cx.assert_editor_state("obj.aˇb");
12783 cx.update_editor(|editor, _, _| {
12784 assert_eq!(editor.context_menu_visible(), false);
12785 });
12786
12787 // Type "b" - new request
12788 cx.simulate_keystroke("b");
12789 let is_incomplete = false;
12790 handle_completion_request(
12791 "obj.<ab|>a",
12792 vec!["ab", "abc"],
12793 is_incomplete,
12794 counter.clone(),
12795 &mut cx,
12796 )
12797 .await;
12798 cx.run_until_parked();
12799 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12800 cx.assert_editor_state("obj.abˇb");
12801 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12802
12803 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
12804 cx.update_editor(|editor, window, cx| {
12805 editor.backspace(&Backspace, window, cx);
12806 });
12807 let is_incomplete = false;
12808 handle_completion_request(
12809 "obj.<a|>b",
12810 vec!["a", "ab", "abc"],
12811 is_incomplete,
12812 counter.clone(),
12813 &mut cx,
12814 )
12815 .await;
12816 cx.run_until_parked();
12817 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12818 cx.assert_editor_state("obj.aˇb");
12819 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12820
12821 // Backspace to delete "a" - dismisses menu.
12822 cx.update_editor(|editor, window, cx| {
12823 editor.backspace(&Backspace, window, cx);
12824 });
12825 cx.run_until_parked();
12826 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12827 cx.assert_editor_state("obj.ˇb");
12828 cx.update_editor(|editor, _, _| {
12829 assert_eq!(editor.context_menu_visible(), false);
12830 });
12831}
12832
12833#[gpui::test]
12834async fn test_word_completion(cx: &mut TestAppContext) {
12835 let lsp_fetch_timeout_ms = 10;
12836 init_test(cx, |language_settings| {
12837 language_settings.defaults.completions = Some(CompletionSettings {
12838 words: WordsCompletionMode::Fallback,
12839 lsp: true,
12840 lsp_fetch_timeout_ms: 10,
12841 lsp_insert_mode: LspInsertMode::Insert,
12842 });
12843 });
12844
12845 let mut cx = EditorLspTestContext::new_rust(
12846 lsp::ServerCapabilities {
12847 completion_provider: Some(lsp::CompletionOptions {
12848 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12849 ..lsp::CompletionOptions::default()
12850 }),
12851 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12852 ..lsp::ServerCapabilities::default()
12853 },
12854 cx,
12855 )
12856 .await;
12857
12858 let throttle_completions = Arc::new(AtomicBool::new(false));
12859
12860 let lsp_throttle_completions = throttle_completions.clone();
12861 let _completion_requests_handler =
12862 cx.lsp
12863 .server
12864 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
12865 let lsp_throttle_completions = lsp_throttle_completions.clone();
12866 let cx = cx.clone();
12867 async move {
12868 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
12869 cx.background_executor()
12870 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
12871 .await;
12872 }
12873 Ok(Some(lsp::CompletionResponse::Array(vec![
12874 lsp::CompletionItem {
12875 label: "first".into(),
12876 ..lsp::CompletionItem::default()
12877 },
12878 lsp::CompletionItem {
12879 label: "last".into(),
12880 ..lsp::CompletionItem::default()
12881 },
12882 ])))
12883 }
12884 });
12885
12886 cx.set_state(indoc! {"
12887 oneˇ
12888 two
12889 three
12890 "});
12891 cx.simulate_keystroke(".");
12892 cx.executor().run_until_parked();
12893 cx.condition(|editor, _| editor.context_menu_visible())
12894 .await;
12895 cx.update_editor(|editor, window, cx| {
12896 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12897 {
12898 assert_eq!(
12899 completion_menu_entries(&menu),
12900 &["first", "last"],
12901 "When LSP server is fast to reply, no fallback word completions are used"
12902 );
12903 } else {
12904 panic!("expected completion menu to be open");
12905 }
12906 editor.cancel(&Cancel, window, cx);
12907 });
12908 cx.executor().run_until_parked();
12909 cx.condition(|editor, _| !editor.context_menu_visible())
12910 .await;
12911
12912 throttle_completions.store(true, atomic::Ordering::Release);
12913 cx.simulate_keystroke(".");
12914 cx.executor()
12915 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
12916 cx.executor().run_until_parked();
12917 cx.condition(|editor, _| editor.context_menu_visible())
12918 .await;
12919 cx.update_editor(|editor, _, _| {
12920 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12921 {
12922 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
12923 "When LSP server is slow, document words can be shown instead, if configured accordingly");
12924 } else {
12925 panic!("expected completion menu to be open");
12926 }
12927 });
12928}
12929
12930#[gpui::test]
12931async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
12932 init_test(cx, |language_settings| {
12933 language_settings.defaults.completions = Some(CompletionSettings {
12934 words: WordsCompletionMode::Enabled,
12935 lsp: true,
12936 lsp_fetch_timeout_ms: 0,
12937 lsp_insert_mode: LspInsertMode::Insert,
12938 });
12939 });
12940
12941 let mut cx = EditorLspTestContext::new_rust(
12942 lsp::ServerCapabilities {
12943 completion_provider: Some(lsp::CompletionOptions {
12944 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12945 ..lsp::CompletionOptions::default()
12946 }),
12947 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12948 ..lsp::ServerCapabilities::default()
12949 },
12950 cx,
12951 )
12952 .await;
12953
12954 let _completion_requests_handler =
12955 cx.lsp
12956 .server
12957 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
12958 Ok(Some(lsp::CompletionResponse::Array(vec![
12959 lsp::CompletionItem {
12960 label: "first".into(),
12961 ..lsp::CompletionItem::default()
12962 },
12963 lsp::CompletionItem {
12964 label: "last".into(),
12965 ..lsp::CompletionItem::default()
12966 },
12967 ])))
12968 });
12969
12970 cx.set_state(indoc! {"ˇ
12971 first
12972 last
12973 second
12974 "});
12975 cx.simulate_keystroke(".");
12976 cx.executor().run_until_parked();
12977 cx.condition(|editor, _| editor.context_menu_visible())
12978 .await;
12979 cx.update_editor(|editor, _, _| {
12980 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12981 {
12982 assert_eq!(
12983 completion_menu_entries(&menu),
12984 &["first", "last", "second"],
12985 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
12986 );
12987 } else {
12988 panic!("expected completion menu to be open");
12989 }
12990 });
12991}
12992
12993#[gpui::test]
12994async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
12995 init_test(cx, |language_settings| {
12996 language_settings.defaults.completions = Some(CompletionSettings {
12997 words: WordsCompletionMode::Disabled,
12998 lsp: true,
12999 lsp_fetch_timeout_ms: 0,
13000 lsp_insert_mode: LspInsertMode::Insert,
13001 });
13002 });
13003
13004 let mut cx = EditorLspTestContext::new_rust(
13005 lsp::ServerCapabilities {
13006 completion_provider: Some(lsp::CompletionOptions {
13007 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
13008 ..lsp::CompletionOptions::default()
13009 }),
13010 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13011 ..lsp::ServerCapabilities::default()
13012 },
13013 cx,
13014 )
13015 .await;
13016
13017 let _completion_requests_handler =
13018 cx.lsp
13019 .server
13020 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
13021 panic!("LSP completions should not be queried when dealing with word completions")
13022 });
13023
13024 cx.set_state(indoc! {"ˇ
13025 first
13026 last
13027 second
13028 "});
13029 cx.update_editor(|editor, window, cx| {
13030 editor.show_word_completions(&ShowWordCompletions, window, cx);
13031 });
13032 cx.executor().run_until_parked();
13033 cx.condition(|editor, _| editor.context_menu_visible())
13034 .await;
13035 cx.update_editor(|editor, _, _| {
13036 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13037 {
13038 assert_eq!(
13039 completion_menu_entries(&menu),
13040 &["first", "last", "second"],
13041 "`ShowWordCompletions` action should show word completions"
13042 );
13043 } else {
13044 panic!("expected completion menu to be open");
13045 }
13046 });
13047
13048 cx.simulate_keystroke("l");
13049 cx.executor().run_until_parked();
13050 cx.condition(|editor, _| editor.context_menu_visible())
13051 .await;
13052 cx.update_editor(|editor, _, _| {
13053 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13054 {
13055 assert_eq!(
13056 completion_menu_entries(&menu),
13057 &["last"],
13058 "After showing word completions, further editing should filter them and not query the LSP"
13059 );
13060 } else {
13061 panic!("expected completion menu to be open");
13062 }
13063 });
13064}
13065
13066#[gpui::test]
13067async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
13068 init_test(cx, |language_settings| {
13069 language_settings.defaults.completions = Some(CompletionSettings {
13070 words: WordsCompletionMode::Fallback,
13071 lsp: false,
13072 lsp_fetch_timeout_ms: 0,
13073 lsp_insert_mode: LspInsertMode::Insert,
13074 });
13075 });
13076
13077 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13078
13079 cx.set_state(indoc! {"ˇ
13080 0_usize
13081 let
13082 33
13083 4.5f32
13084 "});
13085 cx.update_editor(|editor, window, cx| {
13086 editor.show_completions(&ShowCompletions::default(), window, cx);
13087 });
13088 cx.executor().run_until_parked();
13089 cx.condition(|editor, _| editor.context_menu_visible())
13090 .await;
13091 cx.update_editor(|editor, window, cx| {
13092 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13093 {
13094 assert_eq!(
13095 completion_menu_entries(&menu),
13096 &["let"],
13097 "With no digits in the completion query, no digits should be in the word completions"
13098 );
13099 } else {
13100 panic!("expected completion menu to be open");
13101 }
13102 editor.cancel(&Cancel, window, cx);
13103 });
13104
13105 cx.set_state(indoc! {"3ˇ
13106 0_usize
13107 let
13108 3
13109 33.35f32
13110 "});
13111 cx.update_editor(|editor, window, cx| {
13112 editor.show_completions(&ShowCompletions::default(), window, cx);
13113 });
13114 cx.executor().run_until_parked();
13115 cx.condition(|editor, _| editor.context_menu_visible())
13116 .await;
13117 cx.update_editor(|editor, _, _| {
13118 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13119 {
13120 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
13121 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
13122 } else {
13123 panic!("expected completion menu to be open");
13124 }
13125 });
13126}
13127
13128fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
13129 let position = || lsp::Position {
13130 line: params.text_document_position.position.line,
13131 character: params.text_document_position.position.character,
13132 };
13133 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13134 range: lsp::Range {
13135 start: position(),
13136 end: position(),
13137 },
13138 new_text: text.to_string(),
13139 }))
13140}
13141
13142#[gpui::test]
13143async fn test_multiline_completion(cx: &mut TestAppContext) {
13144 init_test(cx, |_| {});
13145
13146 let fs = FakeFs::new(cx.executor());
13147 fs.insert_tree(
13148 path!("/a"),
13149 json!({
13150 "main.ts": "a",
13151 }),
13152 )
13153 .await;
13154
13155 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13156 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13157 let typescript_language = Arc::new(Language::new(
13158 LanguageConfig {
13159 name: "TypeScript".into(),
13160 matcher: LanguageMatcher {
13161 path_suffixes: vec!["ts".to_string()],
13162 ..LanguageMatcher::default()
13163 },
13164 line_comments: vec!["// ".into()],
13165 ..LanguageConfig::default()
13166 },
13167 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
13168 ));
13169 language_registry.add(typescript_language.clone());
13170 let mut fake_servers = language_registry.register_fake_lsp(
13171 "TypeScript",
13172 FakeLspAdapter {
13173 capabilities: lsp::ServerCapabilities {
13174 completion_provider: Some(lsp::CompletionOptions {
13175 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
13176 ..lsp::CompletionOptions::default()
13177 }),
13178 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13179 ..lsp::ServerCapabilities::default()
13180 },
13181 // Emulate vtsls label generation
13182 label_for_completion: Some(Box::new(|item, _| {
13183 let text = if let Some(description) = item
13184 .label_details
13185 .as_ref()
13186 .and_then(|label_details| label_details.description.as_ref())
13187 {
13188 format!("{} {}", item.label, description)
13189 } else if let Some(detail) = &item.detail {
13190 format!("{} {}", item.label, detail)
13191 } else {
13192 item.label.clone()
13193 };
13194 let len = text.len();
13195 Some(language::CodeLabel {
13196 text,
13197 runs: Vec::new(),
13198 filter_range: 0..len,
13199 })
13200 })),
13201 ..FakeLspAdapter::default()
13202 },
13203 );
13204 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13205 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13206 let worktree_id = workspace
13207 .update(cx, |workspace, _window, cx| {
13208 workspace.project().update(cx, |project, cx| {
13209 project.worktrees(cx).next().unwrap().read(cx).id()
13210 })
13211 })
13212 .unwrap();
13213 let _buffer = project
13214 .update(cx, |project, cx| {
13215 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
13216 })
13217 .await
13218 .unwrap();
13219 let editor = workspace
13220 .update(cx, |workspace, window, cx| {
13221 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
13222 })
13223 .unwrap()
13224 .await
13225 .unwrap()
13226 .downcast::<Editor>()
13227 .unwrap();
13228 let fake_server = fake_servers.next().await.unwrap();
13229
13230 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
13231 let multiline_label_2 = "a\nb\nc\n";
13232 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
13233 let multiline_description = "d\ne\nf\n";
13234 let multiline_detail_2 = "g\nh\ni\n";
13235
13236 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
13237 move |params, _| async move {
13238 Ok(Some(lsp::CompletionResponse::Array(vec![
13239 lsp::CompletionItem {
13240 label: multiline_label.to_string(),
13241 text_edit: gen_text_edit(¶ms, "new_text_1"),
13242 ..lsp::CompletionItem::default()
13243 },
13244 lsp::CompletionItem {
13245 label: "single line label 1".to_string(),
13246 detail: Some(multiline_detail.to_string()),
13247 text_edit: gen_text_edit(¶ms, "new_text_2"),
13248 ..lsp::CompletionItem::default()
13249 },
13250 lsp::CompletionItem {
13251 label: "single line label 2".to_string(),
13252 label_details: Some(lsp::CompletionItemLabelDetails {
13253 description: Some(multiline_description.to_string()),
13254 detail: None,
13255 }),
13256 text_edit: gen_text_edit(¶ms, "new_text_2"),
13257 ..lsp::CompletionItem::default()
13258 },
13259 lsp::CompletionItem {
13260 label: multiline_label_2.to_string(),
13261 detail: Some(multiline_detail_2.to_string()),
13262 text_edit: gen_text_edit(¶ms, "new_text_3"),
13263 ..lsp::CompletionItem::default()
13264 },
13265 lsp::CompletionItem {
13266 label: "Label with many spaces and \t but without newlines".to_string(),
13267 detail: Some(
13268 "Details with many spaces and \t but without newlines".to_string(),
13269 ),
13270 text_edit: gen_text_edit(¶ms, "new_text_4"),
13271 ..lsp::CompletionItem::default()
13272 },
13273 ])))
13274 },
13275 );
13276
13277 editor.update_in(cx, |editor, window, cx| {
13278 cx.focus_self(window);
13279 editor.move_to_end(&MoveToEnd, window, cx);
13280 editor.handle_input(".", window, cx);
13281 });
13282 cx.run_until_parked();
13283 completion_handle.next().await.unwrap();
13284
13285 editor.update(cx, |editor, _| {
13286 assert!(editor.context_menu_visible());
13287 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13288 {
13289 let completion_labels = menu
13290 .completions
13291 .borrow()
13292 .iter()
13293 .map(|c| c.label.text.clone())
13294 .collect::<Vec<_>>();
13295 assert_eq!(
13296 completion_labels,
13297 &[
13298 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
13299 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
13300 "single line label 2 d e f ",
13301 "a b c g h i ",
13302 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
13303 ],
13304 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
13305 );
13306
13307 for completion in menu
13308 .completions
13309 .borrow()
13310 .iter() {
13311 assert_eq!(
13312 completion.label.filter_range,
13313 0..completion.label.text.len(),
13314 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
13315 );
13316 }
13317 } else {
13318 panic!("expected completion menu to be open");
13319 }
13320 });
13321}
13322
13323#[gpui::test]
13324async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
13325 init_test(cx, |_| {});
13326 let mut cx = EditorLspTestContext::new_rust(
13327 lsp::ServerCapabilities {
13328 completion_provider: Some(lsp::CompletionOptions {
13329 trigger_characters: Some(vec![".".to_string()]),
13330 ..Default::default()
13331 }),
13332 ..Default::default()
13333 },
13334 cx,
13335 )
13336 .await;
13337 cx.lsp
13338 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13339 Ok(Some(lsp::CompletionResponse::Array(vec![
13340 lsp::CompletionItem {
13341 label: "first".into(),
13342 ..Default::default()
13343 },
13344 lsp::CompletionItem {
13345 label: "last".into(),
13346 ..Default::default()
13347 },
13348 ])))
13349 });
13350 cx.set_state("variableˇ");
13351 cx.simulate_keystroke(".");
13352 cx.executor().run_until_parked();
13353
13354 cx.update_editor(|editor, _, _| {
13355 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13356 {
13357 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
13358 } else {
13359 panic!("expected completion menu to be open");
13360 }
13361 });
13362
13363 cx.update_editor(|editor, window, cx| {
13364 editor.move_page_down(&MovePageDown::default(), window, cx);
13365 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13366 {
13367 assert!(
13368 menu.selected_item == 1,
13369 "expected PageDown to select the last item from the context menu"
13370 );
13371 } else {
13372 panic!("expected completion menu to stay open after PageDown");
13373 }
13374 });
13375
13376 cx.update_editor(|editor, window, cx| {
13377 editor.move_page_up(&MovePageUp::default(), window, cx);
13378 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13379 {
13380 assert!(
13381 menu.selected_item == 0,
13382 "expected PageUp to select the first item from the context menu"
13383 );
13384 } else {
13385 panic!("expected completion menu to stay open after PageUp");
13386 }
13387 });
13388}
13389
13390#[gpui::test]
13391async fn test_as_is_completions(cx: &mut TestAppContext) {
13392 init_test(cx, |_| {});
13393 let mut cx = EditorLspTestContext::new_rust(
13394 lsp::ServerCapabilities {
13395 completion_provider: Some(lsp::CompletionOptions {
13396 ..Default::default()
13397 }),
13398 ..Default::default()
13399 },
13400 cx,
13401 )
13402 .await;
13403 cx.lsp
13404 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13405 Ok(Some(lsp::CompletionResponse::Array(vec![
13406 lsp::CompletionItem {
13407 label: "unsafe".into(),
13408 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13409 range: lsp::Range {
13410 start: lsp::Position {
13411 line: 1,
13412 character: 2,
13413 },
13414 end: lsp::Position {
13415 line: 1,
13416 character: 3,
13417 },
13418 },
13419 new_text: "unsafe".to_string(),
13420 })),
13421 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
13422 ..Default::default()
13423 },
13424 ])))
13425 });
13426 cx.set_state("fn a() {}\n nˇ");
13427 cx.executor().run_until_parked();
13428 cx.update_editor(|editor, window, cx| {
13429 editor.show_completions(
13430 &ShowCompletions {
13431 trigger: Some("\n".into()),
13432 },
13433 window,
13434 cx,
13435 );
13436 });
13437 cx.executor().run_until_parked();
13438
13439 cx.update_editor(|editor, window, cx| {
13440 editor.confirm_completion(&Default::default(), window, cx)
13441 });
13442 cx.executor().run_until_parked();
13443 cx.assert_editor_state("fn a() {}\n unsafeˇ");
13444}
13445
13446#[gpui::test]
13447async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
13448 init_test(cx, |_| {});
13449
13450 let mut cx = EditorLspTestContext::new_rust(
13451 lsp::ServerCapabilities {
13452 completion_provider: Some(lsp::CompletionOptions {
13453 trigger_characters: Some(vec![".".to_string()]),
13454 resolve_provider: Some(true),
13455 ..Default::default()
13456 }),
13457 ..Default::default()
13458 },
13459 cx,
13460 )
13461 .await;
13462
13463 cx.set_state("fn main() { let a = 2ˇ; }");
13464 cx.simulate_keystroke(".");
13465 let completion_item = lsp::CompletionItem {
13466 label: "Some".into(),
13467 kind: Some(lsp::CompletionItemKind::SNIPPET),
13468 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
13469 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
13470 kind: lsp::MarkupKind::Markdown,
13471 value: "```rust\nSome(2)\n```".to_string(),
13472 })),
13473 deprecated: Some(false),
13474 sort_text: Some("Some".to_string()),
13475 filter_text: Some("Some".to_string()),
13476 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
13477 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13478 range: lsp::Range {
13479 start: lsp::Position {
13480 line: 0,
13481 character: 22,
13482 },
13483 end: lsp::Position {
13484 line: 0,
13485 character: 22,
13486 },
13487 },
13488 new_text: "Some(2)".to_string(),
13489 })),
13490 additional_text_edits: Some(vec![lsp::TextEdit {
13491 range: lsp::Range {
13492 start: lsp::Position {
13493 line: 0,
13494 character: 20,
13495 },
13496 end: lsp::Position {
13497 line: 0,
13498 character: 22,
13499 },
13500 },
13501 new_text: "".to_string(),
13502 }]),
13503 ..Default::default()
13504 };
13505
13506 let closure_completion_item = completion_item.clone();
13507 let counter = Arc::new(AtomicUsize::new(0));
13508 let counter_clone = counter.clone();
13509 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13510 let task_completion_item = closure_completion_item.clone();
13511 counter_clone.fetch_add(1, atomic::Ordering::Release);
13512 async move {
13513 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
13514 is_incomplete: true,
13515 item_defaults: None,
13516 items: vec![task_completion_item],
13517 })))
13518 }
13519 });
13520
13521 cx.condition(|editor, _| editor.context_menu_visible())
13522 .await;
13523 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
13524 assert!(request.next().await.is_some());
13525 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13526
13527 cx.simulate_keystrokes("S o m");
13528 cx.condition(|editor, _| editor.context_menu_visible())
13529 .await;
13530 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
13531 assert!(request.next().await.is_some());
13532 assert!(request.next().await.is_some());
13533 assert!(request.next().await.is_some());
13534 request.close();
13535 assert!(request.next().await.is_none());
13536 assert_eq!(
13537 counter.load(atomic::Ordering::Acquire),
13538 4,
13539 "With the completions menu open, only one LSP request should happen per input"
13540 );
13541}
13542
13543#[gpui::test]
13544async fn test_toggle_comment(cx: &mut TestAppContext) {
13545 init_test(cx, |_| {});
13546 let mut cx = EditorTestContext::new(cx).await;
13547 let language = Arc::new(Language::new(
13548 LanguageConfig {
13549 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
13550 ..Default::default()
13551 },
13552 Some(tree_sitter_rust::LANGUAGE.into()),
13553 ));
13554 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
13555
13556 // If multiple selections intersect a line, the line is only toggled once.
13557 cx.set_state(indoc! {"
13558 fn a() {
13559 «//b();
13560 ˇ»// «c();
13561 //ˇ» d();
13562 }
13563 "});
13564
13565 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13566
13567 cx.assert_editor_state(indoc! {"
13568 fn a() {
13569 «b();
13570 c();
13571 ˇ» d();
13572 }
13573 "});
13574
13575 // The comment prefix is inserted at the same column for every line in a
13576 // selection.
13577 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13578
13579 cx.assert_editor_state(indoc! {"
13580 fn a() {
13581 // «b();
13582 // c();
13583 ˇ»// d();
13584 }
13585 "});
13586
13587 // If a selection ends at the beginning of a line, that line is not toggled.
13588 cx.set_selections_state(indoc! {"
13589 fn a() {
13590 // b();
13591 «// c();
13592 ˇ» // d();
13593 }
13594 "});
13595
13596 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13597
13598 cx.assert_editor_state(indoc! {"
13599 fn a() {
13600 // b();
13601 «c();
13602 ˇ» // d();
13603 }
13604 "});
13605
13606 // If a selection span a single line and is empty, the line is toggled.
13607 cx.set_state(indoc! {"
13608 fn a() {
13609 a();
13610 b();
13611 ˇ
13612 }
13613 "});
13614
13615 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13616
13617 cx.assert_editor_state(indoc! {"
13618 fn a() {
13619 a();
13620 b();
13621 //•ˇ
13622 }
13623 "});
13624
13625 // If a selection span multiple lines, empty lines are not toggled.
13626 cx.set_state(indoc! {"
13627 fn a() {
13628 «a();
13629
13630 c();ˇ»
13631 }
13632 "});
13633
13634 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13635
13636 cx.assert_editor_state(indoc! {"
13637 fn a() {
13638 // «a();
13639
13640 // c();ˇ»
13641 }
13642 "});
13643
13644 // If a selection includes multiple comment prefixes, all lines are uncommented.
13645 cx.set_state(indoc! {"
13646 fn a() {
13647 «// a();
13648 /// b();
13649 //! c();ˇ»
13650 }
13651 "});
13652
13653 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13654
13655 cx.assert_editor_state(indoc! {"
13656 fn a() {
13657 «a();
13658 b();
13659 c();ˇ»
13660 }
13661 "});
13662}
13663
13664#[gpui::test]
13665async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
13666 init_test(cx, |_| {});
13667 let mut cx = EditorTestContext::new(cx).await;
13668 let language = Arc::new(Language::new(
13669 LanguageConfig {
13670 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
13671 ..Default::default()
13672 },
13673 Some(tree_sitter_rust::LANGUAGE.into()),
13674 ));
13675 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
13676
13677 let toggle_comments = &ToggleComments {
13678 advance_downwards: false,
13679 ignore_indent: true,
13680 };
13681
13682 // If multiple selections intersect a line, the line is only toggled once.
13683 cx.set_state(indoc! {"
13684 fn a() {
13685 // «b();
13686 // c();
13687 // ˇ» d();
13688 }
13689 "});
13690
13691 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13692
13693 cx.assert_editor_state(indoc! {"
13694 fn a() {
13695 «b();
13696 c();
13697 ˇ» d();
13698 }
13699 "});
13700
13701 // The comment prefix is inserted at the beginning of each line
13702 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13703
13704 cx.assert_editor_state(indoc! {"
13705 fn a() {
13706 // «b();
13707 // c();
13708 // ˇ» d();
13709 }
13710 "});
13711
13712 // If a selection ends at the beginning of a line, that line is not toggled.
13713 cx.set_selections_state(indoc! {"
13714 fn a() {
13715 // b();
13716 // «c();
13717 ˇ»// d();
13718 }
13719 "});
13720
13721 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13722
13723 cx.assert_editor_state(indoc! {"
13724 fn a() {
13725 // b();
13726 «c();
13727 ˇ»// d();
13728 }
13729 "});
13730
13731 // If a selection span a single line and is empty, the line is toggled.
13732 cx.set_state(indoc! {"
13733 fn a() {
13734 a();
13735 b();
13736 ˇ
13737 }
13738 "});
13739
13740 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13741
13742 cx.assert_editor_state(indoc! {"
13743 fn a() {
13744 a();
13745 b();
13746 //ˇ
13747 }
13748 "});
13749
13750 // If a selection span multiple lines, empty lines are not toggled.
13751 cx.set_state(indoc! {"
13752 fn a() {
13753 «a();
13754
13755 c();ˇ»
13756 }
13757 "});
13758
13759 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13760
13761 cx.assert_editor_state(indoc! {"
13762 fn a() {
13763 // «a();
13764
13765 // c();ˇ»
13766 }
13767 "});
13768
13769 // If a selection includes multiple comment prefixes, all lines are uncommented.
13770 cx.set_state(indoc! {"
13771 fn a() {
13772 // «a();
13773 /// b();
13774 //! c();ˇ»
13775 }
13776 "});
13777
13778 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13779
13780 cx.assert_editor_state(indoc! {"
13781 fn a() {
13782 «a();
13783 b();
13784 c();ˇ»
13785 }
13786 "});
13787}
13788
13789#[gpui::test]
13790async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
13791 init_test(cx, |_| {});
13792
13793 let language = Arc::new(Language::new(
13794 LanguageConfig {
13795 line_comments: vec!["// ".into()],
13796 ..Default::default()
13797 },
13798 Some(tree_sitter_rust::LANGUAGE.into()),
13799 ));
13800
13801 let mut cx = EditorTestContext::new(cx).await;
13802
13803 cx.language_registry().add(language.clone());
13804 cx.update_buffer(|buffer, cx| {
13805 buffer.set_language(Some(language), cx);
13806 });
13807
13808 let toggle_comments = &ToggleComments {
13809 advance_downwards: true,
13810 ignore_indent: false,
13811 };
13812
13813 // Single cursor on one line -> advance
13814 // Cursor moves horizontally 3 characters as well on non-blank line
13815 cx.set_state(indoc!(
13816 "fn a() {
13817 ˇdog();
13818 cat();
13819 }"
13820 ));
13821 cx.update_editor(|editor, window, cx| {
13822 editor.toggle_comments(toggle_comments, window, cx);
13823 });
13824 cx.assert_editor_state(indoc!(
13825 "fn a() {
13826 // dog();
13827 catˇ();
13828 }"
13829 ));
13830
13831 // Single selection on one line -> don't advance
13832 cx.set_state(indoc!(
13833 "fn a() {
13834 «dog()ˇ»;
13835 cat();
13836 }"
13837 ));
13838 cx.update_editor(|editor, window, cx| {
13839 editor.toggle_comments(toggle_comments, window, cx);
13840 });
13841 cx.assert_editor_state(indoc!(
13842 "fn a() {
13843 // «dog()ˇ»;
13844 cat();
13845 }"
13846 ));
13847
13848 // Multiple cursors on one line -> advance
13849 cx.set_state(indoc!(
13850 "fn a() {
13851 ˇdˇog();
13852 cat();
13853 }"
13854 ));
13855 cx.update_editor(|editor, window, cx| {
13856 editor.toggle_comments(toggle_comments, window, cx);
13857 });
13858 cx.assert_editor_state(indoc!(
13859 "fn a() {
13860 // dog();
13861 catˇ(ˇ);
13862 }"
13863 ));
13864
13865 // Multiple cursors on one line, with selection -> don't advance
13866 cx.set_state(indoc!(
13867 "fn a() {
13868 ˇdˇog«()ˇ»;
13869 cat();
13870 }"
13871 ));
13872 cx.update_editor(|editor, window, cx| {
13873 editor.toggle_comments(toggle_comments, window, cx);
13874 });
13875 cx.assert_editor_state(indoc!(
13876 "fn a() {
13877 // ˇdˇog«()ˇ»;
13878 cat();
13879 }"
13880 ));
13881
13882 // Single cursor on one line -> advance
13883 // Cursor moves to column 0 on blank line
13884 cx.set_state(indoc!(
13885 "fn a() {
13886 ˇdog();
13887
13888 cat();
13889 }"
13890 ));
13891 cx.update_editor(|editor, window, cx| {
13892 editor.toggle_comments(toggle_comments, window, cx);
13893 });
13894 cx.assert_editor_state(indoc!(
13895 "fn a() {
13896 // dog();
13897 ˇ
13898 cat();
13899 }"
13900 ));
13901
13902 // Single cursor on one line -> advance
13903 // Cursor starts and ends at column 0
13904 cx.set_state(indoc!(
13905 "fn a() {
13906 ˇ dog();
13907 cat();
13908 }"
13909 ));
13910 cx.update_editor(|editor, window, cx| {
13911 editor.toggle_comments(toggle_comments, window, cx);
13912 });
13913 cx.assert_editor_state(indoc!(
13914 "fn a() {
13915 // dog();
13916 ˇ cat();
13917 }"
13918 ));
13919}
13920
13921#[gpui::test]
13922async fn test_toggle_block_comment(cx: &mut TestAppContext) {
13923 init_test(cx, |_| {});
13924
13925 let mut cx = EditorTestContext::new(cx).await;
13926
13927 let html_language = Arc::new(
13928 Language::new(
13929 LanguageConfig {
13930 name: "HTML".into(),
13931 block_comment: Some(BlockCommentConfig {
13932 start: "<!-- ".into(),
13933 prefix: "".into(),
13934 end: " -->".into(),
13935 tab_size: 0,
13936 }),
13937 ..Default::default()
13938 },
13939 Some(tree_sitter_html::LANGUAGE.into()),
13940 )
13941 .with_injection_query(
13942 r#"
13943 (script_element
13944 (raw_text) @injection.content
13945 (#set! injection.language "javascript"))
13946 "#,
13947 )
13948 .unwrap(),
13949 );
13950
13951 let javascript_language = Arc::new(Language::new(
13952 LanguageConfig {
13953 name: "JavaScript".into(),
13954 line_comments: vec!["// ".into()],
13955 ..Default::default()
13956 },
13957 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
13958 ));
13959
13960 cx.language_registry().add(html_language.clone());
13961 cx.language_registry().add(javascript_language.clone());
13962 cx.update_buffer(|buffer, cx| {
13963 buffer.set_language(Some(html_language), cx);
13964 });
13965
13966 // Toggle comments for empty selections
13967 cx.set_state(
13968 &r#"
13969 <p>A</p>ˇ
13970 <p>B</p>ˇ
13971 <p>C</p>ˇ
13972 "#
13973 .unindent(),
13974 );
13975 cx.update_editor(|editor, window, cx| {
13976 editor.toggle_comments(&ToggleComments::default(), window, cx)
13977 });
13978 cx.assert_editor_state(
13979 &r#"
13980 <!-- <p>A</p>ˇ -->
13981 <!-- <p>B</p>ˇ -->
13982 <!-- <p>C</p>ˇ -->
13983 "#
13984 .unindent(),
13985 );
13986 cx.update_editor(|editor, window, cx| {
13987 editor.toggle_comments(&ToggleComments::default(), window, cx)
13988 });
13989 cx.assert_editor_state(
13990 &r#"
13991 <p>A</p>ˇ
13992 <p>B</p>ˇ
13993 <p>C</p>ˇ
13994 "#
13995 .unindent(),
13996 );
13997
13998 // Toggle comments for mixture of empty and non-empty selections, where
13999 // multiple selections occupy a given line.
14000 cx.set_state(
14001 &r#"
14002 <p>A«</p>
14003 <p>ˇ»B</p>ˇ
14004 <p>C«</p>
14005 <p>ˇ»D</p>ˇ
14006 "#
14007 .unindent(),
14008 );
14009
14010 cx.update_editor(|editor, window, cx| {
14011 editor.toggle_comments(&ToggleComments::default(), window, cx)
14012 });
14013 cx.assert_editor_state(
14014 &r#"
14015 <!-- <p>A«</p>
14016 <p>ˇ»B</p>ˇ -->
14017 <!-- <p>C«</p>
14018 <p>ˇ»D</p>ˇ -->
14019 "#
14020 .unindent(),
14021 );
14022 cx.update_editor(|editor, window, cx| {
14023 editor.toggle_comments(&ToggleComments::default(), window, cx)
14024 });
14025 cx.assert_editor_state(
14026 &r#"
14027 <p>A«</p>
14028 <p>ˇ»B</p>ˇ
14029 <p>C«</p>
14030 <p>ˇ»D</p>ˇ
14031 "#
14032 .unindent(),
14033 );
14034
14035 // Toggle comments when different languages are active for different
14036 // selections.
14037 cx.set_state(
14038 &r#"
14039 ˇ<script>
14040 ˇvar x = new Y();
14041 ˇ</script>
14042 "#
14043 .unindent(),
14044 );
14045 cx.executor().run_until_parked();
14046 cx.update_editor(|editor, window, cx| {
14047 editor.toggle_comments(&ToggleComments::default(), window, cx)
14048 });
14049 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
14050 // Uncommenting and commenting from this position brings in even more wrong artifacts.
14051 cx.assert_editor_state(
14052 &r#"
14053 <!-- ˇ<script> -->
14054 // ˇvar x = new Y();
14055 <!-- ˇ</script> -->
14056 "#
14057 .unindent(),
14058 );
14059}
14060
14061#[gpui::test]
14062fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
14063 init_test(cx, |_| {});
14064
14065 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
14066 let multibuffer = cx.new(|cx| {
14067 let mut multibuffer = MultiBuffer::new(ReadWrite);
14068 multibuffer.push_excerpts(
14069 buffer.clone(),
14070 [
14071 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
14072 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
14073 ],
14074 cx,
14075 );
14076 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
14077 multibuffer
14078 });
14079
14080 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
14081 editor.update_in(cx, |editor, window, cx| {
14082 assert_eq!(editor.text(cx), "aaaa\nbbbb");
14083 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14084 s.select_ranges([
14085 Point::new(0, 0)..Point::new(0, 0),
14086 Point::new(1, 0)..Point::new(1, 0),
14087 ])
14088 });
14089
14090 editor.handle_input("X", window, cx);
14091 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
14092 assert_eq!(
14093 editor.selections.ranges(cx),
14094 [
14095 Point::new(0, 1)..Point::new(0, 1),
14096 Point::new(1, 1)..Point::new(1, 1),
14097 ]
14098 );
14099
14100 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
14101 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14102 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
14103 });
14104 editor.backspace(&Default::default(), window, cx);
14105 assert_eq!(editor.text(cx), "Xa\nbbb");
14106 assert_eq!(
14107 editor.selections.ranges(cx),
14108 [Point::new(1, 0)..Point::new(1, 0)]
14109 );
14110
14111 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14112 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
14113 });
14114 editor.backspace(&Default::default(), window, cx);
14115 assert_eq!(editor.text(cx), "X\nbb");
14116 assert_eq!(
14117 editor.selections.ranges(cx),
14118 [Point::new(0, 1)..Point::new(0, 1)]
14119 );
14120 });
14121}
14122
14123#[gpui::test]
14124fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
14125 init_test(cx, |_| {});
14126
14127 let markers = vec![('[', ']').into(), ('(', ')').into()];
14128 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
14129 indoc! {"
14130 [aaaa
14131 (bbbb]
14132 cccc)",
14133 },
14134 markers.clone(),
14135 );
14136 let excerpt_ranges = markers.into_iter().map(|marker| {
14137 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
14138 ExcerptRange::new(context.clone())
14139 });
14140 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
14141 let multibuffer = cx.new(|cx| {
14142 let mut multibuffer = MultiBuffer::new(ReadWrite);
14143 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
14144 multibuffer
14145 });
14146
14147 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
14148 editor.update_in(cx, |editor, window, cx| {
14149 let (expected_text, selection_ranges) = marked_text_ranges(
14150 indoc! {"
14151 aaaa
14152 bˇbbb
14153 bˇbbˇb
14154 cccc"
14155 },
14156 true,
14157 );
14158 assert_eq!(editor.text(cx), expected_text);
14159 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14160 s.select_ranges(selection_ranges)
14161 });
14162
14163 editor.handle_input("X", window, cx);
14164
14165 let (expected_text, expected_selections) = marked_text_ranges(
14166 indoc! {"
14167 aaaa
14168 bXˇbbXb
14169 bXˇbbXˇb
14170 cccc"
14171 },
14172 false,
14173 );
14174 assert_eq!(editor.text(cx), expected_text);
14175 assert_eq!(editor.selections.ranges(cx), expected_selections);
14176
14177 editor.newline(&Newline, window, cx);
14178 let (expected_text, expected_selections) = marked_text_ranges(
14179 indoc! {"
14180 aaaa
14181 bX
14182 ˇbbX
14183 b
14184 bX
14185 ˇbbX
14186 ˇb
14187 cccc"
14188 },
14189 false,
14190 );
14191 assert_eq!(editor.text(cx), expected_text);
14192 assert_eq!(editor.selections.ranges(cx), expected_selections);
14193 });
14194}
14195
14196#[gpui::test]
14197fn test_refresh_selections(cx: &mut TestAppContext) {
14198 init_test(cx, |_| {});
14199
14200 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
14201 let mut excerpt1_id = None;
14202 let multibuffer = cx.new(|cx| {
14203 let mut multibuffer = MultiBuffer::new(ReadWrite);
14204 excerpt1_id = multibuffer
14205 .push_excerpts(
14206 buffer.clone(),
14207 [
14208 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
14209 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
14210 ],
14211 cx,
14212 )
14213 .into_iter()
14214 .next();
14215 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
14216 multibuffer
14217 });
14218
14219 let editor = cx.add_window(|window, cx| {
14220 let mut editor = build_editor(multibuffer.clone(), window, cx);
14221 let snapshot = editor.snapshot(window, cx);
14222 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14223 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
14224 });
14225 editor.begin_selection(
14226 Point::new(2, 1).to_display_point(&snapshot),
14227 true,
14228 1,
14229 window,
14230 cx,
14231 );
14232 assert_eq!(
14233 editor.selections.ranges(cx),
14234 [
14235 Point::new(1, 3)..Point::new(1, 3),
14236 Point::new(2, 1)..Point::new(2, 1),
14237 ]
14238 );
14239 editor
14240 });
14241
14242 // Refreshing selections is a no-op when excerpts haven't changed.
14243 _ = editor.update(cx, |editor, window, cx| {
14244 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14245 assert_eq!(
14246 editor.selections.ranges(cx),
14247 [
14248 Point::new(1, 3)..Point::new(1, 3),
14249 Point::new(2, 1)..Point::new(2, 1),
14250 ]
14251 );
14252 });
14253
14254 multibuffer.update(cx, |multibuffer, cx| {
14255 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
14256 });
14257 _ = editor.update(cx, |editor, window, cx| {
14258 // Removing an excerpt causes the first selection to become degenerate.
14259 assert_eq!(
14260 editor.selections.ranges(cx),
14261 [
14262 Point::new(0, 0)..Point::new(0, 0),
14263 Point::new(0, 1)..Point::new(0, 1)
14264 ]
14265 );
14266
14267 // Refreshing selections will relocate the first selection to the original buffer
14268 // location.
14269 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14270 assert_eq!(
14271 editor.selections.ranges(cx),
14272 [
14273 Point::new(0, 1)..Point::new(0, 1),
14274 Point::new(0, 3)..Point::new(0, 3)
14275 ]
14276 );
14277 assert!(editor.selections.pending_anchor().is_some());
14278 });
14279}
14280
14281#[gpui::test]
14282fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
14283 init_test(cx, |_| {});
14284
14285 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
14286 let mut excerpt1_id = None;
14287 let multibuffer = cx.new(|cx| {
14288 let mut multibuffer = MultiBuffer::new(ReadWrite);
14289 excerpt1_id = multibuffer
14290 .push_excerpts(
14291 buffer.clone(),
14292 [
14293 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
14294 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
14295 ],
14296 cx,
14297 )
14298 .into_iter()
14299 .next();
14300 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
14301 multibuffer
14302 });
14303
14304 let editor = cx.add_window(|window, cx| {
14305 let mut editor = build_editor(multibuffer.clone(), window, cx);
14306 let snapshot = editor.snapshot(window, cx);
14307 editor.begin_selection(
14308 Point::new(1, 3).to_display_point(&snapshot),
14309 false,
14310 1,
14311 window,
14312 cx,
14313 );
14314 assert_eq!(
14315 editor.selections.ranges(cx),
14316 [Point::new(1, 3)..Point::new(1, 3)]
14317 );
14318 editor
14319 });
14320
14321 multibuffer.update(cx, |multibuffer, cx| {
14322 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
14323 });
14324 _ = editor.update(cx, |editor, window, cx| {
14325 assert_eq!(
14326 editor.selections.ranges(cx),
14327 [Point::new(0, 0)..Point::new(0, 0)]
14328 );
14329
14330 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
14331 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14332 assert_eq!(
14333 editor.selections.ranges(cx),
14334 [Point::new(0, 3)..Point::new(0, 3)]
14335 );
14336 assert!(editor.selections.pending_anchor().is_some());
14337 });
14338}
14339
14340#[gpui::test]
14341async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
14342 init_test(cx, |_| {});
14343
14344 let language = Arc::new(
14345 Language::new(
14346 LanguageConfig {
14347 brackets: BracketPairConfig {
14348 pairs: vec![
14349 BracketPair {
14350 start: "{".to_string(),
14351 end: "}".to_string(),
14352 close: true,
14353 surround: true,
14354 newline: true,
14355 },
14356 BracketPair {
14357 start: "/* ".to_string(),
14358 end: " */".to_string(),
14359 close: true,
14360 surround: true,
14361 newline: true,
14362 },
14363 ],
14364 ..Default::default()
14365 },
14366 ..Default::default()
14367 },
14368 Some(tree_sitter_rust::LANGUAGE.into()),
14369 )
14370 .with_indents_query("")
14371 .unwrap(),
14372 );
14373
14374 let text = concat!(
14375 "{ }\n", //
14376 " x\n", //
14377 " /* */\n", //
14378 "x\n", //
14379 "{{} }\n", //
14380 );
14381
14382 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
14383 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
14384 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
14385 editor
14386 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
14387 .await;
14388
14389 editor.update_in(cx, |editor, window, cx| {
14390 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14391 s.select_display_ranges([
14392 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
14393 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
14394 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
14395 ])
14396 });
14397 editor.newline(&Newline, window, cx);
14398
14399 assert_eq!(
14400 editor.buffer().read(cx).read(cx).text(),
14401 concat!(
14402 "{ \n", // Suppress rustfmt
14403 "\n", //
14404 "}\n", //
14405 " x\n", //
14406 " /* \n", //
14407 " \n", //
14408 " */\n", //
14409 "x\n", //
14410 "{{} \n", //
14411 "}\n", //
14412 )
14413 );
14414 });
14415}
14416
14417#[gpui::test]
14418fn test_highlighted_ranges(cx: &mut TestAppContext) {
14419 init_test(cx, |_| {});
14420
14421 let editor = cx.add_window(|window, cx| {
14422 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
14423 build_editor(buffer.clone(), window, cx)
14424 });
14425
14426 _ = editor.update(cx, |editor, window, cx| {
14427 struct Type1;
14428 struct Type2;
14429
14430 let buffer = editor.buffer.read(cx).snapshot(cx);
14431
14432 let anchor_range =
14433 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
14434
14435 editor.highlight_background::<Type1>(
14436 &[
14437 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
14438 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
14439 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
14440 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
14441 ],
14442 |_| Hsla::red(),
14443 cx,
14444 );
14445 editor.highlight_background::<Type2>(
14446 &[
14447 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
14448 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
14449 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
14450 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
14451 ],
14452 |_| Hsla::green(),
14453 cx,
14454 );
14455
14456 let snapshot = editor.snapshot(window, cx);
14457 let mut highlighted_ranges = editor.background_highlights_in_range(
14458 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
14459 &snapshot,
14460 cx.theme(),
14461 );
14462 // Enforce a consistent ordering based on color without relying on the ordering of the
14463 // highlight's `TypeId` which is non-executor.
14464 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
14465 assert_eq!(
14466 highlighted_ranges,
14467 &[
14468 (
14469 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
14470 Hsla::red(),
14471 ),
14472 (
14473 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
14474 Hsla::red(),
14475 ),
14476 (
14477 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
14478 Hsla::green(),
14479 ),
14480 (
14481 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
14482 Hsla::green(),
14483 ),
14484 ]
14485 );
14486 assert_eq!(
14487 editor.background_highlights_in_range(
14488 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
14489 &snapshot,
14490 cx.theme(),
14491 ),
14492 &[(
14493 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
14494 Hsla::red(),
14495 )]
14496 );
14497 });
14498}
14499
14500#[gpui::test]
14501async fn test_following(cx: &mut TestAppContext) {
14502 init_test(cx, |_| {});
14503
14504 let fs = FakeFs::new(cx.executor());
14505 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
14506
14507 let buffer = project.update(cx, |project, cx| {
14508 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
14509 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
14510 });
14511 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
14512 let follower = cx.update(|cx| {
14513 cx.open_window(
14514 WindowOptions {
14515 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
14516 gpui::Point::new(px(0.), px(0.)),
14517 gpui::Point::new(px(10.), px(80.)),
14518 ))),
14519 ..Default::default()
14520 },
14521 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
14522 )
14523 .unwrap()
14524 });
14525
14526 let is_still_following = Rc::new(RefCell::new(true));
14527 let follower_edit_event_count = Rc::new(RefCell::new(0));
14528 let pending_update = Rc::new(RefCell::new(None));
14529 let leader_entity = leader.root(cx).unwrap();
14530 let follower_entity = follower.root(cx).unwrap();
14531 _ = follower.update(cx, {
14532 let update = pending_update.clone();
14533 let is_still_following = is_still_following.clone();
14534 let follower_edit_event_count = follower_edit_event_count.clone();
14535 |_, window, cx| {
14536 cx.subscribe_in(
14537 &leader_entity,
14538 window,
14539 move |_, leader, event, window, cx| {
14540 leader.read(cx).add_event_to_update_proto(
14541 event,
14542 &mut update.borrow_mut(),
14543 window,
14544 cx,
14545 );
14546 },
14547 )
14548 .detach();
14549
14550 cx.subscribe_in(
14551 &follower_entity,
14552 window,
14553 move |_, _, event: &EditorEvent, _window, _cx| {
14554 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
14555 *is_still_following.borrow_mut() = false;
14556 }
14557
14558 if let EditorEvent::BufferEdited = event {
14559 *follower_edit_event_count.borrow_mut() += 1;
14560 }
14561 },
14562 )
14563 .detach();
14564 }
14565 });
14566
14567 // Update the selections only
14568 _ = leader.update(cx, |leader, window, cx| {
14569 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14570 s.select_ranges([1..1])
14571 });
14572 });
14573 follower
14574 .update(cx, |follower, window, cx| {
14575 follower.apply_update_proto(
14576 &project,
14577 pending_update.borrow_mut().take().unwrap(),
14578 window,
14579 cx,
14580 )
14581 })
14582 .unwrap()
14583 .await
14584 .unwrap();
14585 _ = follower.update(cx, |follower, _, cx| {
14586 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
14587 });
14588 assert!(*is_still_following.borrow());
14589 assert_eq!(*follower_edit_event_count.borrow(), 0);
14590
14591 // Update the scroll position only
14592 _ = leader.update(cx, |leader, window, cx| {
14593 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
14594 });
14595 follower
14596 .update(cx, |follower, window, cx| {
14597 follower.apply_update_proto(
14598 &project,
14599 pending_update.borrow_mut().take().unwrap(),
14600 window,
14601 cx,
14602 )
14603 })
14604 .unwrap()
14605 .await
14606 .unwrap();
14607 assert_eq!(
14608 follower
14609 .update(cx, |follower, _, cx| follower.scroll_position(cx))
14610 .unwrap(),
14611 gpui::Point::new(1.5, 3.5)
14612 );
14613 assert!(*is_still_following.borrow());
14614 assert_eq!(*follower_edit_event_count.borrow(), 0);
14615
14616 // Update the selections and scroll position. The follower's scroll position is updated
14617 // via autoscroll, not via the leader's exact scroll position.
14618 _ = leader.update(cx, |leader, window, cx| {
14619 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14620 s.select_ranges([0..0])
14621 });
14622 leader.request_autoscroll(Autoscroll::newest(), cx);
14623 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
14624 });
14625 follower
14626 .update(cx, |follower, window, cx| {
14627 follower.apply_update_proto(
14628 &project,
14629 pending_update.borrow_mut().take().unwrap(),
14630 window,
14631 cx,
14632 )
14633 })
14634 .unwrap()
14635 .await
14636 .unwrap();
14637 _ = follower.update(cx, |follower, _, cx| {
14638 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
14639 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
14640 });
14641 assert!(*is_still_following.borrow());
14642
14643 // Creating a pending selection that precedes another selection
14644 _ = leader.update(cx, |leader, window, cx| {
14645 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14646 s.select_ranges([1..1])
14647 });
14648 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
14649 });
14650 follower
14651 .update(cx, |follower, window, cx| {
14652 follower.apply_update_proto(
14653 &project,
14654 pending_update.borrow_mut().take().unwrap(),
14655 window,
14656 cx,
14657 )
14658 })
14659 .unwrap()
14660 .await
14661 .unwrap();
14662 _ = follower.update(cx, |follower, _, cx| {
14663 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
14664 });
14665 assert!(*is_still_following.borrow());
14666
14667 // Extend the pending selection so that it surrounds another selection
14668 _ = leader.update(cx, |leader, window, cx| {
14669 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
14670 });
14671 follower
14672 .update(cx, |follower, window, cx| {
14673 follower.apply_update_proto(
14674 &project,
14675 pending_update.borrow_mut().take().unwrap(),
14676 window,
14677 cx,
14678 )
14679 })
14680 .unwrap()
14681 .await
14682 .unwrap();
14683 _ = follower.update(cx, |follower, _, cx| {
14684 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
14685 });
14686
14687 // Scrolling locally breaks the follow
14688 _ = follower.update(cx, |follower, window, cx| {
14689 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
14690 follower.set_scroll_anchor(
14691 ScrollAnchor {
14692 anchor: top_anchor,
14693 offset: gpui::Point::new(0.0, 0.5),
14694 },
14695 window,
14696 cx,
14697 );
14698 });
14699 assert!(!(*is_still_following.borrow()));
14700}
14701
14702#[gpui::test]
14703async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
14704 init_test(cx, |_| {});
14705
14706 let fs = FakeFs::new(cx.executor());
14707 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
14708 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14709 let pane = workspace
14710 .update(cx, |workspace, _, _| workspace.active_pane().clone())
14711 .unwrap();
14712
14713 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
14714
14715 let leader = pane.update_in(cx, |_, window, cx| {
14716 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
14717 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
14718 });
14719
14720 // Start following the editor when it has no excerpts.
14721 let mut state_message =
14722 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
14723 let workspace_entity = workspace.root(cx).unwrap();
14724 let follower_1 = cx
14725 .update_window(*workspace.deref(), |_, window, cx| {
14726 Editor::from_state_proto(
14727 workspace_entity,
14728 ViewId {
14729 creator: CollaboratorId::PeerId(PeerId::default()),
14730 id: 0,
14731 },
14732 &mut state_message,
14733 window,
14734 cx,
14735 )
14736 })
14737 .unwrap()
14738 .unwrap()
14739 .await
14740 .unwrap();
14741
14742 let update_message = Rc::new(RefCell::new(None));
14743 follower_1.update_in(cx, {
14744 let update = update_message.clone();
14745 |_, window, cx| {
14746 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
14747 leader.read(cx).add_event_to_update_proto(
14748 event,
14749 &mut update.borrow_mut(),
14750 window,
14751 cx,
14752 );
14753 })
14754 .detach();
14755 }
14756 });
14757
14758 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
14759 (
14760 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
14761 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
14762 )
14763 });
14764
14765 // Insert some excerpts.
14766 leader.update(cx, |leader, cx| {
14767 leader.buffer.update(cx, |multibuffer, cx| {
14768 multibuffer.set_excerpts_for_path(
14769 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
14770 buffer_1.clone(),
14771 vec![
14772 Point::row_range(0..3),
14773 Point::row_range(1..6),
14774 Point::row_range(12..15),
14775 ],
14776 0,
14777 cx,
14778 );
14779 multibuffer.set_excerpts_for_path(
14780 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
14781 buffer_2.clone(),
14782 vec![Point::row_range(0..6), Point::row_range(8..12)],
14783 0,
14784 cx,
14785 );
14786 });
14787 });
14788
14789 // Apply the update of adding the excerpts.
14790 follower_1
14791 .update_in(cx, |follower, window, cx| {
14792 follower.apply_update_proto(
14793 &project,
14794 update_message.borrow().clone().unwrap(),
14795 window,
14796 cx,
14797 )
14798 })
14799 .await
14800 .unwrap();
14801 assert_eq!(
14802 follower_1.update(cx, |editor, cx| editor.text(cx)),
14803 leader.update(cx, |editor, cx| editor.text(cx))
14804 );
14805 update_message.borrow_mut().take();
14806
14807 // Start following separately after it already has excerpts.
14808 let mut state_message =
14809 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
14810 let workspace_entity = workspace.root(cx).unwrap();
14811 let follower_2 = cx
14812 .update_window(*workspace.deref(), |_, window, cx| {
14813 Editor::from_state_proto(
14814 workspace_entity,
14815 ViewId {
14816 creator: CollaboratorId::PeerId(PeerId::default()),
14817 id: 0,
14818 },
14819 &mut state_message,
14820 window,
14821 cx,
14822 )
14823 })
14824 .unwrap()
14825 .unwrap()
14826 .await
14827 .unwrap();
14828 assert_eq!(
14829 follower_2.update(cx, |editor, cx| editor.text(cx)),
14830 leader.update(cx, |editor, cx| editor.text(cx))
14831 );
14832
14833 // Remove some excerpts.
14834 leader.update(cx, |leader, cx| {
14835 leader.buffer.update(cx, |multibuffer, cx| {
14836 let excerpt_ids = multibuffer.excerpt_ids();
14837 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
14838 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
14839 });
14840 });
14841
14842 // Apply the update of removing the excerpts.
14843 follower_1
14844 .update_in(cx, |follower, window, cx| {
14845 follower.apply_update_proto(
14846 &project,
14847 update_message.borrow().clone().unwrap(),
14848 window,
14849 cx,
14850 )
14851 })
14852 .await
14853 .unwrap();
14854 follower_2
14855 .update_in(cx, |follower, window, cx| {
14856 follower.apply_update_proto(
14857 &project,
14858 update_message.borrow().clone().unwrap(),
14859 window,
14860 cx,
14861 )
14862 })
14863 .await
14864 .unwrap();
14865 update_message.borrow_mut().take();
14866 assert_eq!(
14867 follower_1.update(cx, |editor, cx| editor.text(cx)),
14868 leader.update(cx, |editor, cx| editor.text(cx))
14869 );
14870}
14871
14872#[gpui::test]
14873async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14874 init_test(cx, |_| {});
14875
14876 let mut cx = EditorTestContext::new(cx).await;
14877 let lsp_store =
14878 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
14879
14880 cx.set_state(indoc! {"
14881 ˇfn func(abc def: i32) -> u32 {
14882 }
14883 "});
14884
14885 cx.update(|_, cx| {
14886 lsp_store.update(cx, |lsp_store, cx| {
14887 lsp_store
14888 .update_diagnostics(
14889 LanguageServerId(0),
14890 lsp::PublishDiagnosticsParams {
14891 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
14892 version: None,
14893 diagnostics: vec![
14894 lsp::Diagnostic {
14895 range: lsp::Range::new(
14896 lsp::Position::new(0, 11),
14897 lsp::Position::new(0, 12),
14898 ),
14899 severity: Some(lsp::DiagnosticSeverity::ERROR),
14900 ..Default::default()
14901 },
14902 lsp::Diagnostic {
14903 range: lsp::Range::new(
14904 lsp::Position::new(0, 12),
14905 lsp::Position::new(0, 15),
14906 ),
14907 severity: Some(lsp::DiagnosticSeverity::ERROR),
14908 ..Default::default()
14909 },
14910 lsp::Diagnostic {
14911 range: lsp::Range::new(
14912 lsp::Position::new(0, 25),
14913 lsp::Position::new(0, 28),
14914 ),
14915 severity: Some(lsp::DiagnosticSeverity::ERROR),
14916 ..Default::default()
14917 },
14918 ],
14919 },
14920 None,
14921 DiagnosticSourceKind::Pushed,
14922 &[],
14923 cx,
14924 )
14925 .unwrap()
14926 });
14927 });
14928
14929 executor.run_until_parked();
14930
14931 cx.update_editor(|editor, window, cx| {
14932 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
14933 });
14934
14935 cx.assert_editor_state(indoc! {"
14936 fn func(abc def: i32) -> ˇu32 {
14937 }
14938 "});
14939
14940 cx.update_editor(|editor, window, cx| {
14941 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
14942 });
14943
14944 cx.assert_editor_state(indoc! {"
14945 fn func(abc ˇdef: i32) -> u32 {
14946 }
14947 "});
14948
14949 cx.update_editor(|editor, window, cx| {
14950 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
14951 });
14952
14953 cx.assert_editor_state(indoc! {"
14954 fn func(abcˇ def: i32) -> u32 {
14955 }
14956 "});
14957
14958 cx.update_editor(|editor, window, cx| {
14959 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
14960 });
14961
14962 cx.assert_editor_state(indoc! {"
14963 fn func(abc def: i32) -> ˇu32 {
14964 }
14965 "});
14966}
14967
14968#[gpui::test]
14969async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14970 init_test(cx, |_| {});
14971
14972 let mut cx = EditorTestContext::new(cx).await;
14973
14974 let diff_base = r#"
14975 use some::mod;
14976
14977 const A: u32 = 42;
14978
14979 fn main() {
14980 println!("hello");
14981
14982 println!("world");
14983 }
14984 "#
14985 .unindent();
14986
14987 // Edits are modified, removed, modified, added
14988 cx.set_state(
14989 &r#"
14990 use some::modified;
14991
14992 ˇ
14993 fn main() {
14994 println!("hello there");
14995
14996 println!("around the");
14997 println!("world");
14998 }
14999 "#
15000 .unindent(),
15001 );
15002
15003 cx.set_head_text(&diff_base);
15004 executor.run_until_parked();
15005
15006 cx.update_editor(|editor, window, cx| {
15007 //Wrap around the bottom of the buffer
15008 for _ in 0..3 {
15009 editor.go_to_next_hunk(&GoToHunk, window, cx);
15010 }
15011 });
15012
15013 cx.assert_editor_state(
15014 &r#"
15015 ˇuse some::modified;
15016
15017
15018 fn main() {
15019 println!("hello there");
15020
15021 println!("around the");
15022 println!("world");
15023 }
15024 "#
15025 .unindent(),
15026 );
15027
15028 cx.update_editor(|editor, window, cx| {
15029 //Wrap around the top of the buffer
15030 for _ in 0..2 {
15031 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
15032 }
15033 });
15034
15035 cx.assert_editor_state(
15036 &r#"
15037 use some::modified;
15038
15039
15040 fn main() {
15041 ˇ println!("hello there");
15042
15043 println!("around the");
15044 println!("world");
15045 }
15046 "#
15047 .unindent(),
15048 );
15049
15050 cx.update_editor(|editor, window, cx| {
15051 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
15052 });
15053
15054 cx.assert_editor_state(
15055 &r#"
15056 use some::modified;
15057
15058 ˇ
15059 fn main() {
15060 println!("hello there");
15061
15062 println!("around the");
15063 println!("world");
15064 }
15065 "#
15066 .unindent(),
15067 );
15068
15069 cx.update_editor(|editor, window, cx| {
15070 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
15071 });
15072
15073 cx.assert_editor_state(
15074 &r#"
15075 ˇuse some::modified;
15076
15077
15078 fn main() {
15079 println!("hello there");
15080
15081 println!("around the");
15082 println!("world");
15083 }
15084 "#
15085 .unindent(),
15086 );
15087
15088 cx.update_editor(|editor, window, cx| {
15089 for _ in 0..2 {
15090 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
15091 }
15092 });
15093
15094 cx.assert_editor_state(
15095 &r#"
15096 use some::modified;
15097
15098
15099 fn main() {
15100 ˇ println!("hello there");
15101
15102 println!("around the");
15103 println!("world");
15104 }
15105 "#
15106 .unindent(),
15107 );
15108
15109 cx.update_editor(|editor, window, cx| {
15110 editor.fold(&Fold, window, cx);
15111 });
15112
15113 cx.update_editor(|editor, window, cx| {
15114 editor.go_to_next_hunk(&GoToHunk, window, cx);
15115 });
15116
15117 cx.assert_editor_state(
15118 &r#"
15119 ˇuse some::modified;
15120
15121
15122 fn main() {
15123 println!("hello there");
15124
15125 println!("around the");
15126 println!("world");
15127 }
15128 "#
15129 .unindent(),
15130 );
15131}
15132
15133#[test]
15134fn test_split_words() {
15135 fn split(text: &str) -> Vec<&str> {
15136 split_words(text).collect()
15137 }
15138
15139 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
15140 assert_eq!(split("hello_world"), &["hello_", "world"]);
15141 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
15142 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
15143 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
15144 assert_eq!(split("helloworld"), &["helloworld"]);
15145
15146 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
15147}
15148
15149#[gpui::test]
15150async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
15151 init_test(cx, |_| {});
15152
15153 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
15154 let mut assert = |before, after| {
15155 let _state_context = cx.set_state(before);
15156 cx.run_until_parked();
15157 cx.update_editor(|editor, window, cx| {
15158 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
15159 });
15160 cx.run_until_parked();
15161 cx.assert_editor_state(after);
15162 };
15163
15164 // Outside bracket jumps to outside of matching bracket
15165 assert("console.logˇ(var);", "console.log(var)ˇ;");
15166 assert("console.log(var)ˇ;", "console.logˇ(var);");
15167
15168 // Inside bracket jumps to inside of matching bracket
15169 assert("console.log(ˇvar);", "console.log(varˇ);");
15170 assert("console.log(varˇ);", "console.log(ˇvar);");
15171
15172 // When outside a bracket and inside, favor jumping to the inside bracket
15173 assert(
15174 "console.log('foo', [1, 2, 3]ˇ);",
15175 "console.log(ˇ'foo', [1, 2, 3]);",
15176 );
15177 assert(
15178 "console.log(ˇ'foo', [1, 2, 3]);",
15179 "console.log('foo', [1, 2, 3]ˇ);",
15180 );
15181
15182 // Bias forward if two options are equally likely
15183 assert(
15184 "let result = curried_fun()ˇ();",
15185 "let result = curried_fun()()ˇ;",
15186 );
15187
15188 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
15189 assert(
15190 indoc! {"
15191 function test() {
15192 console.log('test')ˇ
15193 }"},
15194 indoc! {"
15195 function test() {
15196 console.logˇ('test')
15197 }"},
15198 );
15199}
15200
15201#[gpui::test]
15202async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
15203 init_test(cx, |_| {});
15204
15205 let fs = FakeFs::new(cx.executor());
15206 fs.insert_tree(
15207 path!("/a"),
15208 json!({
15209 "main.rs": "fn main() { let a = 5; }",
15210 "other.rs": "// Test file",
15211 }),
15212 )
15213 .await;
15214 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15215
15216 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15217 language_registry.add(Arc::new(Language::new(
15218 LanguageConfig {
15219 name: "Rust".into(),
15220 matcher: LanguageMatcher {
15221 path_suffixes: vec!["rs".to_string()],
15222 ..Default::default()
15223 },
15224 brackets: BracketPairConfig {
15225 pairs: vec![BracketPair {
15226 start: "{".to_string(),
15227 end: "}".to_string(),
15228 close: true,
15229 surround: true,
15230 newline: true,
15231 }],
15232 disabled_scopes_by_bracket_ix: Vec::new(),
15233 },
15234 ..Default::default()
15235 },
15236 Some(tree_sitter_rust::LANGUAGE.into()),
15237 )));
15238 let mut fake_servers = language_registry.register_fake_lsp(
15239 "Rust",
15240 FakeLspAdapter {
15241 capabilities: lsp::ServerCapabilities {
15242 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
15243 first_trigger_character: "{".to_string(),
15244 more_trigger_character: None,
15245 }),
15246 ..Default::default()
15247 },
15248 ..Default::default()
15249 },
15250 );
15251
15252 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15253
15254 let cx = &mut VisualTestContext::from_window(*workspace, cx);
15255
15256 let worktree_id = workspace
15257 .update(cx, |workspace, _, cx| {
15258 workspace.project().update(cx, |project, cx| {
15259 project.worktrees(cx).next().unwrap().read(cx).id()
15260 })
15261 })
15262 .unwrap();
15263
15264 let buffer = project
15265 .update(cx, |project, cx| {
15266 project.open_local_buffer(path!("/a/main.rs"), cx)
15267 })
15268 .await
15269 .unwrap();
15270 let editor_handle = workspace
15271 .update(cx, |workspace, window, cx| {
15272 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
15273 })
15274 .unwrap()
15275 .await
15276 .unwrap()
15277 .downcast::<Editor>()
15278 .unwrap();
15279
15280 cx.executor().start_waiting();
15281 let fake_server = fake_servers.next().await.unwrap();
15282
15283 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
15284 |params, _| async move {
15285 assert_eq!(
15286 params.text_document_position.text_document.uri,
15287 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
15288 );
15289 assert_eq!(
15290 params.text_document_position.position,
15291 lsp::Position::new(0, 21),
15292 );
15293
15294 Ok(Some(vec![lsp::TextEdit {
15295 new_text: "]".to_string(),
15296 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15297 }]))
15298 },
15299 );
15300
15301 editor_handle.update_in(cx, |editor, window, cx| {
15302 window.focus(&editor.focus_handle(cx));
15303 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15304 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
15305 });
15306 editor.handle_input("{", window, cx);
15307 });
15308
15309 cx.executor().run_until_parked();
15310
15311 buffer.update(cx, |buffer, _| {
15312 assert_eq!(
15313 buffer.text(),
15314 "fn main() { let a = {5}; }",
15315 "No extra braces from on type formatting should appear in the buffer"
15316 )
15317 });
15318}
15319
15320#[gpui::test(iterations = 20, seeds(31))]
15321async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
15322 init_test(cx, |_| {});
15323
15324 let mut cx = EditorLspTestContext::new_rust(
15325 lsp::ServerCapabilities {
15326 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
15327 first_trigger_character: ".".to_string(),
15328 more_trigger_character: None,
15329 }),
15330 ..Default::default()
15331 },
15332 cx,
15333 )
15334 .await;
15335
15336 cx.update_buffer(|buffer, _| {
15337 // This causes autoindent to be async.
15338 buffer.set_sync_parse_timeout(Duration::ZERO)
15339 });
15340
15341 cx.set_state("fn c() {\n d()ˇ\n}\n");
15342 cx.simulate_keystroke("\n");
15343 cx.run_until_parked();
15344
15345 let buffer_cloned =
15346 cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap().clone());
15347 let mut request =
15348 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
15349 let buffer_cloned = buffer_cloned.clone();
15350 async move {
15351 buffer_cloned.update(&mut cx, |buffer, _| {
15352 assert_eq!(
15353 buffer.text(),
15354 "fn c() {\n d()\n .\n}\n",
15355 "OnTypeFormatting should triggered after autoindent applied"
15356 )
15357 })?;
15358
15359 Ok(Some(vec![]))
15360 }
15361 });
15362
15363 cx.simulate_keystroke(".");
15364 cx.run_until_parked();
15365
15366 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
15367 assert!(request.next().await.is_some());
15368 request.close();
15369 assert!(request.next().await.is_none());
15370}
15371
15372#[gpui::test]
15373async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
15374 init_test(cx, |_| {});
15375
15376 let fs = FakeFs::new(cx.executor());
15377 fs.insert_tree(
15378 path!("/a"),
15379 json!({
15380 "main.rs": "fn main() { let a = 5; }",
15381 "other.rs": "// Test file",
15382 }),
15383 )
15384 .await;
15385
15386 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15387
15388 let server_restarts = Arc::new(AtomicUsize::new(0));
15389 let closure_restarts = Arc::clone(&server_restarts);
15390 let language_server_name = "test language server";
15391 let language_name: LanguageName = "Rust".into();
15392
15393 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15394 language_registry.add(Arc::new(Language::new(
15395 LanguageConfig {
15396 name: language_name.clone(),
15397 matcher: LanguageMatcher {
15398 path_suffixes: vec!["rs".to_string()],
15399 ..Default::default()
15400 },
15401 ..Default::default()
15402 },
15403 Some(tree_sitter_rust::LANGUAGE.into()),
15404 )));
15405 let mut fake_servers = language_registry.register_fake_lsp(
15406 "Rust",
15407 FakeLspAdapter {
15408 name: language_server_name,
15409 initialization_options: Some(json!({
15410 "testOptionValue": true
15411 })),
15412 initializer: Some(Box::new(move |fake_server| {
15413 let task_restarts = Arc::clone(&closure_restarts);
15414 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
15415 task_restarts.fetch_add(1, atomic::Ordering::Release);
15416 futures::future::ready(Ok(()))
15417 });
15418 })),
15419 ..Default::default()
15420 },
15421 );
15422
15423 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15424 let _buffer = project
15425 .update(cx, |project, cx| {
15426 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
15427 })
15428 .await
15429 .unwrap();
15430 let _fake_server = fake_servers.next().await.unwrap();
15431 update_test_language_settings(cx, |language_settings| {
15432 language_settings.languages.0.insert(
15433 language_name.clone(),
15434 LanguageSettingsContent {
15435 tab_size: NonZeroU32::new(8),
15436 ..Default::default()
15437 },
15438 );
15439 });
15440 cx.executor().run_until_parked();
15441 assert_eq!(
15442 server_restarts.load(atomic::Ordering::Acquire),
15443 0,
15444 "Should not restart LSP server on an unrelated change"
15445 );
15446
15447 update_test_project_settings(cx, |project_settings| {
15448 project_settings.lsp.insert(
15449 "Some other server name".into(),
15450 LspSettings {
15451 binary: None,
15452 settings: None,
15453 initialization_options: Some(json!({
15454 "some other init value": false
15455 })),
15456 enable_lsp_tasks: false,
15457 },
15458 );
15459 });
15460 cx.executor().run_until_parked();
15461 assert_eq!(
15462 server_restarts.load(atomic::Ordering::Acquire),
15463 0,
15464 "Should not restart LSP server on an unrelated LSP settings change"
15465 );
15466
15467 update_test_project_settings(cx, |project_settings| {
15468 project_settings.lsp.insert(
15469 language_server_name.into(),
15470 LspSettings {
15471 binary: None,
15472 settings: None,
15473 initialization_options: Some(json!({
15474 "anotherInitValue": false
15475 })),
15476 enable_lsp_tasks: false,
15477 },
15478 );
15479 });
15480 cx.executor().run_until_parked();
15481 assert_eq!(
15482 server_restarts.load(atomic::Ordering::Acquire),
15483 1,
15484 "Should restart LSP server on a related LSP settings change"
15485 );
15486
15487 update_test_project_settings(cx, |project_settings| {
15488 project_settings.lsp.insert(
15489 language_server_name.into(),
15490 LspSettings {
15491 binary: None,
15492 settings: None,
15493 initialization_options: Some(json!({
15494 "anotherInitValue": false
15495 })),
15496 enable_lsp_tasks: false,
15497 },
15498 );
15499 });
15500 cx.executor().run_until_parked();
15501 assert_eq!(
15502 server_restarts.load(atomic::Ordering::Acquire),
15503 1,
15504 "Should not restart LSP server on a related LSP settings change that is the same"
15505 );
15506
15507 update_test_project_settings(cx, |project_settings| {
15508 project_settings.lsp.insert(
15509 language_server_name.into(),
15510 LspSettings {
15511 binary: None,
15512 settings: None,
15513 initialization_options: None,
15514 enable_lsp_tasks: false,
15515 },
15516 );
15517 });
15518 cx.executor().run_until_parked();
15519 assert_eq!(
15520 server_restarts.load(atomic::Ordering::Acquire),
15521 2,
15522 "Should restart LSP server on another related LSP settings change"
15523 );
15524}
15525
15526#[gpui::test]
15527async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
15528 init_test(cx, |_| {});
15529
15530 let mut cx = EditorLspTestContext::new_rust(
15531 lsp::ServerCapabilities {
15532 completion_provider: Some(lsp::CompletionOptions {
15533 trigger_characters: Some(vec![".".to_string()]),
15534 resolve_provider: Some(true),
15535 ..Default::default()
15536 }),
15537 ..Default::default()
15538 },
15539 cx,
15540 )
15541 .await;
15542
15543 cx.set_state("fn main() { let a = 2ˇ; }");
15544 cx.simulate_keystroke(".");
15545 let completion_item = lsp::CompletionItem {
15546 label: "some".into(),
15547 kind: Some(lsp::CompletionItemKind::SNIPPET),
15548 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15549 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15550 kind: lsp::MarkupKind::Markdown,
15551 value: "```rust\nSome(2)\n```".to_string(),
15552 })),
15553 deprecated: Some(false),
15554 sort_text: Some("fffffff2".to_string()),
15555 filter_text: Some("some".to_string()),
15556 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15557 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15558 range: lsp::Range {
15559 start: lsp::Position {
15560 line: 0,
15561 character: 22,
15562 },
15563 end: lsp::Position {
15564 line: 0,
15565 character: 22,
15566 },
15567 },
15568 new_text: "Some(2)".to_string(),
15569 })),
15570 additional_text_edits: Some(vec![lsp::TextEdit {
15571 range: lsp::Range {
15572 start: lsp::Position {
15573 line: 0,
15574 character: 20,
15575 },
15576 end: lsp::Position {
15577 line: 0,
15578 character: 22,
15579 },
15580 },
15581 new_text: "".to_string(),
15582 }]),
15583 ..Default::default()
15584 };
15585
15586 let closure_completion_item = completion_item.clone();
15587 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15588 let task_completion_item = closure_completion_item.clone();
15589 async move {
15590 Ok(Some(lsp::CompletionResponse::Array(vec![
15591 task_completion_item,
15592 ])))
15593 }
15594 });
15595
15596 request.next().await;
15597
15598 cx.condition(|editor, _| editor.context_menu_visible())
15599 .await;
15600 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
15601 editor
15602 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15603 .unwrap()
15604 });
15605 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
15606
15607 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
15608 let task_completion_item = completion_item.clone();
15609 async move { Ok(task_completion_item) }
15610 })
15611 .next()
15612 .await
15613 .unwrap();
15614 apply_additional_edits.await.unwrap();
15615 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
15616}
15617
15618#[gpui::test]
15619async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
15620 init_test(cx, |_| {});
15621
15622 let mut cx = EditorLspTestContext::new_rust(
15623 lsp::ServerCapabilities {
15624 completion_provider: Some(lsp::CompletionOptions {
15625 trigger_characters: Some(vec![".".to_string()]),
15626 resolve_provider: Some(true),
15627 ..Default::default()
15628 }),
15629 ..Default::default()
15630 },
15631 cx,
15632 )
15633 .await;
15634
15635 cx.set_state("fn main() { let a = 2ˇ; }");
15636 cx.simulate_keystroke(".");
15637
15638 let item1 = lsp::CompletionItem {
15639 label: "method id()".to_string(),
15640 filter_text: Some("id".to_string()),
15641 detail: None,
15642 documentation: None,
15643 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15644 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15645 new_text: ".id".to_string(),
15646 })),
15647 ..lsp::CompletionItem::default()
15648 };
15649
15650 let item2 = lsp::CompletionItem {
15651 label: "other".to_string(),
15652 filter_text: Some("other".to_string()),
15653 detail: None,
15654 documentation: None,
15655 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15656 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15657 new_text: ".other".to_string(),
15658 })),
15659 ..lsp::CompletionItem::default()
15660 };
15661
15662 let item1 = item1.clone();
15663 cx.set_request_handler::<lsp::request::Completion, _, _>({
15664 let item1 = item1.clone();
15665 move |_, _, _| {
15666 let item1 = item1.clone();
15667 let item2 = item2.clone();
15668 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
15669 }
15670 })
15671 .next()
15672 .await;
15673
15674 cx.condition(|editor, _| editor.context_menu_visible())
15675 .await;
15676 cx.update_editor(|editor, _, _| {
15677 let context_menu = editor.context_menu.borrow_mut();
15678 let context_menu = context_menu
15679 .as_ref()
15680 .expect("Should have the context menu deployed");
15681 match context_menu {
15682 CodeContextMenu::Completions(completions_menu) => {
15683 let completions = completions_menu.completions.borrow_mut();
15684 assert_eq!(
15685 completions
15686 .iter()
15687 .map(|completion| &completion.label.text)
15688 .collect::<Vec<_>>(),
15689 vec!["method id()", "other"]
15690 )
15691 }
15692 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15693 }
15694 });
15695
15696 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
15697 let item1 = item1.clone();
15698 move |_, item_to_resolve, _| {
15699 let item1 = item1.clone();
15700 async move {
15701 if item1 == item_to_resolve {
15702 Ok(lsp::CompletionItem {
15703 label: "method id()".to_string(),
15704 filter_text: Some("id".to_string()),
15705 detail: Some("Now resolved!".to_string()),
15706 documentation: Some(lsp::Documentation::String("Docs".to_string())),
15707 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15708 range: lsp::Range::new(
15709 lsp::Position::new(0, 22),
15710 lsp::Position::new(0, 22),
15711 ),
15712 new_text: ".id".to_string(),
15713 })),
15714 ..lsp::CompletionItem::default()
15715 })
15716 } else {
15717 Ok(item_to_resolve)
15718 }
15719 }
15720 }
15721 })
15722 .next()
15723 .await
15724 .unwrap();
15725 cx.run_until_parked();
15726
15727 cx.update_editor(|editor, window, cx| {
15728 editor.context_menu_next(&Default::default(), window, cx);
15729 });
15730
15731 cx.update_editor(|editor, _, _| {
15732 let context_menu = editor.context_menu.borrow_mut();
15733 let context_menu = context_menu
15734 .as_ref()
15735 .expect("Should have the context menu deployed");
15736 match context_menu {
15737 CodeContextMenu::Completions(completions_menu) => {
15738 let completions = completions_menu.completions.borrow_mut();
15739 assert_eq!(
15740 completions
15741 .iter()
15742 .map(|completion| &completion.label.text)
15743 .collect::<Vec<_>>(),
15744 vec!["method id() Now resolved!", "other"],
15745 "Should update first completion label, but not second as the filter text did not match."
15746 );
15747 }
15748 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15749 }
15750 });
15751}
15752
15753#[gpui::test]
15754async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
15755 init_test(cx, |_| {});
15756 let mut cx = EditorLspTestContext::new_rust(
15757 lsp::ServerCapabilities {
15758 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
15759 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
15760 completion_provider: Some(lsp::CompletionOptions {
15761 resolve_provider: Some(true),
15762 ..Default::default()
15763 }),
15764 ..Default::default()
15765 },
15766 cx,
15767 )
15768 .await;
15769 cx.set_state(indoc! {"
15770 struct TestStruct {
15771 field: i32
15772 }
15773
15774 fn mainˇ() {
15775 let unused_var = 42;
15776 let test_struct = TestStruct { field: 42 };
15777 }
15778 "});
15779 let symbol_range = cx.lsp_range(indoc! {"
15780 struct TestStruct {
15781 field: i32
15782 }
15783
15784 «fn main»() {
15785 let unused_var = 42;
15786 let test_struct = TestStruct { field: 42 };
15787 }
15788 "});
15789 let mut hover_requests =
15790 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
15791 Ok(Some(lsp::Hover {
15792 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
15793 kind: lsp::MarkupKind::Markdown,
15794 value: "Function documentation".to_string(),
15795 }),
15796 range: Some(symbol_range),
15797 }))
15798 });
15799
15800 // Case 1: Test that code action menu hide hover popover
15801 cx.dispatch_action(Hover);
15802 hover_requests.next().await;
15803 cx.condition(|editor, _| editor.hover_state.visible()).await;
15804 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
15805 move |_, _, _| async move {
15806 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
15807 lsp::CodeAction {
15808 title: "Remove unused variable".to_string(),
15809 kind: Some(CodeActionKind::QUICKFIX),
15810 edit: Some(lsp::WorkspaceEdit {
15811 changes: Some(
15812 [(
15813 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
15814 vec![lsp::TextEdit {
15815 range: lsp::Range::new(
15816 lsp::Position::new(5, 4),
15817 lsp::Position::new(5, 27),
15818 ),
15819 new_text: "".to_string(),
15820 }],
15821 )]
15822 .into_iter()
15823 .collect(),
15824 ),
15825 ..Default::default()
15826 }),
15827 ..Default::default()
15828 },
15829 )]))
15830 },
15831 );
15832 cx.update_editor(|editor, window, cx| {
15833 editor.toggle_code_actions(
15834 &ToggleCodeActions {
15835 deployed_from: None,
15836 quick_launch: false,
15837 },
15838 window,
15839 cx,
15840 );
15841 });
15842 code_action_requests.next().await;
15843 cx.run_until_parked();
15844 cx.condition(|editor, _| editor.context_menu_visible())
15845 .await;
15846 cx.update_editor(|editor, _, _| {
15847 assert!(
15848 !editor.hover_state.visible(),
15849 "Hover popover should be hidden when code action menu is shown"
15850 );
15851 // Hide code actions
15852 editor.context_menu.take();
15853 });
15854
15855 // Case 2: Test that code completions hide hover popover
15856 cx.dispatch_action(Hover);
15857 hover_requests.next().await;
15858 cx.condition(|editor, _| editor.hover_state.visible()).await;
15859 let counter = Arc::new(AtomicUsize::new(0));
15860 let mut completion_requests =
15861 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15862 let counter = counter.clone();
15863 async move {
15864 counter.fetch_add(1, atomic::Ordering::Release);
15865 Ok(Some(lsp::CompletionResponse::Array(vec![
15866 lsp::CompletionItem {
15867 label: "main".into(),
15868 kind: Some(lsp::CompletionItemKind::FUNCTION),
15869 detail: Some("() -> ()".to_string()),
15870 ..Default::default()
15871 },
15872 lsp::CompletionItem {
15873 label: "TestStruct".into(),
15874 kind: Some(lsp::CompletionItemKind::STRUCT),
15875 detail: Some("struct TestStruct".to_string()),
15876 ..Default::default()
15877 },
15878 ])))
15879 }
15880 });
15881 cx.update_editor(|editor, window, cx| {
15882 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15883 });
15884 completion_requests.next().await;
15885 cx.condition(|editor, _| editor.context_menu_visible())
15886 .await;
15887 cx.update_editor(|editor, _, _| {
15888 assert!(
15889 !editor.hover_state.visible(),
15890 "Hover popover should be hidden when completion menu is shown"
15891 );
15892 });
15893}
15894
15895#[gpui::test]
15896async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
15897 init_test(cx, |_| {});
15898
15899 let mut cx = EditorLspTestContext::new_rust(
15900 lsp::ServerCapabilities {
15901 completion_provider: Some(lsp::CompletionOptions {
15902 trigger_characters: Some(vec![".".to_string()]),
15903 resolve_provider: Some(true),
15904 ..Default::default()
15905 }),
15906 ..Default::default()
15907 },
15908 cx,
15909 )
15910 .await;
15911
15912 cx.set_state("fn main() { let a = 2ˇ; }");
15913 cx.simulate_keystroke(".");
15914
15915 let unresolved_item_1 = lsp::CompletionItem {
15916 label: "id".to_string(),
15917 filter_text: Some("id".to_string()),
15918 detail: None,
15919 documentation: None,
15920 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15921 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15922 new_text: ".id".to_string(),
15923 })),
15924 ..lsp::CompletionItem::default()
15925 };
15926 let resolved_item_1 = lsp::CompletionItem {
15927 additional_text_edits: Some(vec![lsp::TextEdit {
15928 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
15929 new_text: "!!".to_string(),
15930 }]),
15931 ..unresolved_item_1.clone()
15932 };
15933 let unresolved_item_2 = lsp::CompletionItem {
15934 label: "other".to_string(),
15935 filter_text: Some("other".to_string()),
15936 detail: None,
15937 documentation: None,
15938 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15939 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15940 new_text: ".other".to_string(),
15941 })),
15942 ..lsp::CompletionItem::default()
15943 };
15944 let resolved_item_2 = lsp::CompletionItem {
15945 additional_text_edits: Some(vec![lsp::TextEdit {
15946 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
15947 new_text: "??".to_string(),
15948 }]),
15949 ..unresolved_item_2.clone()
15950 };
15951
15952 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
15953 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
15954 cx.lsp
15955 .server
15956 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
15957 let unresolved_item_1 = unresolved_item_1.clone();
15958 let resolved_item_1 = resolved_item_1.clone();
15959 let unresolved_item_2 = unresolved_item_2.clone();
15960 let resolved_item_2 = resolved_item_2.clone();
15961 let resolve_requests_1 = resolve_requests_1.clone();
15962 let resolve_requests_2 = resolve_requests_2.clone();
15963 move |unresolved_request, _| {
15964 let unresolved_item_1 = unresolved_item_1.clone();
15965 let resolved_item_1 = resolved_item_1.clone();
15966 let unresolved_item_2 = unresolved_item_2.clone();
15967 let resolved_item_2 = resolved_item_2.clone();
15968 let resolve_requests_1 = resolve_requests_1.clone();
15969 let resolve_requests_2 = resolve_requests_2.clone();
15970 async move {
15971 if unresolved_request == unresolved_item_1 {
15972 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
15973 Ok(resolved_item_1.clone())
15974 } else if unresolved_request == unresolved_item_2 {
15975 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
15976 Ok(resolved_item_2.clone())
15977 } else {
15978 panic!("Unexpected completion item {unresolved_request:?}")
15979 }
15980 }
15981 }
15982 })
15983 .detach();
15984
15985 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15986 let unresolved_item_1 = unresolved_item_1.clone();
15987 let unresolved_item_2 = unresolved_item_2.clone();
15988 async move {
15989 Ok(Some(lsp::CompletionResponse::Array(vec![
15990 unresolved_item_1,
15991 unresolved_item_2,
15992 ])))
15993 }
15994 })
15995 .next()
15996 .await;
15997
15998 cx.condition(|editor, _| editor.context_menu_visible())
15999 .await;
16000 cx.update_editor(|editor, _, _| {
16001 let context_menu = editor.context_menu.borrow_mut();
16002 let context_menu = context_menu
16003 .as_ref()
16004 .expect("Should have the context menu deployed");
16005 match context_menu {
16006 CodeContextMenu::Completions(completions_menu) => {
16007 let completions = completions_menu.completions.borrow_mut();
16008 assert_eq!(
16009 completions
16010 .iter()
16011 .map(|completion| &completion.label.text)
16012 .collect::<Vec<_>>(),
16013 vec!["id", "other"]
16014 )
16015 }
16016 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
16017 }
16018 });
16019 cx.run_until_parked();
16020
16021 cx.update_editor(|editor, window, cx| {
16022 editor.context_menu_next(&ContextMenuNext, window, cx);
16023 });
16024 cx.run_until_parked();
16025 cx.update_editor(|editor, window, cx| {
16026 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
16027 });
16028 cx.run_until_parked();
16029 cx.update_editor(|editor, window, cx| {
16030 editor.context_menu_next(&ContextMenuNext, window, cx);
16031 });
16032 cx.run_until_parked();
16033 cx.update_editor(|editor, window, cx| {
16034 editor
16035 .compose_completion(&ComposeCompletion::default(), window, cx)
16036 .expect("No task returned")
16037 })
16038 .await
16039 .expect("Completion failed");
16040 cx.run_until_parked();
16041
16042 cx.update_editor(|editor, _, cx| {
16043 assert_eq!(
16044 resolve_requests_1.load(atomic::Ordering::Acquire),
16045 1,
16046 "Should always resolve once despite multiple selections"
16047 );
16048 assert_eq!(
16049 resolve_requests_2.load(atomic::Ordering::Acquire),
16050 1,
16051 "Should always resolve once after multiple selections and applying the completion"
16052 );
16053 assert_eq!(
16054 editor.text(cx),
16055 "fn main() { let a = ??.other; }",
16056 "Should use resolved data when applying the completion"
16057 );
16058 });
16059}
16060
16061#[gpui::test]
16062async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
16063 init_test(cx, |_| {});
16064
16065 let item_0 = lsp::CompletionItem {
16066 label: "abs".into(),
16067 insert_text: Some("abs".into()),
16068 data: Some(json!({ "very": "special"})),
16069 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
16070 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
16071 lsp::InsertReplaceEdit {
16072 new_text: "abs".to_string(),
16073 insert: lsp::Range::default(),
16074 replace: lsp::Range::default(),
16075 },
16076 )),
16077 ..lsp::CompletionItem::default()
16078 };
16079 let items = iter::once(item_0.clone())
16080 .chain((11..51).map(|i| lsp::CompletionItem {
16081 label: format!("item_{}", i),
16082 insert_text: Some(format!("item_{}", i)),
16083 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
16084 ..lsp::CompletionItem::default()
16085 }))
16086 .collect::<Vec<_>>();
16087
16088 let default_commit_characters = vec!["?".to_string()];
16089 let default_data = json!({ "default": "data"});
16090 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
16091 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
16092 let default_edit_range = lsp::Range {
16093 start: lsp::Position {
16094 line: 0,
16095 character: 5,
16096 },
16097 end: lsp::Position {
16098 line: 0,
16099 character: 5,
16100 },
16101 };
16102
16103 let mut cx = EditorLspTestContext::new_rust(
16104 lsp::ServerCapabilities {
16105 completion_provider: Some(lsp::CompletionOptions {
16106 trigger_characters: Some(vec![".".to_string()]),
16107 resolve_provider: Some(true),
16108 ..Default::default()
16109 }),
16110 ..Default::default()
16111 },
16112 cx,
16113 )
16114 .await;
16115
16116 cx.set_state("fn main() { let a = 2ˇ; }");
16117 cx.simulate_keystroke(".");
16118
16119 let completion_data = default_data.clone();
16120 let completion_characters = default_commit_characters.clone();
16121 let completion_items = items.clone();
16122 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
16123 let default_data = completion_data.clone();
16124 let default_commit_characters = completion_characters.clone();
16125 let items = completion_items.clone();
16126 async move {
16127 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
16128 items,
16129 item_defaults: Some(lsp::CompletionListItemDefaults {
16130 data: Some(default_data.clone()),
16131 commit_characters: Some(default_commit_characters.clone()),
16132 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
16133 default_edit_range,
16134 )),
16135 insert_text_format: Some(default_insert_text_format),
16136 insert_text_mode: Some(default_insert_text_mode),
16137 }),
16138 ..lsp::CompletionList::default()
16139 })))
16140 }
16141 })
16142 .next()
16143 .await;
16144
16145 let resolved_items = Arc::new(Mutex::new(Vec::new()));
16146 cx.lsp
16147 .server
16148 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
16149 let closure_resolved_items = resolved_items.clone();
16150 move |item_to_resolve, _| {
16151 let closure_resolved_items = closure_resolved_items.clone();
16152 async move {
16153 closure_resolved_items.lock().push(item_to_resolve.clone());
16154 Ok(item_to_resolve)
16155 }
16156 }
16157 })
16158 .detach();
16159
16160 cx.condition(|editor, _| editor.context_menu_visible())
16161 .await;
16162 cx.run_until_parked();
16163 cx.update_editor(|editor, _, _| {
16164 let menu = editor.context_menu.borrow_mut();
16165 match menu.as_ref().expect("should have the completions menu") {
16166 CodeContextMenu::Completions(completions_menu) => {
16167 assert_eq!(
16168 completions_menu
16169 .entries
16170 .borrow()
16171 .iter()
16172 .map(|mat| mat.string.clone())
16173 .collect::<Vec<String>>(),
16174 items
16175 .iter()
16176 .map(|completion| completion.label.clone())
16177 .collect::<Vec<String>>()
16178 );
16179 }
16180 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
16181 }
16182 });
16183 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
16184 // with 4 from the end.
16185 assert_eq!(
16186 *resolved_items.lock(),
16187 [&items[0..16], &items[items.len() - 4..items.len()]]
16188 .concat()
16189 .iter()
16190 .cloned()
16191 .map(|mut item| {
16192 if item.data.is_none() {
16193 item.data = Some(default_data.clone());
16194 }
16195 item
16196 })
16197 .collect::<Vec<lsp::CompletionItem>>(),
16198 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
16199 );
16200 resolved_items.lock().clear();
16201
16202 cx.update_editor(|editor, window, cx| {
16203 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
16204 });
16205 cx.run_until_parked();
16206 // Completions that have already been resolved are skipped.
16207 assert_eq!(
16208 *resolved_items.lock(),
16209 items[items.len() - 17..items.len() - 4]
16210 .iter()
16211 .cloned()
16212 .map(|mut item| {
16213 if item.data.is_none() {
16214 item.data = Some(default_data.clone());
16215 }
16216 item
16217 })
16218 .collect::<Vec<lsp::CompletionItem>>()
16219 );
16220 resolved_items.lock().clear();
16221}
16222
16223#[gpui::test]
16224async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
16225 init_test(cx, |_| {});
16226
16227 let mut cx = EditorLspTestContext::new(
16228 Language::new(
16229 LanguageConfig {
16230 matcher: LanguageMatcher {
16231 path_suffixes: vec!["jsx".into()],
16232 ..Default::default()
16233 },
16234 overrides: [(
16235 "element".into(),
16236 LanguageConfigOverride {
16237 completion_query_characters: Override::Set(['-'].into_iter().collect()),
16238 ..Default::default()
16239 },
16240 )]
16241 .into_iter()
16242 .collect(),
16243 ..Default::default()
16244 },
16245 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
16246 )
16247 .with_override_query("(jsx_self_closing_element) @element")
16248 .unwrap(),
16249 lsp::ServerCapabilities {
16250 completion_provider: Some(lsp::CompletionOptions {
16251 trigger_characters: Some(vec![":".to_string()]),
16252 ..Default::default()
16253 }),
16254 ..Default::default()
16255 },
16256 cx,
16257 )
16258 .await;
16259
16260 cx.lsp
16261 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
16262 Ok(Some(lsp::CompletionResponse::Array(vec![
16263 lsp::CompletionItem {
16264 label: "bg-blue".into(),
16265 ..Default::default()
16266 },
16267 lsp::CompletionItem {
16268 label: "bg-red".into(),
16269 ..Default::default()
16270 },
16271 lsp::CompletionItem {
16272 label: "bg-yellow".into(),
16273 ..Default::default()
16274 },
16275 ])))
16276 });
16277
16278 cx.set_state(r#"<p class="bgˇ" />"#);
16279
16280 // Trigger completion when typing a dash, because the dash is an extra
16281 // word character in the 'element' scope, which contains the cursor.
16282 cx.simulate_keystroke("-");
16283 cx.executor().run_until_parked();
16284 cx.update_editor(|editor, _, _| {
16285 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16286 {
16287 assert_eq!(
16288 completion_menu_entries(&menu),
16289 &["bg-blue", "bg-red", "bg-yellow"]
16290 );
16291 } else {
16292 panic!("expected completion menu to be open");
16293 }
16294 });
16295
16296 cx.simulate_keystroke("l");
16297 cx.executor().run_until_parked();
16298 cx.update_editor(|editor, _, _| {
16299 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16300 {
16301 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
16302 } else {
16303 panic!("expected completion menu to be open");
16304 }
16305 });
16306
16307 // When filtering completions, consider the character after the '-' to
16308 // be the start of a subword.
16309 cx.set_state(r#"<p class="yelˇ" />"#);
16310 cx.simulate_keystroke("l");
16311 cx.executor().run_until_parked();
16312 cx.update_editor(|editor, _, _| {
16313 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16314 {
16315 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
16316 } else {
16317 panic!("expected completion menu to be open");
16318 }
16319 });
16320}
16321
16322fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
16323 let entries = menu.entries.borrow();
16324 entries.iter().map(|mat| mat.string.clone()).collect()
16325}
16326
16327#[gpui::test]
16328async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
16329 init_test(cx, |settings| {
16330 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
16331 Formatter::Prettier,
16332 )))
16333 });
16334
16335 let fs = FakeFs::new(cx.executor());
16336 fs.insert_file(path!("/file.ts"), Default::default()).await;
16337
16338 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
16339 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
16340
16341 language_registry.add(Arc::new(Language::new(
16342 LanguageConfig {
16343 name: "TypeScript".into(),
16344 matcher: LanguageMatcher {
16345 path_suffixes: vec!["ts".to_string()],
16346 ..Default::default()
16347 },
16348 ..Default::default()
16349 },
16350 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
16351 )));
16352 update_test_language_settings(cx, |settings| {
16353 settings.defaults.prettier = Some(PrettierSettings {
16354 allowed: true,
16355 ..PrettierSettings::default()
16356 });
16357 });
16358
16359 let test_plugin = "test_plugin";
16360 let _ = language_registry.register_fake_lsp(
16361 "TypeScript",
16362 FakeLspAdapter {
16363 prettier_plugins: vec![test_plugin],
16364 ..Default::default()
16365 },
16366 );
16367
16368 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
16369 let buffer = project
16370 .update(cx, |project, cx| {
16371 project.open_local_buffer(path!("/file.ts"), cx)
16372 })
16373 .await
16374 .unwrap();
16375
16376 let buffer_text = "one\ntwo\nthree\n";
16377 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16378 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16379 editor.update_in(cx, |editor, window, cx| {
16380 editor.set_text(buffer_text, window, cx)
16381 });
16382
16383 editor
16384 .update_in(cx, |editor, window, cx| {
16385 editor.perform_format(
16386 project.clone(),
16387 FormatTrigger::Manual,
16388 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
16389 window,
16390 cx,
16391 )
16392 })
16393 .unwrap()
16394 .await;
16395 assert_eq!(
16396 editor.update(cx, |editor, cx| editor.text(cx)),
16397 buffer_text.to_string() + prettier_format_suffix,
16398 "Test prettier formatting was not applied to the original buffer text",
16399 );
16400
16401 update_test_language_settings(cx, |settings| {
16402 settings.defaults.formatter = Some(SelectedFormatter::Auto)
16403 });
16404 let format = editor.update_in(cx, |editor, window, cx| {
16405 editor.perform_format(
16406 project.clone(),
16407 FormatTrigger::Manual,
16408 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
16409 window,
16410 cx,
16411 )
16412 });
16413 format.await.unwrap();
16414 assert_eq!(
16415 editor.update(cx, |editor, cx| editor.text(cx)),
16416 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
16417 "Autoformatting (via test prettier) was not applied to the original buffer text",
16418 );
16419}
16420
16421#[gpui::test]
16422async fn test_addition_reverts(cx: &mut TestAppContext) {
16423 init_test(cx, |_| {});
16424 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16425 let base_text = indoc! {r#"
16426 struct Row;
16427 struct Row1;
16428 struct Row2;
16429
16430 struct Row4;
16431 struct Row5;
16432 struct Row6;
16433
16434 struct Row8;
16435 struct Row9;
16436 struct Row10;"#};
16437
16438 // When addition hunks are not adjacent to carets, no hunk revert is performed
16439 assert_hunk_revert(
16440 indoc! {r#"struct Row;
16441 struct Row1;
16442 struct Row1.1;
16443 struct Row1.2;
16444 struct Row2;ˇ
16445
16446 struct Row4;
16447 struct Row5;
16448 struct Row6;
16449
16450 struct Row8;
16451 ˇstruct Row9;
16452 struct Row9.1;
16453 struct Row9.2;
16454 struct Row9.3;
16455 struct Row10;"#},
16456 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
16457 indoc! {r#"struct Row;
16458 struct Row1;
16459 struct Row1.1;
16460 struct Row1.2;
16461 struct Row2;ˇ
16462
16463 struct Row4;
16464 struct Row5;
16465 struct Row6;
16466
16467 struct Row8;
16468 ˇstruct Row9;
16469 struct Row9.1;
16470 struct Row9.2;
16471 struct Row9.3;
16472 struct Row10;"#},
16473 base_text,
16474 &mut cx,
16475 );
16476 // Same for selections
16477 assert_hunk_revert(
16478 indoc! {r#"struct Row;
16479 struct Row1;
16480 struct Row2;
16481 struct Row2.1;
16482 struct Row2.2;
16483 «ˇ
16484 struct Row4;
16485 struct» Row5;
16486 «struct Row6;
16487 ˇ»
16488 struct Row9.1;
16489 struct Row9.2;
16490 struct Row9.3;
16491 struct Row8;
16492 struct Row9;
16493 struct Row10;"#},
16494 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
16495 indoc! {r#"struct Row;
16496 struct Row1;
16497 struct Row2;
16498 struct Row2.1;
16499 struct Row2.2;
16500 «ˇ
16501 struct Row4;
16502 struct» Row5;
16503 «struct Row6;
16504 ˇ»
16505 struct Row9.1;
16506 struct Row9.2;
16507 struct Row9.3;
16508 struct Row8;
16509 struct Row9;
16510 struct Row10;"#},
16511 base_text,
16512 &mut cx,
16513 );
16514
16515 // When carets and selections intersect the addition hunks, those are reverted.
16516 // Adjacent carets got merged.
16517 assert_hunk_revert(
16518 indoc! {r#"struct Row;
16519 ˇ// something on the top
16520 struct Row1;
16521 struct Row2;
16522 struct Roˇw3.1;
16523 struct Row2.2;
16524 struct Row2.3;ˇ
16525
16526 struct Row4;
16527 struct ˇRow5.1;
16528 struct Row5.2;
16529 struct «Rowˇ»5.3;
16530 struct Row5;
16531 struct Row6;
16532 ˇ
16533 struct Row9.1;
16534 struct «Rowˇ»9.2;
16535 struct «ˇRow»9.3;
16536 struct Row8;
16537 struct Row9;
16538 «ˇ// something on bottom»
16539 struct Row10;"#},
16540 vec![
16541 DiffHunkStatusKind::Added,
16542 DiffHunkStatusKind::Added,
16543 DiffHunkStatusKind::Added,
16544 DiffHunkStatusKind::Added,
16545 DiffHunkStatusKind::Added,
16546 ],
16547 indoc! {r#"struct Row;
16548 ˇstruct Row1;
16549 struct Row2;
16550 ˇ
16551 struct Row4;
16552 ˇstruct Row5;
16553 struct Row6;
16554 ˇ
16555 ˇstruct Row8;
16556 struct Row9;
16557 ˇstruct Row10;"#},
16558 base_text,
16559 &mut cx,
16560 );
16561}
16562
16563#[gpui::test]
16564async fn test_modification_reverts(cx: &mut TestAppContext) {
16565 init_test(cx, |_| {});
16566 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16567 let base_text = indoc! {r#"
16568 struct Row;
16569 struct Row1;
16570 struct Row2;
16571
16572 struct Row4;
16573 struct Row5;
16574 struct Row6;
16575
16576 struct Row8;
16577 struct Row9;
16578 struct Row10;"#};
16579
16580 // Modification hunks behave the same as the addition ones.
16581 assert_hunk_revert(
16582 indoc! {r#"struct Row;
16583 struct Row1;
16584 struct Row33;
16585 ˇ
16586 struct Row4;
16587 struct Row5;
16588 struct Row6;
16589 ˇ
16590 struct Row99;
16591 struct Row9;
16592 struct Row10;"#},
16593 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
16594 indoc! {r#"struct Row;
16595 struct Row1;
16596 struct Row33;
16597 ˇ
16598 struct Row4;
16599 struct Row5;
16600 struct Row6;
16601 ˇ
16602 struct Row99;
16603 struct Row9;
16604 struct Row10;"#},
16605 base_text,
16606 &mut cx,
16607 );
16608 assert_hunk_revert(
16609 indoc! {r#"struct Row;
16610 struct Row1;
16611 struct Row33;
16612 «ˇ
16613 struct Row4;
16614 struct» Row5;
16615 «struct Row6;
16616 ˇ»
16617 struct Row99;
16618 struct Row9;
16619 struct Row10;"#},
16620 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
16621 indoc! {r#"struct Row;
16622 struct Row1;
16623 struct Row33;
16624 «ˇ
16625 struct Row4;
16626 struct» Row5;
16627 «struct Row6;
16628 ˇ»
16629 struct Row99;
16630 struct Row9;
16631 struct Row10;"#},
16632 base_text,
16633 &mut cx,
16634 );
16635
16636 assert_hunk_revert(
16637 indoc! {r#"ˇstruct Row1.1;
16638 struct Row1;
16639 «ˇstr»uct Row22;
16640
16641 struct ˇRow44;
16642 struct Row5;
16643 struct «Rˇ»ow66;ˇ
16644
16645 «struˇ»ct Row88;
16646 struct Row9;
16647 struct Row1011;ˇ"#},
16648 vec![
16649 DiffHunkStatusKind::Modified,
16650 DiffHunkStatusKind::Modified,
16651 DiffHunkStatusKind::Modified,
16652 DiffHunkStatusKind::Modified,
16653 DiffHunkStatusKind::Modified,
16654 DiffHunkStatusKind::Modified,
16655 ],
16656 indoc! {r#"struct Row;
16657 ˇstruct Row1;
16658 struct Row2;
16659 ˇ
16660 struct Row4;
16661 ˇstruct Row5;
16662 struct Row6;
16663 ˇ
16664 struct Row8;
16665 ˇstruct Row9;
16666 struct Row10;ˇ"#},
16667 base_text,
16668 &mut cx,
16669 );
16670}
16671
16672#[gpui::test]
16673async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
16674 init_test(cx, |_| {});
16675 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16676 let base_text = indoc! {r#"
16677 one
16678
16679 two
16680 three
16681 "#};
16682
16683 cx.set_head_text(base_text);
16684 cx.set_state("\nˇ\n");
16685 cx.executor().run_until_parked();
16686 cx.update_editor(|editor, _window, cx| {
16687 editor.expand_selected_diff_hunks(cx);
16688 });
16689 cx.executor().run_until_parked();
16690 cx.update_editor(|editor, window, cx| {
16691 editor.backspace(&Default::default(), window, cx);
16692 });
16693 cx.run_until_parked();
16694 cx.assert_state_with_diff(
16695 indoc! {r#"
16696
16697 - two
16698 - threeˇ
16699 +
16700 "#}
16701 .to_string(),
16702 );
16703}
16704
16705#[gpui::test]
16706async fn test_deletion_reverts(cx: &mut TestAppContext) {
16707 init_test(cx, |_| {});
16708 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16709 let base_text = indoc! {r#"struct Row;
16710struct Row1;
16711struct Row2;
16712
16713struct Row4;
16714struct Row5;
16715struct Row6;
16716
16717struct Row8;
16718struct Row9;
16719struct Row10;"#};
16720
16721 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
16722 assert_hunk_revert(
16723 indoc! {r#"struct Row;
16724 struct Row2;
16725
16726 ˇstruct Row4;
16727 struct Row5;
16728 struct Row6;
16729 ˇ
16730 struct Row8;
16731 struct Row10;"#},
16732 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16733 indoc! {r#"struct Row;
16734 struct Row2;
16735
16736 ˇstruct Row4;
16737 struct Row5;
16738 struct Row6;
16739 ˇ
16740 struct Row8;
16741 struct Row10;"#},
16742 base_text,
16743 &mut cx,
16744 );
16745 assert_hunk_revert(
16746 indoc! {r#"struct Row;
16747 struct Row2;
16748
16749 «ˇstruct Row4;
16750 struct» Row5;
16751 «struct Row6;
16752 ˇ»
16753 struct Row8;
16754 struct Row10;"#},
16755 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16756 indoc! {r#"struct Row;
16757 struct Row2;
16758
16759 «ˇstruct Row4;
16760 struct» Row5;
16761 «struct Row6;
16762 ˇ»
16763 struct Row8;
16764 struct Row10;"#},
16765 base_text,
16766 &mut cx,
16767 );
16768
16769 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
16770 assert_hunk_revert(
16771 indoc! {r#"struct Row;
16772 ˇstruct Row2;
16773
16774 struct Row4;
16775 struct Row5;
16776 struct Row6;
16777
16778 struct Row8;ˇ
16779 struct Row10;"#},
16780 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16781 indoc! {r#"struct Row;
16782 struct Row1;
16783 ˇstruct Row2;
16784
16785 struct Row4;
16786 struct Row5;
16787 struct Row6;
16788
16789 struct Row8;ˇ
16790 struct Row9;
16791 struct Row10;"#},
16792 base_text,
16793 &mut cx,
16794 );
16795 assert_hunk_revert(
16796 indoc! {r#"struct Row;
16797 struct Row2«ˇ;
16798 struct Row4;
16799 struct» Row5;
16800 «struct Row6;
16801
16802 struct Row8;ˇ»
16803 struct Row10;"#},
16804 vec![
16805 DiffHunkStatusKind::Deleted,
16806 DiffHunkStatusKind::Deleted,
16807 DiffHunkStatusKind::Deleted,
16808 ],
16809 indoc! {r#"struct Row;
16810 struct Row1;
16811 struct Row2«ˇ;
16812
16813 struct Row4;
16814 struct» Row5;
16815 «struct Row6;
16816
16817 struct Row8;ˇ»
16818 struct Row9;
16819 struct Row10;"#},
16820 base_text,
16821 &mut cx,
16822 );
16823}
16824
16825#[gpui::test]
16826async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
16827 init_test(cx, |_| {});
16828
16829 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
16830 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
16831 let base_text_3 =
16832 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
16833
16834 let text_1 = edit_first_char_of_every_line(base_text_1);
16835 let text_2 = edit_first_char_of_every_line(base_text_2);
16836 let text_3 = edit_first_char_of_every_line(base_text_3);
16837
16838 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
16839 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
16840 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
16841
16842 let multibuffer = cx.new(|cx| {
16843 let mut multibuffer = MultiBuffer::new(ReadWrite);
16844 multibuffer.push_excerpts(
16845 buffer_1.clone(),
16846 [
16847 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16848 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16849 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16850 ],
16851 cx,
16852 );
16853 multibuffer.push_excerpts(
16854 buffer_2.clone(),
16855 [
16856 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16857 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16858 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16859 ],
16860 cx,
16861 );
16862 multibuffer.push_excerpts(
16863 buffer_3.clone(),
16864 [
16865 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16866 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16867 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16868 ],
16869 cx,
16870 );
16871 multibuffer
16872 });
16873
16874 let fs = FakeFs::new(cx.executor());
16875 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
16876 let (editor, cx) = cx
16877 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
16878 editor.update_in(cx, |editor, _window, cx| {
16879 for (buffer, diff_base) in [
16880 (buffer_1.clone(), base_text_1),
16881 (buffer_2.clone(), base_text_2),
16882 (buffer_3.clone(), base_text_3),
16883 ] {
16884 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
16885 editor
16886 .buffer
16887 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
16888 }
16889 });
16890 cx.executor().run_until_parked();
16891
16892 editor.update_in(cx, |editor, window, cx| {
16893 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}");
16894 editor.select_all(&SelectAll, window, cx);
16895 editor.git_restore(&Default::default(), window, cx);
16896 });
16897 cx.executor().run_until_parked();
16898
16899 // When all ranges are selected, all buffer hunks are reverted.
16900 editor.update(cx, |editor, cx| {
16901 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");
16902 });
16903 buffer_1.update(cx, |buffer, _| {
16904 assert_eq!(buffer.text(), base_text_1);
16905 });
16906 buffer_2.update(cx, |buffer, _| {
16907 assert_eq!(buffer.text(), base_text_2);
16908 });
16909 buffer_3.update(cx, |buffer, _| {
16910 assert_eq!(buffer.text(), base_text_3);
16911 });
16912
16913 editor.update_in(cx, |editor, window, cx| {
16914 editor.undo(&Default::default(), window, cx);
16915 });
16916
16917 editor.update_in(cx, |editor, window, cx| {
16918 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16919 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
16920 });
16921 editor.git_restore(&Default::default(), window, cx);
16922 });
16923
16924 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
16925 // but not affect buffer_2 and its related excerpts.
16926 editor.update(cx, |editor, cx| {
16927 assert_eq!(
16928 editor.text(cx),
16929 "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}"
16930 );
16931 });
16932 buffer_1.update(cx, |buffer, _| {
16933 assert_eq!(buffer.text(), base_text_1);
16934 });
16935 buffer_2.update(cx, |buffer, _| {
16936 assert_eq!(
16937 buffer.text(),
16938 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
16939 );
16940 });
16941 buffer_3.update(cx, |buffer, _| {
16942 assert_eq!(
16943 buffer.text(),
16944 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
16945 );
16946 });
16947
16948 fn edit_first_char_of_every_line(text: &str) -> String {
16949 text.split('\n')
16950 .map(|line| format!("X{}", &line[1..]))
16951 .collect::<Vec<_>>()
16952 .join("\n")
16953 }
16954}
16955
16956#[gpui::test]
16957async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
16958 init_test(cx, |_| {});
16959
16960 let cols = 4;
16961 let rows = 10;
16962 let sample_text_1 = sample_text(rows, cols, 'a');
16963 assert_eq!(
16964 sample_text_1,
16965 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
16966 );
16967 let sample_text_2 = sample_text(rows, cols, 'l');
16968 assert_eq!(
16969 sample_text_2,
16970 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
16971 );
16972 let sample_text_3 = sample_text(rows, cols, 'v');
16973 assert_eq!(
16974 sample_text_3,
16975 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
16976 );
16977
16978 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
16979 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
16980 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
16981
16982 let multi_buffer = cx.new(|cx| {
16983 let mut multibuffer = MultiBuffer::new(ReadWrite);
16984 multibuffer.push_excerpts(
16985 buffer_1.clone(),
16986 [
16987 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16988 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16989 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16990 ],
16991 cx,
16992 );
16993 multibuffer.push_excerpts(
16994 buffer_2.clone(),
16995 [
16996 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16997 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16998 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16999 ],
17000 cx,
17001 );
17002 multibuffer.push_excerpts(
17003 buffer_3.clone(),
17004 [
17005 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17006 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17007 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17008 ],
17009 cx,
17010 );
17011 multibuffer
17012 });
17013
17014 let fs = FakeFs::new(cx.executor());
17015 fs.insert_tree(
17016 "/a",
17017 json!({
17018 "main.rs": sample_text_1,
17019 "other.rs": sample_text_2,
17020 "lib.rs": sample_text_3,
17021 }),
17022 )
17023 .await;
17024 let project = Project::test(fs, ["/a".as_ref()], cx).await;
17025 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17026 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17027 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
17028 Editor::new(
17029 EditorMode::full(),
17030 multi_buffer,
17031 Some(project.clone()),
17032 window,
17033 cx,
17034 )
17035 });
17036 let multibuffer_item_id = workspace
17037 .update(cx, |workspace, window, cx| {
17038 assert!(
17039 workspace.active_item(cx).is_none(),
17040 "active item should be None before the first item is added"
17041 );
17042 workspace.add_item_to_active_pane(
17043 Box::new(multi_buffer_editor.clone()),
17044 None,
17045 true,
17046 window,
17047 cx,
17048 );
17049 let active_item = workspace
17050 .active_item(cx)
17051 .expect("should have an active item after adding the multi buffer");
17052 assert!(
17053 !active_item.is_singleton(cx),
17054 "A multi buffer was expected to active after adding"
17055 );
17056 active_item.item_id()
17057 })
17058 .unwrap();
17059 cx.executor().run_until_parked();
17060
17061 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17062 editor.change_selections(
17063 SelectionEffects::scroll(Autoscroll::Next),
17064 window,
17065 cx,
17066 |s| s.select_ranges(Some(1..2)),
17067 );
17068 editor.open_excerpts(&OpenExcerpts, window, cx);
17069 });
17070 cx.executor().run_until_parked();
17071 let first_item_id = workspace
17072 .update(cx, |workspace, window, cx| {
17073 let active_item = workspace
17074 .active_item(cx)
17075 .expect("should have an active item after navigating into the 1st buffer");
17076 let first_item_id = active_item.item_id();
17077 assert_ne!(
17078 first_item_id, multibuffer_item_id,
17079 "Should navigate into the 1st buffer and activate it"
17080 );
17081 assert!(
17082 active_item.is_singleton(cx),
17083 "New active item should be a singleton buffer"
17084 );
17085 assert_eq!(
17086 active_item
17087 .act_as::<Editor>(cx)
17088 .expect("should have navigated into an editor for the 1st buffer")
17089 .read(cx)
17090 .text(cx),
17091 sample_text_1
17092 );
17093
17094 workspace
17095 .go_back(workspace.active_pane().downgrade(), window, cx)
17096 .detach_and_log_err(cx);
17097
17098 first_item_id
17099 })
17100 .unwrap();
17101 cx.executor().run_until_parked();
17102 workspace
17103 .update(cx, |workspace, _, cx| {
17104 let active_item = workspace
17105 .active_item(cx)
17106 .expect("should have an active item after navigating back");
17107 assert_eq!(
17108 active_item.item_id(),
17109 multibuffer_item_id,
17110 "Should navigate back to the multi buffer"
17111 );
17112 assert!(!active_item.is_singleton(cx));
17113 })
17114 .unwrap();
17115
17116 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17117 editor.change_selections(
17118 SelectionEffects::scroll(Autoscroll::Next),
17119 window,
17120 cx,
17121 |s| s.select_ranges(Some(39..40)),
17122 );
17123 editor.open_excerpts(&OpenExcerpts, window, cx);
17124 });
17125 cx.executor().run_until_parked();
17126 let second_item_id = workspace
17127 .update(cx, |workspace, window, cx| {
17128 let active_item = workspace
17129 .active_item(cx)
17130 .expect("should have an active item after navigating into the 2nd buffer");
17131 let second_item_id = active_item.item_id();
17132 assert_ne!(
17133 second_item_id, multibuffer_item_id,
17134 "Should navigate away from the multibuffer"
17135 );
17136 assert_ne!(
17137 second_item_id, first_item_id,
17138 "Should navigate into the 2nd buffer and activate it"
17139 );
17140 assert!(
17141 active_item.is_singleton(cx),
17142 "New active item should be a singleton buffer"
17143 );
17144 assert_eq!(
17145 active_item
17146 .act_as::<Editor>(cx)
17147 .expect("should have navigated into an editor")
17148 .read(cx)
17149 .text(cx),
17150 sample_text_2
17151 );
17152
17153 workspace
17154 .go_back(workspace.active_pane().downgrade(), window, cx)
17155 .detach_and_log_err(cx);
17156
17157 second_item_id
17158 })
17159 .unwrap();
17160 cx.executor().run_until_parked();
17161 workspace
17162 .update(cx, |workspace, _, cx| {
17163 let active_item = workspace
17164 .active_item(cx)
17165 .expect("should have an active item after navigating back from the 2nd buffer");
17166 assert_eq!(
17167 active_item.item_id(),
17168 multibuffer_item_id,
17169 "Should navigate back from the 2nd buffer to the multi buffer"
17170 );
17171 assert!(!active_item.is_singleton(cx));
17172 })
17173 .unwrap();
17174
17175 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17176 editor.change_selections(
17177 SelectionEffects::scroll(Autoscroll::Next),
17178 window,
17179 cx,
17180 |s| s.select_ranges(Some(70..70)),
17181 );
17182 editor.open_excerpts(&OpenExcerpts, window, cx);
17183 });
17184 cx.executor().run_until_parked();
17185 workspace
17186 .update(cx, |workspace, window, cx| {
17187 let active_item = workspace
17188 .active_item(cx)
17189 .expect("should have an active item after navigating into the 3rd buffer");
17190 let third_item_id = active_item.item_id();
17191 assert_ne!(
17192 third_item_id, multibuffer_item_id,
17193 "Should navigate into the 3rd buffer and activate it"
17194 );
17195 assert_ne!(third_item_id, first_item_id);
17196 assert_ne!(third_item_id, second_item_id);
17197 assert!(
17198 active_item.is_singleton(cx),
17199 "New active item should be a singleton buffer"
17200 );
17201 assert_eq!(
17202 active_item
17203 .act_as::<Editor>(cx)
17204 .expect("should have navigated into an editor")
17205 .read(cx)
17206 .text(cx),
17207 sample_text_3
17208 );
17209
17210 workspace
17211 .go_back(workspace.active_pane().downgrade(), window, cx)
17212 .detach_and_log_err(cx);
17213 })
17214 .unwrap();
17215 cx.executor().run_until_parked();
17216 workspace
17217 .update(cx, |workspace, _, cx| {
17218 let active_item = workspace
17219 .active_item(cx)
17220 .expect("should have an active item after navigating back from the 3rd buffer");
17221 assert_eq!(
17222 active_item.item_id(),
17223 multibuffer_item_id,
17224 "Should navigate back from the 3rd buffer to the multi buffer"
17225 );
17226 assert!(!active_item.is_singleton(cx));
17227 })
17228 .unwrap();
17229}
17230
17231#[gpui::test]
17232async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17233 init_test(cx, |_| {});
17234
17235 let mut cx = EditorTestContext::new(cx).await;
17236
17237 let diff_base = r#"
17238 use some::mod;
17239
17240 const A: u32 = 42;
17241
17242 fn main() {
17243 println!("hello");
17244
17245 println!("world");
17246 }
17247 "#
17248 .unindent();
17249
17250 cx.set_state(
17251 &r#"
17252 use some::modified;
17253
17254 ˇ
17255 fn main() {
17256 println!("hello there");
17257
17258 println!("around the");
17259 println!("world");
17260 }
17261 "#
17262 .unindent(),
17263 );
17264
17265 cx.set_head_text(&diff_base);
17266 executor.run_until_parked();
17267
17268 cx.update_editor(|editor, window, cx| {
17269 editor.go_to_next_hunk(&GoToHunk, window, cx);
17270 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17271 });
17272 executor.run_until_parked();
17273 cx.assert_state_with_diff(
17274 r#"
17275 use some::modified;
17276
17277
17278 fn main() {
17279 - println!("hello");
17280 + ˇ println!("hello there");
17281
17282 println!("around the");
17283 println!("world");
17284 }
17285 "#
17286 .unindent(),
17287 );
17288
17289 cx.update_editor(|editor, window, cx| {
17290 for _ in 0..2 {
17291 editor.go_to_next_hunk(&GoToHunk, window, cx);
17292 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17293 }
17294 });
17295 executor.run_until_parked();
17296 cx.assert_state_with_diff(
17297 r#"
17298 - use some::mod;
17299 + ˇuse some::modified;
17300
17301
17302 fn main() {
17303 - println!("hello");
17304 + println!("hello there");
17305
17306 + println!("around the");
17307 println!("world");
17308 }
17309 "#
17310 .unindent(),
17311 );
17312
17313 cx.update_editor(|editor, window, cx| {
17314 editor.go_to_next_hunk(&GoToHunk, window, cx);
17315 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17316 });
17317 executor.run_until_parked();
17318 cx.assert_state_with_diff(
17319 r#"
17320 - use some::mod;
17321 + use some::modified;
17322
17323 - const A: u32 = 42;
17324 ˇ
17325 fn main() {
17326 - println!("hello");
17327 + println!("hello there");
17328
17329 + println!("around the");
17330 println!("world");
17331 }
17332 "#
17333 .unindent(),
17334 );
17335
17336 cx.update_editor(|editor, window, cx| {
17337 editor.cancel(&Cancel, window, cx);
17338 });
17339
17340 cx.assert_state_with_diff(
17341 r#"
17342 use some::modified;
17343
17344 ˇ
17345 fn main() {
17346 println!("hello there");
17347
17348 println!("around the");
17349 println!("world");
17350 }
17351 "#
17352 .unindent(),
17353 );
17354}
17355
17356#[gpui::test]
17357async fn test_diff_base_change_with_expanded_diff_hunks(
17358 executor: BackgroundExecutor,
17359 cx: &mut TestAppContext,
17360) {
17361 init_test(cx, |_| {});
17362
17363 let mut cx = EditorTestContext::new(cx).await;
17364
17365 let diff_base = r#"
17366 use some::mod1;
17367 use some::mod2;
17368
17369 const A: u32 = 42;
17370 const B: u32 = 42;
17371 const C: u32 = 42;
17372
17373 fn main() {
17374 println!("hello");
17375
17376 println!("world");
17377 }
17378 "#
17379 .unindent();
17380
17381 cx.set_state(
17382 &r#"
17383 use some::mod2;
17384
17385 const A: u32 = 42;
17386 const C: u32 = 42;
17387
17388 fn main(ˇ) {
17389 //println!("hello");
17390
17391 println!("world");
17392 //
17393 //
17394 }
17395 "#
17396 .unindent(),
17397 );
17398
17399 cx.set_head_text(&diff_base);
17400 executor.run_until_parked();
17401
17402 cx.update_editor(|editor, window, cx| {
17403 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17404 });
17405 executor.run_until_parked();
17406 cx.assert_state_with_diff(
17407 r#"
17408 - use some::mod1;
17409 use some::mod2;
17410
17411 const A: u32 = 42;
17412 - const B: u32 = 42;
17413 const C: u32 = 42;
17414
17415 fn main(ˇ) {
17416 - println!("hello");
17417 + //println!("hello");
17418
17419 println!("world");
17420 + //
17421 + //
17422 }
17423 "#
17424 .unindent(),
17425 );
17426
17427 cx.set_head_text("new diff base!");
17428 executor.run_until_parked();
17429 cx.assert_state_with_diff(
17430 r#"
17431 - new diff base!
17432 + use some::mod2;
17433 +
17434 + const A: u32 = 42;
17435 + const C: u32 = 42;
17436 +
17437 + fn main(ˇ) {
17438 + //println!("hello");
17439 +
17440 + println!("world");
17441 + //
17442 + //
17443 + }
17444 "#
17445 .unindent(),
17446 );
17447}
17448
17449#[gpui::test]
17450async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
17451 init_test(cx, |_| {});
17452
17453 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
17454 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
17455 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
17456 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
17457 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
17458 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
17459
17460 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
17461 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
17462 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
17463
17464 let multi_buffer = cx.new(|cx| {
17465 let mut multibuffer = MultiBuffer::new(ReadWrite);
17466 multibuffer.push_excerpts(
17467 buffer_1.clone(),
17468 [
17469 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17470 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17471 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17472 ],
17473 cx,
17474 );
17475 multibuffer.push_excerpts(
17476 buffer_2.clone(),
17477 [
17478 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17479 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17480 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17481 ],
17482 cx,
17483 );
17484 multibuffer.push_excerpts(
17485 buffer_3.clone(),
17486 [
17487 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17488 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17489 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17490 ],
17491 cx,
17492 );
17493 multibuffer
17494 });
17495
17496 let editor =
17497 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
17498 editor
17499 .update(cx, |editor, _window, cx| {
17500 for (buffer, diff_base) in [
17501 (buffer_1.clone(), file_1_old),
17502 (buffer_2.clone(), file_2_old),
17503 (buffer_3.clone(), file_3_old),
17504 ] {
17505 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
17506 editor
17507 .buffer
17508 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
17509 }
17510 })
17511 .unwrap();
17512
17513 let mut cx = EditorTestContext::for_editor(editor, cx).await;
17514 cx.run_until_parked();
17515
17516 cx.assert_editor_state(
17517 &"
17518 ˇaaa
17519 ccc
17520 ddd
17521
17522 ggg
17523 hhh
17524
17525
17526 lll
17527 mmm
17528 NNN
17529
17530 qqq
17531 rrr
17532
17533 uuu
17534 111
17535 222
17536 333
17537
17538 666
17539 777
17540
17541 000
17542 !!!"
17543 .unindent(),
17544 );
17545
17546 cx.update_editor(|editor, window, cx| {
17547 editor.select_all(&SelectAll, window, cx);
17548 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17549 });
17550 cx.executor().run_until_parked();
17551
17552 cx.assert_state_with_diff(
17553 "
17554 «aaa
17555 - bbb
17556 ccc
17557 ddd
17558
17559 ggg
17560 hhh
17561
17562
17563 lll
17564 mmm
17565 - nnn
17566 + NNN
17567
17568 qqq
17569 rrr
17570
17571 uuu
17572 111
17573 222
17574 333
17575
17576 + 666
17577 777
17578
17579 000
17580 !!!ˇ»"
17581 .unindent(),
17582 );
17583}
17584
17585#[gpui::test]
17586async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
17587 init_test(cx, |_| {});
17588
17589 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
17590 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
17591
17592 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
17593 let multi_buffer = cx.new(|cx| {
17594 let mut multibuffer = MultiBuffer::new(ReadWrite);
17595 multibuffer.push_excerpts(
17596 buffer.clone(),
17597 [
17598 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
17599 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
17600 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
17601 ],
17602 cx,
17603 );
17604 multibuffer
17605 });
17606
17607 let editor =
17608 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
17609 editor
17610 .update(cx, |editor, _window, cx| {
17611 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
17612 editor
17613 .buffer
17614 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
17615 })
17616 .unwrap();
17617
17618 let mut cx = EditorTestContext::for_editor(editor, cx).await;
17619 cx.run_until_parked();
17620
17621 cx.update_editor(|editor, window, cx| {
17622 editor.expand_all_diff_hunks(&Default::default(), window, cx)
17623 });
17624 cx.executor().run_until_parked();
17625
17626 // When the start of a hunk coincides with the start of its excerpt,
17627 // the hunk is expanded. When the start of a a hunk is earlier than
17628 // the start of its excerpt, the hunk is not expanded.
17629 cx.assert_state_with_diff(
17630 "
17631 ˇaaa
17632 - bbb
17633 + BBB
17634
17635 - ddd
17636 - eee
17637 + DDD
17638 + EEE
17639 fff
17640
17641 iii
17642 "
17643 .unindent(),
17644 );
17645}
17646
17647#[gpui::test]
17648async fn test_edits_around_expanded_insertion_hunks(
17649 executor: BackgroundExecutor,
17650 cx: &mut TestAppContext,
17651) {
17652 init_test(cx, |_| {});
17653
17654 let mut cx = EditorTestContext::new(cx).await;
17655
17656 let diff_base = r#"
17657 use some::mod1;
17658 use some::mod2;
17659
17660 const A: u32 = 42;
17661
17662 fn main() {
17663 println!("hello");
17664
17665 println!("world");
17666 }
17667 "#
17668 .unindent();
17669 executor.run_until_parked();
17670 cx.set_state(
17671 &r#"
17672 use some::mod1;
17673 use some::mod2;
17674
17675 const A: u32 = 42;
17676 const B: u32 = 42;
17677 const C: u32 = 42;
17678 ˇ
17679
17680 fn main() {
17681 println!("hello");
17682
17683 println!("world");
17684 }
17685 "#
17686 .unindent(),
17687 );
17688
17689 cx.set_head_text(&diff_base);
17690 executor.run_until_parked();
17691
17692 cx.update_editor(|editor, window, cx| {
17693 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17694 });
17695 executor.run_until_parked();
17696
17697 cx.assert_state_with_diff(
17698 r#"
17699 use some::mod1;
17700 use some::mod2;
17701
17702 const A: u32 = 42;
17703 + const B: u32 = 42;
17704 + const C: u32 = 42;
17705 + ˇ
17706
17707 fn main() {
17708 println!("hello");
17709
17710 println!("world");
17711 }
17712 "#
17713 .unindent(),
17714 );
17715
17716 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
17717 executor.run_until_parked();
17718
17719 cx.assert_state_with_diff(
17720 r#"
17721 use some::mod1;
17722 use some::mod2;
17723
17724 const A: u32 = 42;
17725 + const B: u32 = 42;
17726 + const C: u32 = 42;
17727 + const D: u32 = 42;
17728 + ˇ
17729
17730 fn main() {
17731 println!("hello");
17732
17733 println!("world");
17734 }
17735 "#
17736 .unindent(),
17737 );
17738
17739 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
17740 executor.run_until_parked();
17741
17742 cx.assert_state_with_diff(
17743 r#"
17744 use some::mod1;
17745 use some::mod2;
17746
17747 const A: u32 = 42;
17748 + const B: u32 = 42;
17749 + const C: u32 = 42;
17750 + const D: u32 = 42;
17751 + const E: u32 = 42;
17752 + ˇ
17753
17754 fn main() {
17755 println!("hello");
17756
17757 println!("world");
17758 }
17759 "#
17760 .unindent(),
17761 );
17762
17763 cx.update_editor(|editor, window, cx| {
17764 editor.delete_line(&DeleteLine, window, cx);
17765 });
17766 executor.run_until_parked();
17767
17768 cx.assert_state_with_diff(
17769 r#"
17770 use some::mod1;
17771 use some::mod2;
17772
17773 const A: u32 = 42;
17774 + const B: u32 = 42;
17775 + const C: u32 = 42;
17776 + const D: u32 = 42;
17777 + const E: u32 = 42;
17778 ˇ
17779 fn main() {
17780 println!("hello");
17781
17782 println!("world");
17783 }
17784 "#
17785 .unindent(),
17786 );
17787
17788 cx.update_editor(|editor, window, cx| {
17789 editor.move_up(&MoveUp, window, cx);
17790 editor.delete_line(&DeleteLine, window, cx);
17791 editor.move_up(&MoveUp, window, cx);
17792 editor.delete_line(&DeleteLine, window, cx);
17793 editor.move_up(&MoveUp, window, cx);
17794 editor.delete_line(&DeleteLine, window, cx);
17795 });
17796 executor.run_until_parked();
17797 cx.assert_state_with_diff(
17798 r#"
17799 use some::mod1;
17800 use some::mod2;
17801
17802 const A: u32 = 42;
17803 + const B: u32 = 42;
17804 ˇ
17805 fn main() {
17806 println!("hello");
17807
17808 println!("world");
17809 }
17810 "#
17811 .unindent(),
17812 );
17813
17814 cx.update_editor(|editor, window, cx| {
17815 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
17816 editor.delete_line(&DeleteLine, window, cx);
17817 });
17818 executor.run_until_parked();
17819 cx.assert_state_with_diff(
17820 r#"
17821 ˇ
17822 fn main() {
17823 println!("hello");
17824
17825 println!("world");
17826 }
17827 "#
17828 .unindent(),
17829 );
17830}
17831
17832#[gpui::test]
17833async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
17834 init_test(cx, |_| {});
17835
17836 let mut cx = EditorTestContext::new(cx).await;
17837 cx.set_head_text(indoc! { "
17838 one
17839 two
17840 three
17841 four
17842 five
17843 "
17844 });
17845 cx.set_state(indoc! { "
17846 one
17847 ˇthree
17848 five
17849 "});
17850 cx.run_until_parked();
17851 cx.update_editor(|editor, window, cx| {
17852 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17853 });
17854 cx.assert_state_with_diff(
17855 indoc! { "
17856 one
17857 - two
17858 ˇthree
17859 - four
17860 five
17861 "}
17862 .to_string(),
17863 );
17864 cx.update_editor(|editor, window, cx| {
17865 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17866 });
17867
17868 cx.assert_state_with_diff(
17869 indoc! { "
17870 one
17871 ˇthree
17872 five
17873 "}
17874 .to_string(),
17875 );
17876
17877 cx.set_state(indoc! { "
17878 one
17879 ˇTWO
17880 three
17881 four
17882 five
17883 "});
17884 cx.run_until_parked();
17885 cx.update_editor(|editor, window, cx| {
17886 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17887 });
17888
17889 cx.assert_state_with_diff(
17890 indoc! { "
17891 one
17892 - two
17893 + ˇTWO
17894 three
17895 four
17896 five
17897 "}
17898 .to_string(),
17899 );
17900 cx.update_editor(|editor, window, cx| {
17901 editor.move_up(&Default::default(), window, cx);
17902 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17903 });
17904 cx.assert_state_with_diff(
17905 indoc! { "
17906 one
17907 ˇTWO
17908 three
17909 four
17910 five
17911 "}
17912 .to_string(),
17913 );
17914}
17915
17916#[gpui::test]
17917async fn test_edits_around_expanded_deletion_hunks(
17918 executor: BackgroundExecutor,
17919 cx: &mut TestAppContext,
17920) {
17921 init_test(cx, |_| {});
17922
17923 let mut cx = EditorTestContext::new(cx).await;
17924
17925 let diff_base = r#"
17926 use some::mod1;
17927 use some::mod2;
17928
17929 const A: u32 = 42;
17930 const B: u32 = 42;
17931 const C: u32 = 42;
17932
17933
17934 fn main() {
17935 println!("hello");
17936
17937 println!("world");
17938 }
17939 "#
17940 .unindent();
17941 executor.run_until_parked();
17942 cx.set_state(
17943 &r#"
17944 use some::mod1;
17945 use some::mod2;
17946
17947 ˇconst B: u32 = 42;
17948 const C: u32 = 42;
17949
17950
17951 fn main() {
17952 println!("hello");
17953
17954 println!("world");
17955 }
17956 "#
17957 .unindent(),
17958 );
17959
17960 cx.set_head_text(&diff_base);
17961 executor.run_until_parked();
17962
17963 cx.update_editor(|editor, window, cx| {
17964 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17965 });
17966 executor.run_until_parked();
17967
17968 cx.assert_state_with_diff(
17969 r#"
17970 use some::mod1;
17971 use some::mod2;
17972
17973 - const A: u32 = 42;
17974 ˇconst B: u32 = 42;
17975 const C: u32 = 42;
17976
17977
17978 fn main() {
17979 println!("hello");
17980
17981 println!("world");
17982 }
17983 "#
17984 .unindent(),
17985 );
17986
17987 cx.update_editor(|editor, window, cx| {
17988 editor.delete_line(&DeleteLine, window, cx);
17989 });
17990 executor.run_until_parked();
17991 cx.assert_state_with_diff(
17992 r#"
17993 use some::mod1;
17994 use some::mod2;
17995
17996 - const A: u32 = 42;
17997 - const B: u32 = 42;
17998 ˇconst C: u32 = 42;
17999
18000
18001 fn main() {
18002 println!("hello");
18003
18004 println!("world");
18005 }
18006 "#
18007 .unindent(),
18008 );
18009
18010 cx.update_editor(|editor, window, cx| {
18011 editor.delete_line(&DeleteLine, window, cx);
18012 });
18013 executor.run_until_parked();
18014 cx.assert_state_with_diff(
18015 r#"
18016 use some::mod1;
18017 use some::mod2;
18018
18019 - const A: u32 = 42;
18020 - const B: u32 = 42;
18021 - const C: u32 = 42;
18022 ˇ
18023
18024 fn main() {
18025 println!("hello");
18026
18027 println!("world");
18028 }
18029 "#
18030 .unindent(),
18031 );
18032
18033 cx.update_editor(|editor, window, cx| {
18034 editor.handle_input("replacement", window, cx);
18035 });
18036 executor.run_until_parked();
18037 cx.assert_state_with_diff(
18038 r#"
18039 use some::mod1;
18040 use some::mod2;
18041
18042 - const A: u32 = 42;
18043 - const B: u32 = 42;
18044 - const C: u32 = 42;
18045 -
18046 + replacementˇ
18047
18048 fn main() {
18049 println!("hello");
18050
18051 println!("world");
18052 }
18053 "#
18054 .unindent(),
18055 );
18056}
18057
18058#[gpui::test]
18059async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
18060 init_test(cx, |_| {});
18061
18062 let mut cx = EditorTestContext::new(cx).await;
18063
18064 let base_text = r#"
18065 one
18066 two
18067 three
18068 four
18069 five
18070 "#
18071 .unindent();
18072 executor.run_until_parked();
18073 cx.set_state(
18074 &r#"
18075 one
18076 two
18077 fˇour
18078 five
18079 "#
18080 .unindent(),
18081 );
18082
18083 cx.set_head_text(&base_text);
18084 executor.run_until_parked();
18085
18086 cx.update_editor(|editor, window, cx| {
18087 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18088 });
18089 executor.run_until_parked();
18090
18091 cx.assert_state_with_diff(
18092 r#"
18093 one
18094 two
18095 - three
18096 fˇour
18097 five
18098 "#
18099 .unindent(),
18100 );
18101
18102 cx.update_editor(|editor, window, cx| {
18103 editor.backspace(&Backspace, window, cx);
18104 editor.backspace(&Backspace, window, cx);
18105 });
18106 executor.run_until_parked();
18107 cx.assert_state_with_diff(
18108 r#"
18109 one
18110 two
18111 - threeˇ
18112 - four
18113 + our
18114 five
18115 "#
18116 .unindent(),
18117 );
18118}
18119
18120#[gpui::test]
18121async fn test_edit_after_expanded_modification_hunk(
18122 executor: BackgroundExecutor,
18123 cx: &mut TestAppContext,
18124) {
18125 init_test(cx, |_| {});
18126
18127 let mut cx = EditorTestContext::new(cx).await;
18128
18129 let diff_base = r#"
18130 use some::mod1;
18131 use some::mod2;
18132
18133 const A: u32 = 42;
18134 const B: u32 = 42;
18135 const C: u32 = 42;
18136 const D: u32 = 42;
18137
18138
18139 fn main() {
18140 println!("hello");
18141
18142 println!("world");
18143 }"#
18144 .unindent();
18145
18146 cx.set_state(
18147 &r#"
18148 use some::mod1;
18149 use some::mod2;
18150
18151 const A: u32 = 42;
18152 const B: u32 = 42;
18153 const C: u32 = 43ˇ
18154 const D: u32 = 42;
18155
18156
18157 fn main() {
18158 println!("hello");
18159
18160 println!("world");
18161 }"#
18162 .unindent(),
18163 );
18164
18165 cx.set_head_text(&diff_base);
18166 executor.run_until_parked();
18167 cx.update_editor(|editor, window, cx| {
18168 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18169 });
18170 executor.run_until_parked();
18171
18172 cx.assert_state_with_diff(
18173 r#"
18174 use some::mod1;
18175 use some::mod2;
18176
18177 const A: u32 = 42;
18178 const B: u32 = 42;
18179 - const C: u32 = 42;
18180 + const C: u32 = 43ˇ
18181 const D: u32 = 42;
18182
18183
18184 fn main() {
18185 println!("hello");
18186
18187 println!("world");
18188 }"#
18189 .unindent(),
18190 );
18191
18192 cx.update_editor(|editor, window, cx| {
18193 editor.handle_input("\nnew_line\n", window, cx);
18194 });
18195 executor.run_until_parked();
18196
18197 cx.assert_state_with_diff(
18198 r#"
18199 use some::mod1;
18200 use some::mod2;
18201
18202 const A: u32 = 42;
18203 const B: u32 = 42;
18204 - const C: u32 = 42;
18205 + const C: u32 = 43
18206 + new_line
18207 + ˇ
18208 const D: u32 = 42;
18209
18210
18211 fn main() {
18212 println!("hello");
18213
18214 println!("world");
18215 }"#
18216 .unindent(),
18217 );
18218}
18219
18220#[gpui::test]
18221async fn test_stage_and_unstage_added_file_hunk(
18222 executor: BackgroundExecutor,
18223 cx: &mut TestAppContext,
18224) {
18225 init_test(cx, |_| {});
18226
18227 let mut cx = EditorTestContext::new(cx).await;
18228 cx.update_editor(|editor, _, cx| {
18229 editor.set_expand_all_diff_hunks(cx);
18230 });
18231
18232 let working_copy = r#"
18233 ˇfn main() {
18234 println!("hello, world!");
18235 }
18236 "#
18237 .unindent();
18238
18239 cx.set_state(&working_copy);
18240 executor.run_until_parked();
18241
18242 cx.assert_state_with_diff(
18243 r#"
18244 + ˇfn main() {
18245 + println!("hello, world!");
18246 + }
18247 "#
18248 .unindent(),
18249 );
18250 cx.assert_index_text(None);
18251
18252 cx.update_editor(|editor, window, cx| {
18253 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18254 });
18255 executor.run_until_parked();
18256 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
18257 cx.assert_state_with_diff(
18258 r#"
18259 + ˇfn main() {
18260 + println!("hello, world!");
18261 + }
18262 "#
18263 .unindent(),
18264 );
18265
18266 cx.update_editor(|editor, window, cx| {
18267 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18268 });
18269 executor.run_until_parked();
18270 cx.assert_index_text(None);
18271}
18272
18273async fn setup_indent_guides_editor(
18274 text: &str,
18275 cx: &mut TestAppContext,
18276) -> (BufferId, EditorTestContext) {
18277 init_test(cx, |_| {});
18278
18279 let mut cx = EditorTestContext::new(cx).await;
18280
18281 let buffer_id = cx.update_editor(|editor, window, cx| {
18282 editor.set_text(text, window, cx);
18283 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
18284
18285 buffer_ids[0]
18286 });
18287
18288 (buffer_id, cx)
18289}
18290
18291fn assert_indent_guides(
18292 range: Range<u32>,
18293 expected: Vec<IndentGuide>,
18294 active_indices: Option<Vec<usize>>,
18295 cx: &mut EditorTestContext,
18296) {
18297 let indent_guides = cx.update_editor(|editor, window, cx| {
18298 let snapshot = editor.snapshot(window, cx).display_snapshot;
18299 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
18300 editor,
18301 MultiBufferRow(range.start)..MultiBufferRow(range.end),
18302 true,
18303 &snapshot,
18304 cx,
18305 );
18306
18307 indent_guides.sort_by(|a, b| {
18308 a.depth.cmp(&b.depth).then(
18309 a.start_row
18310 .cmp(&b.start_row)
18311 .then(a.end_row.cmp(&b.end_row)),
18312 )
18313 });
18314 indent_guides
18315 });
18316
18317 if let Some(expected) = active_indices {
18318 let active_indices = cx.update_editor(|editor, window, cx| {
18319 let snapshot = editor.snapshot(window, cx).display_snapshot;
18320 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
18321 });
18322
18323 assert_eq!(
18324 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
18325 expected,
18326 "Active indent guide indices do not match"
18327 );
18328 }
18329
18330 assert_eq!(indent_guides, expected, "Indent guides do not match");
18331}
18332
18333fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
18334 IndentGuide {
18335 buffer_id,
18336 start_row: MultiBufferRow(start_row),
18337 end_row: MultiBufferRow(end_row),
18338 depth,
18339 tab_size: 4,
18340 settings: IndentGuideSettings {
18341 enabled: true,
18342 line_width: 1,
18343 active_line_width: 1,
18344 ..Default::default()
18345 },
18346 }
18347}
18348
18349#[gpui::test]
18350async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
18351 let (buffer_id, mut cx) = setup_indent_guides_editor(
18352 &"
18353 fn main() {
18354 let a = 1;
18355 }"
18356 .unindent(),
18357 cx,
18358 )
18359 .await;
18360
18361 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
18362}
18363
18364#[gpui::test]
18365async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
18366 let (buffer_id, mut cx) = setup_indent_guides_editor(
18367 &"
18368 fn main() {
18369 let a = 1;
18370 let b = 2;
18371 }"
18372 .unindent(),
18373 cx,
18374 )
18375 .await;
18376
18377 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
18378}
18379
18380#[gpui::test]
18381async fn test_indent_guide_nested(cx: &mut TestAppContext) {
18382 let (buffer_id, mut cx) = setup_indent_guides_editor(
18383 &"
18384 fn main() {
18385 let a = 1;
18386 if a == 3 {
18387 let b = 2;
18388 } else {
18389 let c = 3;
18390 }
18391 }"
18392 .unindent(),
18393 cx,
18394 )
18395 .await;
18396
18397 assert_indent_guides(
18398 0..8,
18399 vec![
18400 indent_guide(buffer_id, 1, 6, 0),
18401 indent_guide(buffer_id, 3, 3, 1),
18402 indent_guide(buffer_id, 5, 5, 1),
18403 ],
18404 None,
18405 &mut cx,
18406 );
18407}
18408
18409#[gpui::test]
18410async fn test_indent_guide_tab(cx: &mut TestAppContext) {
18411 let (buffer_id, mut cx) = setup_indent_guides_editor(
18412 &"
18413 fn main() {
18414 let a = 1;
18415 let b = 2;
18416 let c = 3;
18417 }"
18418 .unindent(),
18419 cx,
18420 )
18421 .await;
18422
18423 assert_indent_guides(
18424 0..5,
18425 vec![
18426 indent_guide(buffer_id, 1, 3, 0),
18427 indent_guide(buffer_id, 2, 2, 1),
18428 ],
18429 None,
18430 &mut cx,
18431 );
18432}
18433
18434#[gpui::test]
18435async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
18436 let (buffer_id, mut cx) = setup_indent_guides_editor(
18437 &"
18438 fn main() {
18439 let a = 1;
18440
18441 let c = 3;
18442 }"
18443 .unindent(),
18444 cx,
18445 )
18446 .await;
18447
18448 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
18449}
18450
18451#[gpui::test]
18452async fn test_indent_guide_complex(cx: &mut TestAppContext) {
18453 let (buffer_id, mut cx) = setup_indent_guides_editor(
18454 &"
18455 fn main() {
18456 let a = 1;
18457
18458 let c = 3;
18459
18460 if a == 3 {
18461 let b = 2;
18462 } else {
18463 let c = 3;
18464 }
18465 }"
18466 .unindent(),
18467 cx,
18468 )
18469 .await;
18470
18471 assert_indent_guides(
18472 0..11,
18473 vec![
18474 indent_guide(buffer_id, 1, 9, 0),
18475 indent_guide(buffer_id, 6, 6, 1),
18476 indent_guide(buffer_id, 8, 8, 1),
18477 ],
18478 None,
18479 &mut cx,
18480 );
18481}
18482
18483#[gpui::test]
18484async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
18485 let (buffer_id, mut cx) = setup_indent_guides_editor(
18486 &"
18487 fn main() {
18488 let a = 1;
18489
18490 let c = 3;
18491
18492 if a == 3 {
18493 let b = 2;
18494 } else {
18495 let c = 3;
18496 }
18497 }"
18498 .unindent(),
18499 cx,
18500 )
18501 .await;
18502
18503 assert_indent_guides(
18504 1..11,
18505 vec![
18506 indent_guide(buffer_id, 1, 9, 0),
18507 indent_guide(buffer_id, 6, 6, 1),
18508 indent_guide(buffer_id, 8, 8, 1),
18509 ],
18510 None,
18511 &mut cx,
18512 );
18513}
18514
18515#[gpui::test]
18516async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
18517 let (buffer_id, mut cx) = setup_indent_guides_editor(
18518 &"
18519 fn main() {
18520 let a = 1;
18521
18522 let c = 3;
18523
18524 if a == 3 {
18525 let b = 2;
18526 } else {
18527 let c = 3;
18528 }
18529 }"
18530 .unindent(),
18531 cx,
18532 )
18533 .await;
18534
18535 assert_indent_guides(
18536 1..10,
18537 vec![
18538 indent_guide(buffer_id, 1, 9, 0),
18539 indent_guide(buffer_id, 6, 6, 1),
18540 indent_guide(buffer_id, 8, 8, 1),
18541 ],
18542 None,
18543 &mut cx,
18544 );
18545}
18546
18547#[gpui::test]
18548async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
18549 let (buffer_id, mut cx) = setup_indent_guides_editor(
18550 &"
18551 fn main() {
18552 if a {
18553 b(
18554 c,
18555 d,
18556 )
18557 } else {
18558 e(
18559 f
18560 )
18561 }
18562 }"
18563 .unindent(),
18564 cx,
18565 )
18566 .await;
18567
18568 assert_indent_guides(
18569 0..11,
18570 vec![
18571 indent_guide(buffer_id, 1, 10, 0),
18572 indent_guide(buffer_id, 2, 5, 1),
18573 indent_guide(buffer_id, 7, 9, 1),
18574 indent_guide(buffer_id, 3, 4, 2),
18575 indent_guide(buffer_id, 8, 8, 2),
18576 ],
18577 None,
18578 &mut cx,
18579 );
18580
18581 cx.update_editor(|editor, window, cx| {
18582 editor.fold_at(MultiBufferRow(2), window, cx);
18583 assert_eq!(
18584 editor.display_text(cx),
18585 "
18586 fn main() {
18587 if a {
18588 b(⋯
18589 )
18590 } else {
18591 e(
18592 f
18593 )
18594 }
18595 }"
18596 .unindent()
18597 );
18598 });
18599
18600 assert_indent_guides(
18601 0..11,
18602 vec![
18603 indent_guide(buffer_id, 1, 10, 0),
18604 indent_guide(buffer_id, 2, 5, 1),
18605 indent_guide(buffer_id, 7, 9, 1),
18606 indent_guide(buffer_id, 8, 8, 2),
18607 ],
18608 None,
18609 &mut cx,
18610 );
18611}
18612
18613#[gpui::test]
18614async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
18615 let (buffer_id, mut cx) = setup_indent_guides_editor(
18616 &"
18617 block1
18618 block2
18619 block3
18620 block4
18621 block2
18622 block1
18623 block1"
18624 .unindent(),
18625 cx,
18626 )
18627 .await;
18628
18629 assert_indent_guides(
18630 1..10,
18631 vec![
18632 indent_guide(buffer_id, 1, 4, 0),
18633 indent_guide(buffer_id, 2, 3, 1),
18634 indent_guide(buffer_id, 3, 3, 2),
18635 ],
18636 None,
18637 &mut cx,
18638 );
18639}
18640
18641#[gpui::test]
18642async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
18643 let (buffer_id, mut cx) = setup_indent_guides_editor(
18644 &"
18645 block1
18646 block2
18647 block3
18648
18649 block1
18650 block1"
18651 .unindent(),
18652 cx,
18653 )
18654 .await;
18655
18656 assert_indent_guides(
18657 0..6,
18658 vec![
18659 indent_guide(buffer_id, 1, 2, 0),
18660 indent_guide(buffer_id, 2, 2, 1),
18661 ],
18662 None,
18663 &mut cx,
18664 );
18665}
18666
18667#[gpui::test]
18668async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
18669 let (buffer_id, mut cx) = setup_indent_guides_editor(
18670 &"
18671 function component() {
18672 \treturn (
18673 \t\t\t
18674 \t\t<div>
18675 \t\t\t<abc></abc>
18676 \t\t</div>
18677 \t)
18678 }"
18679 .unindent(),
18680 cx,
18681 )
18682 .await;
18683
18684 assert_indent_guides(
18685 0..8,
18686 vec![
18687 indent_guide(buffer_id, 1, 6, 0),
18688 indent_guide(buffer_id, 2, 5, 1),
18689 indent_guide(buffer_id, 4, 4, 2),
18690 ],
18691 None,
18692 &mut cx,
18693 );
18694}
18695
18696#[gpui::test]
18697async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
18698 let (buffer_id, mut cx) = setup_indent_guides_editor(
18699 &"
18700 function component() {
18701 \treturn (
18702 \t
18703 \t\t<div>
18704 \t\t\t<abc></abc>
18705 \t\t</div>
18706 \t)
18707 }"
18708 .unindent(),
18709 cx,
18710 )
18711 .await;
18712
18713 assert_indent_guides(
18714 0..8,
18715 vec![
18716 indent_guide(buffer_id, 1, 6, 0),
18717 indent_guide(buffer_id, 2, 5, 1),
18718 indent_guide(buffer_id, 4, 4, 2),
18719 ],
18720 None,
18721 &mut cx,
18722 );
18723}
18724
18725#[gpui::test]
18726async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
18727 let (buffer_id, mut cx) = setup_indent_guides_editor(
18728 &"
18729 block1
18730
18731
18732
18733 block2
18734 "
18735 .unindent(),
18736 cx,
18737 )
18738 .await;
18739
18740 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
18741}
18742
18743#[gpui::test]
18744async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
18745 let (buffer_id, mut cx) = setup_indent_guides_editor(
18746 &"
18747 def a:
18748 \tb = 3
18749 \tif True:
18750 \t\tc = 4
18751 \t\td = 5
18752 \tprint(b)
18753 "
18754 .unindent(),
18755 cx,
18756 )
18757 .await;
18758
18759 assert_indent_guides(
18760 0..6,
18761 vec![
18762 indent_guide(buffer_id, 1, 5, 0),
18763 indent_guide(buffer_id, 3, 4, 1),
18764 ],
18765 None,
18766 &mut cx,
18767 );
18768}
18769
18770#[gpui::test]
18771async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
18772 let (buffer_id, mut cx) = setup_indent_guides_editor(
18773 &"
18774 fn main() {
18775 let a = 1;
18776 }"
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, 1, 0)],
18791 Some(vec![0]),
18792 &mut cx,
18793 );
18794}
18795
18796#[gpui::test]
18797async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
18798 let (buffer_id, mut cx) = setup_indent_guides_editor(
18799 &"
18800 fn main() {
18801 if 1 == 2 {
18802 let a = 1;
18803 }
18804 }"
18805 .unindent(),
18806 cx,
18807 )
18808 .await;
18809
18810 cx.update_editor(|editor, window, cx| {
18811 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18812 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18813 });
18814 });
18815
18816 assert_indent_guides(
18817 0..4,
18818 vec![
18819 indent_guide(buffer_id, 1, 3, 0),
18820 indent_guide(buffer_id, 2, 2, 1),
18821 ],
18822 Some(vec![1]),
18823 &mut cx,
18824 );
18825
18826 cx.update_editor(|editor, window, cx| {
18827 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18828 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
18829 });
18830 });
18831
18832 assert_indent_guides(
18833 0..4,
18834 vec![
18835 indent_guide(buffer_id, 1, 3, 0),
18836 indent_guide(buffer_id, 2, 2, 1),
18837 ],
18838 Some(vec![1]),
18839 &mut cx,
18840 );
18841
18842 cx.update_editor(|editor, window, cx| {
18843 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18844 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
18845 });
18846 });
18847
18848 assert_indent_guides(
18849 0..4,
18850 vec![
18851 indent_guide(buffer_id, 1, 3, 0),
18852 indent_guide(buffer_id, 2, 2, 1),
18853 ],
18854 Some(vec![0]),
18855 &mut cx,
18856 );
18857}
18858
18859#[gpui::test]
18860async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
18861 let (buffer_id, mut cx) = setup_indent_guides_editor(
18862 &"
18863 fn main() {
18864 let a = 1;
18865
18866 let b = 2;
18867 }"
18868 .unindent(),
18869 cx,
18870 )
18871 .await;
18872
18873 cx.update_editor(|editor, window, cx| {
18874 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18875 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
18876 });
18877 });
18878
18879 assert_indent_guides(
18880 0..5,
18881 vec![indent_guide(buffer_id, 1, 3, 0)],
18882 Some(vec![0]),
18883 &mut cx,
18884 );
18885}
18886
18887#[gpui::test]
18888async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
18889 let (buffer_id, mut cx) = setup_indent_guides_editor(
18890 &"
18891 def m:
18892 a = 1
18893 pass"
18894 .unindent(),
18895 cx,
18896 )
18897 .await;
18898
18899 cx.update_editor(|editor, window, cx| {
18900 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18901 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18902 });
18903 });
18904
18905 assert_indent_guides(
18906 0..3,
18907 vec![indent_guide(buffer_id, 1, 2, 0)],
18908 Some(vec![0]),
18909 &mut cx,
18910 );
18911}
18912
18913#[gpui::test]
18914async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
18915 init_test(cx, |_| {});
18916 let mut cx = EditorTestContext::new(cx).await;
18917 let text = indoc! {
18918 "
18919 impl A {
18920 fn b() {
18921 0;
18922 3;
18923 5;
18924 6;
18925 7;
18926 }
18927 }
18928 "
18929 };
18930 let base_text = indoc! {
18931 "
18932 impl A {
18933 fn b() {
18934 0;
18935 1;
18936 2;
18937 3;
18938 4;
18939 }
18940 fn c() {
18941 5;
18942 6;
18943 7;
18944 }
18945 }
18946 "
18947 };
18948
18949 cx.update_editor(|editor, window, cx| {
18950 editor.set_text(text, window, cx);
18951
18952 editor.buffer().update(cx, |multibuffer, cx| {
18953 let buffer = multibuffer.as_singleton().unwrap();
18954 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
18955
18956 multibuffer.set_all_diff_hunks_expanded(cx);
18957 multibuffer.add_diff(diff, cx);
18958
18959 buffer.read(cx).remote_id()
18960 })
18961 });
18962 cx.run_until_parked();
18963
18964 cx.assert_state_with_diff(
18965 indoc! { "
18966 impl A {
18967 fn b() {
18968 0;
18969 - 1;
18970 - 2;
18971 3;
18972 - 4;
18973 - }
18974 - fn c() {
18975 5;
18976 6;
18977 7;
18978 }
18979 }
18980 ˇ"
18981 }
18982 .to_string(),
18983 );
18984
18985 let mut actual_guides = cx.update_editor(|editor, window, cx| {
18986 editor
18987 .snapshot(window, cx)
18988 .buffer_snapshot
18989 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
18990 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
18991 .collect::<Vec<_>>()
18992 });
18993 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
18994 assert_eq!(
18995 actual_guides,
18996 vec![
18997 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
18998 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
18999 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
19000 ]
19001 );
19002}
19003
19004#[gpui::test]
19005async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19006 init_test(cx, |_| {});
19007 let mut cx = EditorTestContext::new(cx).await;
19008
19009 let diff_base = r#"
19010 a
19011 b
19012 c
19013 "#
19014 .unindent();
19015
19016 cx.set_state(
19017 &r#"
19018 ˇA
19019 b
19020 C
19021 "#
19022 .unindent(),
19023 );
19024 cx.set_head_text(&diff_base);
19025 cx.update_editor(|editor, window, cx| {
19026 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19027 });
19028 executor.run_until_parked();
19029
19030 let both_hunks_expanded = r#"
19031 - a
19032 + ˇA
19033 b
19034 - c
19035 + C
19036 "#
19037 .unindent();
19038
19039 cx.assert_state_with_diff(both_hunks_expanded.clone());
19040
19041 let hunk_ranges = cx.update_editor(|editor, window, cx| {
19042 let snapshot = editor.snapshot(window, cx);
19043 let hunks = editor
19044 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19045 .collect::<Vec<_>>();
19046 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
19047 let buffer_id = hunks[0].buffer_id;
19048 hunks
19049 .into_iter()
19050 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
19051 .collect::<Vec<_>>()
19052 });
19053 assert_eq!(hunk_ranges.len(), 2);
19054
19055 cx.update_editor(|editor, _, cx| {
19056 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
19057 });
19058 executor.run_until_parked();
19059
19060 let second_hunk_expanded = r#"
19061 ˇA
19062 b
19063 - c
19064 + C
19065 "#
19066 .unindent();
19067
19068 cx.assert_state_with_diff(second_hunk_expanded);
19069
19070 cx.update_editor(|editor, _, cx| {
19071 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
19072 });
19073 executor.run_until_parked();
19074
19075 cx.assert_state_with_diff(both_hunks_expanded.clone());
19076
19077 cx.update_editor(|editor, _, cx| {
19078 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
19079 });
19080 executor.run_until_parked();
19081
19082 let first_hunk_expanded = r#"
19083 - a
19084 + ˇA
19085 b
19086 C
19087 "#
19088 .unindent();
19089
19090 cx.assert_state_with_diff(first_hunk_expanded);
19091
19092 cx.update_editor(|editor, _, cx| {
19093 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
19094 });
19095 executor.run_until_parked();
19096
19097 cx.assert_state_with_diff(both_hunks_expanded);
19098
19099 cx.set_state(
19100 &r#"
19101 ˇA
19102 b
19103 "#
19104 .unindent(),
19105 );
19106 cx.run_until_parked();
19107
19108 // TODO this cursor position seems bad
19109 cx.assert_state_with_diff(
19110 r#"
19111 - ˇa
19112 + A
19113 b
19114 "#
19115 .unindent(),
19116 );
19117
19118 cx.update_editor(|editor, window, cx| {
19119 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19120 });
19121
19122 cx.assert_state_with_diff(
19123 r#"
19124 - ˇa
19125 + A
19126 b
19127 - c
19128 "#
19129 .unindent(),
19130 );
19131
19132 let hunk_ranges = cx.update_editor(|editor, window, cx| {
19133 let snapshot = editor.snapshot(window, cx);
19134 let hunks = editor
19135 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19136 .collect::<Vec<_>>();
19137 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
19138 let buffer_id = hunks[0].buffer_id;
19139 hunks
19140 .into_iter()
19141 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
19142 .collect::<Vec<_>>()
19143 });
19144 assert_eq!(hunk_ranges.len(), 2);
19145
19146 cx.update_editor(|editor, _, cx| {
19147 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
19148 });
19149 executor.run_until_parked();
19150
19151 cx.assert_state_with_diff(
19152 r#"
19153 - ˇa
19154 + A
19155 b
19156 "#
19157 .unindent(),
19158 );
19159}
19160
19161#[gpui::test]
19162async fn test_toggle_deletion_hunk_at_start_of_file(
19163 executor: BackgroundExecutor,
19164 cx: &mut TestAppContext,
19165) {
19166 init_test(cx, |_| {});
19167 let mut cx = EditorTestContext::new(cx).await;
19168
19169 let diff_base = r#"
19170 a
19171 b
19172 c
19173 "#
19174 .unindent();
19175
19176 cx.set_state(
19177 &r#"
19178 ˇb
19179 c
19180 "#
19181 .unindent(),
19182 );
19183 cx.set_head_text(&diff_base);
19184 cx.update_editor(|editor, window, cx| {
19185 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19186 });
19187 executor.run_until_parked();
19188
19189 let hunk_expanded = r#"
19190 - a
19191 ˇb
19192 c
19193 "#
19194 .unindent();
19195
19196 cx.assert_state_with_diff(hunk_expanded.clone());
19197
19198 let hunk_ranges = cx.update_editor(|editor, window, cx| {
19199 let snapshot = editor.snapshot(window, cx);
19200 let hunks = editor
19201 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19202 .collect::<Vec<_>>();
19203 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
19204 let buffer_id = hunks[0].buffer_id;
19205 hunks
19206 .into_iter()
19207 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
19208 .collect::<Vec<_>>()
19209 });
19210 assert_eq!(hunk_ranges.len(), 1);
19211
19212 cx.update_editor(|editor, _, cx| {
19213 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
19214 });
19215 executor.run_until_parked();
19216
19217 let hunk_collapsed = r#"
19218 ˇb
19219 c
19220 "#
19221 .unindent();
19222
19223 cx.assert_state_with_diff(hunk_collapsed);
19224
19225 cx.update_editor(|editor, _, cx| {
19226 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
19227 });
19228 executor.run_until_parked();
19229
19230 cx.assert_state_with_diff(hunk_expanded.clone());
19231}
19232
19233#[gpui::test]
19234async fn test_display_diff_hunks(cx: &mut TestAppContext) {
19235 init_test(cx, |_| {});
19236
19237 let fs = FakeFs::new(cx.executor());
19238 fs.insert_tree(
19239 path!("/test"),
19240 json!({
19241 ".git": {},
19242 "file-1": "ONE\n",
19243 "file-2": "TWO\n",
19244 "file-3": "THREE\n",
19245 }),
19246 )
19247 .await;
19248
19249 fs.set_head_for_repo(
19250 path!("/test/.git").as_ref(),
19251 &[
19252 ("file-1".into(), "one\n".into()),
19253 ("file-2".into(), "two\n".into()),
19254 ("file-3".into(), "three\n".into()),
19255 ],
19256 "deadbeef",
19257 );
19258
19259 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
19260 let mut buffers = vec![];
19261 for i in 1..=3 {
19262 let buffer = project
19263 .update(cx, |project, cx| {
19264 let path = format!(path!("/test/file-{}"), i);
19265 project.open_local_buffer(path, cx)
19266 })
19267 .await
19268 .unwrap();
19269 buffers.push(buffer);
19270 }
19271
19272 let multibuffer = cx.new(|cx| {
19273 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
19274 multibuffer.set_all_diff_hunks_expanded(cx);
19275 for buffer in &buffers {
19276 let snapshot = buffer.read(cx).snapshot();
19277 multibuffer.set_excerpts_for_path(
19278 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
19279 buffer.clone(),
19280 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
19281 DEFAULT_MULTIBUFFER_CONTEXT,
19282 cx,
19283 );
19284 }
19285 multibuffer
19286 });
19287
19288 let editor = cx.add_window(|window, cx| {
19289 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
19290 });
19291 cx.run_until_parked();
19292
19293 let snapshot = editor
19294 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19295 .unwrap();
19296 let hunks = snapshot
19297 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
19298 .map(|hunk| match hunk {
19299 DisplayDiffHunk::Unfolded {
19300 display_row_range, ..
19301 } => display_row_range,
19302 DisplayDiffHunk::Folded { .. } => unreachable!(),
19303 })
19304 .collect::<Vec<_>>();
19305 assert_eq!(
19306 hunks,
19307 [
19308 DisplayRow(2)..DisplayRow(4),
19309 DisplayRow(7)..DisplayRow(9),
19310 DisplayRow(12)..DisplayRow(14),
19311 ]
19312 );
19313}
19314
19315#[gpui::test]
19316async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
19317 init_test(cx, |_| {});
19318
19319 let mut cx = EditorTestContext::new(cx).await;
19320 cx.set_head_text(indoc! { "
19321 one
19322 two
19323 three
19324 four
19325 five
19326 "
19327 });
19328 cx.set_index_text(indoc! { "
19329 one
19330 two
19331 three
19332 four
19333 five
19334 "
19335 });
19336 cx.set_state(indoc! {"
19337 one
19338 TWO
19339 ˇTHREE
19340 FOUR
19341 five
19342 "});
19343 cx.run_until_parked();
19344 cx.update_editor(|editor, window, cx| {
19345 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19346 });
19347 cx.run_until_parked();
19348 cx.assert_index_text(Some(indoc! {"
19349 one
19350 TWO
19351 THREE
19352 FOUR
19353 five
19354 "}));
19355 cx.set_state(indoc! { "
19356 one
19357 TWO
19358 ˇTHREE-HUNDRED
19359 FOUR
19360 five
19361 "});
19362 cx.run_until_parked();
19363 cx.update_editor(|editor, window, cx| {
19364 let snapshot = editor.snapshot(window, cx);
19365 let hunks = editor
19366 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19367 .collect::<Vec<_>>();
19368 assert_eq!(hunks.len(), 1);
19369 assert_eq!(
19370 hunks[0].status(),
19371 DiffHunkStatus {
19372 kind: DiffHunkStatusKind::Modified,
19373 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
19374 }
19375 );
19376
19377 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19378 });
19379 cx.run_until_parked();
19380 cx.assert_index_text(Some(indoc! {"
19381 one
19382 TWO
19383 THREE-HUNDRED
19384 FOUR
19385 five
19386 "}));
19387}
19388
19389#[gpui::test]
19390fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
19391 init_test(cx, |_| {});
19392
19393 let editor = cx.add_window(|window, cx| {
19394 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
19395 build_editor(buffer, window, cx)
19396 });
19397
19398 let render_args = Arc::new(Mutex::new(None));
19399 let snapshot = editor
19400 .update(cx, |editor, window, cx| {
19401 let snapshot = editor.buffer().read(cx).snapshot(cx);
19402 let range =
19403 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
19404
19405 struct RenderArgs {
19406 row: MultiBufferRow,
19407 folded: bool,
19408 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
19409 }
19410
19411 let crease = Crease::inline(
19412 range,
19413 FoldPlaceholder::test(),
19414 {
19415 let toggle_callback = render_args.clone();
19416 move |row, folded, callback, _window, _cx| {
19417 *toggle_callback.lock() = Some(RenderArgs {
19418 row,
19419 folded,
19420 callback,
19421 });
19422 div()
19423 }
19424 },
19425 |_row, _folded, _window, _cx| div(),
19426 );
19427
19428 editor.insert_creases(Some(crease), cx);
19429 let snapshot = editor.snapshot(window, cx);
19430 let _div = snapshot.render_crease_toggle(
19431 MultiBufferRow(1),
19432 false,
19433 cx.entity().clone(),
19434 window,
19435 cx,
19436 );
19437 snapshot
19438 })
19439 .unwrap();
19440
19441 let render_args = render_args.lock().take().unwrap();
19442 assert_eq!(render_args.row, MultiBufferRow(1));
19443 assert!(!render_args.folded);
19444 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
19445
19446 cx.update_window(*editor, |_, window, cx| {
19447 (render_args.callback)(true, window, cx)
19448 })
19449 .unwrap();
19450 let snapshot = editor
19451 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19452 .unwrap();
19453 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
19454
19455 cx.update_window(*editor, |_, window, cx| {
19456 (render_args.callback)(false, window, cx)
19457 })
19458 .unwrap();
19459 let snapshot = editor
19460 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19461 .unwrap();
19462 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
19463}
19464
19465#[gpui::test]
19466async fn test_input_text(cx: &mut TestAppContext) {
19467 init_test(cx, |_| {});
19468 let mut cx = EditorTestContext::new(cx).await;
19469
19470 cx.set_state(
19471 &r#"ˇone
19472 two
19473
19474 three
19475 fourˇ
19476 five
19477
19478 siˇx"#
19479 .unindent(),
19480 );
19481
19482 cx.dispatch_action(HandleInput(String::new()));
19483 cx.assert_editor_state(
19484 &r#"ˇone
19485 two
19486
19487 three
19488 fourˇ
19489 five
19490
19491 siˇx"#
19492 .unindent(),
19493 );
19494
19495 cx.dispatch_action(HandleInput("AAAA".to_string()));
19496 cx.assert_editor_state(
19497 &r#"AAAAˇone
19498 two
19499
19500 three
19501 fourAAAAˇ
19502 five
19503
19504 siAAAAˇx"#
19505 .unindent(),
19506 );
19507}
19508
19509#[gpui::test]
19510async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
19511 init_test(cx, |_| {});
19512
19513 let mut cx = EditorTestContext::new(cx).await;
19514 cx.set_state(
19515 r#"let foo = 1;
19516let foo = 2;
19517let foo = 3;
19518let fooˇ = 4;
19519let foo = 5;
19520let foo = 6;
19521let foo = 7;
19522let foo = 8;
19523let foo = 9;
19524let foo = 10;
19525let foo = 11;
19526let foo = 12;
19527let foo = 13;
19528let foo = 14;
19529let foo = 15;"#,
19530 );
19531
19532 cx.update_editor(|e, window, cx| {
19533 assert_eq!(
19534 e.next_scroll_position,
19535 NextScrollCursorCenterTopBottom::Center,
19536 "Default next scroll direction is center",
19537 );
19538
19539 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19540 assert_eq!(
19541 e.next_scroll_position,
19542 NextScrollCursorCenterTopBottom::Top,
19543 "After center, next scroll direction should be top",
19544 );
19545
19546 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19547 assert_eq!(
19548 e.next_scroll_position,
19549 NextScrollCursorCenterTopBottom::Bottom,
19550 "After top, next scroll direction should be bottom",
19551 );
19552
19553 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19554 assert_eq!(
19555 e.next_scroll_position,
19556 NextScrollCursorCenterTopBottom::Center,
19557 "After bottom, scrolling should start over",
19558 );
19559
19560 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19561 assert_eq!(
19562 e.next_scroll_position,
19563 NextScrollCursorCenterTopBottom::Top,
19564 "Scrolling continues if retriggered fast enough"
19565 );
19566 });
19567
19568 cx.executor()
19569 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
19570 cx.executor().run_until_parked();
19571 cx.update_editor(|e, _, _| {
19572 assert_eq!(
19573 e.next_scroll_position,
19574 NextScrollCursorCenterTopBottom::Center,
19575 "If scrolling is not triggered fast enough, it should reset"
19576 );
19577 });
19578}
19579
19580#[gpui::test]
19581async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
19582 init_test(cx, |_| {});
19583 let mut cx = EditorLspTestContext::new_rust(
19584 lsp::ServerCapabilities {
19585 definition_provider: Some(lsp::OneOf::Left(true)),
19586 references_provider: Some(lsp::OneOf::Left(true)),
19587 ..lsp::ServerCapabilities::default()
19588 },
19589 cx,
19590 )
19591 .await;
19592
19593 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
19594 let go_to_definition = cx
19595 .lsp
19596 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
19597 move |params, _| async move {
19598 if empty_go_to_definition {
19599 Ok(None)
19600 } else {
19601 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
19602 uri: params.text_document_position_params.text_document.uri,
19603 range: lsp::Range::new(
19604 lsp::Position::new(4, 3),
19605 lsp::Position::new(4, 6),
19606 ),
19607 })))
19608 }
19609 },
19610 );
19611 let references = cx
19612 .lsp
19613 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
19614 Ok(Some(vec![lsp::Location {
19615 uri: params.text_document_position.text_document.uri,
19616 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
19617 }]))
19618 });
19619 (go_to_definition, references)
19620 };
19621
19622 cx.set_state(
19623 &r#"fn one() {
19624 let mut a = ˇtwo();
19625 }
19626
19627 fn two() {}"#
19628 .unindent(),
19629 );
19630 set_up_lsp_handlers(false, &mut cx);
19631 let navigated = cx
19632 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19633 .await
19634 .expect("Failed to navigate to definition");
19635 assert_eq!(
19636 navigated,
19637 Navigated::Yes,
19638 "Should have navigated to definition from the GetDefinition response"
19639 );
19640 cx.assert_editor_state(
19641 &r#"fn one() {
19642 let mut a = two();
19643 }
19644
19645 fn «twoˇ»() {}"#
19646 .unindent(),
19647 );
19648
19649 let editors = cx.update_workspace(|workspace, _, cx| {
19650 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19651 });
19652 cx.update_editor(|_, _, test_editor_cx| {
19653 assert_eq!(
19654 editors.len(),
19655 1,
19656 "Initially, only one, test, editor should be open in the workspace"
19657 );
19658 assert_eq!(
19659 test_editor_cx.entity(),
19660 editors.last().expect("Asserted len is 1").clone()
19661 );
19662 });
19663
19664 set_up_lsp_handlers(true, &mut cx);
19665 let navigated = cx
19666 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19667 .await
19668 .expect("Failed to navigate to lookup references");
19669 assert_eq!(
19670 navigated,
19671 Navigated::Yes,
19672 "Should have navigated to references as a fallback after empty GoToDefinition response"
19673 );
19674 // We should not change the selections in the existing file,
19675 // if opening another milti buffer with the references
19676 cx.assert_editor_state(
19677 &r#"fn one() {
19678 let mut a = two();
19679 }
19680
19681 fn «twoˇ»() {}"#
19682 .unindent(),
19683 );
19684 let editors = cx.update_workspace(|workspace, _, cx| {
19685 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19686 });
19687 cx.update_editor(|_, _, test_editor_cx| {
19688 assert_eq!(
19689 editors.len(),
19690 2,
19691 "After falling back to references search, we open a new editor with the results"
19692 );
19693 let references_fallback_text = editors
19694 .into_iter()
19695 .find(|new_editor| *new_editor != test_editor_cx.entity())
19696 .expect("Should have one non-test editor now")
19697 .read(test_editor_cx)
19698 .text(test_editor_cx);
19699 assert_eq!(
19700 references_fallback_text, "fn one() {\n let mut a = two();\n}",
19701 "Should use the range from the references response and not the GoToDefinition one"
19702 );
19703 });
19704}
19705
19706#[gpui::test]
19707async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
19708 init_test(cx, |_| {});
19709 cx.update(|cx| {
19710 let mut editor_settings = EditorSettings::get_global(cx).clone();
19711 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
19712 EditorSettings::override_global(editor_settings, cx);
19713 });
19714 let mut cx = EditorLspTestContext::new_rust(
19715 lsp::ServerCapabilities {
19716 definition_provider: Some(lsp::OneOf::Left(true)),
19717 references_provider: Some(lsp::OneOf::Left(true)),
19718 ..lsp::ServerCapabilities::default()
19719 },
19720 cx,
19721 )
19722 .await;
19723 let original_state = r#"fn one() {
19724 let mut a = ˇtwo();
19725 }
19726
19727 fn two() {}"#
19728 .unindent();
19729 cx.set_state(&original_state);
19730
19731 let mut go_to_definition = cx
19732 .lsp
19733 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
19734 move |_, _| async move { Ok(None) },
19735 );
19736 let _references = cx
19737 .lsp
19738 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
19739 panic!("Should not call for references with no go to definition fallback")
19740 });
19741
19742 let navigated = cx
19743 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19744 .await
19745 .expect("Failed to navigate to lookup references");
19746 go_to_definition
19747 .next()
19748 .await
19749 .expect("Should have called the go_to_definition handler");
19750
19751 assert_eq!(
19752 navigated,
19753 Navigated::No,
19754 "Should have navigated to references as a fallback after empty GoToDefinition response"
19755 );
19756 cx.assert_editor_state(&original_state);
19757 let editors = cx.update_workspace(|workspace, _, cx| {
19758 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19759 });
19760 cx.update_editor(|_, _, _| {
19761 assert_eq!(
19762 editors.len(),
19763 1,
19764 "After unsuccessful fallback, no other editor should have been opened"
19765 );
19766 });
19767}
19768
19769#[gpui::test]
19770async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
19771 init_test(cx, |_| {});
19772
19773 let language = Arc::new(Language::new(
19774 LanguageConfig::default(),
19775 Some(tree_sitter_rust::LANGUAGE.into()),
19776 ));
19777
19778 let text = r#"
19779 #[cfg(test)]
19780 mod tests() {
19781 #[test]
19782 fn runnable_1() {
19783 let a = 1;
19784 }
19785
19786 #[test]
19787 fn runnable_2() {
19788 let a = 1;
19789 let b = 2;
19790 }
19791 }
19792 "#
19793 .unindent();
19794
19795 let fs = FakeFs::new(cx.executor());
19796 fs.insert_file("/file.rs", Default::default()).await;
19797
19798 let project = Project::test(fs, ["/a".as_ref()], cx).await;
19799 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19800 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19801 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
19802 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
19803
19804 let editor = cx.new_window_entity(|window, cx| {
19805 Editor::new(
19806 EditorMode::full(),
19807 multi_buffer,
19808 Some(project.clone()),
19809 window,
19810 cx,
19811 )
19812 });
19813
19814 editor.update_in(cx, |editor, window, cx| {
19815 let snapshot = editor.buffer().read(cx).snapshot(cx);
19816 editor.tasks.insert(
19817 (buffer.read(cx).remote_id(), 3),
19818 RunnableTasks {
19819 templates: vec![],
19820 offset: snapshot.anchor_before(43),
19821 column: 0,
19822 extra_variables: HashMap::default(),
19823 context_range: BufferOffset(43)..BufferOffset(85),
19824 },
19825 );
19826 editor.tasks.insert(
19827 (buffer.read(cx).remote_id(), 8),
19828 RunnableTasks {
19829 templates: vec![],
19830 offset: snapshot.anchor_before(86),
19831 column: 0,
19832 extra_variables: HashMap::default(),
19833 context_range: BufferOffset(86)..BufferOffset(191),
19834 },
19835 );
19836
19837 // Test finding task when cursor is inside function body
19838 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19839 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
19840 });
19841 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
19842 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
19843
19844 // Test finding task when cursor is on function name
19845 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19846 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
19847 });
19848 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
19849 assert_eq!(row, 8, "Should find task when cursor is on function name");
19850 });
19851}
19852
19853#[gpui::test]
19854async fn test_folding_buffers(cx: &mut TestAppContext) {
19855 init_test(cx, |_| {});
19856
19857 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
19858 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
19859 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
19860
19861 let fs = FakeFs::new(cx.executor());
19862 fs.insert_tree(
19863 path!("/a"),
19864 json!({
19865 "first.rs": sample_text_1,
19866 "second.rs": sample_text_2,
19867 "third.rs": sample_text_3,
19868 }),
19869 )
19870 .await;
19871 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19872 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19873 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19874 let worktree = project.update(cx, |project, cx| {
19875 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
19876 assert_eq!(worktrees.len(), 1);
19877 worktrees.pop().unwrap()
19878 });
19879 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
19880
19881 let buffer_1 = project
19882 .update(cx, |project, cx| {
19883 project.open_buffer((worktree_id, "first.rs"), cx)
19884 })
19885 .await
19886 .unwrap();
19887 let buffer_2 = project
19888 .update(cx, |project, cx| {
19889 project.open_buffer((worktree_id, "second.rs"), cx)
19890 })
19891 .await
19892 .unwrap();
19893 let buffer_3 = project
19894 .update(cx, |project, cx| {
19895 project.open_buffer((worktree_id, "third.rs"), cx)
19896 })
19897 .await
19898 .unwrap();
19899
19900 let multi_buffer = cx.new(|cx| {
19901 let mut multi_buffer = MultiBuffer::new(ReadWrite);
19902 multi_buffer.push_excerpts(
19903 buffer_1.clone(),
19904 [
19905 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19906 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19907 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19908 ],
19909 cx,
19910 );
19911 multi_buffer.push_excerpts(
19912 buffer_2.clone(),
19913 [
19914 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19915 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19916 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19917 ],
19918 cx,
19919 );
19920 multi_buffer.push_excerpts(
19921 buffer_3.clone(),
19922 [
19923 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19924 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19925 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19926 ],
19927 cx,
19928 );
19929 multi_buffer
19930 });
19931 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19932 Editor::new(
19933 EditorMode::full(),
19934 multi_buffer.clone(),
19935 Some(project.clone()),
19936 window,
19937 cx,
19938 )
19939 });
19940
19941 assert_eq!(
19942 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19943 "\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",
19944 );
19945
19946 multi_buffer_editor.update(cx, |editor, cx| {
19947 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
19948 });
19949 assert_eq!(
19950 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19951 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
19952 "After folding the first buffer, its text should not be displayed"
19953 );
19954
19955 multi_buffer_editor.update(cx, |editor, cx| {
19956 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
19957 });
19958 assert_eq!(
19959 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19960 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
19961 "After folding the second buffer, its text should not be displayed"
19962 );
19963
19964 multi_buffer_editor.update(cx, |editor, cx| {
19965 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
19966 });
19967 assert_eq!(
19968 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19969 "\n\n\n\n\n",
19970 "After folding the third buffer, its text should not be displayed"
19971 );
19972
19973 // Emulate selection inside the fold logic, that should work
19974 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19975 editor
19976 .snapshot(window, cx)
19977 .next_line_boundary(Point::new(0, 4));
19978 });
19979
19980 multi_buffer_editor.update(cx, |editor, cx| {
19981 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
19982 });
19983 assert_eq!(
19984 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19985 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
19986 "After unfolding the second buffer, its text should be displayed"
19987 );
19988
19989 // Typing inside of buffer 1 causes that buffer to be unfolded.
19990 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19991 assert_eq!(
19992 multi_buffer
19993 .read(cx)
19994 .snapshot(cx)
19995 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
19996 .collect::<String>(),
19997 "bbbb"
19998 );
19999 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
20000 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
20001 });
20002 editor.handle_input("B", window, cx);
20003 });
20004
20005 assert_eq!(
20006 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20007 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
20008 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
20009 );
20010
20011 multi_buffer_editor.update(cx, |editor, cx| {
20012 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
20013 });
20014 assert_eq!(
20015 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20016 "\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",
20017 "After unfolding the all buffers, all original text should be displayed"
20018 );
20019}
20020
20021#[gpui::test]
20022async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
20023 init_test(cx, |_| {});
20024
20025 let sample_text_1 = "1111\n2222\n3333".to_string();
20026 let sample_text_2 = "4444\n5555\n6666".to_string();
20027 let sample_text_3 = "7777\n8888\n9999".to_string();
20028
20029 let fs = FakeFs::new(cx.executor());
20030 fs.insert_tree(
20031 path!("/a"),
20032 json!({
20033 "first.rs": sample_text_1,
20034 "second.rs": sample_text_2,
20035 "third.rs": sample_text_3,
20036 }),
20037 )
20038 .await;
20039 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20040 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20041 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20042 let worktree = project.update(cx, |project, cx| {
20043 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
20044 assert_eq!(worktrees.len(), 1);
20045 worktrees.pop().unwrap()
20046 });
20047 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
20048
20049 let buffer_1 = project
20050 .update(cx, |project, cx| {
20051 project.open_buffer((worktree_id, "first.rs"), cx)
20052 })
20053 .await
20054 .unwrap();
20055 let buffer_2 = project
20056 .update(cx, |project, cx| {
20057 project.open_buffer((worktree_id, "second.rs"), cx)
20058 })
20059 .await
20060 .unwrap();
20061 let buffer_3 = project
20062 .update(cx, |project, cx| {
20063 project.open_buffer((worktree_id, "third.rs"), cx)
20064 })
20065 .await
20066 .unwrap();
20067
20068 let multi_buffer = cx.new(|cx| {
20069 let mut multi_buffer = MultiBuffer::new(ReadWrite);
20070 multi_buffer.push_excerpts(
20071 buffer_1.clone(),
20072 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
20073 cx,
20074 );
20075 multi_buffer.push_excerpts(
20076 buffer_2.clone(),
20077 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
20078 cx,
20079 );
20080 multi_buffer.push_excerpts(
20081 buffer_3.clone(),
20082 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
20083 cx,
20084 );
20085 multi_buffer
20086 });
20087
20088 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
20089 Editor::new(
20090 EditorMode::full(),
20091 multi_buffer,
20092 Some(project.clone()),
20093 window,
20094 cx,
20095 )
20096 });
20097
20098 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
20099 assert_eq!(
20100 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20101 full_text,
20102 );
20103
20104 multi_buffer_editor.update(cx, |editor, cx| {
20105 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
20106 });
20107 assert_eq!(
20108 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20109 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
20110 "After folding the first buffer, its text should not be displayed"
20111 );
20112
20113 multi_buffer_editor.update(cx, |editor, cx| {
20114 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
20115 });
20116
20117 assert_eq!(
20118 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20119 "\n\n\n\n\n\n7777\n8888\n9999",
20120 "After folding the second buffer, its text should not be displayed"
20121 );
20122
20123 multi_buffer_editor.update(cx, |editor, cx| {
20124 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
20125 });
20126 assert_eq!(
20127 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20128 "\n\n\n\n\n",
20129 "After folding the third buffer, its text should not be displayed"
20130 );
20131
20132 multi_buffer_editor.update(cx, |editor, cx| {
20133 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
20134 });
20135 assert_eq!(
20136 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20137 "\n\n\n\n4444\n5555\n6666\n\n",
20138 "After unfolding the second buffer, its text should be displayed"
20139 );
20140
20141 multi_buffer_editor.update(cx, |editor, cx| {
20142 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
20143 });
20144 assert_eq!(
20145 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20146 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
20147 "After unfolding the first buffer, its text should be displayed"
20148 );
20149
20150 multi_buffer_editor.update(cx, |editor, cx| {
20151 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
20152 });
20153 assert_eq!(
20154 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20155 full_text,
20156 "After unfolding all buffers, all original text should be displayed"
20157 );
20158}
20159
20160#[gpui::test]
20161async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
20162 init_test(cx, |_| {});
20163
20164 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
20165
20166 let fs = FakeFs::new(cx.executor());
20167 fs.insert_tree(
20168 path!("/a"),
20169 json!({
20170 "main.rs": sample_text,
20171 }),
20172 )
20173 .await;
20174 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20175 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20176 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20177 let worktree = project.update(cx, |project, cx| {
20178 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
20179 assert_eq!(worktrees.len(), 1);
20180 worktrees.pop().unwrap()
20181 });
20182 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
20183
20184 let buffer_1 = project
20185 .update(cx, |project, cx| {
20186 project.open_buffer((worktree_id, "main.rs"), cx)
20187 })
20188 .await
20189 .unwrap();
20190
20191 let multi_buffer = cx.new(|cx| {
20192 let mut multi_buffer = MultiBuffer::new(ReadWrite);
20193 multi_buffer.push_excerpts(
20194 buffer_1.clone(),
20195 [ExcerptRange::new(
20196 Point::new(0, 0)
20197 ..Point::new(
20198 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
20199 0,
20200 ),
20201 )],
20202 cx,
20203 );
20204 multi_buffer
20205 });
20206 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
20207 Editor::new(
20208 EditorMode::full(),
20209 multi_buffer,
20210 Some(project.clone()),
20211 window,
20212 cx,
20213 )
20214 });
20215
20216 let selection_range = Point::new(1, 0)..Point::new(2, 0);
20217 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20218 enum TestHighlight {}
20219 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
20220 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
20221 editor.highlight_text::<TestHighlight>(
20222 vec![highlight_range.clone()],
20223 HighlightStyle::color(Hsla::green()),
20224 cx,
20225 );
20226 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20227 s.select_ranges(Some(highlight_range))
20228 });
20229 });
20230
20231 let full_text = format!("\n\n{sample_text}");
20232 assert_eq!(
20233 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20234 full_text,
20235 );
20236}
20237
20238#[gpui::test]
20239async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
20240 init_test(cx, |_| {});
20241 cx.update(|cx| {
20242 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
20243 "keymaps/default-linux.json",
20244 cx,
20245 )
20246 .unwrap();
20247 cx.bind_keys(default_key_bindings);
20248 });
20249
20250 let (editor, cx) = cx.add_window_view(|window, cx| {
20251 let multi_buffer = MultiBuffer::build_multi(
20252 [
20253 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
20254 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
20255 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
20256 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
20257 ],
20258 cx,
20259 );
20260 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
20261
20262 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
20263 // fold all but the second buffer, so that we test navigating between two
20264 // adjacent folded buffers, as well as folded buffers at the start and
20265 // end the multibuffer
20266 editor.fold_buffer(buffer_ids[0], cx);
20267 editor.fold_buffer(buffer_ids[2], cx);
20268 editor.fold_buffer(buffer_ids[3], cx);
20269
20270 editor
20271 });
20272 cx.simulate_resize(size(px(1000.), px(1000.)));
20273
20274 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
20275 cx.assert_excerpts_with_selections(indoc! {"
20276 [EXCERPT]
20277 ˇ[FOLDED]
20278 [EXCERPT]
20279 a1
20280 b1
20281 [EXCERPT]
20282 [FOLDED]
20283 [EXCERPT]
20284 [FOLDED]
20285 "
20286 });
20287 cx.simulate_keystroke("down");
20288 cx.assert_excerpts_with_selections(indoc! {"
20289 [EXCERPT]
20290 [FOLDED]
20291 [EXCERPT]
20292 ˇa1
20293 b1
20294 [EXCERPT]
20295 [FOLDED]
20296 [EXCERPT]
20297 [FOLDED]
20298 "
20299 });
20300 cx.simulate_keystroke("down");
20301 cx.assert_excerpts_with_selections(indoc! {"
20302 [EXCERPT]
20303 [FOLDED]
20304 [EXCERPT]
20305 a1
20306 ˇb1
20307 [EXCERPT]
20308 [FOLDED]
20309 [EXCERPT]
20310 [FOLDED]
20311 "
20312 });
20313 cx.simulate_keystroke("down");
20314 cx.assert_excerpts_with_selections(indoc! {"
20315 [EXCERPT]
20316 [FOLDED]
20317 [EXCERPT]
20318 a1
20319 b1
20320 ˇ[EXCERPT]
20321 [FOLDED]
20322 [EXCERPT]
20323 [FOLDED]
20324 "
20325 });
20326 cx.simulate_keystroke("down");
20327 cx.assert_excerpts_with_selections(indoc! {"
20328 [EXCERPT]
20329 [FOLDED]
20330 [EXCERPT]
20331 a1
20332 b1
20333 [EXCERPT]
20334 ˇ[FOLDED]
20335 [EXCERPT]
20336 [FOLDED]
20337 "
20338 });
20339 for _ in 0..5 {
20340 cx.simulate_keystroke("down");
20341 cx.assert_excerpts_with_selections(indoc! {"
20342 [EXCERPT]
20343 [FOLDED]
20344 [EXCERPT]
20345 a1
20346 b1
20347 [EXCERPT]
20348 [FOLDED]
20349 [EXCERPT]
20350 ˇ[FOLDED]
20351 "
20352 });
20353 }
20354
20355 cx.simulate_keystroke("up");
20356 cx.assert_excerpts_with_selections(indoc! {"
20357 [EXCERPT]
20358 [FOLDED]
20359 [EXCERPT]
20360 a1
20361 b1
20362 [EXCERPT]
20363 ˇ[FOLDED]
20364 [EXCERPT]
20365 [FOLDED]
20366 "
20367 });
20368 cx.simulate_keystroke("up");
20369 cx.assert_excerpts_with_selections(indoc! {"
20370 [EXCERPT]
20371 [FOLDED]
20372 [EXCERPT]
20373 a1
20374 b1
20375 ˇ[EXCERPT]
20376 [FOLDED]
20377 [EXCERPT]
20378 [FOLDED]
20379 "
20380 });
20381 cx.simulate_keystroke("up");
20382 cx.assert_excerpts_with_selections(indoc! {"
20383 [EXCERPT]
20384 [FOLDED]
20385 [EXCERPT]
20386 a1
20387 ˇb1
20388 [EXCERPT]
20389 [FOLDED]
20390 [EXCERPT]
20391 [FOLDED]
20392 "
20393 });
20394 cx.simulate_keystroke("up");
20395 cx.assert_excerpts_with_selections(indoc! {"
20396 [EXCERPT]
20397 [FOLDED]
20398 [EXCERPT]
20399 ˇa1
20400 b1
20401 [EXCERPT]
20402 [FOLDED]
20403 [EXCERPT]
20404 [FOLDED]
20405 "
20406 });
20407 for _ in 0..5 {
20408 cx.simulate_keystroke("up");
20409 cx.assert_excerpts_with_selections(indoc! {"
20410 [EXCERPT]
20411 ˇ[FOLDED]
20412 [EXCERPT]
20413 a1
20414 b1
20415 [EXCERPT]
20416 [FOLDED]
20417 [EXCERPT]
20418 [FOLDED]
20419 "
20420 });
20421 }
20422}
20423
20424#[gpui::test]
20425async fn test_inline_completion_text(cx: &mut TestAppContext) {
20426 init_test(cx, |_| {});
20427
20428 // Simple insertion
20429 assert_highlighted_edits(
20430 "Hello, world!",
20431 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
20432 true,
20433 cx,
20434 |highlighted_edits, cx| {
20435 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
20436 assert_eq!(highlighted_edits.highlights.len(), 1);
20437 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
20438 assert_eq!(
20439 highlighted_edits.highlights[0].1.background_color,
20440 Some(cx.theme().status().created_background)
20441 );
20442 },
20443 )
20444 .await;
20445
20446 // Replacement
20447 assert_highlighted_edits(
20448 "This is a test.",
20449 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
20450 false,
20451 cx,
20452 |highlighted_edits, cx| {
20453 assert_eq!(highlighted_edits.text, "That is a test.");
20454 assert_eq!(highlighted_edits.highlights.len(), 1);
20455 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
20456 assert_eq!(
20457 highlighted_edits.highlights[0].1.background_color,
20458 Some(cx.theme().status().created_background)
20459 );
20460 },
20461 )
20462 .await;
20463
20464 // Multiple edits
20465 assert_highlighted_edits(
20466 "Hello, world!",
20467 vec![
20468 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
20469 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
20470 ],
20471 false,
20472 cx,
20473 |highlighted_edits, cx| {
20474 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
20475 assert_eq!(highlighted_edits.highlights.len(), 2);
20476 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
20477 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
20478 assert_eq!(
20479 highlighted_edits.highlights[0].1.background_color,
20480 Some(cx.theme().status().created_background)
20481 );
20482 assert_eq!(
20483 highlighted_edits.highlights[1].1.background_color,
20484 Some(cx.theme().status().created_background)
20485 );
20486 },
20487 )
20488 .await;
20489
20490 // Multiple lines with edits
20491 assert_highlighted_edits(
20492 "First line\nSecond line\nThird line\nFourth line",
20493 vec![
20494 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
20495 (
20496 Point::new(2, 0)..Point::new(2, 10),
20497 "New third line".to_string(),
20498 ),
20499 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
20500 ],
20501 false,
20502 cx,
20503 |highlighted_edits, cx| {
20504 assert_eq!(
20505 highlighted_edits.text,
20506 "Second modified\nNew third line\nFourth updated line"
20507 );
20508 assert_eq!(highlighted_edits.highlights.len(), 3);
20509 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
20510 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
20511 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
20512 for highlight in &highlighted_edits.highlights {
20513 assert_eq!(
20514 highlight.1.background_color,
20515 Some(cx.theme().status().created_background)
20516 );
20517 }
20518 },
20519 )
20520 .await;
20521}
20522
20523#[gpui::test]
20524async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
20525 init_test(cx, |_| {});
20526
20527 // Deletion
20528 assert_highlighted_edits(
20529 "Hello, world!",
20530 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
20531 true,
20532 cx,
20533 |highlighted_edits, cx| {
20534 assert_eq!(highlighted_edits.text, "Hello, world!");
20535 assert_eq!(highlighted_edits.highlights.len(), 1);
20536 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
20537 assert_eq!(
20538 highlighted_edits.highlights[0].1.background_color,
20539 Some(cx.theme().status().deleted_background)
20540 );
20541 },
20542 )
20543 .await;
20544
20545 // Insertion
20546 assert_highlighted_edits(
20547 "Hello, world!",
20548 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
20549 true,
20550 cx,
20551 |highlighted_edits, cx| {
20552 assert_eq!(highlighted_edits.highlights.len(), 1);
20553 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
20554 assert_eq!(
20555 highlighted_edits.highlights[0].1.background_color,
20556 Some(cx.theme().status().created_background)
20557 );
20558 },
20559 )
20560 .await;
20561}
20562
20563async fn assert_highlighted_edits(
20564 text: &str,
20565 edits: Vec<(Range<Point>, String)>,
20566 include_deletions: bool,
20567 cx: &mut TestAppContext,
20568 assertion_fn: impl Fn(HighlightedText, &App),
20569) {
20570 let window = cx.add_window(|window, cx| {
20571 let buffer = MultiBuffer::build_simple(text, cx);
20572 Editor::new(EditorMode::full(), buffer, None, window, cx)
20573 });
20574 let cx = &mut VisualTestContext::from_window(*window, cx);
20575
20576 let (buffer, snapshot) = window
20577 .update(cx, |editor, _window, cx| {
20578 (
20579 editor.buffer().clone(),
20580 editor.buffer().read(cx).snapshot(cx),
20581 )
20582 })
20583 .unwrap();
20584
20585 let edits = edits
20586 .into_iter()
20587 .map(|(range, edit)| {
20588 (
20589 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
20590 edit,
20591 )
20592 })
20593 .collect::<Vec<_>>();
20594
20595 let text_anchor_edits = edits
20596 .clone()
20597 .into_iter()
20598 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
20599 .collect::<Vec<_>>();
20600
20601 let edit_preview = window
20602 .update(cx, |_, _window, cx| {
20603 buffer
20604 .read(cx)
20605 .as_singleton()
20606 .unwrap()
20607 .read(cx)
20608 .preview_edits(text_anchor_edits.into(), cx)
20609 })
20610 .unwrap()
20611 .await;
20612
20613 cx.update(|_window, cx| {
20614 let highlighted_edits = inline_completion_edit_text(
20615 &snapshot.as_singleton().unwrap().2,
20616 &edits,
20617 &edit_preview,
20618 include_deletions,
20619 cx,
20620 );
20621 assertion_fn(highlighted_edits, cx)
20622 });
20623}
20624
20625#[track_caller]
20626fn assert_breakpoint(
20627 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
20628 path: &Arc<Path>,
20629 expected: Vec<(u32, Breakpoint)>,
20630) {
20631 if expected.len() == 0usize {
20632 assert!(!breakpoints.contains_key(path), "{}", path.display());
20633 } else {
20634 let mut breakpoint = breakpoints
20635 .get(path)
20636 .unwrap()
20637 .into_iter()
20638 .map(|breakpoint| {
20639 (
20640 breakpoint.row,
20641 Breakpoint {
20642 message: breakpoint.message.clone(),
20643 state: breakpoint.state,
20644 condition: breakpoint.condition.clone(),
20645 hit_condition: breakpoint.hit_condition.clone(),
20646 },
20647 )
20648 })
20649 .collect::<Vec<_>>();
20650
20651 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
20652
20653 assert_eq!(expected, breakpoint);
20654 }
20655}
20656
20657fn add_log_breakpoint_at_cursor(
20658 editor: &mut Editor,
20659 log_message: &str,
20660 window: &mut Window,
20661 cx: &mut Context<Editor>,
20662) {
20663 let (anchor, bp) = editor
20664 .breakpoints_at_cursors(window, cx)
20665 .first()
20666 .and_then(|(anchor, bp)| {
20667 if let Some(bp) = bp {
20668 Some((*anchor, bp.clone()))
20669 } else {
20670 None
20671 }
20672 })
20673 .unwrap_or_else(|| {
20674 let cursor_position: Point = editor.selections.newest(cx).head();
20675
20676 let breakpoint_position = editor
20677 .snapshot(window, cx)
20678 .display_snapshot
20679 .buffer_snapshot
20680 .anchor_before(Point::new(cursor_position.row, 0));
20681
20682 (breakpoint_position, Breakpoint::new_log(&log_message))
20683 });
20684
20685 editor.edit_breakpoint_at_anchor(
20686 anchor,
20687 bp,
20688 BreakpointEditAction::EditLogMessage(log_message.into()),
20689 cx,
20690 );
20691}
20692
20693#[gpui::test]
20694async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
20695 init_test(cx, |_| {});
20696
20697 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20698 let fs = FakeFs::new(cx.executor());
20699 fs.insert_tree(
20700 path!("/a"),
20701 json!({
20702 "main.rs": sample_text,
20703 }),
20704 )
20705 .await;
20706 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20707 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20708 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20709
20710 let fs = FakeFs::new(cx.executor());
20711 fs.insert_tree(
20712 path!("/a"),
20713 json!({
20714 "main.rs": sample_text,
20715 }),
20716 )
20717 .await;
20718 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20719 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20720 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20721 let worktree_id = workspace
20722 .update(cx, |workspace, _window, cx| {
20723 workspace.project().update(cx, |project, cx| {
20724 project.worktrees(cx).next().unwrap().read(cx).id()
20725 })
20726 })
20727 .unwrap();
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 // assert we can add breakpoint on the first line
20755 editor.update_in(cx, |editor, window, cx| {
20756 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20757 editor.move_to_end(&MoveToEnd, window, cx);
20758 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20759 });
20760
20761 let breakpoints = editor.update(cx, |editor, cx| {
20762 editor
20763 .breakpoint_store()
20764 .as_ref()
20765 .unwrap()
20766 .read(cx)
20767 .all_source_breakpoints(cx)
20768 .clone()
20769 });
20770
20771 assert_eq!(1, breakpoints.len());
20772 assert_breakpoint(
20773 &breakpoints,
20774 &abs_path,
20775 vec![
20776 (0, Breakpoint::new_standard()),
20777 (3, Breakpoint::new_standard()),
20778 ],
20779 );
20780
20781 editor.update_in(cx, |editor, window, cx| {
20782 editor.move_to_beginning(&MoveToBeginning, window, cx);
20783 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20784 });
20785
20786 let breakpoints = editor.update(cx, |editor, cx| {
20787 editor
20788 .breakpoint_store()
20789 .as_ref()
20790 .unwrap()
20791 .read(cx)
20792 .all_source_breakpoints(cx)
20793 .clone()
20794 });
20795
20796 assert_eq!(1, breakpoints.len());
20797 assert_breakpoint(
20798 &breakpoints,
20799 &abs_path,
20800 vec![(3, Breakpoint::new_standard())],
20801 );
20802
20803 editor.update_in(cx, |editor, window, cx| {
20804 editor.move_to_end(&MoveToEnd, window, cx);
20805 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20806 });
20807
20808 let breakpoints = editor.update(cx, |editor, cx| {
20809 editor
20810 .breakpoint_store()
20811 .as_ref()
20812 .unwrap()
20813 .read(cx)
20814 .all_source_breakpoints(cx)
20815 .clone()
20816 });
20817
20818 assert_eq!(0, breakpoints.len());
20819 assert_breakpoint(&breakpoints, &abs_path, vec![]);
20820}
20821
20822#[gpui::test]
20823async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
20824 init_test(cx, |_| {});
20825
20826 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20827
20828 let fs = FakeFs::new(cx.executor());
20829 fs.insert_tree(
20830 path!("/a"),
20831 json!({
20832 "main.rs": sample_text,
20833 }),
20834 )
20835 .await;
20836 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20837 let (workspace, cx) =
20838 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20839
20840 let worktree_id = workspace.update(cx, |workspace, cx| {
20841 workspace.project().update(cx, |project, cx| {
20842 project.worktrees(cx).next().unwrap().read(cx).id()
20843 })
20844 });
20845
20846 let buffer = project
20847 .update(cx, |project, cx| {
20848 project.open_buffer((worktree_id, "main.rs"), cx)
20849 })
20850 .await
20851 .unwrap();
20852
20853 let (editor, cx) = cx.add_window_view(|window, cx| {
20854 Editor::new(
20855 EditorMode::full(),
20856 MultiBuffer::build_from_buffer(buffer, cx),
20857 Some(project.clone()),
20858 window,
20859 cx,
20860 )
20861 });
20862
20863 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20864 let abs_path = project.read_with(cx, |project, cx| {
20865 project
20866 .absolute_path(&project_path, cx)
20867 .map(|path_buf| Arc::from(path_buf.to_owned()))
20868 .unwrap()
20869 });
20870
20871 editor.update_in(cx, |editor, window, cx| {
20872 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
20873 });
20874
20875 let breakpoints = editor.update(cx, |editor, cx| {
20876 editor
20877 .breakpoint_store()
20878 .as_ref()
20879 .unwrap()
20880 .read(cx)
20881 .all_source_breakpoints(cx)
20882 .clone()
20883 });
20884
20885 assert_breakpoint(
20886 &breakpoints,
20887 &abs_path,
20888 vec![(0, Breakpoint::new_log("hello world"))],
20889 );
20890
20891 // Removing a log message from a log breakpoint should remove it
20892 editor.update_in(cx, |editor, window, cx| {
20893 add_log_breakpoint_at_cursor(editor, "", window, cx);
20894 });
20895
20896 let breakpoints = editor.update(cx, |editor, cx| {
20897 editor
20898 .breakpoint_store()
20899 .as_ref()
20900 .unwrap()
20901 .read(cx)
20902 .all_source_breakpoints(cx)
20903 .clone()
20904 });
20905
20906 assert_breakpoint(&breakpoints, &abs_path, vec![]);
20907
20908 editor.update_in(cx, |editor, window, cx| {
20909 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20910 editor.move_to_end(&MoveToEnd, window, cx);
20911 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20912 // Not adding a log message to a standard breakpoint shouldn't remove it
20913 add_log_breakpoint_at_cursor(editor, "", window, cx);
20914 });
20915
20916 let breakpoints = editor.update(cx, |editor, cx| {
20917 editor
20918 .breakpoint_store()
20919 .as_ref()
20920 .unwrap()
20921 .read(cx)
20922 .all_source_breakpoints(cx)
20923 .clone()
20924 });
20925
20926 assert_breakpoint(
20927 &breakpoints,
20928 &abs_path,
20929 vec![
20930 (0, Breakpoint::new_standard()),
20931 (3, Breakpoint::new_standard()),
20932 ],
20933 );
20934
20935 editor.update_in(cx, |editor, window, cx| {
20936 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
20937 });
20938
20939 let breakpoints = editor.update(cx, |editor, cx| {
20940 editor
20941 .breakpoint_store()
20942 .as_ref()
20943 .unwrap()
20944 .read(cx)
20945 .all_source_breakpoints(cx)
20946 .clone()
20947 });
20948
20949 assert_breakpoint(
20950 &breakpoints,
20951 &abs_path,
20952 vec![
20953 (0, Breakpoint::new_standard()),
20954 (3, Breakpoint::new_log("hello world")),
20955 ],
20956 );
20957
20958 editor.update_in(cx, |editor, window, cx| {
20959 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
20960 });
20961
20962 let breakpoints = editor.update(cx, |editor, cx| {
20963 editor
20964 .breakpoint_store()
20965 .as_ref()
20966 .unwrap()
20967 .read(cx)
20968 .all_source_breakpoints(cx)
20969 .clone()
20970 });
20971
20972 assert_breakpoint(
20973 &breakpoints,
20974 &abs_path,
20975 vec![
20976 (0, Breakpoint::new_standard()),
20977 (3, Breakpoint::new_log("hello Earth!!")),
20978 ],
20979 );
20980}
20981
20982/// This also tests that Editor::breakpoint_at_cursor_head is working properly
20983/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
20984/// or when breakpoints were placed out of order. This tests for a regression too
20985#[gpui::test]
20986async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
20987 init_test(cx, |_| {});
20988
20989 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20990 let fs = FakeFs::new(cx.executor());
20991 fs.insert_tree(
20992 path!("/a"),
20993 json!({
20994 "main.rs": sample_text,
20995 }),
20996 )
20997 .await;
20998 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20999 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21000 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21001
21002 let fs = FakeFs::new(cx.executor());
21003 fs.insert_tree(
21004 path!("/a"),
21005 json!({
21006 "main.rs": sample_text,
21007 }),
21008 )
21009 .await;
21010 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21011 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21012 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21013 let worktree_id = workspace
21014 .update(cx, |workspace, _window, cx| {
21015 workspace.project().update(cx, |project, cx| {
21016 project.worktrees(cx).next().unwrap().read(cx).id()
21017 })
21018 })
21019 .unwrap();
21020
21021 let buffer = project
21022 .update(cx, |project, cx| {
21023 project.open_buffer((worktree_id, "main.rs"), cx)
21024 })
21025 .await
21026 .unwrap();
21027
21028 let (editor, cx) = cx.add_window_view(|window, cx| {
21029 Editor::new(
21030 EditorMode::full(),
21031 MultiBuffer::build_from_buffer(buffer, cx),
21032 Some(project.clone()),
21033 window,
21034 cx,
21035 )
21036 });
21037
21038 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
21039 let abs_path = project.read_with(cx, |project, cx| {
21040 project
21041 .absolute_path(&project_path, cx)
21042 .map(|path_buf| Arc::from(path_buf.to_owned()))
21043 .unwrap()
21044 });
21045
21046 // assert we can add breakpoint on the first line
21047 editor.update_in(cx, |editor, window, cx| {
21048 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21049 editor.move_to_end(&MoveToEnd, window, cx);
21050 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21051 editor.move_up(&MoveUp, window, cx);
21052 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21053 });
21054
21055 let breakpoints = editor.update(cx, |editor, cx| {
21056 editor
21057 .breakpoint_store()
21058 .as_ref()
21059 .unwrap()
21060 .read(cx)
21061 .all_source_breakpoints(cx)
21062 .clone()
21063 });
21064
21065 assert_eq!(1, breakpoints.len());
21066 assert_breakpoint(
21067 &breakpoints,
21068 &abs_path,
21069 vec![
21070 (0, Breakpoint::new_standard()),
21071 (2, Breakpoint::new_standard()),
21072 (3, Breakpoint::new_standard()),
21073 ],
21074 );
21075
21076 editor.update_in(cx, |editor, window, cx| {
21077 editor.move_to_beginning(&MoveToBeginning, window, cx);
21078 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
21079 editor.move_to_end(&MoveToEnd, window, cx);
21080 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
21081 // Disabling a breakpoint that doesn't exist should do nothing
21082 editor.move_up(&MoveUp, window, cx);
21083 editor.move_up(&MoveUp, window, cx);
21084 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
21085 });
21086
21087 let breakpoints = editor.update(cx, |editor, cx| {
21088 editor
21089 .breakpoint_store()
21090 .as_ref()
21091 .unwrap()
21092 .read(cx)
21093 .all_source_breakpoints(cx)
21094 .clone()
21095 });
21096
21097 let disable_breakpoint = {
21098 let mut bp = Breakpoint::new_standard();
21099 bp.state = BreakpointState::Disabled;
21100 bp
21101 };
21102
21103 assert_eq!(1, breakpoints.len());
21104 assert_breakpoint(
21105 &breakpoints,
21106 &abs_path,
21107 vec![
21108 (0, disable_breakpoint.clone()),
21109 (2, Breakpoint::new_standard()),
21110 (3, disable_breakpoint.clone()),
21111 ],
21112 );
21113
21114 editor.update_in(cx, |editor, window, cx| {
21115 editor.move_to_beginning(&MoveToBeginning, window, cx);
21116 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
21117 editor.move_to_end(&MoveToEnd, window, cx);
21118 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
21119 editor.move_up(&MoveUp, window, cx);
21120 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
21121 });
21122
21123 let breakpoints = editor.update(cx, |editor, cx| {
21124 editor
21125 .breakpoint_store()
21126 .as_ref()
21127 .unwrap()
21128 .read(cx)
21129 .all_source_breakpoints(cx)
21130 .clone()
21131 });
21132
21133 assert_eq!(1, breakpoints.len());
21134 assert_breakpoint(
21135 &breakpoints,
21136 &abs_path,
21137 vec![
21138 (0, Breakpoint::new_standard()),
21139 (2, disable_breakpoint),
21140 (3, Breakpoint::new_standard()),
21141 ],
21142 );
21143}
21144
21145#[gpui::test]
21146async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
21147 init_test(cx, |_| {});
21148 let capabilities = lsp::ServerCapabilities {
21149 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
21150 prepare_provider: Some(true),
21151 work_done_progress_options: Default::default(),
21152 })),
21153 ..Default::default()
21154 };
21155 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
21156
21157 cx.set_state(indoc! {"
21158 struct Fˇoo {}
21159 "});
21160
21161 cx.update_editor(|editor, _, cx| {
21162 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
21163 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
21164 editor.highlight_background::<DocumentHighlightRead>(
21165 &[highlight_range],
21166 |theme| theme.colors().editor_document_highlight_read_background,
21167 cx,
21168 );
21169 });
21170
21171 let mut prepare_rename_handler = cx
21172 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
21173 move |_, _, _| async move {
21174 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
21175 start: lsp::Position {
21176 line: 0,
21177 character: 7,
21178 },
21179 end: lsp::Position {
21180 line: 0,
21181 character: 10,
21182 },
21183 })))
21184 },
21185 );
21186 let prepare_rename_task = cx
21187 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
21188 .expect("Prepare rename was not started");
21189 prepare_rename_handler.next().await.unwrap();
21190 prepare_rename_task.await.expect("Prepare rename failed");
21191
21192 let mut rename_handler =
21193 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
21194 let edit = lsp::TextEdit {
21195 range: lsp::Range {
21196 start: lsp::Position {
21197 line: 0,
21198 character: 7,
21199 },
21200 end: lsp::Position {
21201 line: 0,
21202 character: 10,
21203 },
21204 },
21205 new_text: "FooRenamed".to_string(),
21206 };
21207 Ok(Some(lsp::WorkspaceEdit::new(
21208 // Specify the same edit twice
21209 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
21210 )))
21211 });
21212 let rename_task = cx
21213 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
21214 .expect("Confirm rename was not started");
21215 rename_handler.next().await.unwrap();
21216 rename_task.await.expect("Confirm rename failed");
21217 cx.run_until_parked();
21218
21219 // Despite two edits, only one is actually applied as those are identical
21220 cx.assert_editor_state(indoc! {"
21221 struct FooRenamedˇ {}
21222 "});
21223}
21224
21225#[gpui::test]
21226async fn test_rename_without_prepare(cx: &mut TestAppContext) {
21227 init_test(cx, |_| {});
21228 // These capabilities indicate that the server does not support prepare rename.
21229 let capabilities = lsp::ServerCapabilities {
21230 rename_provider: Some(lsp::OneOf::Left(true)),
21231 ..Default::default()
21232 };
21233 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
21234
21235 cx.set_state(indoc! {"
21236 struct Fˇoo {}
21237 "});
21238
21239 cx.update_editor(|editor, _window, cx| {
21240 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
21241 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
21242 editor.highlight_background::<DocumentHighlightRead>(
21243 &[highlight_range],
21244 |theme| theme.colors().editor_document_highlight_read_background,
21245 cx,
21246 );
21247 });
21248
21249 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
21250 .expect("Prepare rename was not started")
21251 .await
21252 .expect("Prepare rename failed");
21253
21254 let mut rename_handler =
21255 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
21256 let edit = lsp::TextEdit {
21257 range: lsp::Range {
21258 start: lsp::Position {
21259 line: 0,
21260 character: 7,
21261 },
21262 end: lsp::Position {
21263 line: 0,
21264 character: 10,
21265 },
21266 },
21267 new_text: "FooRenamed".to_string(),
21268 };
21269 Ok(Some(lsp::WorkspaceEdit::new(
21270 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
21271 )))
21272 });
21273 let rename_task = cx
21274 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
21275 .expect("Confirm rename was not started");
21276 rename_handler.next().await.unwrap();
21277 rename_task.await.expect("Confirm rename failed");
21278 cx.run_until_parked();
21279
21280 // Correct range is renamed, as `surrounding_word` is used to find it.
21281 cx.assert_editor_state(indoc! {"
21282 struct FooRenamedˇ {}
21283 "});
21284}
21285
21286#[gpui::test]
21287async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
21288 init_test(cx, |_| {});
21289 let mut cx = EditorTestContext::new(cx).await;
21290
21291 let language = Arc::new(
21292 Language::new(
21293 LanguageConfig::default(),
21294 Some(tree_sitter_html::LANGUAGE.into()),
21295 )
21296 .with_brackets_query(
21297 r#"
21298 ("<" @open "/>" @close)
21299 ("</" @open ">" @close)
21300 ("<" @open ">" @close)
21301 ("\"" @open "\"" @close)
21302 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
21303 "#,
21304 )
21305 .unwrap(),
21306 );
21307 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
21308
21309 cx.set_state(indoc! {"
21310 <span>ˇ</span>
21311 "});
21312 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21313 cx.assert_editor_state(indoc! {"
21314 <span>
21315 ˇ
21316 </span>
21317 "});
21318
21319 cx.set_state(indoc! {"
21320 <span><span></span>ˇ</span>
21321 "});
21322 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21323 cx.assert_editor_state(indoc! {"
21324 <span><span></span>
21325 ˇ</span>
21326 "});
21327
21328 cx.set_state(indoc! {"
21329 <span>ˇ
21330 </span>
21331 "});
21332 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21333 cx.assert_editor_state(indoc! {"
21334 <span>
21335 ˇ
21336 </span>
21337 "});
21338}
21339
21340#[gpui::test(iterations = 10)]
21341async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
21342 init_test(cx, |_| {});
21343
21344 let fs = FakeFs::new(cx.executor());
21345 fs.insert_tree(
21346 path!("/dir"),
21347 json!({
21348 "a.ts": "a",
21349 }),
21350 )
21351 .await;
21352
21353 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
21354 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21355 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21356
21357 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21358 language_registry.add(Arc::new(Language::new(
21359 LanguageConfig {
21360 name: "TypeScript".into(),
21361 matcher: LanguageMatcher {
21362 path_suffixes: vec!["ts".to_string()],
21363 ..Default::default()
21364 },
21365 ..Default::default()
21366 },
21367 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
21368 )));
21369 let mut fake_language_servers = language_registry.register_fake_lsp(
21370 "TypeScript",
21371 FakeLspAdapter {
21372 capabilities: lsp::ServerCapabilities {
21373 code_lens_provider: Some(lsp::CodeLensOptions {
21374 resolve_provider: Some(true),
21375 }),
21376 execute_command_provider: Some(lsp::ExecuteCommandOptions {
21377 commands: vec!["_the/command".to_string()],
21378 ..lsp::ExecuteCommandOptions::default()
21379 }),
21380 ..lsp::ServerCapabilities::default()
21381 },
21382 ..FakeLspAdapter::default()
21383 },
21384 );
21385
21386 let (buffer, _handle) = project
21387 .update(cx, |p, cx| {
21388 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
21389 })
21390 .await
21391 .unwrap();
21392 cx.executor().run_until_parked();
21393
21394 let fake_server = fake_language_servers.next().await.unwrap();
21395
21396 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
21397 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
21398 drop(buffer_snapshot);
21399 let actions = cx
21400 .update_window(*workspace, |_, window, cx| {
21401 project.code_actions(&buffer, anchor..anchor, window, cx)
21402 })
21403 .unwrap();
21404
21405 fake_server
21406 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
21407 Ok(Some(vec![
21408 lsp::CodeLens {
21409 range: lsp::Range::default(),
21410 command: Some(lsp::Command {
21411 title: "Code lens command".to_owned(),
21412 command: "_the/command".to_owned(),
21413 arguments: None,
21414 }),
21415 data: None,
21416 },
21417 lsp::CodeLens {
21418 range: lsp::Range::default(),
21419 command: Some(lsp::Command {
21420 title: "Command not in capabilities".to_owned(),
21421 command: "not in capabilities".to_owned(),
21422 arguments: None,
21423 }),
21424 data: None,
21425 },
21426 lsp::CodeLens {
21427 range: lsp::Range {
21428 start: lsp::Position {
21429 line: 1,
21430 character: 1,
21431 },
21432 end: lsp::Position {
21433 line: 1,
21434 character: 1,
21435 },
21436 },
21437 command: Some(lsp::Command {
21438 title: "Command not in range".to_owned(),
21439 command: "_the/command".to_owned(),
21440 arguments: None,
21441 }),
21442 data: None,
21443 },
21444 ]))
21445 })
21446 .next()
21447 .await;
21448
21449 let actions = actions.await.unwrap();
21450 assert_eq!(
21451 actions.len(),
21452 1,
21453 "Should have only one valid action for the 0..0 range"
21454 );
21455 let action = actions[0].clone();
21456 let apply = project.update(cx, |project, cx| {
21457 project.apply_code_action(buffer.clone(), action, true, cx)
21458 });
21459
21460 // Resolving the code action does not populate its edits. In absence of
21461 // edits, we must execute the given command.
21462 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
21463 |mut lens, _| async move {
21464 let lens_command = lens.command.as_mut().expect("should have a command");
21465 assert_eq!(lens_command.title, "Code lens command");
21466 lens_command.arguments = Some(vec![json!("the-argument")]);
21467 Ok(lens)
21468 },
21469 );
21470
21471 // While executing the command, the language server sends the editor
21472 // a `workspaceEdit` request.
21473 fake_server
21474 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
21475 let fake = fake_server.clone();
21476 move |params, _| {
21477 assert_eq!(params.command, "_the/command");
21478 let fake = fake.clone();
21479 async move {
21480 fake.server
21481 .request::<lsp::request::ApplyWorkspaceEdit>(
21482 lsp::ApplyWorkspaceEditParams {
21483 label: None,
21484 edit: lsp::WorkspaceEdit {
21485 changes: Some(
21486 [(
21487 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
21488 vec![lsp::TextEdit {
21489 range: lsp::Range::new(
21490 lsp::Position::new(0, 0),
21491 lsp::Position::new(0, 0),
21492 ),
21493 new_text: "X".into(),
21494 }],
21495 )]
21496 .into_iter()
21497 .collect(),
21498 ),
21499 ..Default::default()
21500 },
21501 },
21502 )
21503 .await
21504 .into_response()
21505 .unwrap();
21506 Ok(Some(json!(null)))
21507 }
21508 }
21509 })
21510 .next()
21511 .await;
21512
21513 // Applying the code lens command returns a project transaction containing the edits
21514 // sent by the language server in its `workspaceEdit` request.
21515 let transaction = apply.await.unwrap();
21516 assert!(transaction.0.contains_key(&buffer));
21517 buffer.update(cx, |buffer, cx| {
21518 assert_eq!(buffer.text(), "Xa");
21519 buffer.undo(cx);
21520 assert_eq!(buffer.text(), "a");
21521 });
21522}
21523
21524#[gpui::test]
21525async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
21526 init_test(cx, |_| {});
21527
21528 let fs = FakeFs::new(cx.executor());
21529 let main_text = r#"fn main() {
21530println!("1");
21531println!("2");
21532println!("3");
21533println!("4");
21534println!("5");
21535}"#;
21536 let lib_text = "mod foo {}";
21537 fs.insert_tree(
21538 path!("/a"),
21539 json!({
21540 "lib.rs": lib_text,
21541 "main.rs": main_text,
21542 }),
21543 )
21544 .await;
21545
21546 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21547 let (workspace, cx) =
21548 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21549 let worktree_id = workspace.update(cx, |workspace, cx| {
21550 workspace.project().update(cx, |project, cx| {
21551 project.worktrees(cx).next().unwrap().read(cx).id()
21552 })
21553 });
21554
21555 let expected_ranges = vec![
21556 Point::new(0, 0)..Point::new(0, 0),
21557 Point::new(1, 0)..Point::new(1, 1),
21558 Point::new(2, 0)..Point::new(2, 2),
21559 Point::new(3, 0)..Point::new(3, 3),
21560 ];
21561
21562 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
21563 let editor_1 = workspace
21564 .update_in(cx, |workspace, window, cx| {
21565 workspace.open_path(
21566 (worktree_id, "main.rs"),
21567 Some(pane_1.downgrade()),
21568 true,
21569 window,
21570 cx,
21571 )
21572 })
21573 .unwrap()
21574 .await
21575 .downcast::<Editor>()
21576 .unwrap();
21577 pane_1.update(cx, |pane, cx| {
21578 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21579 open_editor.update(cx, |editor, cx| {
21580 assert_eq!(
21581 editor.display_text(cx),
21582 main_text,
21583 "Original main.rs text on initial open",
21584 );
21585 assert_eq!(
21586 editor
21587 .selections
21588 .all::<Point>(cx)
21589 .into_iter()
21590 .map(|s| s.range())
21591 .collect::<Vec<_>>(),
21592 vec![Point::zero()..Point::zero()],
21593 "Default selections on initial open",
21594 );
21595 })
21596 });
21597 editor_1.update_in(cx, |editor, window, cx| {
21598 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21599 s.select_ranges(expected_ranges.clone());
21600 });
21601 });
21602
21603 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
21604 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
21605 });
21606 let editor_2 = workspace
21607 .update_in(cx, |workspace, window, cx| {
21608 workspace.open_path(
21609 (worktree_id, "main.rs"),
21610 Some(pane_2.downgrade()),
21611 true,
21612 window,
21613 cx,
21614 )
21615 })
21616 .unwrap()
21617 .await
21618 .downcast::<Editor>()
21619 .unwrap();
21620 pane_2.update(cx, |pane, cx| {
21621 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21622 open_editor.update(cx, |editor, cx| {
21623 assert_eq!(
21624 editor.display_text(cx),
21625 main_text,
21626 "Original main.rs text on initial open in another panel",
21627 );
21628 assert_eq!(
21629 editor
21630 .selections
21631 .all::<Point>(cx)
21632 .into_iter()
21633 .map(|s| s.range())
21634 .collect::<Vec<_>>(),
21635 vec![Point::zero()..Point::zero()],
21636 "Default selections on initial open in another panel",
21637 );
21638 })
21639 });
21640
21641 editor_2.update_in(cx, |editor, window, cx| {
21642 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
21643 });
21644
21645 let _other_editor_1 = workspace
21646 .update_in(cx, |workspace, window, cx| {
21647 workspace.open_path(
21648 (worktree_id, "lib.rs"),
21649 Some(pane_1.downgrade()),
21650 true,
21651 window,
21652 cx,
21653 )
21654 })
21655 .unwrap()
21656 .await
21657 .downcast::<Editor>()
21658 .unwrap();
21659 pane_1
21660 .update_in(cx, |pane, window, cx| {
21661 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
21662 })
21663 .await
21664 .unwrap();
21665 drop(editor_1);
21666 pane_1.update(cx, |pane, cx| {
21667 pane.active_item()
21668 .unwrap()
21669 .downcast::<Editor>()
21670 .unwrap()
21671 .update(cx, |editor, cx| {
21672 assert_eq!(
21673 editor.display_text(cx),
21674 lib_text,
21675 "Other file should be open and active",
21676 );
21677 });
21678 assert_eq!(pane.items().count(), 1, "No other editors should be open");
21679 });
21680
21681 let _other_editor_2 = workspace
21682 .update_in(cx, |workspace, window, cx| {
21683 workspace.open_path(
21684 (worktree_id, "lib.rs"),
21685 Some(pane_2.downgrade()),
21686 true,
21687 window,
21688 cx,
21689 )
21690 })
21691 .unwrap()
21692 .await
21693 .downcast::<Editor>()
21694 .unwrap();
21695 pane_2
21696 .update_in(cx, |pane, window, cx| {
21697 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
21698 })
21699 .await
21700 .unwrap();
21701 drop(editor_2);
21702 pane_2.update(cx, |pane, cx| {
21703 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21704 open_editor.update(cx, |editor, cx| {
21705 assert_eq!(
21706 editor.display_text(cx),
21707 lib_text,
21708 "Other file should be open and active in another panel too",
21709 );
21710 });
21711 assert_eq!(
21712 pane.items().count(),
21713 1,
21714 "No other editors should be open in another pane",
21715 );
21716 });
21717
21718 let _editor_1_reopened = workspace
21719 .update_in(cx, |workspace, window, cx| {
21720 workspace.open_path(
21721 (worktree_id, "main.rs"),
21722 Some(pane_1.downgrade()),
21723 true,
21724 window,
21725 cx,
21726 )
21727 })
21728 .unwrap()
21729 .await
21730 .downcast::<Editor>()
21731 .unwrap();
21732 let _editor_2_reopened = workspace
21733 .update_in(cx, |workspace, window, cx| {
21734 workspace.open_path(
21735 (worktree_id, "main.rs"),
21736 Some(pane_2.downgrade()),
21737 true,
21738 window,
21739 cx,
21740 )
21741 })
21742 .unwrap()
21743 .await
21744 .downcast::<Editor>()
21745 .unwrap();
21746 pane_1.update(cx, |pane, cx| {
21747 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21748 open_editor.update(cx, |editor, cx| {
21749 assert_eq!(
21750 editor.display_text(cx),
21751 main_text,
21752 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
21753 );
21754 assert_eq!(
21755 editor
21756 .selections
21757 .all::<Point>(cx)
21758 .into_iter()
21759 .map(|s| s.range())
21760 .collect::<Vec<_>>(),
21761 expected_ranges,
21762 "Previous editor in the 1st panel had selections and should get them restored on reopen",
21763 );
21764 })
21765 });
21766 pane_2.update(cx, |pane, cx| {
21767 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21768 open_editor.update(cx, |editor, cx| {
21769 assert_eq!(
21770 editor.display_text(cx),
21771 r#"fn main() {
21772⋯rintln!("1");
21773⋯intln!("2");
21774⋯ntln!("3");
21775println!("4");
21776println!("5");
21777}"#,
21778 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
21779 );
21780 assert_eq!(
21781 editor
21782 .selections
21783 .all::<Point>(cx)
21784 .into_iter()
21785 .map(|s| s.range())
21786 .collect::<Vec<_>>(),
21787 vec![Point::zero()..Point::zero()],
21788 "Previous editor in the 2nd pane had no selections changed hence should restore none",
21789 );
21790 })
21791 });
21792}
21793
21794#[gpui::test]
21795async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
21796 init_test(cx, |_| {});
21797
21798 let fs = FakeFs::new(cx.executor());
21799 let main_text = r#"fn main() {
21800println!("1");
21801println!("2");
21802println!("3");
21803println!("4");
21804println!("5");
21805}"#;
21806 let lib_text = "mod foo {}";
21807 fs.insert_tree(
21808 path!("/a"),
21809 json!({
21810 "lib.rs": lib_text,
21811 "main.rs": main_text,
21812 }),
21813 )
21814 .await;
21815
21816 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21817 let (workspace, cx) =
21818 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21819 let worktree_id = workspace.update(cx, |workspace, cx| {
21820 workspace.project().update(cx, |project, cx| {
21821 project.worktrees(cx).next().unwrap().read(cx).id()
21822 })
21823 });
21824
21825 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
21826 let editor = workspace
21827 .update_in(cx, |workspace, window, cx| {
21828 workspace.open_path(
21829 (worktree_id, "main.rs"),
21830 Some(pane.downgrade()),
21831 true,
21832 window,
21833 cx,
21834 )
21835 })
21836 .unwrap()
21837 .await
21838 .downcast::<Editor>()
21839 .unwrap();
21840 pane.update(cx, |pane, cx| {
21841 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21842 open_editor.update(cx, |editor, cx| {
21843 assert_eq!(
21844 editor.display_text(cx),
21845 main_text,
21846 "Original main.rs text on initial open",
21847 );
21848 })
21849 });
21850 editor.update_in(cx, |editor, window, cx| {
21851 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
21852 });
21853
21854 cx.update_global(|store: &mut SettingsStore, cx| {
21855 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
21856 s.restore_on_file_reopen = Some(false);
21857 });
21858 });
21859 editor.update_in(cx, |editor, window, cx| {
21860 editor.fold_ranges(
21861 vec![
21862 Point::new(1, 0)..Point::new(1, 1),
21863 Point::new(2, 0)..Point::new(2, 2),
21864 Point::new(3, 0)..Point::new(3, 3),
21865 ],
21866 false,
21867 window,
21868 cx,
21869 );
21870 });
21871 pane.update_in(cx, |pane, window, cx| {
21872 pane.close_all_items(&CloseAllItems::default(), window, cx)
21873 })
21874 .await
21875 .unwrap();
21876 pane.update(cx, |pane, _| {
21877 assert!(pane.active_item().is_none());
21878 });
21879 cx.update_global(|store: &mut SettingsStore, cx| {
21880 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
21881 s.restore_on_file_reopen = Some(true);
21882 });
21883 });
21884
21885 let _editor_reopened = workspace
21886 .update_in(cx, |workspace, window, cx| {
21887 workspace.open_path(
21888 (worktree_id, "main.rs"),
21889 Some(pane.downgrade()),
21890 true,
21891 window,
21892 cx,
21893 )
21894 })
21895 .unwrap()
21896 .await
21897 .downcast::<Editor>()
21898 .unwrap();
21899 pane.update(cx, |pane, cx| {
21900 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21901 open_editor.update(cx, |editor, cx| {
21902 assert_eq!(
21903 editor.display_text(cx),
21904 main_text,
21905 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
21906 );
21907 })
21908 });
21909}
21910
21911#[gpui::test]
21912async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
21913 struct EmptyModalView {
21914 focus_handle: gpui::FocusHandle,
21915 }
21916 impl EventEmitter<DismissEvent> for EmptyModalView {}
21917 impl Render for EmptyModalView {
21918 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
21919 div()
21920 }
21921 }
21922 impl Focusable for EmptyModalView {
21923 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
21924 self.focus_handle.clone()
21925 }
21926 }
21927 impl workspace::ModalView for EmptyModalView {}
21928 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
21929 EmptyModalView {
21930 focus_handle: cx.focus_handle(),
21931 }
21932 }
21933
21934 init_test(cx, |_| {});
21935
21936 let fs = FakeFs::new(cx.executor());
21937 let project = Project::test(fs, [], cx).await;
21938 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21939 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
21940 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21941 let editor = cx.new_window_entity(|window, cx| {
21942 Editor::new(
21943 EditorMode::full(),
21944 buffer,
21945 Some(project.clone()),
21946 window,
21947 cx,
21948 )
21949 });
21950 workspace
21951 .update(cx, |workspace, window, cx| {
21952 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
21953 })
21954 .unwrap();
21955 editor.update_in(cx, |editor, window, cx| {
21956 editor.open_context_menu(&OpenContextMenu, window, cx);
21957 assert!(editor.mouse_context_menu.is_some());
21958 });
21959 workspace
21960 .update(cx, |workspace, window, cx| {
21961 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
21962 })
21963 .unwrap();
21964 cx.read(|cx| {
21965 assert!(editor.read(cx).mouse_context_menu.is_none());
21966 });
21967}
21968
21969#[gpui::test]
21970async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
21971 init_test(cx, |_| {});
21972
21973 let fs = FakeFs::new(cx.executor());
21974 fs.insert_file(path!("/file.html"), Default::default())
21975 .await;
21976
21977 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
21978
21979 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21980 let html_language = Arc::new(Language::new(
21981 LanguageConfig {
21982 name: "HTML".into(),
21983 matcher: LanguageMatcher {
21984 path_suffixes: vec!["html".to_string()],
21985 ..LanguageMatcher::default()
21986 },
21987 brackets: BracketPairConfig {
21988 pairs: vec![BracketPair {
21989 start: "<".into(),
21990 end: ">".into(),
21991 close: true,
21992 ..Default::default()
21993 }],
21994 ..Default::default()
21995 },
21996 ..Default::default()
21997 },
21998 Some(tree_sitter_html::LANGUAGE.into()),
21999 ));
22000 language_registry.add(html_language);
22001 let mut fake_servers = language_registry.register_fake_lsp(
22002 "HTML",
22003 FakeLspAdapter {
22004 capabilities: lsp::ServerCapabilities {
22005 completion_provider: Some(lsp::CompletionOptions {
22006 resolve_provider: Some(true),
22007 ..Default::default()
22008 }),
22009 ..Default::default()
22010 },
22011 ..Default::default()
22012 },
22013 );
22014
22015 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22016 let cx = &mut VisualTestContext::from_window(*workspace, cx);
22017
22018 let worktree_id = workspace
22019 .update(cx, |workspace, _window, cx| {
22020 workspace.project().update(cx, |project, cx| {
22021 project.worktrees(cx).next().unwrap().read(cx).id()
22022 })
22023 })
22024 .unwrap();
22025 project
22026 .update(cx, |project, cx| {
22027 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
22028 })
22029 .await
22030 .unwrap();
22031 let editor = workspace
22032 .update(cx, |workspace, window, cx| {
22033 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
22034 })
22035 .unwrap()
22036 .await
22037 .unwrap()
22038 .downcast::<Editor>()
22039 .unwrap();
22040
22041 let fake_server = fake_servers.next().await.unwrap();
22042 editor.update_in(cx, |editor, window, cx| {
22043 editor.set_text("<ad></ad>", window, cx);
22044 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
22045 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
22046 });
22047 let Some((buffer, _)) = editor
22048 .buffer
22049 .read(cx)
22050 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
22051 else {
22052 panic!("Failed to get buffer for selection position");
22053 };
22054 let buffer = buffer.read(cx);
22055 let buffer_id = buffer.remote_id();
22056 let opening_range =
22057 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
22058 let closing_range =
22059 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
22060 let mut linked_ranges = HashMap::default();
22061 linked_ranges.insert(
22062 buffer_id,
22063 vec![(opening_range.clone(), vec![closing_range.clone()])],
22064 );
22065 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
22066 });
22067 let mut completion_handle =
22068 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
22069 Ok(Some(lsp::CompletionResponse::Array(vec![
22070 lsp::CompletionItem {
22071 label: "head".to_string(),
22072 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
22073 lsp::InsertReplaceEdit {
22074 new_text: "head".to_string(),
22075 insert: lsp::Range::new(
22076 lsp::Position::new(0, 1),
22077 lsp::Position::new(0, 3),
22078 ),
22079 replace: lsp::Range::new(
22080 lsp::Position::new(0, 1),
22081 lsp::Position::new(0, 3),
22082 ),
22083 },
22084 )),
22085 ..Default::default()
22086 },
22087 ])))
22088 });
22089 editor.update_in(cx, |editor, window, cx| {
22090 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
22091 });
22092 cx.run_until_parked();
22093 completion_handle.next().await.unwrap();
22094 editor.update(cx, |editor, _| {
22095 assert!(
22096 editor.context_menu_visible(),
22097 "Completion menu should be visible"
22098 );
22099 });
22100 editor.update_in(cx, |editor, window, cx| {
22101 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
22102 });
22103 cx.executor().run_until_parked();
22104 editor.update(cx, |editor, cx| {
22105 assert_eq!(editor.text(cx), "<head></head>");
22106 });
22107}
22108
22109#[gpui::test]
22110async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
22111 init_test(cx, |_| {});
22112
22113 let fs = FakeFs::new(cx.executor());
22114 fs.insert_tree(
22115 path!("/root"),
22116 json!({
22117 "a": {
22118 "main.rs": "fn main() {}",
22119 },
22120 "foo": {
22121 "bar": {
22122 "external_file.rs": "pub mod external {}",
22123 }
22124 }
22125 }),
22126 )
22127 .await;
22128
22129 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
22130 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22131 language_registry.add(rust_lang());
22132 let _fake_servers = language_registry.register_fake_lsp(
22133 "Rust",
22134 FakeLspAdapter {
22135 ..FakeLspAdapter::default()
22136 },
22137 );
22138 let (workspace, cx) =
22139 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22140 let worktree_id = workspace.update(cx, |workspace, cx| {
22141 workspace.project().update(cx, |project, cx| {
22142 project.worktrees(cx).next().unwrap().read(cx).id()
22143 })
22144 });
22145
22146 let assert_language_servers_count =
22147 |expected: usize, context: &str, cx: &mut VisualTestContext| {
22148 project.update(cx, |project, cx| {
22149 let current = project
22150 .lsp_store()
22151 .read(cx)
22152 .as_local()
22153 .unwrap()
22154 .language_servers
22155 .len();
22156 assert_eq!(expected, current, "{context}");
22157 });
22158 };
22159
22160 assert_language_servers_count(
22161 0,
22162 "No servers should be running before any file is open",
22163 cx,
22164 );
22165 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
22166 let main_editor = workspace
22167 .update_in(cx, |workspace, window, cx| {
22168 workspace.open_path(
22169 (worktree_id, "main.rs"),
22170 Some(pane.downgrade()),
22171 true,
22172 window,
22173 cx,
22174 )
22175 })
22176 .unwrap()
22177 .await
22178 .downcast::<Editor>()
22179 .unwrap();
22180 pane.update(cx, |pane, cx| {
22181 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22182 open_editor.update(cx, |editor, cx| {
22183 assert_eq!(
22184 editor.display_text(cx),
22185 "fn main() {}",
22186 "Original main.rs text on initial open",
22187 );
22188 });
22189 assert_eq!(open_editor, main_editor);
22190 });
22191 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
22192
22193 let external_editor = workspace
22194 .update_in(cx, |workspace, window, cx| {
22195 workspace.open_abs_path(
22196 PathBuf::from("/root/foo/bar/external_file.rs"),
22197 OpenOptions::default(),
22198 window,
22199 cx,
22200 )
22201 })
22202 .await
22203 .expect("opening external file")
22204 .downcast::<Editor>()
22205 .expect("downcasted external file's open element to editor");
22206 pane.update(cx, |pane, cx| {
22207 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22208 open_editor.update(cx, |editor, cx| {
22209 assert_eq!(
22210 editor.display_text(cx),
22211 "pub mod external {}",
22212 "External file is open now",
22213 );
22214 });
22215 assert_eq!(open_editor, external_editor);
22216 });
22217 assert_language_servers_count(
22218 1,
22219 "Second, external, *.rs file should join the existing server",
22220 cx,
22221 );
22222
22223 pane.update_in(cx, |pane, window, cx| {
22224 pane.close_active_item(&CloseActiveItem::default(), window, cx)
22225 })
22226 .await
22227 .unwrap();
22228 pane.update_in(cx, |pane, window, cx| {
22229 pane.navigate_backward(window, cx);
22230 });
22231 cx.run_until_parked();
22232 pane.update(cx, |pane, cx| {
22233 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22234 open_editor.update(cx, |editor, cx| {
22235 assert_eq!(
22236 editor.display_text(cx),
22237 "pub mod external {}",
22238 "External file is open now",
22239 );
22240 });
22241 });
22242 assert_language_servers_count(
22243 1,
22244 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
22245 cx,
22246 );
22247
22248 cx.update(|_, cx| {
22249 workspace::reload(&workspace::Reload::default(), cx);
22250 });
22251 assert_language_servers_count(
22252 1,
22253 "After reloading the worktree with local and external files opened, only one project should be started",
22254 cx,
22255 );
22256}
22257
22258#[gpui::test]
22259async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
22260 init_test(cx, |_| {});
22261
22262 let mut cx = EditorTestContext::new(cx).await;
22263 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22264 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22265
22266 // test cursor move to start of each line on tab
22267 // for `if`, `elif`, `else`, `while`, `with` and `for`
22268 cx.set_state(indoc! {"
22269 def main():
22270 ˇ for item in items:
22271 ˇ while item.active:
22272 ˇ if item.value > 10:
22273 ˇ continue
22274 ˇ elif item.value < 0:
22275 ˇ break
22276 ˇ else:
22277 ˇ with item.context() as ctx:
22278 ˇ yield count
22279 ˇ else:
22280 ˇ log('while else')
22281 ˇ else:
22282 ˇ log('for else')
22283 "});
22284 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22285 cx.assert_editor_state(indoc! {"
22286 def main():
22287 ˇfor item in items:
22288 ˇwhile item.active:
22289 ˇif item.value > 10:
22290 ˇcontinue
22291 ˇelif item.value < 0:
22292 ˇbreak
22293 ˇelse:
22294 ˇwith item.context() as ctx:
22295 ˇyield count
22296 ˇelse:
22297 ˇlog('while else')
22298 ˇelse:
22299 ˇlog('for else')
22300 "});
22301 // test relative indent is preserved when tab
22302 // for `if`, `elif`, `else`, `while`, `with` and `for`
22303 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22304 cx.assert_editor_state(indoc! {"
22305 def main():
22306 ˇfor item in items:
22307 ˇwhile item.active:
22308 ˇif item.value > 10:
22309 ˇcontinue
22310 ˇelif item.value < 0:
22311 ˇbreak
22312 ˇelse:
22313 ˇwith item.context() as ctx:
22314 ˇyield count
22315 ˇelse:
22316 ˇlog('while else')
22317 ˇelse:
22318 ˇlog('for else')
22319 "});
22320
22321 // test cursor move to start of each line on tab
22322 // for `try`, `except`, `else`, `finally`, `match` and `def`
22323 cx.set_state(indoc! {"
22324 def main():
22325 ˇ try:
22326 ˇ fetch()
22327 ˇ except ValueError:
22328 ˇ handle_error()
22329 ˇ else:
22330 ˇ match value:
22331 ˇ case _:
22332 ˇ finally:
22333 ˇ def status():
22334 ˇ return 0
22335 "});
22336 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22337 cx.assert_editor_state(indoc! {"
22338 def main():
22339 ˇtry:
22340 ˇfetch()
22341 ˇexcept ValueError:
22342 ˇhandle_error()
22343 ˇelse:
22344 ˇmatch value:
22345 ˇcase _:
22346 ˇfinally:
22347 ˇdef status():
22348 ˇreturn 0
22349 "});
22350 // test relative indent is preserved when tab
22351 // for `try`, `except`, `else`, `finally`, `match` and `def`
22352 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22353 cx.assert_editor_state(indoc! {"
22354 def main():
22355 ˇtry:
22356 ˇfetch()
22357 ˇexcept ValueError:
22358 ˇhandle_error()
22359 ˇelse:
22360 ˇmatch value:
22361 ˇcase _:
22362 ˇfinally:
22363 ˇdef status():
22364 ˇreturn 0
22365 "});
22366}
22367
22368#[gpui::test]
22369async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
22370 init_test(cx, |_| {});
22371
22372 let mut cx = EditorTestContext::new(cx).await;
22373 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22374 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22375
22376 // test `else` auto outdents when typed inside `if` block
22377 cx.set_state(indoc! {"
22378 def main():
22379 if i == 2:
22380 return
22381 ˇ
22382 "});
22383 cx.update_editor(|editor, window, cx| {
22384 editor.handle_input("else:", window, cx);
22385 });
22386 cx.assert_editor_state(indoc! {"
22387 def main():
22388 if i == 2:
22389 return
22390 else:ˇ
22391 "});
22392
22393 // test `except` auto outdents when typed inside `try` block
22394 cx.set_state(indoc! {"
22395 def main():
22396 try:
22397 i = 2
22398 ˇ
22399 "});
22400 cx.update_editor(|editor, window, cx| {
22401 editor.handle_input("except:", window, cx);
22402 });
22403 cx.assert_editor_state(indoc! {"
22404 def main():
22405 try:
22406 i = 2
22407 except:ˇ
22408 "});
22409
22410 // test `else` auto outdents when typed inside `except` block
22411 cx.set_state(indoc! {"
22412 def main():
22413 try:
22414 i = 2
22415 except:
22416 j = 2
22417 ˇ
22418 "});
22419 cx.update_editor(|editor, window, cx| {
22420 editor.handle_input("else:", window, cx);
22421 });
22422 cx.assert_editor_state(indoc! {"
22423 def main():
22424 try:
22425 i = 2
22426 except:
22427 j = 2
22428 else:ˇ
22429 "});
22430
22431 // test `finally` auto outdents when typed inside `else` block
22432 cx.set_state(indoc! {"
22433 def main():
22434 try:
22435 i = 2
22436 except:
22437 j = 2
22438 else:
22439 k = 2
22440 ˇ
22441 "});
22442 cx.update_editor(|editor, window, cx| {
22443 editor.handle_input("finally:", window, cx);
22444 });
22445 cx.assert_editor_state(indoc! {"
22446 def main():
22447 try:
22448 i = 2
22449 except:
22450 j = 2
22451 else:
22452 k = 2
22453 finally:ˇ
22454 "});
22455
22456 // test `else` does not outdents when typed inside `except` block right after for block
22457 cx.set_state(indoc! {"
22458 def main():
22459 try:
22460 i = 2
22461 except:
22462 for i in range(n):
22463 pass
22464 ˇ
22465 "});
22466 cx.update_editor(|editor, window, cx| {
22467 editor.handle_input("else:", window, cx);
22468 });
22469 cx.assert_editor_state(indoc! {"
22470 def main():
22471 try:
22472 i = 2
22473 except:
22474 for i in range(n):
22475 pass
22476 else:ˇ
22477 "});
22478
22479 // test `finally` auto outdents when typed inside `else` block right after for block
22480 cx.set_state(indoc! {"
22481 def main():
22482 try:
22483 i = 2
22484 except:
22485 j = 2
22486 else:
22487 for i in range(n):
22488 pass
22489 ˇ
22490 "});
22491 cx.update_editor(|editor, window, cx| {
22492 editor.handle_input("finally:", window, cx);
22493 });
22494 cx.assert_editor_state(indoc! {"
22495 def main():
22496 try:
22497 i = 2
22498 except:
22499 j = 2
22500 else:
22501 for i in range(n):
22502 pass
22503 finally:ˇ
22504 "});
22505
22506 // test `except` outdents to inner "try" block
22507 cx.set_state(indoc! {"
22508 def main():
22509 try:
22510 i = 2
22511 if i == 2:
22512 try:
22513 i = 3
22514 ˇ
22515 "});
22516 cx.update_editor(|editor, window, cx| {
22517 editor.handle_input("except:", window, cx);
22518 });
22519 cx.assert_editor_state(indoc! {"
22520 def main():
22521 try:
22522 i = 2
22523 if i == 2:
22524 try:
22525 i = 3
22526 except:ˇ
22527 "});
22528
22529 // test `except` outdents to outer "try" block
22530 cx.set_state(indoc! {"
22531 def main():
22532 try:
22533 i = 2
22534 if i == 2:
22535 try:
22536 i = 3
22537 ˇ
22538 "});
22539 cx.update_editor(|editor, window, cx| {
22540 editor.handle_input("except:", window, cx);
22541 });
22542 cx.assert_editor_state(indoc! {"
22543 def main():
22544 try:
22545 i = 2
22546 if i == 2:
22547 try:
22548 i = 3
22549 except:ˇ
22550 "});
22551
22552 // test `else` stays at correct indent when typed after `for` block
22553 cx.set_state(indoc! {"
22554 def main():
22555 for i in range(10):
22556 if i == 3:
22557 break
22558 ˇ
22559 "});
22560 cx.update_editor(|editor, window, cx| {
22561 editor.handle_input("else:", window, cx);
22562 });
22563 cx.assert_editor_state(indoc! {"
22564 def main():
22565 for i in range(10):
22566 if i == 3:
22567 break
22568 else:ˇ
22569 "});
22570
22571 // test does not outdent on typing after line with square brackets
22572 cx.set_state(indoc! {"
22573 def f() -> list[str]:
22574 ˇ
22575 "});
22576 cx.update_editor(|editor, window, cx| {
22577 editor.handle_input("a", window, cx);
22578 });
22579 cx.assert_editor_state(indoc! {"
22580 def f() -> list[str]:
22581 aˇ
22582 "});
22583
22584 // test does not outdent on typing : after case keyword
22585 cx.set_state(indoc! {"
22586 match 1:
22587 caseˇ
22588 "});
22589 cx.update_editor(|editor, window, cx| {
22590 editor.handle_input(":", window, cx);
22591 });
22592 cx.assert_editor_state(indoc! {"
22593 match 1:
22594 case:ˇ
22595 "});
22596}
22597
22598#[gpui::test]
22599async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
22600 init_test(cx, |_| {});
22601 update_test_language_settings(cx, |settings| {
22602 settings.defaults.extend_comment_on_newline = Some(false);
22603 });
22604 let mut cx = EditorTestContext::new(cx).await;
22605 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22606 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22607
22608 // test correct indent after newline on comment
22609 cx.set_state(indoc! {"
22610 # COMMENT:ˇ
22611 "});
22612 cx.update_editor(|editor, window, cx| {
22613 editor.newline(&Newline, window, cx);
22614 });
22615 cx.assert_editor_state(indoc! {"
22616 # COMMENT:
22617 ˇ
22618 "});
22619
22620 // test correct indent after newline in brackets
22621 cx.set_state(indoc! {"
22622 {ˇ}
22623 "});
22624 cx.update_editor(|editor, window, cx| {
22625 editor.newline(&Newline, window, cx);
22626 });
22627 cx.run_until_parked();
22628 cx.assert_editor_state(indoc! {"
22629 {
22630 ˇ
22631 }
22632 "});
22633
22634 cx.set_state(indoc! {"
22635 (ˇ)
22636 "});
22637 cx.update_editor(|editor, window, cx| {
22638 editor.newline(&Newline, window, cx);
22639 });
22640 cx.run_until_parked();
22641 cx.assert_editor_state(indoc! {"
22642 (
22643 ˇ
22644 )
22645 "});
22646
22647 // do not indent after empty lists or dictionaries
22648 cx.set_state(indoc! {"
22649 a = []ˇ
22650 "});
22651 cx.update_editor(|editor, window, cx| {
22652 editor.newline(&Newline, window, cx);
22653 });
22654 cx.run_until_parked();
22655 cx.assert_editor_state(indoc! {"
22656 a = []
22657 ˇ
22658 "});
22659}
22660
22661fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
22662 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
22663 point..point
22664}
22665
22666#[track_caller]
22667fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
22668 let (text, ranges) = marked_text_ranges(marked_text, true);
22669 assert_eq!(editor.text(cx), text);
22670 assert_eq!(
22671 editor.selections.ranges(cx),
22672 ranges,
22673 "Assert selections are {}",
22674 marked_text
22675 );
22676}
22677
22678pub fn handle_signature_help_request(
22679 cx: &mut EditorLspTestContext,
22680 mocked_response: lsp::SignatureHelp,
22681) -> impl Future<Output = ()> + use<> {
22682 let mut request =
22683 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
22684 let mocked_response = mocked_response.clone();
22685 async move { Ok(Some(mocked_response)) }
22686 });
22687
22688 async move {
22689 request.next().await;
22690 }
22691}
22692
22693#[track_caller]
22694pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
22695 cx.update_editor(|editor, _, _| {
22696 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
22697 let entries = menu.entries.borrow();
22698 let entries = entries
22699 .iter()
22700 .map(|entry| entry.string.as_str())
22701 .collect::<Vec<_>>();
22702 assert_eq!(entries, expected);
22703 } else {
22704 panic!("Expected completions menu");
22705 }
22706 });
22707}
22708
22709/// Handle completion request passing a marked string specifying where the completion
22710/// should be triggered from using '|' character, what range should be replaced, and what completions
22711/// should be returned using '<' and '>' to delimit the range.
22712///
22713/// Also see `handle_completion_request_with_insert_and_replace`.
22714#[track_caller]
22715pub fn handle_completion_request(
22716 marked_string: &str,
22717 completions: Vec<&'static str>,
22718 is_incomplete: bool,
22719 counter: Arc<AtomicUsize>,
22720 cx: &mut EditorLspTestContext,
22721) -> impl Future<Output = ()> {
22722 let complete_from_marker: TextRangeMarker = '|'.into();
22723 let replace_range_marker: TextRangeMarker = ('<', '>').into();
22724 let (_, mut marked_ranges) = marked_text_ranges_by(
22725 marked_string,
22726 vec![complete_from_marker.clone(), replace_range_marker.clone()],
22727 );
22728
22729 let complete_from_position =
22730 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
22731 let replace_range =
22732 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
22733
22734 let mut request =
22735 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
22736 let completions = completions.clone();
22737 counter.fetch_add(1, atomic::Ordering::Release);
22738 async move {
22739 assert_eq!(params.text_document_position.text_document.uri, url.clone());
22740 assert_eq!(
22741 params.text_document_position.position,
22742 complete_from_position
22743 );
22744 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
22745 is_incomplete: is_incomplete,
22746 item_defaults: None,
22747 items: completions
22748 .iter()
22749 .map(|completion_text| lsp::CompletionItem {
22750 label: completion_text.to_string(),
22751 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
22752 range: replace_range,
22753 new_text: completion_text.to_string(),
22754 })),
22755 ..Default::default()
22756 })
22757 .collect(),
22758 })))
22759 }
22760 });
22761
22762 async move {
22763 request.next().await;
22764 }
22765}
22766
22767/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
22768/// given instead, which also contains an `insert` range.
22769///
22770/// This function uses markers to define ranges:
22771/// - `|` marks the cursor position
22772/// - `<>` marks the replace range
22773/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
22774pub fn handle_completion_request_with_insert_and_replace(
22775 cx: &mut EditorLspTestContext,
22776 marked_string: &str,
22777 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
22778 counter: Arc<AtomicUsize>,
22779) -> impl Future<Output = ()> {
22780 let complete_from_marker: TextRangeMarker = '|'.into();
22781 let replace_range_marker: TextRangeMarker = ('<', '>').into();
22782 let insert_range_marker: TextRangeMarker = ('{', '}').into();
22783
22784 let (_, mut marked_ranges) = marked_text_ranges_by(
22785 marked_string,
22786 vec![
22787 complete_from_marker.clone(),
22788 replace_range_marker.clone(),
22789 insert_range_marker.clone(),
22790 ],
22791 );
22792
22793 let complete_from_position =
22794 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
22795 let replace_range =
22796 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
22797
22798 let insert_range = match marked_ranges.remove(&insert_range_marker) {
22799 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
22800 _ => lsp::Range {
22801 start: replace_range.start,
22802 end: complete_from_position,
22803 },
22804 };
22805
22806 let mut request =
22807 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
22808 let completions = completions.clone();
22809 counter.fetch_add(1, atomic::Ordering::Release);
22810 async move {
22811 assert_eq!(params.text_document_position.text_document.uri, url.clone());
22812 assert_eq!(
22813 params.text_document_position.position, complete_from_position,
22814 "marker `|` position doesn't match",
22815 );
22816 Ok(Some(lsp::CompletionResponse::Array(
22817 completions
22818 .iter()
22819 .map(|(label, new_text)| lsp::CompletionItem {
22820 label: label.to_string(),
22821 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
22822 lsp::InsertReplaceEdit {
22823 insert: insert_range,
22824 replace: replace_range,
22825 new_text: new_text.to_string(),
22826 },
22827 )),
22828 ..Default::default()
22829 })
22830 .collect(),
22831 )))
22832 }
22833 });
22834
22835 async move {
22836 request.next().await;
22837 }
22838}
22839
22840fn handle_resolve_completion_request(
22841 cx: &mut EditorLspTestContext,
22842 edits: Option<Vec<(&'static str, &'static str)>>,
22843) -> impl Future<Output = ()> {
22844 let edits = edits.map(|edits| {
22845 edits
22846 .iter()
22847 .map(|(marked_string, new_text)| {
22848 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
22849 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
22850 lsp::TextEdit::new(replace_range, new_text.to_string())
22851 })
22852 .collect::<Vec<_>>()
22853 });
22854
22855 let mut request =
22856 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
22857 let edits = edits.clone();
22858 async move {
22859 Ok(lsp::CompletionItem {
22860 additional_text_edits: edits,
22861 ..Default::default()
22862 })
22863 }
22864 });
22865
22866 async move {
22867 request.next().await;
22868 }
22869}
22870
22871pub(crate) fn update_test_language_settings(
22872 cx: &mut TestAppContext,
22873 f: impl Fn(&mut AllLanguageSettingsContent),
22874) {
22875 cx.update(|cx| {
22876 SettingsStore::update_global(cx, |store, cx| {
22877 store.update_user_settings::<AllLanguageSettings>(cx, f);
22878 });
22879 });
22880}
22881
22882pub(crate) fn update_test_project_settings(
22883 cx: &mut TestAppContext,
22884 f: impl Fn(&mut ProjectSettings),
22885) {
22886 cx.update(|cx| {
22887 SettingsStore::update_global(cx, |store, cx| {
22888 store.update_user_settings::<ProjectSettings>(cx, f);
22889 });
22890 });
22891}
22892
22893pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
22894 cx.update(|cx| {
22895 assets::Assets.load_test_fonts(cx);
22896 let store = SettingsStore::test(cx);
22897 cx.set_global(store);
22898 theme::init(theme::LoadThemes::JustBase, cx);
22899 release_channel::init(SemanticVersion::default(), cx);
22900 client::init_settings(cx);
22901 language::init(cx);
22902 Project::init_settings(cx);
22903 workspace::init_settings(cx);
22904 crate::init(cx);
22905 });
22906 zlog::init_test();
22907 update_test_language_settings(cx, f);
22908}
22909
22910#[track_caller]
22911fn assert_hunk_revert(
22912 not_reverted_text_with_selections: &str,
22913 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
22914 expected_reverted_text_with_selections: &str,
22915 base_text: &str,
22916 cx: &mut EditorLspTestContext,
22917) {
22918 cx.set_state(not_reverted_text_with_selections);
22919 cx.set_head_text(base_text);
22920 cx.executor().run_until_parked();
22921
22922 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
22923 let snapshot = editor.snapshot(window, cx);
22924 let reverted_hunk_statuses = snapshot
22925 .buffer_snapshot
22926 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
22927 .map(|hunk| hunk.status().kind)
22928 .collect::<Vec<_>>();
22929
22930 editor.git_restore(&Default::default(), window, cx);
22931 reverted_hunk_statuses
22932 });
22933 cx.executor().run_until_parked();
22934 cx.assert_editor_state(expected_reverted_text_with_selections);
22935 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
22936}
22937
22938#[gpui::test(iterations = 10)]
22939async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
22940 init_test(cx, |_| {});
22941
22942 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
22943 let counter = diagnostic_requests.clone();
22944
22945 let fs = FakeFs::new(cx.executor());
22946 fs.insert_tree(
22947 path!("/a"),
22948 json!({
22949 "first.rs": "fn main() { let a = 5; }",
22950 "second.rs": "// Test file",
22951 }),
22952 )
22953 .await;
22954
22955 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22956 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22957 let cx = &mut VisualTestContext::from_window(*workspace, cx);
22958
22959 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22960 language_registry.add(rust_lang());
22961 let mut fake_servers = language_registry.register_fake_lsp(
22962 "Rust",
22963 FakeLspAdapter {
22964 capabilities: lsp::ServerCapabilities {
22965 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
22966 lsp::DiagnosticOptions {
22967 identifier: None,
22968 inter_file_dependencies: true,
22969 workspace_diagnostics: true,
22970 work_done_progress_options: Default::default(),
22971 },
22972 )),
22973 ..Default::default()
22974 },
22975 ..Default::default()
22976 },
22977 );
22978
22979 let editor = workspace
22980 .update(cx, |workspace, window, cx| {
22981 workspace.open_abs_path(
22982 PathBuf::from(path!("/a/first.rs")),
22983 OpenOptions::default(),
22984 window,
22985 cx,
22986 )
22987 })
22988 .unwrap()
22989 .await
22990 .unwrap()
22991 .downcast::<Editor>()
22992 .unwrap();
22993 let fake_server = fake_servers.next().await.unwrap();
22994 let server_id = fake_server.server.server_id();
22995 let mut first_request = fake_server
22996 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
22997 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
22998 let result_id = Some(new_result_id.to_string());
22999 assert_eq!(
23000 params.text_document.uri,
23001 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
23002 );
23003 async move {
23004 Ok(lsp::DocumentDiagnosticReportResult::Report(
23005 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
23006 related_documents: None,
23007 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
23008 items: Vec::new(),
23009 result_id,
23010 },
23011 }),
23012 ))
23013 }
23014 });
23015
23016 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
23017 project.update(cx, |project, cx| {
23018 let buffer_id = editor
23019 .read(cx)
23020 .buffer()
23021 .read(cx)
23022 .as_singleton()
23023 .expect("created a singleton buffer")
23024 .read(cx)
23025 .remote_id();
23026 let buffer_result_id = project
23027 .lsp_store()
23028 .read(cx)
23029 .result_id(server_id, buffer_id, cx);
23030 assert_eq!(expected, buffer_result_id);
23031 });
23032 };
23033
23034 ensure_result_id(None, cx);
23035 cx.executor().advance_clock(Duration::from_millis(60));
23036 cx.executor().run_until_parked();
23037 assert_eq!(
23038 diagnostic_requests.load(atomic::Ordering::Acquire),
23039 1,
23040 "Opening file should trigger diagnostic request"
23041 );
23042 first_request
23043 .next()
23044 .await
23045 .expect("should have sent the first diagnostics pull request");
23046 ensure_result_id(Some("1".to_string()), cx);
23047
23048 // Editing should trigger diagnostics
23049 editor.update_in(cx, |editor, window, cx| {
23050 editor.handle_input("2", window, cx)
23051 });
23052 cx.executor().advance_clock(Duration::from_millis(60));
23053 cx.executor().run_until_parked();
23054 assert_eq!(
23055 diagnostic_requests.load(atomic::Ordering::Acquire),
23056 2,
23057 "Editing should trigger diagnostic request"
23058 );
23059 ensure_result_id(Some("2".to_string()), cx);
23060
23061 // Moving cursor should not trigger diagnostic request
23062 editor.update_in(cx, |editor, window, cx| {
23063 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23064 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
23065 });
23066 });
23067 cx.executor().advance_clock(Duration::from_millis(60));
23068 cx.executor().run_until_parked();
23069 assert_eq!(
23070 diagnostic_requests.load(atomic::Ordering::Acquire),
23071 2,
23072 "Cursor movement should not trigger diagnostic request"
23073 );
23074 ensure_result_id(Some("2".to_string()), cx);
23075 // Multiple rapid edits should be debounced
23076 for _ in 0..5 {
23077 editor.update_in(cx, |editor, window, cx| {
23078 editor.handle_input("x", window, cx)
23079 });
23080 }
23081 cx.executor().advance_clock(Duration::from_millis(60));
23082 cx.executor().run_until_parked();
23083
23084 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
23085 assert!(
23086 final_requests <= 4,
23087 "Multiple rapid edits should be debounced (got {final_requests} requests)",
23088 );
23089 ensure_result_id(Some(final_requests.to_string()), cx);
23090}
23091
23092#[gpui::test]
23093async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
23094 // Regression test for issue #11671
23095 // Previously, adding a cursor after moving multiple cursors would reset
23096 // the cursor count instead of adding to the existing cursors.
23097 init_test(cx, |_| {});
23098 let mut cx = EditorTestContext::new(cx).await;
23099
23100 // Create a simple buffer with cursor at start
23101 cx.set_state(indoc! {"
23102 ˇaaaa
23103 bbbb
23104 cccc
23105 dddd
23106 eeee
23107 ffff
23108 gggg
23109 hhhh"});
23110
23111 // Add 2 cursors below (so we have 3 total)
23112 cx.update_editor(|editor, window, cx| {
23113 editor.add_selection_below(&Default::default(), window, cx);
23114 editor.add_selection_below(&Default::default(), window, cx);
23115 });
23116
23117 // Verify we have 3 cursors
23118 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
23119 assert_eq!(
23120 initial_count, 3,
23121 "Should have 3 cursors after adding 2 below"
23122 );
23123
23124 // Move down one line
23125 cx.update_editor(|editor, window, cx| {
23126 editor.move_down(&MoveDown, window, cx);
23127 });
23128
23129 // Add another cursor below
23130 cx.update_editor(|editor, window, cx| {
23131 editor.add_selection_below(&Default::default(), window, cx);
23132 });
23133
23134 // Should now have 4 cursors (3 original + 1 new)
23135 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
23136 assert_eq!(
23137 final_count, 4,
23138 "Should have 4 cursors after moving and adding another"
23139 );
23140}
23141
23142#[gpui::test(iterations = 10)]
23143async fn test_document_colors(cx: &mut TestAppContext) {
23144 let expected_color = Rgba {
23145 r: 0.33,
23146 g: 0.33,
23147 b: 0.33,
23148 a: 0.33,
23149 };
23150
23151 init_test(cx, |_| {});
23152
23153 let fs = FakeFs::new(cx.executor());
23154 fs.insert_tree(
23155 path!("/a"),
23156 json!({
23157 "first.rs": "fn main() { let a = 5; }",
23158 }),
23159 )
23160 .await;
23161
23162 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23163 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23164 let cx = &mut VisualTestContext::from_window(*workspace, cx);
23165
23166 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23167 language_registry.add(rust_lang());
23168 let mut fake_servers = language_registry.register_fake_lsp(
23169 "Rust",
23170 FakeLspAdapter {
23171 capabilities: lsp::ServerCapabilities {
23172 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
23173 ..lsp::ServerCapabilities::default()
23174 },
23175 name: "rust-analyzer",
23176 ..FakeLspAdapter::default()
23177 },
23178 );
23179 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
23180 "Rust",
23181 FakeLspAdapter {
23182 capabilities: lsp::ServerCapabilities {
23183 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
23184 ..lsp::ServerCapabilities::default()
23185 },
23186 name: "not-rust-analyzer",
23187 ..FakeLspAdapter::default()
23188 },
23189 );
23190
23191 let editor = workspace
23192 .update(cx, |workspace, window, cx| {
23193 workspace.open_abs_path(
23194 PathBuf::from(path!("/a/first.rs")),
23195 OpenOptions::default(),
23196 window,
23197 cx,
23198 )
23199 })
23200 .unwrap()
23201 .await
23202 .unwrap()
23203 .downcast::<Editor>()
23204 .unwrap();
23205 let fake_language_server = fake_servers.next().await.unwrap();
23206 let fake_language_server_without_capabilities =
23207 fake_servers_without_capabilities.next().await.unwrap();
23208 let requests_made = Arc::new(AtomicUsize::new(0));
23209 let closure_requests_made = Arc::clone(&requests_made);
23210 let mut color_request_handle = fake_language_server
23211 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
23212 let requests_made = Arc::clone(&closure_requests_made);
23213 async move {
23214 assert_eq!(
23215 params.text_document.uri,
23216 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
23217 );
23218 requests_made.fetch_add(1, atomic::Ordering::Release);
23219 Ok(vec![
23220 lsp::ColorInformation {
23221 range: lsp::Range {
23222 start: lsp::Position {
23223 line: 0,
23224 character: 0,
23225 },
23226 end: lsp::Position {
23227 line: 0,
23228 character: 1,
23229 },
23230 },
23231 color: lsp::Color {
23232 red: 0.33,
23233 green: 0.33,
23234 blue: 0.33,
23235 alpha: 0.33,
23236 },
23237 },
23238 lsp::ColorInformation {
23239 range: lsp::Range {
23240 start: lsp::Position {
23241 line: 0,
23242 character: 0,
23243 },
23244 end: lsp::Position {
23245 line: 0,
23246 character: 1,
23247 },
23248 },
23249 color: lsp::Color {
23250 red: 0.33,
23251 green: 0.33,
23252 blue: 0.33,
23253 alpha: 0.33,
23254 },
23255 },
23256 ])
23257 }
23258 });
23259
23260 let _handle = fake_language_server_without_capabilities
23261 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
23262 panic!("Should not be called");
23263 });
23264 cx.executor().advance_clock(Duration::from_millis(100));
23265 color_request_handle.next().await.unwrap();
23266 cx.run_until_parked();
23267 assert_eq!(
23268 1,
23269 requests_made.load(atomic::Ordering::Acquire),
23270 "Should query for colors once per editor open"
23271 );
23272 editor.update_in(cx, |editor, _, cx| {
23273 assert_eq!(
23274 vec![expected_color],
23275 extract_color_inlays(editor, cx),
23276 "Should have an initial inlay"
23277 );
23278 });
23279
23280 // opening another file in a split should not influence the LSP query counter
23281 workspace
23282 .update(cx, |workspace, window, cx| {
23283 assert_eq!(
23284 workspace.panes().len(),
23285 1,
23286 "Should have one pane with one editor"
23287 );
23288 workspace.move_item_to_pane_in_direction(
23289 &MoveItemToPaneInDirection {
23290 direction: SplitDirection::Right,
23291 focus: false,
23292 clone: true,
23293 },
23294 window,
23295 cx,
23296 );
23297 })
23298 .unwrap();
23299 cx.run_until_parked();
23300 workspace
23301 .update(cx, |workspace, _, cx| {
23302 let panes = workspace.panes();
23303 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
23304 for pane in panes {
23305 let editor = pane
23306 .read(cx)
23307 .active_item()
23308 .and_then(|item| item.downcast::<Editor>())
23309 .expect("Should have opened an editor in each split");
23310 let editor_file = editor
23311 .read(cx)
23312 .buffer()
23313 .read(cx)
23314 .as_singleton()
23315 .expect("test deals with singleton buffers")
23316 .read(cx)
23317 .file()
23318 .expect("test buffese should have a file")
23319 .path();
23320 assert_eq!(
23321 editor_file.as_ref(),
23322 Path::new("first.rs"),
23323 "Both editors should be opened for the same file"
23324 )
23325 }
23326 })
23327 .unwrap();
23328
23329 cx.executor().advance_clock(Duration::from_millis(500));
23330 let save = editor.update_in(cx, |editor, window, cx| {
23331 editor.move_to_end(&MoveToEnd, window, cx);
23332 editor.handle_input("dirty", window, cx);
23333 editor.save(
23334 SaveOptions {
23335 format: true,
23336 autosave: true,
23337 },
23338 project.clone(),
23339 window,
23340 cx,
23341 )
23342 });
23343 save.await.unwrap();
23344
23345 color_request_handle.next().await.unwrap();
23346 cx.run_until_parked();
23347 assert_eq!(
23348 3,
23349 requests_made.load(atomic::Ordering::Acquire),
23350 "Should query for colors once per save and once per formatting after save"
23351 );
23352
23353 drop(editor);
23354 let close = workspace
23355 .update(cx, |workspace, window, cx| {
23356 workspace.active_pane().update(cx, |pane, cx| {
23357 pane.close_active_item(&CloseActiveItem::default(), window, cx)
23358 })
23359 })
23360 .unwrap();
23361 close.await.unwrap();
23362 let close = workspace
23363 .update(cx, |workspace, window, cx| {
23364 workspace.active_pane().update(cx, |pane, cx| {
23365 pane.close_active_item(&CloseActiveItem::default(), window, cx)
23366 })
23367 })
23368 .unwrap();
23369 close.await.unwrap();
23370 assert_eq!(
23371 3,
23372 requests_made.load(atomic::Ordering::Acquire),
23373 "After saving and closing all editors, no extra requests should be made"
23374 );
23375 workspace
23376 .update(cx, |workspace, _, cx| {
23377 assert!(
23378 workspace.active_item(cx).is_none(),
23379 "Should close all editors"
23380 )
23381 })
23382 .unwrap();
23383
23384 workspace
23385 .update(cx, |workspace, window, cx| {
23386 workspace.active_pane().update(cx, |pane, cx| {
23387 pane.navigate_backward(window, cx);
23388 })
23389 })
23390 .unwrap();
23391 cx.executor().advance_clock(Duration::from_millis(100));
23392 cx.run_until_parked();
23393 let editor = workspace
23394 .update(cx, |workspace, _, cx| {
23395 workspace
23396 .active_item(cx)
23397 .expect("Should have reopened the editor again after navigating back")
23398 .downcast::<Editor>()
23399 .expect("Should be an editor")
23400 })
23401 .unwrap();
23402 color_request_handle.next().await.unwrap();
23403 assert_eq!(
23404 3,
23405 requests_made.load(atomic::Ordering::Acquire),
23406 "Cache should be reused on buffer close and reopen"
23407 );
23408 editor.update(cx, |editor, cx| {
23409 assert_eq!(
23410 vec![expected_color],
23411 extract_color_inlays(editor, cx),
23412 "Should have an initial inlay"
23413 );
23414 });
23415}
23416
23417#[gpui::test]
23418async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
23419 init_test(cx, |_| {});
23420 let (editor, cx) = cx.add_window_view(Editor::single_line);
23421 editor.update_in(cx, |editor, window, cx| {
23422 editor.set_text("oops\n\nwow\n", window, cx)
23423 });
23424 cx.run_until_parked();
23425 editor.update(cx, |editor, cx| {
23426 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
23427 });
23428 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
23429 cx.run_until_parked();
23430 editor.update(cx, |editor, cx| {
23431 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
23432 });
23433}
23434
23435#[track_caller]
23436fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
23437 editor
23438 .all_inlays(cx)
23439 .into_iter()
23440 .filter_map(|inlay| inlay.get_color())
23441 .map(Rgba::from)
23442 .collect()
23443}