1use super::*;
2use crate::{
3 JoinLines,
4 code_context_menus::CodeContextMenu,
5 edit_prediction_tests::FakeEditPredictionProvider,
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_convert_to_sentence_case(cx: &mut TestAppContext) {
4729 init_test(cx, |_| {});
4730
4731 let mut cx = EditorTestContext::new(cx).await;
4732
4733 cx.set_state(indoc! {"
4734 «implement-windows-supportˇ»
4735 "});
4736 cx.update_editor(|e, window, cx| {
4737 e.convert_to_sentence_case(&ConvertToSentenceCase, window, cx)
4738 });
4739 cx.assert_editor_state(indoc! {"
4740 «Implement windows supportˇ»
4741 "});
4742}
4743
4744#[gpui::test]
4745async fn test_manipulate_text(cx: &mut TestAppContext) {
4746 init_test(cx, |_| {});
4747
4748 let mut cx = EditorTestContext::new(cx).await;
4749
4750 // Test convert_to_upper_case()
4751 cx.set_state(indoc! {"
4752 «hello worldˇ»
4753 "});
4754 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4755 cx.assert_editor_state(indoc! {"
4756 «HELLO WORLDˇ»
4757 "});
4758
4759 // Test convert_to_lower_case()
4760 cx.set_state(indoc! {"
4761 «HELLO WORLDˇ»
4762 "});
4763 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
4764 cx.assert_editor_state(indoc! {"
4765 «hello worldˇ»
4766 "});
4767
4768 // Test multiple line, single selection case
4769 cx.set_state(indoc! {"
4770 «The quick brown
4771 fox jumps over
4772 the lazy dogˇ»
4773 "});
4774 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
4775 cx.assert_editor_state(indoc! {"
4776 «The Quick Brown
4777 Fox Jumps Over
4778 The Lazy Dogˇ»
4779 "});
4780
4781 // Test multiple line, single selection case
4782 cx.set_state(indoc! {"
4783 «The quick brown
4784 fox jumps over
4785 the lazy dogˇ»
4786 "});
4787 cx.update_editor(|e, window, cx| {
4788 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
4789 });
4790 cx.assert_editor_state(indoc! {"
4791 «TheQuickBrown
4792 FoxJumpsOver
4793 TheLazyDogˇ»
4794 "});
4795
4796 // From here on out, test more complex cases of manipulate_text()
4797
4798 // Test no selection case - should affect words cursors are in
4799 // Cursor at beginning, middle, and end of word
4800 cx.set_state(indoc! {"
4801 ˇhello big beauˇtiful worldˇ
4802 "});
4803 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4804 cx.assert_editor_state(indoc! {"
4805 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
4806 "});
4807
4808 // Test multiple selections on a single line and across multiple lines
4809 cx.set_state(indoc! {"
4810 «Theˇ» quick «brown
4811 foxˇ» jumps «overˇ»
4812 the «lazyˇ» dog
4813 "});
4814 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4815 cx.assert_editor_state(indoc! {"
4816 «THEˇ» quick «BROWN
4817 FOXˇ» jumps «OVERˇ»
4818 the «LAZYˇ» dog
4819 "});
4820
4821 // Test case where text length grows
4822 cx.set_state(indoc! {"
4823 «tschüߡ»
4824 "});
4825 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4826 cx.assert_editor_state(indoc! {"
4827 «TSCHÜSSˇ»
4828 "});
4829
4830 // Test to make sure we don't crash when text shrinks
4831 cx.set_state(indoc! {"
4832 aaa_bbbˇ
4833 "});
4834 cx.update_editor(|e, window, cx| {
4835 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4836 });
4837 cx.assert_editor_state(indoc! {"
4838 «aaaBbbˇ»
4839 "});
4840
4841 // Test to make sure we all aware of the fact that each word can grow and shrink
4842 // Final selections should be aware of this fact
4843 cx.set_state(indoc! {"
4844 aaa_bˇbb bbˇb_ccc ˇccc_ddd
4845 "});
4846 cx.update_editor(|e, window, cx| {
4847 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4848 });
4849 cx.assert_editor_state(indoc! {"
4850 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
4851 "});
4852
4853 cx.set_state(indoc! {"
4854 «hElLo, WoRld!ˇ»
4855 "});
4856 cx.update_editor(|e, window, cx| {
4857 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
4858 });
4859 cx.assert_editor_state(indoc! {"
4860 «HeLlO, wOrLD!ˇ»
4861 "});
4862}
4863
4864#[gpui::test]
4865fn test_duplicate_line(cx: &mut TestAppContext) {
4866 init_test(cx, |_| {});
4867
4868 let editor = cx.add_window(|window, cx| {
4869 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4870 build_editor(buffer, window, cx)
4871 });
4872 _ = editor.update(cx, |editor, window, cx| {
4873 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4874 s.select_display_ranges([
4875 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4876 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4877 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4878 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4879 ])
4880 });
4881 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4882 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4883 assert_eq!(
4884 editor.selections.display_ranges(cx),
4885 vec![
4886 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4887 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
4888 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4889 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4890 ]
4891 );
4892 });
4893
4894 let editor = cx.add_window(|window, cx| {
4895 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4896 build_editor(buffer, window, cx)
4897 });
4898 _ = editor.update(cx, |editor, window, cx| {
4899 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4900 s.select_display_ranges([
4901 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4902 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4903 ])
4904 });
4905 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4906 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4907 assert_eq!(
4908 editor.selections.display_ranges(cx),
4909 vec![
4910 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
4911 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
4912 ]
4913 );
4914 });
4915
4916 // With `move_upwards` the selections stay in place, except for
4917 // the lines inserted above them
4918 let editor = cx.add_window(|window, cx| {
4919 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4920 build_editor(buffer, window, cx)
4921 });
4922 _ = editor.update(cx, |editor, window, cx| {
4923 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4924 s.select_display_ranges([
4925 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4926 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4927 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4928 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4929 ])
4930 });
4931 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4932 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4933 assert_eq!(
4934 editor.selections.display_ranges(cx),
4935 vec![
4936 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4937 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4938 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4939 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4940 ]
4941 );
4942 });
4943
4944 let editor = cx.add_window(|window, cx| {
4945 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4946 build_editor(buffer, window, cx)
4947 });
4948 _ = editor.update(cx, |editor, window, cx| {
4949 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4950 s.select_display_ranges([
4951 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4952 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4953 ])
4954 });
4955 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4956 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4957 assert_eq!(
4958 editor.selections.display_ranges(cx),
4959 vec![
4960 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4961 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4962 ]
4963 );
4964 });
4965
4966 let editor = cx.add_window(|window, cx| {
4967 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4968 build_editor(buffer, window, cx)
4969 });
4970 _ = editor.update(cx, |editor, window, cx| {
4971 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4972 s.select_display_ranges([
4973 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4974 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4975 ])
4976 });
4977 editor.duplicate_selection(&DuplicateSelection, window, cx);
4978 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
4979 assert_eq!(
4980 editor.selections.display_ranges(cx),
4981 vec![
4982 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4983 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4984 ]
4985 );
4986 });
4987}
4988
4989#[gpui::test]
4990fn test_move_line_up_down(cx: &mut TestAppContext) {
4991 init_test(cx, |_| {});
4992
4993 let editor = cx.add_window(|window, cx| {
4994 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4995 build_editor(buffer, window, cx)
4996 });
4997 _ = editor.update(cx, |editor, window, cx| {
4998 editor.fold_creases(
4999 vec![
5000 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5001 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5002 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5003 ],
5004 true,
5005 window,
5006 cx,
5007 );
5008 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5009 s.select_display_ranges([
5010 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5011 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5012 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5013 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
5014 ])
5015 });
5016 assert_eq!(
5017 editor.display_text(cx),
5018 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
5019 );
5020
5021 editor.move_line_up(&MoveLineUp, window, cx);
5022 assert_eq!(
5023 editor.display_text(cx),
5024 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
5025 );
5026 assert_eq!(
5027 editor.selections.display_ranges(cx),
5028 vec![
5029 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5030 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5031 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5032 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 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\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
5042 );
5043 assert_eq!(
5044 editor.selections.display_ranges(cx),
5045 vec![
5046 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 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_down(&MoveLineDown, window, cx);
5056 assert_eq!(
5057 editor.display_text(cx),
5058 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
5059 );
5060 assert_eq!(
5061 editor.selections.display_ranges(cx),
5062 vec![
5063 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5064 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5065 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5066 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5067 ]
5068 );
5069 });
5070
5071 _ = editor.update(cx, |editor, window, cx| {
5072 editor.move_line_up(&MoveLineUp, window, cx);
5073 assert_eq!(
5074 editor.display_text(cx),
5075 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
5076 );
5077 assert_eq!(
5078 editor.selections.display_ranges(cx),
5079 vec![
5080 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5081 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5082 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5083 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5084 ]
5085 );
5086 });
5087}
5088
5089#[gpui::test]
5090fn test_move_line_up_selection_at_end_of_fold(cx: &mut TestAppContext) {
5091 init_test(cx, |_| {});
5092 let editor = cx.add_window(|window, cx| {
5093 let buffer = MultiBuffer::build_simple("\n\n\n\n\n\naaaa\nbbbb\ncccc", cx);
5094 build_editor(buffer, window, cx)
5095 });
5096 _ = editor.update(cx, |editor, window, cx| {
5097 editor.fold_creases(
5098 vec![Crease::simple(
5099 Point::new(6, 4)..Point::new(7, 4),
5100 FoldPlaceholder::test(),
5101 )],
5102 true,
5103 window,
5104 cx,
5105 );
5106 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5107 s.select_ranges([Point::new(7, 4)..Point::new(7, 4)])
5108 });
5109 assert_eq!(editor.display_text(cx), "\n\n\n\n\n\naaaa⋯\ncccc");
5110 editor.move_line_up(&MoveLineUp, window, cx);
5111 let buffer_text = editor.buffer.read(cx).snapshot(cx).text();
5112 assert_eq!(buffer_text, "\n\n\n\n\naaaa\nbbbb\n\ncccc");
5113 });
5114}
5115
5116#[gpui::test]
5117fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
5118 init_test(cx, |_| {});
5119
5120 let editor = cx.add_window(|window, cx| {
5121 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5122 build_editor(buffer, window, cx)
5123 });
5124 _ = editor.update(cx, |editor, window, cx| {
5125 let snapshot = editor.buffer.read(cx).snapshot(cx);
5126 editor.insert_blocks(
5127 [BlockProperties {
5128 style: BlockStyle::Fixed,
5129 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
5130 height: Some(1),
5131 render: Arc::new(|_| div().into_any()),
5132 priority: 0,
5133 }],
5134 Some(Autoscroll::fit()),
5135 cx,
5136 );
5137 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5138 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
5139 });
5140 editor.move_line_down(&MoveLineDown, window, cx);
5141 });
5142}
5143
5144#[gpui::test]
5145async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
5146 init_test(cx, |_| {});
5147
5148 let mut cx = EditorTestContext::new(cx).await;
5149 cx.set_state(
5150 &"
5151 ˇzero
5152 one
5153 two
5154 three
5155 four
5156 five
5157 "
5158 .unindent(),
5159 );
5160
5161 // Create a four-line block that replaces three lines of text.
5162 cx.update_editor(|editor, window, cx| {
5163 let snapshot = editor.snapshot(window, cx);
5164 let snapshot = &snapshot.buffer_snapshot;
5165 let placement = BlockPlacement::Replace(
5166 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
5167 );
5168 editor.insert_blocks(
5169 [BlockProperties {
5170 placement,
5171 height: Some(4),
5172 style: BlockStyle::Sticky,
5173 render: Arc::new(|_| gpui::div().into_any_element()),
5174 priority: 0,
5175 }],
5176 None,
5177 cx,
5178 );
5179 });
5180
5181 // Move down so that the cursor touches the block.
5182 cx.update_editor(|editor, window, cx| {
5183 editor.move_down(&Default::default(), window, cx);
5184 });
5185 cx.assert_editor_state(
5186 &"
5187 zero
5188 «one
5189 two
5190 threeˇ»
5191 four
5192 five
5193 "
5194 .unindent(),
5195 );
5196
5197 // Move down past the block.
5198 cx.update_editor(|editor, window, cx| {
5199 editor.move_down(&Default::default(), window, cx);
5200 });
5201 cx.assert_editor_state(
5202 &"
5203 zero
5204 one
5205 two
5206 three
5207 ˇfour
5208 five
5209 "
5210 .unindent(),
5211 );
5212}
5213
5214#[gpui::test]
5215fn test_transpose(cx: &mut TestAppContext) {
5216 init_test(cx, |_| {});
5217
5218 _ = cx.add_window(|window, cx| {
5219 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
5220 editor.set_style(EditorStyle::default(), window, cx);
5221 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5222 s.select_ranges([1..1])
5223 });
5224 editor.transpose(&Default::default(), window, cx);
5225 assert_eq!(editor.text(cx), "bac");
5226 assert_eq!(editor.selections.ranges(cx), [2..2]);
5227
5228 editor.transpose(&Default::default(), window, cx);
5229 assert_eq!(editor.text(cx), "bca");
5230 assert_eq!(editor.selections.ranges(cx), [3..3]);
5231
5232 editor.transpose(&Default::default(), window, cx);
5233 assert_eq!(editor.text(cx), "bac");
5234 assert_eq!(editor.selections.ranges(cx), [3..3]);
5235
5236 editor
5237 });
5238
5239 _ = cx.add_window(|window, cx| {
5240 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5241 editor.set_style(EditorStyle::default(), window, cx);
5242 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5243 s.select_ranges([3..3])
5244 });
5245 editor.transpose(&Default::default(), window, cx);
5246 assert_eq!(editor.text(cx), "acb\nde");
5247 assert_eq!(editor.selections.ranges(cx), [3..3]);
5248
5249 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5250 s.select_ranges([4..4])
5251 });
5252 editor.transpose(&Default::default(), window, cx);
5253 assert_eq!(editor.text(cx), "acbd\ne");
5254 assert_eq!(editor.selections.ranges(cx), [5..5]);
5255
5256 editor.transpose(&Default::default(), window, cx);
5257 assert_eq!(editor.text(cx), "acbde\n");
5258 assert_eq!(editor.selections.ranges(cx), [6..6]);
5259
5260 editor.transpose(&Default::default(), window, cx);
5261 assert_eq!(editor.text(cx), "acbd\ne");
5262 assert_eq!(editor.selections.ranges(cx), [6..6]);
5263
5264 editor
5265 });
5266
5267 _ = cx.add_window(|window, cx| {
5268 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5269 editor.set_style(EditorStyle::default(), window, cx);
5270 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5271 s.select_ranges([1..1, 2..2, 4..4])
5272 });
5273 editor.transpose(&Default::default(), window, cx);
5274 assert_eq!(editor.text(cx), "bacd\ne");
5275 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
5276
5277 editor.transpose(&Default::default(), window, cx);
5278 assert_eq!(editor.text(cx), "bcade\n");
5279 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
5280
5281 editor.transpose(&Default::default(), window, cx);
5282 assert_eq!(editor.text(cx), "bcda\ne");
5283 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5284
5285 editor.transpose(&Default::default(), window, cx);
5286 assert_eq!(editor.text(cx), "bcade\n");
5287 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5288
5289 editor.transpose(&Default::default(), window, cx);
5290 assert_eq!(editor.text(cx), "bcaed\n");
5291 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
5292
5293 editor
5294 });
5295
5296 _ = cx.add_window(|window, cx| {
5297 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
5298 editor.set_style(EditorStyle::default(), window, cx);
5299 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5300 s.select_ranges([4..4])
5301 });
5302 editor.transpose(&Default::default(), window, cx);
5303 assert_eq!(editor.text(cx), "🏀🍐✋");
5304 assert_eq!(editor.selections.ranges(cx), [8..8]);
5305
5306 editor.transpose(&Default::default(), window, cx);
5307 assert_eq!(editor.text(cx), "🏀✋🍐");
5308 assert_eq!(editor.selections.ranges(cx), [11..11]);
5309
5310 editor.transpose(&Default::default(), window, cx);
5311 assert_eq!(editor.text(cx), "🏀🍐✋");
5312 assert_eq!(editor.selections.ranges(cx), [11..11]);
5313
5314 editor
5315 });
5316}
5317
5318#[gpui::test]
5319async fn test_rewrap(cx: &mut TestAppContext) {
5320 init_test(cx, |settings| {
5321 settings.languages.0.extend([
5322 (
5323 "Markdown".into(),
5324 LanguageSettingsContent {
5325 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5326 preferred_line_length: Some(40),
5327 ..Default::default()
5328 },
5329 ),
5330 (
5331 "Plain Text".into(),
5332 LanguageSettingsContent {
5333 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5334 preferred_line_length: Some(40),
5335 ..Default::default()
5336 },
5337 ),
5338 (
5339 "C++".into(),
5340 LanguageSettingsContent {
5341 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5342 preferred_line_length: Some(40),
5343 ..Default::default()
5344 },
5345 ),
5346 (
5347 "Python".into(),
5348 LanguageSettingsContent {
5349 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5350 preferred_line_length: Some(40),
5351 ..Default::default()
5352 },
5353 ),
5354 (
5355 "Rust".into(),
5356 LanguageSettingsContent {
5357 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5358 preferred_line_length: Some(40),
5359 ..Default::default()
5360 },
5361 ),
5362 ])
5363 });
5364
5365 let mut cx = EditorTestContext::new(cx).await;
5366
5367 let cpp_language = Arc::new(Language::new(
5368 LanguageConfig {
5369 name: "C++".into(),
5370 line_comments: vec!["// ".into()],
5371 ..LanguageConfig::default()
5372 },
5373 None,
5374 ));
5375 let python_language = Arc::new(Language::new(
5376 LanguageConfig {
5377 name: "Python".into(),
5378 line_comments: vec!["# ".into()],
5379 ..LanguageConfig::default()
5380 },
5381 None,
5382 ));
5383 let markdown_language = Arc::new(Language::new(
5384 LanguageConfig {
5385 name: "Markdown".into(),
5386 rewrap_prefixes: vec![
5387 regex::Regex::new("\\d+\\.\\s+").unwrap(),
5388 regex::Regex::new("[-*+]\\s+").unwrap(),
5389 ],
5390 ..LanguageConfig::default()
5391 },
5392 None,
5393 ));
5394 let rust_language = Arc::new(Language::new(
5395 LanguageConfig {
5396 name: "Rust".into(),
5397 line_comments: vec!["// ".into(), "/// ".into()],
5398 ..LanguageConfig::default()
5399 },
5400 Some(tree_sitter_rust::LANGUAGE.into()),
5401 ));
5402
5403 let plaintext_language = Arc::new(Language::new(
5404 LanguageConfig {
5405 name: "Plain Text".into(),
5406 ..LanguageConfig::default()
5407 },
5408 None,
5409 ));
5410
5411 // Test basic rewrapping of a long line with a cursor
5412 assert_rewrap(
5413 indoc! {"
5414 // ˇThis is a long comment that needs to be wrapped.
5415 "},
5416 indoc! {"
5417 // ˇThis is a long comment that needs to
5418 // be wrapped.
5419 "},
5420 cpp_language.clone(),
5421 &mut cx,
5422 );
5423
5424 // Test rewrapping a full selection
5425 assert_rewrap(
5426 indoc! {"
5427 «// This selected long comment needs to be wrapped.ˇ»"
5428 },
5429 indoc! {"
5430 «// This selected long comment needs to
5431 // be wrapped.ˇ»"
5432 },
5433 cpp_language.clone(),
5434 &mut cx,
5435 );
5436
5437 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
5438 assert_rewrap(
5439 indoc! {"
5440 // ˇThis is the first line.
5441 // Thisˇ is the second line.
5442 // This is the thirdˇ line, all part of one paragraph.
5443 "},
5444 indoc! {"
5445 // ˇThis is the first line. Thisˇ is the
5446 // second line. This is the thirdˇ line,
5447 // all part of one paragraph.
5448 "},
5449 cpp_language.clone(),
5450 &mut cx,
5451 );
5452
5453 // Test multiple cursors in different paragraphs trigger separate rewraps
5454 assert_rewrap(
5455 indoc! {"
5456 // ˇThis is the first paragraph, first line.
5457 // ˇThis is the first paragraph, second line.
5458
5459 // ˇThis is the second paragraph, first line.
5460 // ˇThis is the second paragraph, second line.
5461 "},
5462 indoc! {"
5463 // ˇThis is the first paragraph, first
5464 // line. ˇThis is the first paragraph,
5465 // second line.
5466
5467 // ˇThis is the second paragraph, first
5468 // line. ˇThis is the second paragraph,
5469 // second line.
5470 "},
5471 cpp_language.clone(),
5472 &mut cx,
5473 );
5474
5475 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
5476 assert_rewrap(
5477 indoc! {"
5478 «// A regular long long comment to be wrapped.
5479 /// A documentation long comment to be wrapped.ˇ»
5480 "},
5481 indoc! {"
5482 «// A regular long long comment to be
5483 // wrapped.
5484 /// A documentation long comment to be
5485 /// wrapped.ˇ»
5486 "},
5487 rust_language.clone(),
5488 &mut cx,
5489 );
5490
5491 // Test that change in indentation level trigger seperate rewraps
5492 assert_rewrap(
5493 indoc! {"
5494 fn foo() {
5495 «// This is a long comment at the base indent.
5496 // This is a long comment at the next indent.ˇ»
5497 }
5498 "},
5499 indoc! {"
5500 fn foo() {
5501 «// This is a long comment at the
5502 // base indent.
5503 // This is a long comment at the
5504 // next indent.ˇ»
5505 }
5506 "},
5507 rust_language.clone(),
5508 &mut cx,
5509 );
5510
5511 // Test that different comment prefix characters (e.g., '#') are handled correctly
5512 assert_rewrap(
5513 indoc! {"
5514 # ˇThis is a long comment using a pound sign.
5515 "},
5516 indoc! {"
5517 # ˇThis is a long comment using a pound
5518 # sign.
5519 "},
5520 python_language.clone(),
5521 &mut cx,
5522 );
5523
5524 // Test rewrapping only affects comments, not code even when selected
5525 assert_rewrap(
5526 indoc! {"
5527 «/// This doc comment is long and should be wrapped.
5528 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
5529 "},
5530 indoc! {"
5531 «/// This doc comment is long and should
5532 /// be wrapped.
5533 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
5534 "},
5535 rust_language.clone(),
5536 &mut cx,
5537 );
5538
5539 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
5540 assert_rewrap(
5541 indoc! {"
5542 # Header
5543
5544 A long long long line of markdown text to wrap.ˇ
5545 "},
5546 indoc! {"
5547 # Header
5548
5549 A long long long line of markdown text
5550 to wrap.ˇ
5551 "},
5552 markdown_language.clone(),
5553 &mut cx,
5554 );
5555
5556 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
5557 assert_rewrap(
5558 indoc! {"
5559 «1. This is a numbered list item that is very long and needs to be wrapped properly.
5560 2. This is a numbered list item that is very long and needs to be wrapped properly.
5561 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
5562 "},
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
5572 with the numbered item.ˇ»
5573 "},
5574 markdown_language.clone(),
5575 &mut cx,
5576 );
5577
5578 // Test that rewrapping add indents for rewrapping boundary if not exists already.
5579 assert_rewrap(
5580 indoc! {"
5581 «1. This is a numbered list item that is
5582 very long and needs to be wrapped
5583 properly.
5584 2. This is a numbered list item that is
5585 very long and needs to be wrapped
5586 properly.
5587 - This is an unordered list item that is
5588 also very long and should not merge with
5589 the numbered item.ˇ»
5590 "},
5591 indoc! {"
5592 «1. This is a numbered list item that is
5593 very long and needs to be wrapped
5594 properly.
5595 2. This is a numbered list item that is
5596 very long and needs to be wrapped
5597 properly.
5598 - This is an unordered list item that is
5599 also very long and should not merge
5600 with the numbered item.ˇ»
5601 "},
5602 markdown_language.clone(),
5603 &mut cx,
5604 );
5605
5606 // Test that rewrapping maintain indents even when they already exists.
5607 assert_rewrap(
5608 indoc! {"
5609 «1. This is a numbered list
5610 item that is very long and needs to be wrapped properly.
5611 2. This is a numbered list
5612 item that is very long and needs to be wrapped properly.
5613 - This is an unordered list item that is also very long and
5614 should not merge with the numbered item.ˇ»
5615 "},
5616 indoc! {"
5617 «1. This is a numbered list item that is
5618 very long and needs to be wrapped
5619 properly.
5620 2. This is a numbered list item that is
5621 very long and needs to be wrapped
5622 properly.
5623 - This is an unordered list item that is
5624 also very long and should not merge
5625 with the numbered item.ˇ»
5626 "},
5627 markdown_language.clone(),
5628 &mut cx,
5629 );
5630
5631 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
5632 assert_rewrap(
5633 indoc! {"
5634 ˇThis is a very long line of plain text that will be wrapped.
5635 "},
5636 indoc! {"
5637 ˇThis is a very long line of plain text
5638 that will be wrapped.
5639 "},
5640 plaintext_language.clone(),
5641 &mut cx,
5642 );
5643
5644 // Test that non-commented code acts as a paragraph boundary within a selection
5645 assert_rewrap(
5646 indoc! {"
5647 «// This is the first long comment block to be wrapped.
5648 fn my_func(a: u32);
5649 // This is the second long comment block to be wrapped.ˇ»
5650 "},
5651 indoc! {"
5652 «// This is the first long comment block
5653 // to be wrapped.
5654 fn my_func(a: u32);
5655 // This is the second long comment block
5656 // to be wrapped.ˇ»
5657 "},
5658 rust_language.clone(),
5659 &mut cx,
5660 );
5661
5662 // Test rewrapping multiple selections, including ones with blank lines or tabs
5663 assert_rewrap(
5664 indoc! {"
5665 «ˇThis is a very long line that will be wrapped.
5666
5667 This is another paragraph in the same selection.»
5668
5669 «\tThis is a very long indented line that will be wrapped.ˇ»
5670 "},
5671 indoc! {"
5672 «ˇThis is a very long line that will be
5673 wrapped.
5674
5675 This is another paragraph in the same
5676 selection.»
5677
5678 «\tThis is a very long indented line
5679 \tthat will be wrapped.ˇ»
5680 "},
5681 plaintext_language.clone(),
5682 &mut cx,
5683 );
5684
5685 // Test that an empty comment line acts as a paragraph boundary
5686 assert_rewrap(
5687 indoc! {"
5688 // ˇThis is a long comment that will be wrapped.
5689 //
5690 // And this is another long comment that will also be wrapped.ˇ
5691 "},
5692 indoc! {"
5693 // ˇThis is a long comment that will be
5694 // wrapped.
5695 //
5696 // And this is another long comment that
5697 // will also be wrapped.ˇ
5698 "},
5699 cpp_language,
5700 &mut cx,
5701 );
5702
5703 #[track_caller]
5704 fn assert_rewrap(
5705 unwrapped_text: &str,
5706 wrapped_text: &str,
5707 language: Arc<Language>,
5708 cx: &mut EditorTestContext,
5709 ) {
5710 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
5711 cx.set_state(unwrapped_text);
5712 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
5713 cx.assert_editor_state(wrapped_text);
5714 }
5715}
5716
5717#[gpui::test]
5718async fn test_hard_wrap(cx: &mut TestAppContext) {
5719 init_test(cx, |_| {});
5720 let mut cx = EditorTestContext::new(cx).await;
5721
5722 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
5723 cx.update_editor(|editor, _, cx| {
5724 editor.set_hard_wrap(Some(14), cx);
5725 });
5726
5727 cx.set_state(indoc!(
5728 "
5729 one two three ˇ
5730 "
5731 ));
5732 cx.simulate_input("four");
5733 cx.run_until_parked();
5734
5735 cx.assert_editor_state(indoc!(
5736 "
5737 one two three
5738 fourˇ
5739 "
5740 ));
5741
5742 cx.update_editor(|editor, window, cx| {
5743 editor.newline(&Default::default(), window, cx);
5744 });
5745 cx.run_until_parked();
5746 cx.assert_editor_state(indoc!(
5747 "
5748 one two three
5749 four
5750 ˇ
5751 "
5752 ));
5753
5754 cx.simulate_input("five");
5755 cx.run_until_parked();
5756 cx.assert_editor_state(indoc!(
5757 "
5758 one two three
5759 four
5760 fiveˇ
5761 "
5762 ));
5763
5764 cx.update_editor(|editor, window, cx| {
5765 editor.newline(&Default::default(), window, cx);
5766 });
5767 cx.run_until_parked();
5768 cx.simulate_input("# ");
5769 cx.run_until_parked();
5770 cx.assert_editor_state(indoc!(
5771 "
5772 one two three
5773 four
5774 five
5775 # ˇ
5776 "
5777 ));
5778
5779 cx.update_editor(|editor, window, cx| {
5780 editor.newline(&Default::default(), window, cx);
5781 });
5782 cx.run_until_parked();
5783 cx.assert_editor_state(indoc!(
5784 "
5785 one two three
5786 four
5787 five
5788 #\x20
5789 #ˇ
5790 "
5791 ));
5792
5793 cx.simulate_input(" 6");
5794 cx.run_until_parked();
5795 cx.assert_editor_state(indoc!(
5796 "
5797 one two three
5798 four
5799 five
5800 #
5801 # 6ˇ
5802 "
5803 ));
5804}
5805
5806#[gpui::test]
5807async fn test_clipboard(cx: &mut TestAppContext) {
5808 init_test(cx, |_| {});
5809
5810 let mut cx = EditorTestContext::new(cx).await;
5811
5812 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
5813 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5814 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
5815
5816 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
5817 cx.set_state("two ˇfour ˇsix ˇ");
5818 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5819 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
5820
5821 // Paste again but with only two cursors. Since the number of cursors doesn't
5822 // match the number of slices in the clipboard, the entire clipboard text
5823 // is pasted at each cursor.
5824 cx.set_state("ˇtwo one✅ four three six five ˇ");
5825 cx.update_editor(|e, window, cx| {
5826 e.handle_input("( ", window, cx);
5827 e.paste(&Paste, window, cx);
5828 e.handle_input(") ", window, cx);
5829 });
5830 cx.assert_editor_state(
5831 &([
5832 "( one✅ ",
5833 "three ",
5834 "five ) ˇtwo one✅ four three six five ( one✅ ",
5835 "three ",
5836 "five ) ˇ",
5837 ]
5838 .join("\n")),
5839 );
5840
5841 // Cut with three selections, one of which is full-line.
5842 cx.set_state(indoc! {"
5843 1«2ˇ»3
5844 4ˇ567
5845 «8ˇ»9"});
5846 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5847 cx.assert_editor_state(indoc! {"
5848 1ˇ3
5849 ˇ9"});
5850
5851 // Paste with three selections, noticing how the copied selection that was full-line
5852 // gets inserted before the second cursor.
5853 cx.set_state(indoc! {"
5854 1ˇ3
5855 9ˇ
5856 «oˇ»ne"});
5857 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5858 cx.assert_editor_state(indoc! {"
5859 12ˇ3
5860 4567
5861 9ˇ
5862 8ˇne"});
5863
5864 // Copy with a single cursor only, which writes the whole line into the clipboard.
5865 cx.set_state(indoc! {"
5866 The quick brown
5867 fox juˇmps over
5868 the lazy dog"});
5869 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5870 assert_eq!(
5871 cx.read_from_clipboard()
5872 .and_then(|item| item.text().as_deref().map(str::to_string)),
5873 Some("fox jumps over\n".to_string())
5874 );
5875
5876 // Paste with three selections, noticing how the copied full-line selection is inserted
5877 // before the empty selections but replaces the selection that is non-empty.
5878 cx.set_state(indoc! {"
5879 Tˇhe quick brown
5880 «foˇ»x jumps over
5881 tˇhe lazy dog"});
5882 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5883 cx.assert_editor_state(indoc! {"
5884 fox jumps over
5885 Tˇhe quick brown
5886 fox jumps over
5887 ˇx jumps over
5888 fox jumps over
5889 tˇhe lazy dog"});
5890}
5891
5892#[gpui::test]
5893async fn test_copy_trim(cx: &mut TestAppContext) {
5894 init_test(cx, |_| {});
5895
5896 let mut cx = EditorTestContext::new(cx).await;
5897 cx.set_state(
5898 r#" «for selection in selections.iter() {
5899 let mut start = selection.start;
5900 let mut end = selection.end;
5901 let is_entire_line = selection.is_empty();
5902 if is_entire_line {
5903 start = Point::new(start.row, 0);ˇ»
5904 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5905 }
5906 "#,
5907 );
5908 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5909 assert_eq!(
5910 cx.read_from_clipboard()
5911 .and_then(|item| item.text().as_deref().map(str::to_string)),
5912 Some(
5913 "for selection in selections.iter() {
5914 let mut start = selection.start;
5915 let mut end = selection.end;
5916 let is_entire_line = selection.is_empty();
5917 if is_entire_line {
5918 start = Point::new(start.row, 0);"
5919 .to_string()
5920 ),
5921 "Regular copying preserves all indentation selected",
5922 );
5923 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5924 assert_eq!(
5925 cx.read_from_clipboard()
5926 .and_then(|item| item.text().as_deref().map(str::to_string)),
5927 Some(
5928 "for selection in selections.iter() {
5929let mut start = selection.start;
5930let mut end = selection.end;
5931let is_entire_line = selection.is_empty();
5932if is_entire_line {
5933 start = Point::new(start.row, 0);"
5934 .to_string()
5935 ),
5936 "Copying with stripping should strip all leading whitespaces"
5937 );
5938
5939 cx.set_state(
5940 r#" « for selection in selections.iter() {
5941 let mut start = selection.start;
5942 let mut end = selection.end;
5943 let is_entire_line = selection.is_empty();
5944 if is_entire_line {
5945 start = Point::new(start.row, 0);ˇ»
5946 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5947 }
5948 "#,
5949 );
5950 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5951 assert_eq!(
5952 cx.read_from_clipboard()
5953 .and_then(|item| item.text().as_deref().map(str::to_string)),
5954 Some(
5955 " for selection in selections.iter() {
5956 let mut start = selection.start;
5957 let mut end = selection.end;
5958 let is_entire_line = selection.is_empty();
5959 if is_entire_line {
5960 start = Point::new(start.row, 0);"
5961 .to_string()
5962 ),
5963 "Regular copying preserves all indentation selected",
5964 );
5965 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5966 assert_eq!(
5967 cx.read_from_clipboard()
5968 .and_then(|item| item.text().as_deref().map(str::to_string)),
5969 Some(
5970 "for selection in selections.iter() {
5971let mut start = selection.start;
5972let mut end = selection.end;
5973let is_entire_line = selection.is_empty();
5974if is_entire_line {
5975 start = Point::new(start.row, 0);"
5976 .to_string()
5977 ),
5978 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
5979 );
5980
5981 cx.set_state(
5982 r#" «ˇ for selection in selections.iter() {
5983 let mut start = selection.start;
5984 let mut end = selection.end;
5985 let is_entire_line = selection.is_empty();
5986 if is_entire_line {
5987 start = Point::new(start.row, 0);»
5988 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5989 }
5990 "#,
5991 );
5992 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5993 assert_eq!(
5994 cx.read_from_clipboard()
5995 .and_then(|item| item.text().as_deref().map(str::to_string)),
5996 Some(
5997 " for selection in selections.iter() {
5998 let mut start = selection.start;
5999 let mut end = selection.end;
6000 let is_entire_line = selection.is_empty();
6001 if is_entire_line {
6002 start = Point::new(start.row, 0);"
6003 .to_string()
6004 ),
6005 "Regular copying for reverse selection works the same",
6006 );
6007 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6008 assert_eq!(
6009 cx.read_from_clipboard()
6010 .and_then(|item| item.text().as_deref().map(str::to_string)),
6011 Some(
6012 "for selection in selections.iter() {
6013let mut start = selection.start;
6014let mut end = selection.end;
6015let is_entire_line = selection.is_empty();
6016if is_entire_line {
6017 start = Point::new(start.row, 0);"
6018 .to_string()
6019 ),
6020 "Copying with stripping for reverse selection works the same"
6021 );
6022
6023 cx.set_state(
6024 r#" for selection «in selections.iter() {
6025 let mut start = selection.start;
6026 let mut end = selection.end;
6027 let is_entire_line = selection.is_empty();
6028 if is_entire_line {
6029 start = Point::new(start.row, 0);ˇ»
6030 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6031 }
6032 "#,
6033 );
6034 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6035 assert_eq!(
6036 cx.read_from_clipboard()
6037 .and_then(|item| item.text().as_deref().map(str::to_string)),
6038 Some(
6039 "in selections.iter() {
6040 let mut start = selection.start;
6041 let mut end = selection.end;
6042 let is_entire_line = selection.is_empty();
6043 if is_entire_line {
6044 start = Point::new(start.row, 0);"
6045 .to_string()
6046 ),
6047 "When selecting past the indent, the copying works as usual",
6048 );
6049 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6050 assert_eq!(
6051 cx.read_from_clipboard()
6052 .and_then(|item| item.text().as_deref().map(str::to_string)),
6053 Some(
6054 "in selections.iter() {
6055 let mut start = selection.start;
6056 let mut end = selection.end;
6057 let is_entire_line = selection.is_empty();
6058 if is_entire_line {
6059 start = Point::new(start.row, 0);"
6060 .to_string()
6061 ),
6062 "When selecting past the indent, nothing is trimmed"
6063 );
6064
6065 cx.set_state(
6066 r#" «for selection in selections.iter() {
6067 let mut start = selection.start;
6068
6069 let mut end = selection.end;
6070 let is_entire_line = selection.is_empty();
6071 if is_entire_line {
6072 start = Point::new(start.row, 0);
6073ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
6074 }
6075 "#,
6076 );
6077 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6078 assert_eq!(
6079 cx.read_from_clipboard()
6080 .and_then(|item| item.text().as_deref().map(str::to_string)),
6081 Some(
6082 "for selection in selections.iter() {
6083let mut start = selection.start;
6084
6085let mut end = selection.end;
6086let is_entire_line = selection.is_empty();
6087if is_entire_line {
6088 start = Point::new(start.row, 0);
6089"
6090 .to_string()
6091 ),
6092 "Copying with stripping should ignore empty lines"
6093 );
6094}
6095
6096#[gpui::test]
6097async fn test_paste_multiline(cx: &mut TestAppContext) {
6098 init_test(cx, |_| {});
6099
6100 let mut cx = EditorTestContext::new(cx).await;
6101 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
6102
6103 // Cut an indented block, without the leading whitespace.
6104 cx.set_state(indoc! {"
6105 const a: B = (
6106 c(),
6107 «d(
6108 e,
6109 f
6110 )ˇ»
6111 );
6112 "});
6113 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6114 cx.assert_editor_state(indoc! {"
6115 const a: B = (
6116 c(),
6117 ˇ
6118 );
6119 "});
6120
6121 // Paste it at the same position.
6122 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6123 cx.assert_editor_state(indoc! {"
6124 const a: B = (
6125 c(),
6126 d(
6127 e,
6128 f
6129 )ˇ
6130 );
6131 "});
6132
6133 // Paste it at a line with a lower indent level.
6134 cx.set_state(indoc! {"
6135 ˇ
6136 const a: B = (
6137 c(),
6138 );
6139 "});
6140 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6141 cx.assert_editor_state(indoc! {"
6142 d(
6143 e,
6144 f
6145 )ˇ
6146 const a: B = (
6147 c(),
6148 );
6149 "});
6150
6151 // Cut an indented block, with the leading whitespace.
6152 cx.set_state(indoc! {"
6153 const a: B = (
6154 c(),
6155 « d(
6156 e,
6157 f
6158 )
6159 ˇ»);
6160 "});
6161 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6162 cx.assert_editor_state(indoc! {"
6163 const a: B = (
6164 c(),
6165 ˇ);
6166 "});
6167
6168 // Paste it at the same position.
6169 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6170 cx.assert_editor_state(indoc! {"
6171 const a: B = (
6172 c(),
6173 d(
6174 e,
6175 f
6176 )
6177 ˇ);
6178 "});
6179
6180 // Paste it at a line with a higher indent level.
6181 cx.set_state(indoc! {"
6182 const a: B = (
6183 c(),
6184 d(
6185 e,
6186 fˇ
6187 )
6188 );
6189 "});
6190 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6191 cx.assert_editor_state(indoc! {"
6192 const a: B = (
6193 c(),
6194 d(
6195 e,
6196 f d(
6197 e,
6198 f
6199 )
6200 ˇ
6201 )
6202 );
6203 "});
6204
6205 // Copy an indented block, starting mid-line
6206 cx.set_state(indoc! {"
6207 const a: B = (
6208 c(),
6209 somethin«g(
6210 e,
6211 f
6212 )ˇ»
6213 );
6214 "});
6215 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6216
6217 // Paste it on a line with a lower indent level
6218 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
6219 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6220 cx.assert_editor_state(indoc! {"
6221 const a: B = (
6222 c(),
6223 something(
6224 e,
6225 f
6226 )
6227 );
6228 g(
6229 e,
6230 f
6231 )ˇ"});
6232}
6233
6234#[gpui::test]
6235async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
6236 init_test(cx, |_| {});
6237
6238 cx.write_to_clipboard(ClipboardItem::new_string(
6239 " d(\n e\n );\n".into(),
6240 ));
6241
6242 let mut cx = EditorTestContext::new(cx).await;
6243 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
6244
6245 cx.set_state(indoc! {"
6246 fn a() {
6247 b();
6248 if c() {
6249 ˇ
6250 }
6251 }
6252 "});
6253
6254 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6255 cx.assert_editor_state(indoc! {"
6256 fn a() {
6257 b();
6258 if c() {
6259 d(
6260 e
6261 );
6262 ˇ
6263 }
6264 }
6265 "});
6266
6267 cx.set_state(indoc! {"
6268 fn a() {
6269 b();
6270 ˇ
6271 }
6272 "});
6273
6274 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6275 cx.assert_editor_state(indoc! {"
6276 fn a() {
6277 b();
6278 d(
6279 e
6280 );
6281 ˇ
6282 }
6283 "});
6284}
6285
6286#[gpui::test]
6287fn test_select_all(cx: &mut TestAppContext) {
6288 init_test(cx, |_| {});
6289
6290 let editor = cx.add_window(|window, cx| {
6291 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
6292 build_editor(buffer, window, cx)
6293 });
6294 _ = editor.update(cx, |editor, window, cx| {
6295 editor.select_all(&SelectAll, window, cx);
6296 assert_eq!(
6297 editor.selections.display_ranges(cx),
6298 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
6299 );
6300 });
6301}
6302
6303#[gpui::test]
6304fn test_select_line(cx: &mut TestAppContext) {
6305 init_test(cx, |_| {});
6306
6307 let editor = cx.add_window(|window, cx| {
6308 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
6309 build_editor(buffer, window, cx)
6310 });
6311 _ = editor.update(cx, |editor, window, cx| {
6312 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6313 s.select_display_ranges([
6314 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6315 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
6316 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
6317 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
6318 ])
6319 });
6320 editor.select_line(&SelectLine, window, cx);
6321 assert_eq!(
6322 editor.selections.display_ranges(cx),
6323 vec![
6324 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
6325 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
6326 ]
6327 );
6328 });
6329
6330 _ = editor.update(cx, |editor, window, cx| {
6331 editor.select_line(&SelectLine, window, cx);
6332 assert_eq!(
6333 editor.selections.display_ranges(cx),
6334 vec![
6335 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
6336 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
6337 ]
6338 );
6339 });
6340
6341 _ = editor.update(cx, |editor, window, cx| {
6342 editor.select_line(&SelectLine, window, cx);
6343 assert_eq!(
6344 editor.selections.display_ranges(cx),
6345 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
6346 );
6347 });
6348}
6349
6350#[gpui::test]
6351async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
6352 init_test(cx, |_| {});
6353 let mut cx = EditorTestContext::new(cx).await;
6354
6355 #[track_caller]
6356 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
6357 cx.set_state(initial_state);
6358 cx.update_editor(|e, window, cx| {
6359 e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
6360 });
6361 cx.assert_editor_state(expected_state);
6362 }
6363
6364 // Selection starts and ends at the middle of lines, left-to-right
6365 test(
6366 &mut cx,
6367 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
6368 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
6369 );
6370 // Same thing, right-to-left
6371 test(
6372 &mut cx,
6373 "aa\nb«b\ncc\ndd\neˇ»e\nff",
6374 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
6375 );
6376
6377 // Whole buffer, left-to-right, last line *doesn't* end with newline
6378 test(
6379 &mut cx,
6380 "«ˇaa\nbb\ncc\ndd\nee\nff»",
6381 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
6382 );
6383 // Same thing, right-to-left
6384 test(
6385 &mut cx,
6386 "«aa\nbb\ncc\ndd\nee\nffˇ»",
6387 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
6388 );
6389
6390 // Whole buffer, left-to-right, last line ends with newline
6391 test(
6392 &mut cx,
6393 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
6394 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
6395 );
6396 // Same thing, right-to-left
6397 test(
6398 &mut cx,
6399 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
6400 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
6401 );
6402
6403 // Starts at the end of a line, ends at the start of another
6404 test(
6405 &mut cx,
6406 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
6407 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
6408 );
6409}
6410
6411#[gpui::test]
6412async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
6413 init_test(cx, |_| {});
6414
6415 let editor = cx.add_window(|window, cx| {
6416 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
6417 build_editor(buffer, window, cx)
6418 });
6419
6420 // setup
6421 _ = editor.update(cx, |editor, window, cx| {
6422 editor.fold_creases(
6423 vec![
6424 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
6425 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
6426 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
6427 ],
6428 true,
6429 window,
6430 cx,
6431 );
6432 assert_eq!(
6433 editor.display_text(cx),
6434 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
6435 );
6436 });
6437
6438 _ = editor.update(cx, |editor, window, cx| {
6439 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6440 s.select_display_ranges([
6441 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6442 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
6443 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
6444 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
6445 ])
6446 });
6447 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
6448 assert_eq!(
6449 editor.display_text(cx),
6450 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
6451 );
6452 });
6453 EditorTestContext::for_editor(editor, cx)
6454 .await
6455 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
6456
6457 _ = editor.update(cx, |editor, window, cx| {
6458 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6459 s.select_display_ranges([
6460 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
6461 ])
6462 });
6463 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
6464 assert_eq!(
6465 editor.display_text(cx),
6466 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
6467 );
6468 assert_eq!(
6469 editor.selections.display_ranges(cx),
6470 [
6471 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
6472 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
6473 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
6474 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
6475 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
6476 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
6477 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
6478 ]
6479 );
6480 });
6481 EditorTestContext::for_editor(editor, cx)
6482 .await
6483 .assert_editor_state(
6484 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
6485 );
6486}
6487
6488#[gpui::test]
6489async fn test_add_selection_above_below(cx: &mut TestAppContext) {
6490 init_test(cx, |_| {});
6491
6492 let mut cx = EditorTestContext::new(cx).await;
6493
6494 cx.set_state(indoc!(
6495 r#"abc
6496 defˇghi
6497
6498 jk
6499 nlmo
6500 "#
6501 ));
6502
6503 cx.update_editor(|editor, window, cx| {
6504 editor.add_selection_above(&Default::default(), window, cx);
6505 });
6506
6507 cx.assert_editor_state(indoc!(
6508 r#"abcˇ
6509 defˇghi
6510
6511 jk
6512 nlmo
6513 "#
6514 ));
6515
6516 cx.update_editor(|editor, window, cx| {
6517 editor.add_selection_above(&Default::default(), window, cx);
6518 });
6519
6520 cx.assert_editor_state(indoc!(
6521 r#"abcˇ
6522 defˇghi
6523
6524 jk
6525 nlmo
6526 "#
6527 ));
6528
6529 cx.update_editor(|editor, window, cx| {
6530 editor.add_selection_below(&Default::default(), window, cx);
6531 });
6532
6533 cx.assert_editor_state(indoc!(
6534 r#"abc
6535 defˇghi
6536
6537 jk
6538 nlmo
6539 "#
6540 ));
6541
6542 cx.update_editor(|editor, window, cx| {
6543 editor.undo_selection(&Default::default(), window, cx);
6544 });
6545
6546 cx.assert_editor_state(indoc!(
6547 r#"abcˇ
6548 defˇghi
6549
6550 jk
6551 nlmo
6552 "#
6553 ));
6554
6555 cx.update_editor(|editor, window, cx| {
6556 editor.redo_selection(&Default::default(), window, cx);
6557 });
6558
6559 cx.assert_editor_state(indoc!(
6560 r#"abc
6561 defˇghi
6562
6563 jk
6564 nlmo
6565 "#
6566 ));
6567
6568 cx.update_editor(|editor, window, cx| {
6569 editor.add_selection_below(&Default::default(), window, cx);
6570 });
6571
6572 cx.assert_editor_state(indoc!(
6573 r#"abc
6574 defˇghi
6575 ˇ
6576 jk
6577 nlmo
6578 "#
6579 ));
6580
6581 cx.update_editor(|editor, window, cx| {
6582 editor.add_selection_below(&Default::default(), window, cx);
6583 });
6584
6585 cx.assert_editor_state(indoc!(
6586 r#"abc
6587 defˇghi
6588 ˇ
6589 jkˇ
6590 nlmo
6591 "#
6592 ));
6593
6594 cx.update_editor(|editor, window, cx| {
6595 editor.add_selection_below(&Default::default(), window, cx);
6596 });
6597
6598 cx.assert_editor_state(indoc!(
6599 r#"abc
6600 defˇghi
6601 ˇ
6602 jkˇ
6603 nlmˇo
6604 "#
6605 ));
6606
6607 cx.update_editor(|editor, window, cx| {
6608 editor.add_selection_below(&Default::default(), window, cx);
6609 });
6610
6611 cx.assert_editor_state(indoc!(
6612 r#"abc
6613 defˇghi
6614 ˇ
6615 jkˇ
6616 nlmˇo
6617 ˇ"#
6618 ));
6619
6620 // change selections
6621 cx.set_state(indoc!(
6622 r#"abc
6623 def«ˇg»hi
6624
6625 jk
6626 nlmo
6627 "#
6628 ));
6629
6630 cx.update_editor(|editor, window, cx| {
6631 editor.add_selection_below(&Default::default(), window, cx);
6632 });
6633
6634 cx.assert_editor_state(indoc!(
6635 r#"abc
6636 def«ˇg»hi
6637
6638 jk
6639 nlm«ˇo»
6640 "#
6641 ));
6642
6643 cx.update_editor(|editor, window, cx| {
6644 editor.add_selection_below(&Default::default(), window, cx);
6645 });
6646
6647 cx.assert_editor_state(indoc!(
6648 r#"abc
6649 def«ˇg»hi
6650
6651 jk
6652 nlm«ˇo»
6653 "#
6654 ));
6655
6656 cx.update_editor(|editor, window, cx| {
6657 editor.add_selection_above(&Default::default(), window, cx);
6658 });
6659
6660 cx.assert_editor_state(indoc!(
6661 r#"abc
6662 def«ˇg»hi
6663
6664 jk
6665 nlmo
6666 "#
6667 ));
6668
6669 cx.update_editor(|editor, window, cx| {
6670 editor.add_selection_above(&Default::default(), window, cx);
6671 });
6672
6673 cx.assert_editor_state(indoc!(
6674 r#"abc
6675 def«ˇg»hi
6676
6677 jk
6678 nlmo
6679 "#
6680 ));
6681
6682 // Change selections again
6683 cx.set_state(indoc!(
6684 r#"a«bc
6685 defgˇ»hi
6686
6687 jk
6688 nlmo
6689 "#
6690 ));
6691
6692 cx.update_editor(|editor, window, cx| {
6693 editor.add_selection_below(&Default::default(), window, cx);
6694 });
6695
6696 cx.assert_editor_state(indoc!(
6697 r#"a«bcˇ»
6698 d«efgˇ»hi
6699
6700 j«kˇ»
6701 nlmo
6702 "#
6703 ));
6704
6705 cx.update_editor(|editor, window, cx| {
6706 editor.add_selection_below(&Default::default(), window, cx);
6707 });
6708 cx.assert_editor_state(indoc!(
6709 r#"a«bcˇ»
6710 d«efgˇ»hi
6711
6712 j«kˇ»
6713 n«lmoˇ»
6714 "#
6715 ));
6716 cx.update_editor(|editor, window, cx| {
6717 editor.add_selection_above(&Default::default(), window, cx);
6718 });
6719
6720 cx.assert_editor_state(indoc!(
6721 r#"a«bcˇ»
6722 d«efgˇ»hi
6723
6724 j«kˇ»
6725 nlmo
6726 "#
6727 ));
6728
6729 // Change selections again
6730 cx.set_state(indoc!(
6731 r#"abc
6732 d«ˇefghi
6733
6734 jk
6735 nlm»o
6736 "#
6737 ));
6738
6739 cx.update_editor(|editor, window, cx| {
6740 editor.add_selection_above(&Default::default(), window, cx);
6741 });
6742
6743 cx.assert_editor_state(indoc!(
6744 r#"a«ˇbc»
6745 d«ˇef»ghi
6746
6747 j«ˇk»
6748 n«ˇlm»o
6749 "#
6750 ));
6751
6752 cx.update_editor(|editor, window, cx| {
6753 editor.add_selection_below(&Default::default(), window, cx);
6754 });
6755
6756 cx.assert_editor_state(indoc!(
6757 r#"abc
6758 d«ˇef»ghi
6759
6760 j«ˇk»
6761 n«ˇlm»o
6762 "#
6763 ));
6764}
6765
6766#[gpui::test]
6767async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
6768 init_test(cx, |_| {});
6769 let mut cx = EditorTestContext::new(cx).await;
6770
6771 cx.set_state(indoc!(
6772 r#"line onˇe
6773 liˇne two
6774 line three
6775 line four"#
6776 ));
6777
6778 cx.update_editor(|editor, window, cx| {
6779 editor.add_selection_below(&Default::default(), window, cx);
6780 });
6781
6782 // test multiple cursors expand in the same direction
6783 cx.assert_editor_state(indoc!(
6784 r#"line onˇe
6785 liˇne twˇo
6786 liˇne three
6787 line four"#
6788 ));
6789
6790 cx.update_editor(|editor, window, cx| {
6791 editor.add_selection_below(&Default::default(), window, cx);
6792 });
6793
6794 cx.update_editor(|editor, window, cx| {
6795 editor.add_selection_below(&Default::default(), window, cx);
6796 });
6797
6798 // test multiple cursors expand below overflow
6799 cx.assert_editor_state(indoc!(
6800 r#"line onˇe
6801 liˇne twˇo
6802 liˇne thˇree
6803 liˇne foˇur"#
6804 ));
6805
6806 cx.update_editor(|editor, window, cx| {
6807 editor.add_selection_above(&Default::default(), window, cx);
6808 });
6809
6810 // test multiple cursors retrieves back correctly
6811 cx.assert_editor_state(indoc!(
6812 r#"line onˇe
6813 liˇne twˇo
6814 liˇne thˇree
6815 line four"#
6816 ));
6817
6818 cx.update_editor(|editor, window, cx| {
6819 editor.add_selection_above(&Default::default(), window, cx);
6820 });
6821
6822 cx.update_editor(|editor, window, cx| {
6823 editor.add_selection_above(&Default::default(), window, cx);
6824 });
6825
6826 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
6827 cx.assert_editor_state(indoc!(
6828 r#"liˇne onˇe
6829 liˇne two
6830 line three
6831 line four"#
6832 ));
6833
6834 cx.update_editor(|editor, window, cx| {
6835 editor.undo_selection(&Default::default(), window, cx);
6836 });
6837
6838 // test undo
6839 cx.assert_editor_state(indoc!(
6840 r#"line onˇe
6841 liˇne twˇo
6842 line three
6843 line four"#
6844 ));
6845
6846 cx.update_editor(|editor, window, cx| {
6847 editor.redo_selection(&Default::default(), window, cx);
6848 });
6849
6850 // test redo
6851 cx.assert_editor_state(indoc!(
6852 r#"liˇne onˇe
6853 liˇne two
6854 line three
6855 line four"#
6856 ));
6857
6858 cx.set_state(indoc!(
6859 r#"abcd
6860 ef«ghˇ»
6861 ijkl
6862 «mˇ»nop"#
6863 ));
6864
6865 cx.update_editor(|editor, window, cx| {
6866 editor.add_selection_above(&Default::default(), window, cx);
6867 });
6868
6869 // test multiple selections expand in the same direction
6870 cx.assert_editor_state(indoc!(
6871 r#"ab«cdˇ»
6872 ef«ghˇ»
6873 «iˇ»jkl
6874 «mˇ»nop"#
6875 ));
6876
6877 cx.update_editor(|editor, window, cx| {
6878 editor.add_selection_above(&Default::default(), window, cx);
6879 });
6880
6881 // test multiple selection upward overflow
6882 cx.assert_editor_state(indoc!(
6883 r#"ab«cdˇ»
6884 «eˇ»f«ghˇ»
6885 «iˇ»jkl
6886 «mˇ»nop"#
6887 ));
6888
6889 cx.update_editor(|editor, window, cx| {
6890 editor.add_selection_below(&Default::default(), window, cx);
6891 });
6892
6893 // test multiple selection retrieves back correctly
6894 cx.assert_editor_state(indoc!(
6895 r#"abcd
6896 ef«ghˇ»
6897 «iˇ»jkl
6898 «mˇ»nop"#
6899 ));
6900
6901 cx.update_editor(|editor, window, cx| {
6902 editor.add_selection_below(&Default::default(), window, cx);
6903 });
6904
6905 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
6906 cx.assert_editor_state(indoc!(
6907 r#"abcd
6908 ef«ghˇ»
6909 ij«klˇ»
6910 «mˇ»nop"#
6911 ));
6912
6913 cx.update_editor(|editor, window, cx| {
6914 editor.undo_selection(&Default::default(), window, cx);
6915 });
6916
6917 // test undo
6918 cx.assert_editor_state(indoc!(
6919 r#"abcd
6920 ef«ghˇ»
6921 «iˇ»jkl
6922 «mˇ»nop"#
6923 ));
6924
6925 cx.update_editor(|editor, window, cx| {
6926 editor.redo_selection(&Default::default(), window, cx);
6927 });
6928
6929 // test redo
6930 cx.assert_editor_state(indoc!(
6931 r#"abcd
6932 ef«ghˇ»
6933 ij«klˇ»
6934 «mˇ»nop"#
6935 ));
6936}
6937
6938#[gpui::test]
6939async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
6940 init_test(cx, |_| {});
6941 let mut cx = EditorTestContext::new(cx).await;
6942
6943 cx.set_state(indoc!(
6944 r#"line onˇe
6945 liˇne two
6946 line three
6947 line four"#
6948 ));
6949
6950 cx.update_editor(|editor, window, cx| {
6951 editor.add_selection_below(&Default::default(), window, cx);
6952 editor.add_selection_below(&Default::default(), window, cx);
6953 editor.add_selection_below(&Default::default(), window, cx);
6954 });
6955
6956 // initial state with two multi cursor groups
6957 cx.assert_editor_state(indoc!(
6958 r#"line onˇe
6959 liˇne twˇo
6960 liˇne thˇree
6961 liˇne foˇur"#
6962 ));
6963
6964 // add single cursor in middle - simulate opt click
6965 cx.update_editor(|editor, window, cx| {
6966 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
6967 editor.begin_selection(new_cursor_point, true, 1, window, cx);
6968 editor.end_selection(window, cx);
6969 });
6970
6971 cx.assert_editor_state(indoc!(
6972 r#"line onˇe
6973 liˇne twˇo
6974 liˇneˇ thˇree
6975 liˇne foˇur"#
6976 ));
6977
6978 cx.update_editor(|editor, window, cx| {
6979 editor.add_selection_above(&Default::default(), window, cx);
6980 });
6981
6982 // test new added selection expands above and existing selection shrinks
6983 cx.assert_editor_state(indoc!(
6984 r#"line onˇe
6985 liˇneˇ twˇo
6986 liˇneˇ thˇree
6987 line four"#
6988 ));
6989
6990 cx.update_editor(|editor, window, cx| {
6991 editor.add_selection_above(&Default::default(), window, cx);
6992 });
6993
6994 // test new added selection expands above and existing selection shrinks
6995 cx.assert_editor_state(indoc!(
6996 r#"lineˇ onˇe
6997 liˇneˇ twˇo
6998 lineˇ three
6999 line four"#
7000 ));
7001
7002 // intial state with two selection groups
7003 cx.set_state(indoc!(
7004 r#"abcd
7005 ef«ghˇ»
7006 ijkl
7007 «mˇ»nop"#
7008 ));
7009
7010 cx.update_editor(|editor, window, cx| {
7011 editor.add_selection_above(&Default::default(), window, cx);
7012 editor.add_selection_above(&Default::default(), window, cx);
7013 });
7014
7015 cx.assert_editor_state(indoc!(
7016 r#"ab«cdˇ»
7017 «eˇ»f«ghˇ»
7018 «iˇ»jkl
7019 «mˇ»nop"#
7020 ));
7021
7022 // add single selection in middle - simulate opt drag
7023 cx.update_editor(|editor, window, cx| {
7024 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
7025 editor.begin_selection(new_cursor_point, true, 1, window, cx);
7026 editor.update_selection(
7027 DisplayPoint::new(DisplayRow(2), 4),
7028 0,
7029 gpui::Point::<f32>::default(),
7030 window,
7031 cx,
7032 );
7033 editor.end_selection(window, cx);
7034 });
7035
7036 cx.assert_editor_state(indoc!(
7037 r#"ab«cdˇ»
7038 «eˇ»f«ghˇ»
7039 «iˇ»jk«lˇ»
7040 «mˇ»nop"#
7041 ));
7042
7043 cx.update_editor(|editor, window, cx| {
7044 editor.add_selection_below(&Default::default(), window, cx);
7045 });
7046
7047 // test new added selection expands below, others shrinks from above
7048 cx.assert_editor_state(indoc!(
7049 r#"abcd
7050 ef«ghˇ»
7051 «iˇ»jk«lˇ»
7052 «mˇ»no«pˇ»"#
7053 ));
7054}
7055
7056#[gpui::test]
7057async fn test_select_next(cx: &mut TestAppContext) {
7058 init_test(cx, |_| {});
7059
7060 let mut cx = EditorTestContext::new(cx).await;
7061 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
7062
7063 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7064 .unwrap();
7065 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7066
7067 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7068 .unwrap();
7069 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
7070
7071 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7072 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7073
7074 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7075 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
7076
7077 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7078 .unwrap();
7079 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7080
7081 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7082 .unwrap();
7083 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7084
7085 // Test selection direction should be preserved
7086 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
7087
7088 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7089 .unwrap();
7090 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
7091}
7092
7093#[gpui::test]
7094async fn test_select_all_matches(cx: &mut TestAppContext) {
7095 init_test(cx, |_| {});
7096
7097 let mut cx = EditorTestContext::new(cx).await;
7098
7099 // Test caret-only selections
7100 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
7101 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7102 .unwrap();
7103 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7104
7105 // Test left-to-right selections
7106 cx.set_state("abc\n«abcˇ»\nabc");
7107 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7108 .unwrap();
7109 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
7110
7111 // Test right-to-left selections
7112 cx.set_state("abc\n«ˇabc»\nabc");
7113 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7114 .unwrap();
7115 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
7116
7117 // Test selecting whitespace with caret selection
7118 cx.set_state("abc\nˇ abc\nabc");
7119 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7120 .unwrap();
7121 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
7122
7123 // Test selecting whitespace with left-to-right selection
7124 cx.set_state("abc\n«ˇ »abc\nabc");
7125 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7126 .unwrap();
7127 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
7128
7129 // Test no matches with right-to-left selection
7130 cx.set_state("abc\n« ˇ»abc\nabc");
7131 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7132 .unwrap();
7133 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
7134
7135 // Test with a single word and clip_at_line_ends=true (#29823)
7136 cx.set_state("aˇbc");
7137 cx.update_editor(|e, window, cx| {
7138 e.set_clip_at_line_ends(true, cx);
7139 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
7140 e.set_clip_at_line_ends(false, cx);
7141 });
7142 cx.assert_editor_state("«abcˇ»");
7143}
7144
7145#[gpui::test]
7146async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
7147 init_test(cx, |_| {});
7148
7149 let mut cx = EditorTestContext::new(cx).await;
7150
7151 let large_body_1 = "\nd".repeat(200);
7152 let large_body_2 = "\ne".repeat(200);
7153
7154 cx.set_state(&format!(
7155 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
7156 ));
7157 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
7158 let scroll_position = editor.scroll_position(cx);
7159 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
7160 scroll_position
7161 });
7162
7163 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7164 .unwrap();
7165 cx.assert_editor_state(&format!(
7166 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
7167 ));
7168 let scroll_position_after_selection =
7169 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
7170 assert_eq!(
7171 initial_scroll_position, scroll_position_after_selection,
7172 "Scroll position should not change after selecting all matches"
7173 );
7174}
7175
7176#[gpui::test]
7177async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
7178 init_test(cx, |_| {});
7179
7180 let mut cx = EditorLspTestContext::new_rust(
7181 lsp::ServerCapabilities {
7182 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7183 ..Default::default()
7184 },
7185 cx,
7186 )
7187 .await;
7188
7189 cx.set_state(indoc! {"
7190 line 1
7191 line 2
7192 linˇe 3
7193 line 4
7194 line 5
7195 "});
7196
7197 // Make an edit
7198 cx.update_editor(|editor, window, cx| {
7199 editor.handle_input("X", window, cx);
7200 });
7201
7202 // Move cursor to a different position
7203 cx.update_editor(|editor, window, cx| {
7204 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7205 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
7206 });
7207 });
7208
7209 cx.assert_editor_state(indoc! {"
7210 line 1
7211 line 2
7212 linXe 3
7213 line 4
7214 liˇne 5
7215 "});
7216
7217 cx.lsp
7218 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
7219 Ok(Some(vec![lsp::TextEdit::new(
7220 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
7221 "PREFIX ".to_string(),
7222 )]))
7223 });
7224
7225 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
7226 .unwrap()
7227 .await
7228 .unwrap();
7229
7230 cx.assert_editor_state(indoc! {"
7231 PREFIX line 1
7232 line 2
7233 linXe 3
7234 line 4
7235 liˇne 5
7236 "});
7237
7238 // Undo formatting
7239 cx.update_editor(|editor, window, cx| {
7240 editor.undo(&Default::default(), window, cx);
7241 });
7242
7243 // Verify cursor moved back to position after edit
7244 cx.assert_editor_state(indoc! {"
7245 line 1
7246 line 2
7247 linXˇe 3
7248 line 4
7249 line 5
7250 "});
7251}
7252
7253#[gpui::test]
7254async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext) {
7255 init_test(cx, |_| {});
7256
7257 let mut cx = EditorTestContext::new(cx).await;
7258
7259 let provider = cx.new(|_| FakeEditPredictionProvider::default());
7260 cx.update_editor(|editor, window, cx| {
7261 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
7262 });
7263
7264 cx.set_state(indoc! {"
7265 line 1
7266 line 2
7267 linˇe 3
7268 line 4
7269 line 5
7270 line 6
7271 line 7
7272 line 8
7273 line 9
7274 line 10
7275 "});
7276
7277 let snapshot = cx.buffer_snapshot();
7278 let edit_position = snapshot.anchor_after(Point::new(2, 4));
7279
7280 cx.update(|_, cx| {
7281 provider.update(cx, |provider, _| {
7282 provider.set_edit_prediction(Some(edit_prediction::EditPrediction {
7283 id: None,
7284 edits: vec![(edit_position..edit_position, "X".into())],
7285 edit_preview: None,
7286 }))
7287 })
7288 });
7289
7290 cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
7291 cx.update_editor(|editor, window, cx| {
7292 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
7293 });
7294
7295 cx.assert_editor_state(indoc! {"
7296 line 1
7297 line 2
7298 lineXˇ 3
7299 line 4
7300 line 5
7301 line 6
7302 line 7
7303 line 8
7304 line 9
7305 line 10
7306 "});
7307
7308 cx.update_editor(|editor, window, cx| {
7309 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7310 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
7311 });
7312 });
7313
7314 cx.assert_editor_state(indoc! {"
7315 line 1
7316 line 2
7317 lineX 3
7318 line 4
7319 line 5
7320 line 6
7321 line 7
7322 line 8
7323 line 9
7324 liˇne 10
7325 "});
7326
7327 cx.update_editor(|editor, window, cx| {
7328 editor.undo(&Default::default(), window, cx);
7329 });
7330
7331 cx.assert_editor_state(indoc! {"
7332 line 1
7333 line 2
7334 lineˇ 3
7335 line 4
7336 line 5
7337 line 6
7338 line 7
7339 line 8
7340 line 9
7341 line 10
7342 "});
7343}
7344
7345#[gpui::test]
7346async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
7347 init_test(cx, |_| {});
7348
7349 let mut cx = EditorTestContext::new(cx).await;
7350 cx.set_state(
7351 r#"let foo = 2;
7352lˇet foo = 2;
7353let fooˇ = 2;
7354let foo = 2;
7355let foo = ˇ2;"#,
7356 );
7357
7358 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7359 .unwrap();
7360 cx.assert_editor_state(
7361 r#"let foo = 2;
7362«letˇ» foo = 2;
7363let «fooˇ» = 2;
7364let foo = 2;
7365let foo = «2ˇ»;"#,
7366 );
7367
7368 // noop for multiple selections with different contents
7369 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7370 .unwrap();
7371 cx.assert_editor_state(
7372 r#"let foo = 2;
7373«letˇ» foo = 2;
7374let «fooˇ» = 2;
7375let foo = 2;
7376let foo = «2ˇ»;"#,
7377 );
7378
7379 // Test last selection direction should be preserved
7380 cx.set_state(
7381 r#"let foo = 2;
7382let foo = 2;
7383let «fooˇ» = 2;
7384let «ˇfoo» = 2;
7385let foo = 2;"#,
7386 );
7387
7388 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7389 .unwrap();
7390 cx.assert_editor_state(
7391 r#"let foo = 2;
7392let foo = 2;
7393let «fooˇ» = 2;
7394let «ˇfoo» = 2;
7395let «ˇfoo» = 2;"#,
7396 );
7397}
7398
7399#[gpui::test]
7400async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
7401 init_test(cx, |_| {});
7402
7403 let mut cx =
7404 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
7405
7406 cx.assert_editor_state(indoc! {"
7407 ˇbbb
7408 ccc
7409
7410 bbb
7411 ccc
7412 "});
7413 cx.dispatch_action(SelectPrevious::default());
7414 cx.assert_editor_state(indoc! {"
7415 «bbbˇ»
7416 ccc
7417
7418 bbb
7419 ccc
7420 "});
7421 cx.dispatch_action(SelectPrevious::default());
7422 cx.assert_editor_state(indoc! {"
7423 «bbbˇ»
7424 ccc
7425
7426 «bbbˇ»
7427 ccc
7428 "});
7429}
7430
7431#[gpui::test]
7432async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
7433 init_test(cx, |_| {});
7434
7435 let mut cx = EditorTestContext::new(cx).await;
7436 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
7437
7438 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7439 .unwrap();
7440 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7441
7442 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7443 .unwrap();
7444 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
7445
7446 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7447 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7448
7449 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7450 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
7451
7452 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7453 .unwrap();
7454 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
7455
7456 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7457 .unwrap();
7458 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7459}
7460
7461#[gpui::test]
7462async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
7463 init_test(cx, |_| {});
7464
7465 let mut cx = EditorTestContext::new(cx).await;
7466 cx.set_state("aˇ");
7467
7468 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7469 .unwrap();
7470 cx.assert_editor_state("«aˇ»");
7471 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7472 .unwrap();
7473 cx.assert_editor_state("«aˇ»");
7474}
7475
7476#[gpui::test]
7477async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
7478 init_test(cx, |_| {});
7479
7480 let mut cx = EditorTestContext::new(cx).await;
7481 cx.set_state(
7482 r#"let foo = 2;
7483lˇet foo = 2;
7484let fooˇ = 2;
7485let foo = 2;
7486let foo = ˇ2;"#,
7487 );
7488
7489 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7490 .unwrap();
7491 cx.assert_editor_state(
7492 r#"let foo = 2;
7493«letˇ» foo = 2;
7494let «fooˇ» = 2;
7495let foo = 2;
7496let foo = «2ˇ»;"#,
7497 );
7498
7499 // noop for multiple selections with different contents
7500 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7501 .unwrap();
7502 cx.assert_editor_state(
7503 r#"let foo = 2;
7504«letˇ» foo = 2;
7505let «fooˇ» = 2;
7506let foo = 2;
7507let foo = «2ˇ»;"#,
7508 );
7509}
7510
7511#[gpui::test]
7512async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
7513 init_test(cx, |_| {});
7514
7515 let mut cx = EditorTestContext::new(cx).await;
7516 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
7517
7518 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7519 .unwrap();
7520 // selection direction is preserved
7521 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
7522
7523 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7524 .unwrap();
7525 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
7526
7527 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7528 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
7529
7530 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7531 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
7532
7533 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7534 .unwrap();
7535 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
7536
7537 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7538 .unwrap();
7539 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
7540}
7541
7542#[gpui::test]
7543async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
7544 init_test(cx, |_| {});
7545
7546 let language = Arc::new(Language::new(
7547 LanguageConfig::default(),
7548 Some(tree_sitter_rust::LANGUAGE.into()),
7549 ));
7550
7551 let text = r#"
7552 use mod1::mod2::{mod3, mod4};
7553
7554 fn fn_1(param1: bool, param2: &str) {
7555 let var1 = "text";
7556 }
7557 "#
7558 .unindent();
7559
7560 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7561 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7562 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7563
7564 editor
7565 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7566 .await;
7567
7568 editor.update_in(cx, |editor, window, cx| {
7569 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7570 s.select_display_ranges([
7571 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
7572 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
7573 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
7574 ]);
7575 });
7576 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7577 });
7578 editor.update(cx, |editor, cx| {
7579 assert_text_with_selections(
7580 editor,
7581 indoc! {r#"
7582 use mod1::mod2::{mod3, «mod4ˇ»};
7583
7584 fn fn_1«ˇ(param1: bool, param2: &str)» {
7585 let var1 = "«ˇtext»";
7586 }
7587 "#},
7588 cx,
7589 );
7590 });
7591
7592 editor.update_in(cx, |editor, window, cx| {
7593 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7594 });
7595 editor.update(cx, |editor, cx| {
7596 assert_text_with_selections(
7597 editor,
7598 indoc! {r#"
7599 use mod1::mod2::«{mod3, mod4}ˇ»;
7600
7601 «ˇfn fn_1(param1: bool, param2: &str) {
7602 let var1 = "text";
7603 }»
7604 "#},
7605 cx,
7606 );
7607 });
7608
7609 editor.update_in(cx, |editor, window, cx| {
7610 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7611 });
7612 assert_eq!(
7613 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
7614 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
7615 );
7616
7617 // Trying to expand the selected syntax node one more time has no effect.
7618 editor.update_in(cx, |editor, window, cx| {
7619 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7620 });
7621 assert_eq!(
7622 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
7623 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
7624 );
7625
7626 editor.update_in(cx, |editor, window, cx| {
7627 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7628 });
7629 editor.update(cx, |editor, cx| {
7630 assert_text_with_selections(
7631 editor,
7632 indoc! {r#"
7633 use mod1::mod2::«{mod3, mod4}ˇ»;
7634
7635 «ˇfn fn_1(param1: bool, param2: &str) {
7636 let var1 = "text";
7637 }»
7638 "#},
7639 cx,
7640 );
7641 });
7642
7643 editor.update_in(cx, |editor, window, cx| {
7644 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7645 });
7646 editor.update(cx, |editor, cx| {
7647 assert_text_with_selections(
7648 editor,
7649 indoc! {r#"
7650 use mod1::mod2::{mod3, «mod4ˇ»};
7651
7652 fn fn_1«ˇ(param1: bool, param2: &str)» {
7653 let var1 = "«ˇtext»";
7654 }
7655 "#},
7656 cx,
7657 );
7658 });
7659
7660 editor.update_in(cx, |editor, window, cx| {
7661 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7662 });
7663 editor.update(cx, |editor, cx| {
7664 assert_text_with_selections(
7665 editor,
7666 indoc! {r#"
7667 use mod1::mod2::{mod3, mo«ˇ»d4};
7668
7669 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
7670 let var1 = "te«ˇ»xt";
7671 }
7672 "#},
7673 cx,
7674 );
7675 });
7676
7677 // Trying to shrink the selected syntax node one more time has no effect.
7678 editor.update_in(cx, |editor, window, cx| {
7679 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7680 });
7681 editor.update_in(cx, |editor, _, cx| {
7682 assert_text_with_selections(
7683 editor,
7684 indoc! {r#"
7685 use mod1::mod2::{mod3, mo«ˇ»d4};
7686
7687 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
7688 let var1 = "te«ˇ»xt";
7689 }
7690 "#},
7691 cx,
7692 );
7693 });
7694
7695 // Ensure that we keep expanding the selection if the larger selection starts or ends within
7696 // a fold.
7697 editor.update_in(cx, |editor, window, cx| {
7698 editor.fold_creases(
7699 vec![
7700 Crease::simple(
7701 Point::new(0, 21)..Point::new(0, 24),
7702 FoldPlaceholder::test(),
7703 ),
7704 Crease::simple(
7705 Point::new(3, 20)..Point::new(3, 22),
7706 FoldPlaceholder::test(),
7707 ),
7708 ],
7709 true,
7710 window,
7711 cx,
7712 );
7713 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7714 });
7715 editor.update(cx, |editor, cx| {
7716 assert_text_with_selections(
7717 editor,
7718 indoc! {r#"
7719 use mod1::mod2::«{mod3, mod4}ˇ»;
7720
7721 fn fn_1«ˇ(param1: bool, param2: &str)» {
7722 let var1 = "«ˇtext»";
7723 }
7724 "#},
7725 cx,
7726 );
7727 });
7728}
7729
7730#[gpui::test]
7731async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
7732 init_test(cx, |_| {});
7733
7734 let language = Arc::new(Language::new(
7735 LanguageConfig::default(),
7736 Some(tree_sitter_rust::LANGUAGE.into()),
7737 ));
7738
7739 let text = "let a = 2;";
7740
7741 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7742 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7743 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7744
7745 editor
7746 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7747 .await;
7748
7749 // Test case 1: Cursor at end of word
7750 editor.update_in(cx, |editor, window, cx| {
7751 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7752 s.select_display_ranges([
7753 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
7754 ]);
7755 });
7756 });
7757 editor.update(cx, |editor, cx| {
7758 assert_text_with_selections(editor, "let aˇ = 2;", cx);
7759 });
7760 editor.update_in(cx, |editor, window, cx| {
7761 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7762 });
7763 editor.update(cx, |editor, cx| {
7764 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
7765 });
7766 editor.update_in(cx, |editor, window, cx| {
7767 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7768 });
7769 editor.update(cx, |editor, cx| {
7770 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
7771 });
7772
7773 // Test case 2: Cursor at end of statement
7774 editor.update_in(cx, |editor, window, cx| {
7775 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7776 s.select_display_ranges([
7777 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
7778 ]);
7779 });
7780 });
7781 editor.update(cx, |editor, cx| {
7782 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
7783 });
7784 editor.update_in(cx, |editor, window, cx| {
7785 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7786 });
7787 editor.update(cx, |editor, cx| {
7788 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
7789 });
7790}
7791
7792#[gpui::test]
7793async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
7794 init_test(cx, |_| {});
7795
7796 let language = Arc::new(Language::new(
7797 LanguageConfig::default(),
7798 Some(tree_sitter_rust::LANGUAGE.into()),
7799 ));
7800
7801 let text = r#"
7802 use mod1::mod2::{mod3, mod4};
7803
7804 fn fn_1(param1: bool, param2: &str) {
7805 let var1 = "hello world";
7806 }
7807 "#
7808 .unindent();
7809
7810 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7811 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7812 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7813
7814 editor
7815 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7816 .await;
7817
7818 // Test 1: Cursor on a letter of a string word
7819 editor.update_in(cx, |editor, window, cx| {
7820 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7821 s.select_display_ranges([
7822 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
7823 ]);
7824 });
7825 });
7826 editor.update_in(cx, |editor, window, cx| {
7827 assert_text_with_selections(
7828 editor,
7829 indoc! {r#"
7830 use mod1::mod2::{mod3, mod4};
7831
7832 fn fn_1(param1: bool, param2: &str) {
7833 let var1 = "hˇello world";
7834 }
7835 "#},
7836 cx,
7837 );
7838 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7839 assert_text_with_selections(
7840 editor,
7841 indoc! {r#"
7842 use mod1::mod2::{mod3, mod4};
7843
7844 fn fn_1(param1: bool, param2: &str) {
7845 let var1 = "«ˇhello» world";
7846 }
7847 "#},
7848 cx,
7849 );
7850 });
7851
7852 // Test 2: Partial selection within a word
7853 editor.update_in(cx, |editor, window, cx| {
7854 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7855 s.select_display_ranges([
7856 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
7857 ]);
7858 });
7859 });
7860 editor.update_in(cx, |editor, window, cx| {
7861 assert_text_with_selections(
7862 editor,
7863 indoc! {r#"
7864 use mod1::mod2::{mod3, mod4};
7865
7866 fn fn_1(param1: bool, param2: &str) {
7867 let var1 = "h«elˇ»lo world";
7868 }
7869 "#},
7870 cx,
7871 );
7872 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7873 assert_text_with_selections(
7874 editor,
7875 indoc! {r#"
7876 use mod1::mod2::{mod3, mod4};
7877
7878 fn fn_1(param1: bool, param2: &str) {
7879 let var1 = "«ˇhello» world";
7880 }
7881 "#},
7882 cx,
7883 );
7884 });
7885
7886 // Test 3: Complete word already selected
7887 editor.update_in(cx, |editor, window, cx| {
7888 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7889 s.select_display_ranges([
7890 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
7891 ]);
7892 });
7893 });
7894 editor.update_in(cx, |editor, window, cx| {
7895 assert_text_with_selections(
7896 editor,
7897 indoc! {r#"
7898 use mod1::mod2::{mod3, mod4};
7899
7900 fn fn_1(param1: bool, param2: &str) {
7901 let var1 = "«helloˇ» world";
7902 }
7903 "#},
7904 cx,
7905 );
7906 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7907 assert_text_with_selections(
7908 editor,
7909 indoc! {r#"
7910 use mod1::mod2::{mod3, mod4};
7911
7912 fn fn_1(param1: bool, param2: &str) {
7913 let var1 = "«hello worldˇ»";
7914 }
7915 "#},
7916 cx,
7917 );
7918 });
7919
7920 // Test 4: Selection spanning across words
7921 editor.update_in(cx, |editor, window, cx| {
7922 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7923 s.select_display_ranges([
7924 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
7925 ]);
7926 });
7927 });
7928 editor.update_in(cx, |editor, window, cx| {
7929 assert_text_with_selections(
7930 editor,
7931 indoc! {r#"
7932 use mod1::mod2::{mod3, mod4};
7933
7934 fn fn_1(param1: bool, param2: &str) {
7935 let var1 = "hel«lo woˇ»rld";
7936 }
7937 "#},
7938 cx,
7939 );
7940 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7941 assert_text_with_selections(
7942 editor,
7943 indoc! {r#"
7944 use mod1::mod2::{mod3, mod4};
7945
7946 fn fn_1(param1: bool, param2: &str) {
7947 let var1 = "«ˇhello world»";
7948 }
7949 "#},
7950 cx,
7951 );
7952 });
7953
7954 // Test 5: Expansion beyond string
7955 editor.update_in(cx, |editor, window, cx| {
7956 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7957 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7958 assert_text_with_selections(
7959 editor,
7960 indoc! {r#"
7961 use mod1::mod2::{mod3, mod4};
7962
7963 fn fn_1(param1: bool, param2: &str) {
7964 «ˇlet var1 = "hello world";»
7965 }
7966 "#},
7967 cx,
7968 );
7969 });
7970}
7971
7972#[gpui::test]
7973async fn test_fold_function_bodies(cx: &mut TestAppContext) {
7974 init_test(cx, |_| {});
7975
7976 let base_text = r#"
7977 impl A {
7978 // this is an uncommitted comment
7979
7980 fn b() {
7981 c();
7982 }
7983
7984 // this is another uncommitted comment
7985
7986 fn d() {
7987 // e
7988 // f
7989 }
7990 }
7991
7992 fn g() {
7993 // h
7994 }
7995 "#
7996 .unindent();
7997
7998 let text = r#"
7999 ˇimpl A {
8000
8001 fn b() {
8002 c();
8003 }
8004
8005 fn d() {
8006 // e
8007 // f
8008 }
8009 }
8010
8011 fn g() {
8012 // h
8013 }
8014 "#
8015 .unindent();
8016
8017 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
8018 cx.set_state(&text);
8019 cx.set_head_text(&base_text);
8020 cx.update_editor(|editor, window, cx| {
8021 editor.expand_all_diff_hunks(&Default::default(), window, cx);
8022 });
8023
8024 cx.assert_state_with_diff(
8025 "
8026 ˇimpl A {
8027 - // this is an uncommitted comment
8028
8029 fn b() {
8030 c();
8031 }
8032
8033 - // this is another uncommitted comment
8034 -
8035 fn d() {
8036 // e
8037 // f
8038 }
8039 }
8040
8041 fn g() {
8042 // h
8043 }
8044 "
8045 .unindent(),
8046 );
8047
8048 let expected_display_text = "
8049 impl A {
8050 // this is an uncommitted comment
8051
8052 fn b() {
8053 ⋯
8054 }
8055
8056 // this is another uncommitted comment
8057
8058 fn d() {
8059 ⋯
8060 }
8061 }
8062
8063 fn g() {
8064 ⋯
8065 }
8066 "
8067 .unindent();
8068
8069 cx.update_editor(|editor, window, cx| {
8070 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
8071 assert_eq!(editor.display_text(cx), expected_display_text);
8072 });
8073}
8074
8075#[gpui::test]
8076async fn test_autoindent(cx: &mut TestAppContext) {
8077 init_test(cx, |_| {});
8078
8079 let language = Arc::new(
8080 Language::new(
8081 LanguageConfig {
8082 brackets: BracketPairConfig {
8083 pairs: vec![
8084 BracketPair {
8085 start: "{".to_string(),
8086 end: "}".to_string(),
8087 close: false,
8088 surround: false,
8089 newline: true,
8090 },
8091 BracketPair {
8092 start: "(".to_string(),
8093 end: ")".to_string(),
8094 close: false,
8095 surround: false,
8096 newline: true,
8097 },
8098 ],
8099 ..Default::default()
8100 },
8101 ..Default::default()
8102 },
8103 Some(tree_sitter_rust::LANGUAGE.into()),
8104 )
8105 .with_indents_query(
8106 r#"
8107 (_ "(" ")" @end) @indent
8108 (_ "{" "}" @end) @indent
8109 "#,
8110 )
8111 .unwrap(),
8112 );
8113
8114 let text = "fn a() {}";
8115
8116 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8117 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8118 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8119 editor
8120 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8121 .await;
8122
8123 editor.update_in(cx, |editor, window, cx| {
8124 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8125 s.select_ranges([5..5, 8..8, 9..9])
8126 });
8127 editor.newline(&Newline, window, cx);
8128 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
8129 assert_eq!(
8130 editor.selections.ranges(cx),
8131 &[
8132 Point::new(1, 4)..Point::new(1, 4),
8133 Point::new(3, 4)..Point::new(3, 4),
8134 Point::new(5, 0)..Point::new(5, 0)
8135 ]
8136 );
8137 });
8138}
8139
8140#[gpui::test]
8141async fn test_autoindent_selections(cx: &mut TestAppContext) {
8142 init_test(cx, |_| {});
8143
8144 {
8145 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
8146 cx.set_state(indoc! {"
8147 impl A {
8148
8149 fn b() {}
8150
8151 «fn c() {
8152
8153 }ˇ»
8154 }
8155 "});
8156
8157 cx.update_editor(|editor, window, cx| {
8158 editor.autoindent(&Default::default(), window, cx);
8159 });
8160
8161 cx.assert_editor_state(indoc! {"
8162 impl A {
8163
8164 fn b() {}
8165
8166 «fn c() {
8167
8168 }ˇ»
8169 }
8170 "});
8171 }
8172
8173 {
8174 let mut cx = EditorTestContext::new_multibuffer(
8175 cx,
8176 [indoc! { "
8177 impl A {
8178 «
8179 // a
8180 fn b(){}
8181 »
8182 «
8183 }
8184 fn c(){}
8185 »
8186 "}],
8187 );
8188
8189 let buffer = cx.update_editor(|editor, _, cx| {
8190 let buffer = editor.buffer().update(cx, |buffer, _| {
8191 buffer.all_buffers().iter().next().unwrap().clone()
8192 });
8193 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
8194 buffer
8195 });
8196
8197 cx.run_until_parked();
8198 cx.update_editor(|editor, window, cx| {
8199 editor.select_all(&Default::default(), window, cx);
8200 editor.autoindent(&Default::default(), window, cx)
8201 });
8202 cx.run_until_parked();
8203
8204 cx.update(|_, cx| {
8205 assert_eq!(
8206 buffer.read(cx).text(),
8207 indoc! { "
8208 impl A {
8209
8210 // a
8211 fn b(){}
8212
8213
8214 }
8215 fn c(){}
8216
8217 " }
8218 )
8219 });
8220 }
8221}
8222
8223#[gpui::test]
8224async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
8225 init_test(cx, |_| {});
8226
8227 let mut cx = EditorTestContext::new(cx).await;
8228
8229 let language = Arc::new(Language::new(
8230 LanguageConfig {
8231 brackets: BracketPairConfig {
8232 pairs: vec![
8233 BracketPair {
8234 start: "{".to_string(),
8235 end: "}".to_string(),
8236 close: true,
8237 surround: true,
8238 newline: true,
8239 },
8240 BracketPair {
8241 start: "(".to_string(),
8242 end: ")".to_string(),
8243 close: true,
8244 surround: true,
8245 newline: true,
8246 },
8247 BracketPair {
8248 start: "/*".to_string(),
8249 end: " */".to_string(),
8250 close: true,
8251 surround: true,
8252 newline: true,
8253 },
8254 BracketPair {
8255 start: "[".to_string(),
8256 end: "]".to_string(),
8257 close: false,
8258 surround: false,
8259 newline: true,
8260 },
8261 BracketPair {
8262 start: "\"".to_string(),
8263 end: "\"".to_string(),
8264 close: true,
8265 surround: true,
8266 newline: false,
8267 },
8268 BracketPair {
8269 start: "<".to_string(),
8270 end: ">".to_string(),
8271 close: false,
8272 surround: true,
8273 newline: true,
8274 },
8275 ],
8276 ..Default::default()
8277 },
8278 autoclose_before: "})]".to_string(),
8279 ..Default::default()
8280 },
8281 Some(tree_sitter_rust::LANGUAGE.into()),
8282 ));
8283
8284 cx.language_registry().add(language.clone());
8285 cx.update_buffer(|buffer, cx| {
8286 buffer.set_language(Some(language), cx);
8287 });
8288
8289 cx.set_state(
8290 &r#"
8291 🏀ˇ
8292 εˇ
8293 ❤️ˇ
8294 "#
8295 .unindent(),
8296 );
8297
8298 // autoclose multiple nested brackets at multiple cursors
8299 cx.update_editor(|editor, window, cx| {
8300 editor.handle_input("{", window, cx);
8301 editor.handle_input("{", window, cx);
8302 editor.handle_input("{", window, cx);
8303 });
8304 cx.assert_editor_state(
8305 &"
8306 🏀{{{ˇ}}}
8307 ε{{{ˇ}}}
8308 ❤️{{{ˇ}}}
8309 "
8310 .unindent(),
8311 );
8312
8313 // insert a different closing bracket
8314 cx.update_editor(|editor, window, cx| {
8315 editor.handle_input(")", window, cx);
8316 });
8317 cx.assert_editor_state(
8318 &"
8319 🏀{{{)ˇ}}}
8320 ε{{{)ˇ}}}
8321 ❤️{{{)ˇ}}}
8322 "
8323 .unindent(),
8324 );
8325
8326 // skip over the auto-closed brackets when typing a closing bracket
8327 cx.update_editor(|editor, window, cx| {
8328 editor.move_right(&MoveRight, window, cx);
8329 editor.handle_input("}", window, cx);
8330 editor.handle_input("}", window, cx);
8331 editor.handle_input("}", window, cx);
8332 });
8333 cx.assert_editor_state(
8334 &"
8335 🏀{{{)}}}}ˇ
8336 ε{{{)}}}}ˇ
8337 ❤️{{{)}}}}ˇ
8338 "
8339 .unindent(),
8340 );
8341
8342 // autoclose multi-character pairs
8343 cx.set_state(
8344 &"
8345 ˇ
8346 ˇ
8347 "
8348 .unindent(),
8349 );
8350 cx.update_editor(|editor, window, cx| {
8351 editor.handle_input("/", window, cx);
8352 editor.handle_input("*", window, cx);
8353 });
8354 cx.assert_editor_state(
8355 &"
8356 /*ˇ */
8357 /*ˇ */
8358 "
8359 .unindent(),
8360 );
8361
8362 // one cursor autocloses a multi-character pair, one cursor
8363 // does not autoclose.
8364 cx.set_state(
8365 &"
8366 /ˇ
8367 ˇ
8368 "
8369 .unindent(),
8370 );
8371 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
8372 cx.assert_editor_state(
8373 &"
8374 /*ˇ */
8375 *ˇ
8376 "
8377 .unindent(),
8378 );
8379
8380 // Don't autoclose if the next character isn't whitespace and isn't
8381 // listed in the language's "autoclose_before" section.
8382 cx.set_state("ˇa b");
8383 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8384 cx.assert_editor_state("{ˇa b");
8385
8386 // Don't autoclose if `close` is false for the bracket pair
8387 cx.set_state("ˇ");
8388 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
8389 cx.assert_editor_state("[ˇ");
8390
8391 // Surround with brackets if text is selected
8392 cx.set_state("«aˇ» b");
8393 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8394 cx.assert_editor_state("{«aˇ»} b");
8395
8396 // Autoclose when not immediately after a word character
8397 cx.set_state("a ˇ");
8398 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8399 cx.assert_editor_state("a \"ˇ\"");
8400
8401 // Autoclose pair where the start and end characters are the same
8402 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8403 cx.assert_editor_state("a \"\"ˇ");
8404
8405 // Don't autoclose when immediately after a word character
8406 cx.set_state("aˇ");
8407 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8408 cx.assert_editor_state("a\"ˇ");
8409
8410 // Do autoclose when after a non-word character
8411 cx.set_state("{ˇ");
8412 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8413 cx.assert_editor_state("{\"ˇ\"");
8414
8415 // Non identical pairs autoclose regardless of preceding character
8416 cx.set_state("aˇ");
8417 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8418 cx.assert_editor_state("a{ˇ}");
8419
8420 // Don't autoclose pair if autoclose is disabled
8421 cx.set_state("ˇ");
8422 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
8423 cx.assert_editor_state("<ˇ");
8424
8425 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
8426 cx.set_state("«aˇ» b");
8427 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
8428 cx.assert_editor_state("<«aˇ»> b");
8429}
8430
8431#[gpui::test]
8432async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
8433 init_test(cx, |settings| {
8434 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
8435 });
8436
8437 let mut cx = EditorTestContext::new(cx).await;
8438
8439 let language = Arc::new(Language::new(
8440 LanguageConfig {
8441 brackets: BracketPairConfig {
8442 pairs: vec![
8443 BracketPair {
8444 start: "{".to_string(),
8445 end: "}".to_string(),
8446 close: true,
8447 surround: true,
8448 newline: true,
8449 },
8450 BracketPair {
8451 start: "(".to_string(),
8452 end: ")".to_string(),
8453 close: true,
8454 surround: true,
8455 newline: true,
8456 },
8457 BracketPair {
8458 start: "[".to_string(),
8459 end: "]".to_string(),
8460 close: false,
8461 surround: false,
8462 newline: true,
8463 },
8464 ],
8465 ..Default::default()
8466 },
8467 autoclose_before: "})]".to_string(),
8468 ..Default::default()
8469 },
8470 Some(tree_sitter_rust::LANGUAGE.into()),
8471 ));
8472
8473 cx.language_registry().add(language.clone());
8474 cx.update_buffer(|buffer, cx| {
8475 buffer.set_language(Some(language), cx);
8476 });
8477
8478 cx.set_state(
8479 &"
8480 ˇ
8481 ˇ
8482 ˇ
8483 "
8484 .unindent(),
8485 );
8486
8487 // ensure only matching closing brackets are skipped over
8488 cx.update_editor(|editor, window, cx| {
8489 editor.handle_input("}", window, cx);
8490 editor.move_left(&MoveLeft, window, cx);
8491 editor.handle_input(")", window, cx);
8492 editor.move_left(&MoveLeft, window, cx);
8493 });
8494 cx.assert_editor_state(
8495 &"
8496 ˇ)}
8497 ˇ)}
8498 ˇ)}
8499 "
8500 .unindent(),
8501 );
8502
8503 // skip-over closing brackets at multiple cursors
8504 cx.update_editor(|editor, window, cx| {
8505 editor.handle_input(")", window, cx);
8506 editor.handle_input("}", window, cx);
8507 });
8508 cx.assert_editor_state(
8509 &"
8510 )}ˇ
8511 )}ˇ
8512 )}ˇ
8513 "
8514 .unindent(),
8515 );
8516
8517 // ignore non-close brackets
8518 cx.update_editor(|editor, window, cx| {
8519 editor.handle_input("]", window, cx);
8520 editor.move_left(&MoveLeft, window, cx);
8521 editor.handle_input("]", window, cx);
8522 });
8523 cx.assert_editor_state(
8524 &"
8525 )}]ˇ]
8526 )}]ˇ]
8527 )}]ˇ]
8528 "
8529 .unindent(),
8530 );
8531}
8532
8533#[gpui::test]
8534async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
8535 init_test(cx, |_| {});
8536
8537 let mut cx = EditorTestContext::new(cx).await;
8538
8539 let html_language = Arc::new(
8540 Language::new(
8541 LanguageConfig {
8542 name: "HTML".into(),
8543 brackets: BracketPairConfig {
8544 pairs: vec![
8545 BracketPair {
8546 start: "<".into(),
8547 end: ">".into(),
8548 close: true,
8549 ..Default::default()
8550 },
8551 BracketPair {
8552 start: "{".into(),
8553 end: "}".into(),
8554 close: true,
8555 ..Default::default()
8556 },
8557 BracketPair {
8558 start: "(".into(),
8559 end: ")".into(),
8560 close: true,
8561 ..Default::default()
8562 },
8563 ],
8564 ..Default::default()
8565 },
8566 autoclose_before: "})]>".into(),
8567 ..Default::default()
8568 },
8569 Some(tree_sitter_html::LANGUAGE.into()),
8570 )
8571 .with_injection_query(
8572 r#"
8573 (script_element
8574 (raw_text) @injection.content
8575 (#set! injection.language "javascript"))
8576 "#,
8577 )
8578 .unwrap(),
8579 );
8580
8581 let javascript_language = Arc::new(Language::new(
8582 LanguageConfig {
8583 name: "JavaScript".into(),
8584 brackets: BracketPairConfig {
8585 pairs: vec![
8586 BracketPair {
8587 start: "/*".into(),
8588 end: " */".into(),
8589 close: true,
8590 ..Default::default()
8591 },
8592 BracketPair {
8593 start: "{".into(),
8594 end: "}".into(),
8595 close: true,
8596 ..Default::default()
8597 },
8598 BracketPair {
8599 start: "(".into(),
8600 end: ")".into(),
8601 close: true,
8602 ..Default::default()
8603 },
8604 ],
8605 ..Default::default()
8606 },
8607 autoclose_before: "})]>".into(),
8608 ..Default::default()
8609 },
8610 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
8611 ));
8612
8613 cx.language_registry().add(html_language.clone());
8614 cx.language_registry().add(javascript_language.clone());
8615 cx.executor().run_until_parked();
8616
8617 cx.update_buffer(|buffer, cx| {
8618 buffer.set_language(Some(html_language), cx);
8619 });
8620
8621 cx.set_state(
8622 &r#"
8623 <body>ˇ
8624 <script>
8625 var x = 1;ˇ
8626 </script>
8627 </body>ˇ
8628 "#
8629 .unindent(),
8630 );
8631
8632 // Precondition: different languages are active at different locations.
8633 cx.update_editor(|editor, window, cx| {
8634 let snapshot = editor.snapshot(window, cx);
8635 let cursors = editor.selections.ranges::<usize>(cx);
8636 let languages = cursors
8637 .iter()
8638 .map(|c| snapshot.language_at(c.start).unwrap().name())
8639 .collect::<Vec<_>>();
8640 assert_eq!(
8641 languages,
8642 &["HTML".into(), "JavaScript".into(), "HTML".into()]
8643 );
8644 });
8645
8646 // Angle brackets autoclose in HTML, but not JavaScript.
8647 cx.update_editor(|editor, window, cx| {
8648 editor.handle_input("<", window, cx);
8649 editor.handle_input("a", window, cx);
8650 });
8651 cx.assert_editor_state(
8652 &r#"
8653 <body><aˇ>
8654 <script>
8655 var x = 1;<aˇ
8656 </script>
8657 </body><aˇ>
8658 "#
8659 .unindent(),
8660 );
8661
8662 // Curly braces and parens autoclose in both HTML and JavaScript.
8663 cx.update_editor(|editor, window, cx| {
8664 editor.handle_input(" b=", window, cx);
8665 editor.handle_input("{", window, cx);
8666 editor.handle_input("c", window, cx);
8667 editor.handle_input("(", window, cx);
8668 });
8669 cx.assert_editor_state(
8670 &r#"
8671 <body><a b={c(ˇ)}>
8672 <script>
8673 var x = 1;<a b={c(ˇ)}
8674 </script>
8675 </body><a b={c(ˇ)}>
8676 "#
8677 .unindent(),
8678 );
8679
8680 // Brackets that were already autoclosed are skipped.
8681 cx.update_editor(|editor, window, cx| {
8682 editor.handle_input(")", window, cx);
8683 editor.handle_input("d", window, cx);
8684 editor.handle_input("}", window, cx);
8685 });
8686 cx.assert_editor_state(
8687 &r#"
8688 <body><a b={c()d}ˇ>
8689 <script>
8690 var x = 1;<a b={c()d}ˇ
8691 </script>
8692 </body><a b={c()d}ˇ>
8693 "#
8694 .unindent(),
8695 );
8696 cx.update_editor(|editor, window, cx| {
8697 editor.handle_input(">", window, cx);
8698 });
8699 cx.assert_editor_state(
8700 &r#"
8701 <body><a b={c()d}>ˇ
8702 <script>
8703 var x = 1;<a b={c()d}>ˇ
8704 </script>
8705 </body><a b={c()d}>ˇ
8706 "#
8707 .unindent(),
8708 );
8709
8710 // Reset
8711 cx.set_state(
8712 &r#"
8713 <body>ˇ
8714 <script>
8715 var x = 1;ˇ
8716 </script>
8717 </body>ˇ
8718 "#
8719 .unindent(),
8720 );
8721
8722 cx.update_editor(|editor, window, cx| {
8723 editor.handle_input("<", window, cx);
8724 });
8725 cx.assert_editor_state(
8726 &r#"
8727 <body><ˇ>
8728 <script>
8729 var x = 1;<ˇ
8730 </script>
8731 </body><ˇ>
8732 "#
8733 .unindent(),
8734 );
8735
8736 // When backspacing, the closing angle brackets are removed.
8737 cx.update_editor(|editor, window, cx| {
8738 editor.backspace(&Backspace, window, cx);
8739 });
8740 cx.assert_editor_state(
8741 &r#"
8742 <body>ˇ
8743 <script>
8744 var x = 1;ˇ
8745 </script>
8746 </body>ˇ
8747 "#
8748 .unindent(),
8749 );
8750
8751 // Block comments autoclose in JavaScript, but not HTML.
8752 cx.update_editor(|editor, window, cx| {
8753 editor.handle_input("/", window, cx);
8754 editor.handle_input("*", window, cx);
8755 });
8756 cx.assert_editor_state(
8757 &r#"
8758 <body>/*ˇ
8759 <script>
8760 var x = 1;/*ˇ */
8761 </script>
8762 </body>/*ˇ
8763 "#
8764 .unindent(),
8765 );
8766}
8767
8768#[gpui::test]
8769async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
8770 init_test(cx, |_| {});
8771
8772 let mut cx = EditorTestContext::new(cx).await;
8773
8774 let rust_language = Arc::new(
8775 Language::new(
8776 LanguageConfig {
8777 name: "Rust".into(),
8778 brackets: serde_json::from_value(json!([
8779 { "start": "{", "end": "}", "close": true, "newline": true },
8780 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
8781 ]))
8782 .unwrap(),
8783 autoclose_before: "})]>".into(),
8784 ..Default::default()
8785 },
8786 Some(tree_sitter_rust::LANGUAGE.into()),
8787 )
8788 .with_override_query("(string_literal) @string")
8789 .unwrap(),
8790 );
8791
8792 cx.language_registry().add(rust_language.clone());
8793 cx.update_buffer(|buffer, cx| {
8794 buffer.set_language(Some(rust_language), cx);
8795 });
8796
8797 cx.set_state(
8798 &r#"
8799 let x = ˇ
8800 "#
8801 .unindent(),
8802 );
8803
8804 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
8805 cx.update_editor(|editor, window, cx| {
8806 editor.handle_input("\"", window, cx);
8807 });
8808 cx.assert_editor_state(
8809 &r#"
8810 let x = "ˇ"
8811 "#
8812 .unindent(),
8813 );
8814
8815 // Inserting another quotation mark. The cursor moves across the existing
8816 // automatically-inserted quotation mark.
8817 cx.update_editor(|editor, window, cx| {
8818 editor.handle_input("\"", window, cx);
8819 });
8820 cx.assert_editor_state(
8821 &r#"
8822 let x = ""ˇ
8823 "#
8824 .unindent(),
8825 );
8826
8827 // Reset
8828 cx.set_state(
8829 &r#"
8830 let x = ˇ
8831 "#
8832 .unindent(),
8833 );
8834
8835 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
8836 cx.update_editor(|editor, window, cx| {
8837 editor.handle_input("\"", window, cx);
8838 editor.handle_input(" ", window, cx);
8839 editor.move_left(&Default::default(), window, cx);
8840 editor.handle_input("\\", window, cx);
8841 editor.handle_input("\"", window, cx);
8842 });
8843 cx.assert_editor_state(
8844 &r#"
8845 let x = "\"ˇ "
8846 "#
8847 .unindent(),
8848 );
8849
8850 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
8851 // mark. Nothing is inserted.
8852 cx.update_editor(|editor, window, cx| {
8853 editor.move_right(&Default::default(), window, cx);
8854 editor.handle_input("\"", window, cx);
8855 });
8856 cx.assert_editor_state(
8857 &r#"
8858 let x = "\" "ˇ
8859 "#
8860 .unindent(),
8861 );
8862}
8863
8864#[gpui::test]
8865async fn test_surround_with_pair(cx: &mut TestAppContext) {
8866 init_test(cx, |_| {});
8867
8868 let language = Arc::new(Language::new(
8869 LanguageConfig {
8870 brackets: BracketPairConfig {
8871 pairs: vec![
8872 BracketPair {
8873 start: "{".to_string(),
8874 end: "}".to_string(),
8875 close: true,
8876 surround: true,
8877 newline: true,
8878 },
8879 BracketPair {
8880 start: "/* ".to_string(),
8881 end: "*/".to_string(),
8882 close: true,
8883 surround: true,
8884 ..Default::default()
8885 },
8886 ],
8887 ..Default::default()
8888 },
8889 ..Default::default()
8890 },
8891 Some(tree_sitter_rust::LANGUAGE.into()),
8892 ));
8893
8894 let text = r#"
8895 a
8896 b
8897 c
8898 "#
8899 .unindent();
8900
8901 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8902 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8903 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8904 editor
8905 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8906 .await;
8907
8908 editor.update_in(cx, |editor, window, cx| {
8909 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8910 s.select_display_ranges([
8911 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8912 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8913 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
8914 ])
8915 });
8916
8917 editor.handle_input("{", window, cx);
8918 editor.handle_input("{", window, cx);
8919 editor.handle_input("{", window, cx);
8920 assert_eq!(
8921 editor.text(cx),
8922 "
8923 {{{a}}}
8924 {{{b}}}
8925 {{{c}}}
8926 "
8927 .unindent()
8928 );
8929 assert_eq!(
8930 editor.selections.display_ranges(cx),
8931 [
8932 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
8933 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
8934 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
8935 ]
8936 );
8937
8938 editor.undo(&Undo, window, cx);
8939 editor.undo(&Undo, window, cx);
8940 editor.undo(&Undo, window, cx);
8941 assert_eq!(
8942 editor.text(cx),
8943 "
8944 a
8945 b
8946 c
8947 "
8948 .unindent()
8949 );
8950 assert_eq!(
8951 editor.selections.display_ranges(cx),
8952 [
8953 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8954 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8955 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8956 ]
8957 );
8958
8959 // Ensure inserting the first character of a multi-byte bracket pair
8960 // doesn't surround the selections with the bracket.
8961 editor.handle_input("/", window, cx);
8962 assert_eq!(
8963 editor.text(cx),
8964 "
8965 /
8966 /
8967 /
8968 "
8969 .unindent()
8970 );
8971 assert_eq!(
8972 editor.selections.display_ranges(cx),
8973 [
8974 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8975 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8976 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8977 ]
8978 );
8979
8980 editor.undo(&Undo, window, cx);
8981 assert_eq!(
8982 editor.text(cx),
8983 "
8984 a
8985 b
8986 c
8987 "
8988 .unindent()
8989 );
8990 assert_eq!(
8991 editor.selections.display_ranges(cx),
8992 [
8993 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8994 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8995 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8996 ]
8997 );
8998
8999 // Ensure inserting the last character of a multi-byte bracket pair
9000 // doesn't surround the selections with the bracket.
9001 editor.handle_input("*", window, cx);
9002 assert_eq!(
9003 editor.text(cx),
9004 "
9005 *
9006 *
9007 *
9008 "
9009 .unindent()
9010 );
9011 assert_eq!(
9012 editor.selections.display_ranges(cx),
9013 [
9014 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
9015 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
9016 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
9017 ]
9018 );
9019 });
9020}
9021
9022#[gpui::test]
9023async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
9024 init_test(cx, |_| {});
9025
9026 let language = Arc::new(Language::new(
9027 LanguageConfig {
9028 brackets: BracketPairConfig {
9029 pairs: vec![BracketPair {
9030 start: "{".to_string(),
9031 end: "}".to_string(),
9032 close: true,
9033 surround: true,
9034 newline: true,
9035 }],
9036 ..Default::default()
9037 },
9038 autoclose_before: "}".to_string(),
9039 ..Default::default()
9040 },
9041 Some(tree_sitter_rust::LANGUAGE.into()),
9042 ));
9043
9044 let text = r#"
9045 a
9046 b
9047 c
9048 "#
9049 .unindent();
9050
9051 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9052 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9053 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9054 editor
9055 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9056 .await;
9057
9058 editor.update_in(cx, |editor, window, cx| {
9059 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9060 s.select_ranges([
9061 Point::new(0, 1)..Point::new(0, 1),
9062 Point::new(1, 1)..Point::new(1, 1),
9063 Point::new(2, 1)..Point::new(2, 1),
9064 ])
9065 });
9066
9067 editor.handle_input("{", window, cx);
9068 editor.handle_input("{", window, cx);
9069 editor.handle_input("_", window, cx);
9070 assert_eq!(
9071 editor.text(cx),
9072 "
9073 a{{_}}
9074 b{{_}}
9075 c{{_}}
9076 "
9077 .unindent()
9078 );
9079 assert_eq!(
9080 editor.selections.ranges::<Point>(cx),
9081 [
9082 Point::new(0, 4)..Point::new(0, 4),
9083 Point::new(1, 4)..Point::new(1, 4),
9084 Point::new(2, 4)..Point::new(2, 4)
9085 ]
9086 );
9087
9088 editor.backspace(&Default::default(), window, cx);
9089 editor.backspace(&Default::default(), window, cx);
9090 assert_eq!(
9091 editor.text(cx),
9092 "
9093 a{}
9094 b{}
9095 c{}
9096 "
9097 .unindent()
9098 );
9099 assert_eq!(
9100 editor.selections.ranges::<Point>(cx),
9101 [
9102 Point::new(0, 2)..Point::new(0, 2),
9103 Point::new(1, 2)..Point::new(1, 2),
9104 Point::new(2, 2)..Point::new(2, 2)
9105 ]
9106 );
9107
9108 editor.delete_to_previous_word_start(&Default::default(), window, cx);
9109 assert_eq!(
9110 editor.text(cx),
9111 "
9112 a
9113 b
9114 c
9115 "
9116 .unindent()
9117 );
9118 assert_eq!(
9119 editor.selections.ranges::<Point>(cx),
9120 [
9121 Point::new(0, 1)..Point::new(0, 1),
9122 Point::new(1, 1)..Point::new(1, 1),
9123 Point::new(2, 1)..Point::new(2, 1)
9124 ]
9125 );
9126 });
9127}
9128
9129#[gpui::test]
9130async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
9131 init_test(cx, |settings| {
9132 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
9133 });
9134
9135 let mut cx = EditorTestContext::new(cx).await;
9136
9137 let language = Arc::new(Language::new(
9138 LanguageConfig {
9139 brackets: BracketPairConfig {
9140 pairs: vec![
9141 BracketPair {
9142 start: "{".to_string(),
9143 end: "}".to_string(),
9144 close: true,
9145 surround: true,
9146 newline: true,
9147 },
9148 BracketPair {
9149 start: "(".to_string(),
9150 end: ")".to_string(),
9151 close: true,
9152 surround: true,
9153 newline: true,
9154 },
9155 BracketPair {
9156 start: "[".to_string(),
9157 end: "]".to_string(),
9158 close: false,
9159 surround: true,
9160 newline: true,
9161 },
9162 ],
9163 ..Default::default()
9164 },
9165 autoclose_before: "})]".to_string(),
9166 ..Default::default()
9167 },
9168 Some(tree_sitter_rust::LANGUAGE.into()),
9169 ));
9170
9171 cx.language_registry().add(language.clone());
9172 cx.update_buffer(|buffer, cx| {
9173 buffer.set_language(Some(language), cx);
9174 });
9175
9176 cx.set_state(
9177 &"
9178 {(ˇ)}
9179 [[ˇ]]
9180 {(ˇ)}
9181 "
9182 .unindent(),
9183 );
9184
9185 cx.update_editor(|editor, window, cx| {
9186 editor.backspace(&Default::default(), window, cx);
9187 editor.backspace(&Default::default(), window, cx);
9188 });
9189
9190 cx.assert_editor_state(
9191 &"
9192 ˇ
9193 ˇ]]
9194 ˇ
9195 "
9196 .unindent(),
9197 );
9198
9199 cx.update_editor(|editor, window, cx| {
9200 editor.handle_input("{", window, cx);
9201 editor.handle_input("{", window, cx);
9202 editor.move_right(&MoveRight, window, cx);
9203 editor.move_right(&MoveRight, window, cx);
9204 editor.move_left(&MoveLeft, window, cx);
9205 editor.move_left(&MoveLeft, window, cx);
9206 editor.backspace(&Default::default(), window, cx);
9207 });
9208
9209 cx.assert_editor_state(
9210 &"
9211 {ˇ}
9212 {ˇ}]]
9213 {ˇ}
9214 "
9215 .unindent(),
9216 );
9217
9218 cx.update_editor(|editor, window, cx| {
9219 editor.backspace(&Default::default(), window, cx);
9220 });
9221
9222 cx.assert_editor_state(
9223 &"
9224 ˇ
9225 ˇ]]
9226 ˇ
9227 "
9228 .unindent(),
9229 );
9230}
9231
9232#[gpui::test]
9233async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
9234 init_test(cx, |_| {});
9235
9236 let language = Arc::new(Language::new(
9237 LanguageConfig::default(),
9238 Some(tree_sitter_rust::LANGUAGE.into()),
9239 ));
9240
9241 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
9242 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9243 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9244 editor
9245 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9246 .await;
9247
9248 editor.update_in(cx, |editor, window, cx| {
9249 editor.set_auto_replace_emoji_shortcode(true);
9250
9251 editor.handle_input("Hello ", window, cx);
9252 editor.handle_input(":wave", window, cx);
9253 assert_eq!(editor.text(cx), "Hello :wave".unindent());
9254
9255 editor.handle_input(":", window, cx);
9256 assert_eq!(editor.text(cx), "Hello 👋".unindent());
9257
9258 editor.handle_input(" :smile", window, cx);
9259 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
9260
9261 editor.handle_input(":", window, cx);
9262 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
9263
9264 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
9265 editor.handle_input(":wave", window, cx);
9266 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
9267
9268 editor.handle_input(":", window, cx);
9269 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
9270
9271 editor.handle_input(":1", window, cx);
9272 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
9273
9274 editor.handle_input(":", window, cx);
9275 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
9276
9277 // Ensure shortcode does not get replaced when it is part of a word
9278 editor.handle_input(" Test:wave", window, cx);
9279 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
9280
9281 editor.handle_input(":", window, cx);
9282 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
9283
9284 editor.set_auto_replace_emoji_shortcode(false);
9285
9286 // Ensure shortcode does not get replaced when auto replace is off
9287 editor.handle_input(" :wave", window, cx);
9288 assert_eq!(
9289 editor.text(cx),
9290 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
9291 );
9292
9293 editor.handle_input(":", window, cx);
9294 assert_eq!(
9295 editor.text(cx),
9296 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
9297 );
9298 });
9299}
9300
9301#[gpui::test]
9302async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
9303 init_test(cx, |_| {});
9304
9305 let (text, insertion_ranges) = marked_text_ranges(
9306 indoc! {"
9307 ˇ
9308 "},
9309 false,
9310 );
9311
9312 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
9313 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9314
9315 _ = editor.update_in(cx, |editor, window, cx| {
9316 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
9317
9318 editor
9319 .insert_snippet(&insertion_ranges, snippet, window, cx)
9320 .unwrap();
9321
9322 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
9323 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
9324 assert_eq!(editor.text(cx), expected_text);
9325 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
9326 }
9327
9328 assert(
9329 editor,
9330 cx,
9331 indoc! {"
9332 type «» =•
9333 "},
9334 );
9335
9336 assert!(editor.context_menu_visible(), "There should be a matches");
9337 });
9338}
9339
9340#[gpui::test]
9341async fn test_snippets(cx: &mut TestAppContext) {
9342 init_test(cx, |_| {});
9343
9344 let mut cx = EditorTestContext::new(cx).await;
9345
9346 cx.set_state(indoc! {"
9347 a.ˇ b
9348 a.ˇ b
9349 a.ˇ b
9350 "});
9351
9352 cx.update_editor(|editor, window, cx| {
9353 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
9354 let insertion_ranges = editor
9355 .selections
9356 .all(cx)
9357 .iter()
9358 .map(|s| s.range().clone())
9359 .collect::<Vec<_>>();
9360 editor
9361 .insert_snippet(&insertion_ranges, snippet, window, cx)
9362 .unwrap();
9363 });
9364
9365 cx.assert_editor_state(indoc! {"
9366 a.f(«oneˇ», two, «threeˇ») b
9367 a.f(«oneˇ», two, «threeˇ») b
9368 a.f(«oneˇ», two, «threeˇ») b
9369 "});
9370
9371 // Can't move earlier than the first tab stop
9372 cx.update_editor(|editor, window, cx| {
9373 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
9374 });
9375 cx.assert_editor_state(indoc! {"
9376 a.f(«oneˇ», two, «threeˇ») b
9377 a.f(«oneˇ», two, «threeˇ») b
9378 a.f(«oneˇ», two, «threeˇ») b
9379 "});
9380
9381 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9382 cx.assert_editor_state(indoc! {"
9383 a.f(one, «twoˇ», three) b
9384 a.f(one, «twoˇ», three) b
9385 a.f(one, «twoˇ», three) b
9386 "});
9387
9388 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
9389 cx.assert_editor_state(indoc! {"
9390 a.f(«oneˇ», two, «threeˇ») b
9391 a.f(«oneˇ», two, «threeˇ») b
9392 a.f(«oneˇ», two, «threeˇ») b
9393 "});
9394
9395 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9396 cx.assert_editor_state(indoc! {"
9397 a.f(one, «twoˇ», three) b
9398 a.f(one, «twoˇ», three) b
9399 a.f(one, «twoˇ», three) b
9400 "});
9401 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9402 cx.assert_editor_state(indoc! {"
9403 a.f(one, two, three)ˇ b
9404 a.f(one, two, three)ˇ b
9405 a.f(one, two, three)ˇ b
9406 "});
9407
9408 // As soon as the last tab stop is reached, snippet state is gone
9409 cx.update_editor(|editor, window, cx| {
9410 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
9411 });
9412 cx.assert_editor_state(indoc! {"
9413 a.f(one, two, three)ˇ b
9414 a.f(one, two, three)ˇ b
9415 a.f(one, two, three)ˇ b
9416 "});
9417}
9418
9419#[gpui::test]
9420async fn test_snippet_indentation(cx: &mut TestAppContext) {
9421 init_test(cx, |_| {});
9422
9423 let mut cx = EditorTestContext::new(cx).await;
9424
9425 cx.update_editor(|editor, window, cx| {
9426 let snippet = Snippet::parse(indoc! {"
9427 /*
9428 * Multiline comment with leading indentation
9429 *
9430 * $1
9431 */
9432 $0"})
9433 .unwrap();
9434 let insertion_ranges = editor
9435 .selections
9436 .all(cx)
9437 .iter()
9438 .map(|s| s.range().clone())
9439 .collect::<Vec<_>>();
9440 editor
9441 .insert_snippet(&insertion_ranges, snippet, window, cx)
9442 .unwrap();
9443 });
9444
9445 cx.assert_editor_state(indoc! {"
9446 /*
9447 * Multiline comment with leading indentation
9448 *
9449 * ˇ
9450 */
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 /*
9456 * Multiline comment with leading indentation
9457 *
9458 *•
9459 */
9460 ˇ"});
9461}
9462
9463#[gpui::test]
9464async fn test_document_format_during_save(cx: &mut TestAppContext) {
9465 init_test(cx, |_| {});
9466
9467 let fs = FakeFs::new(cx.executor());
9468 fs.insert_file(path!("/file.rs"), Default::default()).await;
9469
9470 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
9471
9472 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9473 language_registry.add(rust_lang());
9474 let mut fake_servers = language_registry.register_fake_lsp(
9475 "Rust",
9476 FakeLspAdapter {
9477 capabilities: lsp::ServerCapabilities {
9478 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9479 ..Default::default()
9480 },
9481 ..Default::default()
9482 },
9483 );
9484
9485 let buffer = project
9486 .update(cx, |project, cx| {
9487 project.open_local_buffer(path!("/file.rs"), cx)
9488 })
9489 .await
9490 .unwrap();
9491
9492 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9493 let (editor, cx) = cx.add_window_view(|window, cx| {
9494 build_editor_with_project(project.clone(), buffer, window, cx)
9495 });
9496 editor.update_in(cx, |editor, window, cx| {
9497 editor.set_text("one\ntwo\nthree\n", window, cx)
9498 });
9499 assert!(cx.read(|cx| editor.is_dirty(cx)));
9500
9501 cx.executor().start_waiting();
9502 let fake_server = fake_servers.next().await.unwrap();
9503
9504 {
9505 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9506 move |params, _| async move {
9507 assert_eq!(
9508 params.text_document.uri,
9509 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9510 );
9511 assert_eq!(params.options.tab_size, 4);
9512 Ok(Some(vec![lsp::TextEdit::new(
9513 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9514 ", ".to_string(),
9515 )]))
9516 },
9517 );
9518 let save = editor
9519 .update_in(cx, |editor, window, cx| {
9520 editor.save(
9521 SaveOptions {
9522 format: true,
9523 autosave: false,
9524 },
9525 project.clone(),
9526 window,
9527 cx,
9528 )
9529 })
9530 .unwrap();
9531 cx.executor().start_waiting();
9532 save.await;
9533
9534 assert_eq!(
9535 editor.update(cx, |editor, cx| editor.text(cx)),
9536 "one, two\nthree\n"
9537 );
9538 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9539 }
9540
9541 {
9542 editor.update_in(cx, |editor, window, cx| {
9543 editor.set_text("one\ntwo\nthree\n", window, cx)
9544 });
9545 assert!(cx.read(|cx| editor.is_dirty(cx)));
9546
9547 // Ensure we can still save even if formatting hangs.
9548 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9549 move |params, _| async move {
9550 assert_eq!(
9551 params.text_document.uri,
9552 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9553 );
9554 futures::future::pending::<()>().await;
9555 unreachable!()
9556 },
9557 );
9558 let save = editor
9559 .update_in(cx, |editor, window, cx| {
9560 editor.save(
9561 SaveOptions {
9562 format: true,
9563 autosave: false,
9564 },
9565 project.clone(),
9566 window,
9567 cx,
9568 )
9569 })
9570 .unwrap();
9571 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9572 cx.executor().start_waiting();
9573 save.await;
9574 assert_eq!(
9575 editor.update(cx, |editor, cx| editor.text(cx)),
9576 "one\ntwo\nthree\n"
9577 );
9578 }
9579
9580 // Set rust language override and assert overridden tabsize is sent to language server
9581 update_test_language_settings(cx, |settings| {
9582 settings.languages.0.insert(
9583 "Rust".into(),
9584 LanguageSettingsContent {
9585 tab_size: NonZeroU32::new(8),
9586 ..Default::default()
9587 },
9588 );
9589 });
9590
9591 {
9592 editor.update_in(cx, |editor, window, cx| {
9593 editor.set_text("somehting_new\n", window, cx)
9594 });
9595 assert!(cx.read(|cx| editor.is_dirty(cx)));
9596 let _formatting_request_signal = fake_server
9597 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
9598 assert_eq!(
9599 params.text_document.uri,
9600 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9601 );
9602 assert_eq!(params.options.tab_size, 8);
9603 Ok(Some(vec![]))
9604 });
9605 let save = editor
9606 .update_in(cx, |editor, window, cx| {
9607 editor.save(
9608 SaveOptions {
9609 format: true,
9610 autosave: false,
9611 },
9612 project.clone(),
9613 window,
9614 cx,
9615 )
9616 })
9617 .unwrap();
9618 cx.executor().start_waiting();
9619 save.await;
9620 }
9621}
9622
9623#[gpui::test]
9624async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
9625 init_test(cx, |settings| {
9626 settings.defaults.ensure_final_newline_on_save = Some(false);
9627 });
9628
9629 let fs = FakeFs::new(cx.executor());
9630 fs.insert_file(path!("/file.txt"), "foo".into()).await;
9631
9632 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
9633
9634 let buffer = project
9635 .update(cx, |project, cx| {
9636 project.open_local_buffer(path!("/file.txt"), cx)
9637 })
9638 .await
9639 .unwrap();
9640
9641 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9642 let (editor, cx) = cx.add_window_view(|window, cx| {
9643 build_editor_with_project(project.clone(), buffer, window, cx)
9644 });
9645 editor.update_in(cx, |editor, window, cx| {
9646 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
9647 s.select_ranges([0..0])
9648 });
9649 });
9650 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9651
9652 editor.update_in(cx, |editor, window, cx| {
9653 editor.handle_input("\n", window, cx)
9654 });
9655 cx.run_until_parked();
9656 save(&editor, &project, cx).await;
9657 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
9658
9659 editor.update_in(cx, |editor, window, cx| {
9660 editor.undo(&Default::default(), window, cx);
9661 });
9662 save(&editor, &project, cx).await;
9663 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
9664
9665 editor.update_in(cx, |editor, window, cx| {
9666 editor.redo(&Default::default(), window, cx);
9667 });
9668 cx.run_until_parked();
9669 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
9670
9671 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
9672 let save = editor
9673 .update_in(cx, |editor, window, cx| {
9674 editor.save(
9675 SaveOptions {
9676 format: true,
9677 autosave: false,
9678 },
9679 project.clone(),
9680 window,
9681 cx,
9682 )
9683 })
9684 .unwrap();
9685 cx.executor().start_waiting();
9686 save.await;
9687 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9688 }
9689}
9690
9691#[gpui::test]
9692async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
9693 init_test(cx, |_| {});
9694
9695 let cols = 4;
9696 let rows = 10;
9697 let sample_text_1 = sample_text(rows, cols, 'a');
9698 assert_eq!(
9699 sample_text_1,
9700 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
9701 );
9702 let sample_text_2 = sample_text(rows, cols, 'l');
9703 assert_eq!(
9704 sample_text_2,
9705 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
9706 );
9707 let sample_text_3 = sample_text(rows, cols, 'v');
9708 assert_eq!(
9709 sample_text_3,
9710 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
9711 );
9712
9713 let fs = FakeFs::new(cx.executor());
9714 fs.insert_tree(
9715 path!("/a"),
9716 json!({
9717 "main.rs": sample_text_1,
9718 "other.rs": sample_text_2,
9719 "lib.rs": sample_text_3,
9720 }),
9721 )
9722 .await;
9723
9724 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
9725 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9726 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
9727
9728 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9729 language_registry.add(rust_lang());
9730 let mut fake_servers = language_registry.register_fake_lsp(
9731 "Rust",
9732 FakeLspAdapter {
9733 capabilities: lsp::ServerCapabilities {
9734 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9735 ..Default::default()
9736 },
9737 ..Default::default()
9738 },
9739 );
9740
9741 let worktree = project.update(cx, |project, cx| {
9742 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
9743 assert_eq!(worktrees.len(), 1);
9744 worktrees.pop().unwrap()
9745 });
9746 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
9747
9748 let buffer_1 = project
9749 .update(cx, |project, cx| {
9750 project.open_buffer((worktree_id, "main.rs"), cx)
9751 })
9752 .await
9753 .unwrap();
9754 let buffer_2 = project
9755 .update(cx, |project, cx| {
9756 project.open_buffer((worktree_id, "other.rs"), cx)
9757 })
9758 .await
9759 .unwrap();
9760 let buffer_3 = project
9761 .update(cx, |project, cx| {
9762 project.open_buffer((worktree_id, "lib.rs"), cx)
9763 })
9764 .await
9765 .unwrap();
9766
9767 let multi_buffer = cx.new(|cx| {
9768 let mut multi_buffer = MultiBuffer::new(ReadWrite);
9769 multi_buffer.push_excerpts(
9770 buffer_1.clone(),
9771 [
9772 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9773 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9774 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9775 ],
9776 cx,
9777 );
9778 multi_buffer.push_excerpts(
9779 buffer_2.clone(),
9780 [
9781 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9782 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9783 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9784 ],
9785 cx,
9786 );
9787 multi_buffer.push_excerpts(
9788 buffer_3.clone(),
9789 [
9790 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9791 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9792 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9793 ],
9794 cx,
9795 );
9796 multi_buffer
9797 });
9798 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
9799 Editor::new(
9800 EditorMode::full(),
9801 multi_buffer,
9802 Some(project.clone()),
9803 window,
9804 cx,
9805 )
9806 });
9807
9808 multi_buffer_editor.update_in(cx, |editor, window, cx| {
9809 editor.change_selections(
9810 SelectionEffects::scroll(Autoscroll::Next),
9811 window,
9812 cx,
9813 |s| s.select_ranges(Some(1..2)),
9814 );
9815 editor.insert("|one|two|three|", window, cx);
9816 });
9817 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
9818 multi_buffer_editor.update_in(cx, |editor, window, cx| {
9819 editor.change_selections(
9820 SelectionEffects::scroll(Autoscroll::Next),
9821 window,
9822 cx,
9823 |s| s.select_ranges(Some(60..70)),
9824 );
9825 editor.insert("|four|five|six|", window, cx);
9826 });
9827 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
9828
9829 // First two buffers should be edited, but not the third one.
9830 assert_eq!(
9831 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
9832 "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}",
9833 );
9834 buffer_1.update(cx, |buffer, _| {
9835 assert!(buffer.is_dirty());
9836 assert_eq!(
9837 buffer.text(),
9838 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
9839 )
9840 });
9841 buffer_2.update(cx, |buffer, _| {
9842 assert!(buffer.is_dirty());
9843 assert_eq!(
9844 buffer.text(),
9845 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
9846 )
9847 });
9848 buffer_3.update(cx, |buffer, _| {
9849 assert!(!buffer.is_dirty());
9850 assert_eq!(buffer.text(), sample_text_3,)
9851 });
9852 cx.executor().run_until_parked();
9853
9854 cx.executor().start_waiting();
9855 let save = multi_buffer_editor
9856 .update_in(cx, |editor, window, cx| {
9857 editor.save(
9858 SaveOptions {
9859 format: true,
9860 autosave: false,
9861 },
9862 project.clone(),
9863 window,
9864 cx,
9865 )
9866 })
9867 .unwrap();
9868
9869 let fake_server = fake_servers.next().await.unwrap();
9870 fake_server
9871 .server
9872 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
9873 Ok(Some(vec![lsp::TextEdit::new(
9874 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9875 format!("[{} formatted]", params.text_document.uri),
9876 )]))
9877 })
9878 .detach();
9879 save.await;
9880
9881 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
9882 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
9883 assert_eq!(
9884 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
9885 uri!(
9886 "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}"
9887 ),
9888 );
9889 buffer_1.update(cx, |buffer, _| {
9890 assert!(!buffer.is_dirty());
9891 assert_eq!(
9892 buffer.text(),
9893 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
9894 )
9895 });
9896 buffer_2.update(cx, |buffer, _| {
9897 assert!(!buffer.is_dirty());
9898 assert_eq!(
9899 buffer.text(),
9900 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
9901 )
9902 });
9903 buffer_3.update(cx, |buffer, _| {
9904 assert!(!buffer.is_dirty());
9905 assert_eq!(buffer.text(), sample_text_3,)
9906 });
9907}
9908
9909#[gpui::test]
9910async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
9911 init_test(cx, |_| {});
9912
9913 let fs = FakeFs::new(cx.executor());
9914 fs.insert_tree(
9915 path!("/dir"),
9916 json!({
9917 "file1.rs": "fn main() { println!(\"hello\"); }",
9918 "file2.rs": "fn test() { println!(\"test\"); }",
9919 "file3.rs": "fn other() { println!(\"other\"); }\n",
9920 }),
9921 )
9922 .await;
9923
9924 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
9925 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9926 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
9927
9928 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9929 language_registry.add(rust_lang());
9930
9931 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
9932 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
9933
9934 // Open three buffers
9935 let buffer_1 = project
9936 .update(cx, |project, cx| {
9937 project.open_buffer((worktree_id, "file1.rs"), cx)
9938 })
9939 .await
9940 .unwrap();
9941 let buffer_2 = project
9942 .update(cx, |project, cx| {
9943 project.open_buffer((worktree_id, "file2.rs"), cx)
9944 })
9945 .await
9946 .unwrap();
9947 let buffer_3 = project
9948 .update(cx, |project, cx| {
9949 project.open_buffer((worktree_id, "file3.rs"), cx)
9950 })
9951 .await
9952 .unwrap();
9953
9954 // Create a multi-buffer with all three buffers
9955 let multi_buffer = cx.new(|cx| {
9956 let mut multi_buffer = MultiBuffer::new(ReadWrite);
9957 multi_buffer.push_excerpts(
9958 buffer_1.clone(),
9959 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9960 cx,
9961 );
9962 multi_buffer.push_excerpts(
9963 buffer_2.clone(),
9964 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9965 cx,
9966 );
9967 multi_buffer.push_excerpts(
9968 buffer_3.clone(),
9969 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9970 cx,
9971 );
9972 multi_buffer
9973 });
9974
9975 let editor = cx.new_window_entity(|window, cx| {
9976 Editor::new(
9977 EditorMode::full(),
9978 multi_buffer,
9979 Some(project.clone()),
9980 window,
9981 cx,
9982 )
9983 });
9984
9985 // Edit only the first buffer
9986 editor.update_in(cx, |editor, window, cx| {
9987 editor.change_selections(
9988 SelectionEffects::scroll(Autoscroll::Next),
9989 window,
9990 cx,
9991 |s| s.select_ranges(Some(10..10)),
9992 );
9993 editor.insert("// edited", window, cx);
9994 });
9995
9996 // Verify that only buffer 1 is dirty
9997 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
9998 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9999 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
10000
10001 // Get write counts after file creation (files were created with initial content)
10002 // We expect each file to have been written once during creation
10003 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
10004 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
10005 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
10006
10007 // Perform autosave
10008 let save_task = editor.update_in(cx, |editor, window, cx| {
10009 editor.save(
10010 SaveOptions {
10011 format: true,
10012 autosave: true,
10013 },
10014 project.clone(),
10015 window,
10016 cx,
10017 )
10018 });
10019 save_task.await.unwrap();
10020
10021 // Only the dirty buffer should have been saved
10022 assert_eq!(
10023 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
10024 1,
10025 "Buffer 1 was dirty, so it should have been written once during autosave"
10026 );
10027 assert_eq!(
10028 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
10029 0,
10030 "Buffer 2 was clean, so it should not have been written during autosave"
10031 );
10032 assert_eq!(
10033 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
10034 0,
10035 "Buffer 3 was clean, so it should not have been written during autosave"
10036 );
10037
10038 // Verify buffer states after autosave
10039 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
10040 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
10041 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
10042
10043 // Now perform a manual save (format = true)
10044 let save_task = editor.update_in(cx, |editor, window, cx| {
10045 editor.save(
10046 SaveOptions {
10047 format: true,
10048 autosave: false,
10049 },
10050 project.clone(),
10051 window,
10052 cx,
10053 )
10054 });
10055 save_task.await.unwrap();
10056
10057 // During manual save, clean buffers don't get written to disk
10058 // They just get did_save called for language server notifications
10059 assert_eq!(
10060 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
10061 1,
10062 "Buffer 1 should only have been written once total (during autosave, not manual save)"
10063 );
10064 assert_eq!(
10065 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
10066 0,
10067 "Buffer 2 should not have been written at all"
10068 );
10069 assert_eq!(
10070 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
10071 0,
10072 "Buffer 3 should not have been written at all"
10073 );
10074}
10075
10076async fn setup_range_format_test(
10077 cx: &mut TestAppContext,
10078) -> (
10079 Entity<Project>,
10080 Entity<Editor>,
10081 &mut gpui::VisualTestContext,
10082 lsp::FakeLanguageServer,
10083) {
10084 init_test(cx, |_| {});
10085
10086 let fs = FakeFs::new(cx.executor());
10087 fs.insert_file(path!("/file.rs"), Default::default()).await;
10088
10089 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10090
10091 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10092 language_registry.add(rust_lang());
10093 let mut fake_servers = language_registry.register_fake_lsp(
10094 "Rust",
10095 FakeLspAdapter {
10096 capabilities: lsp::ServerCapabilities {
10097 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
10098 ..lsp::ServerCapabilities::default()
10099 },
10100 ..FakeLspAdapter::default()
10101 },
10102 );
10103
10104 let buffer = project
10105 .update(cx, |project, cx| {
10106 project.open_local_buffer(path!("/file.rs"), cx)
10107 })
10108 .await
10109 .unwrap();
10110
10111 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10112 let (editor, cx) = cx.add_window_view(|window, cx| {
10113 build_editor_with_project(project.clone(), buffer, window, cx)
10114 });
10115
10116 cx.executor().start_waiting();
10117 let fake_server = fake_servers.next().await.unwrap();
10118
10119 (project, editor, cx, fake_server)
10120}
10121
10122#[gpui::test]
10123async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
10124 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
10125
10126 editor.update_in(cx, |editor, window, cx| {
10127 editor.set_text("one\ntwo\nthree\n", window, cx)
10128 });
10129 assert!(cx.read(|cx| editor.is_dirty(cx)));
10130
10131 let save = editor
10132 .update_in(cx, |editor, window, cx| {
10133 editor.save(
10134 SaveOptions {
10135 format: true,
10136 autosave: false,
10137 },
10138 project.clone(),
10139 window,
10140 cx,
10141 )
10142 })
10143 .unwrap();
10144 fake_server
10145 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
10146 assert_eq!(
10147 params.text_document.uri,
10148 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10149 );
10150 assert_eq!(params.options.tab_size, 4);
10151 Ok(Some(vec![lsp::TextEdit::new(
10152 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10153 ", ".to_string(),
10154 )]))
10155 })
10156 .next()
10157 .await;
10158 cx.executor().start_waiting();
10159 save.await;
10160 assert_eq!(
10161 editor.update(cx, |editor, cx| editor.text(cx)),
10162 "one, two\nthree\n"
10163 );
10164 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10165}
10166
10167#[gpui::test]
10168async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
10169 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
10170
10171 editor.update_in(cx, |editor, window, cx| {
10172 editor.set_text("one\ntwo\nthree\n", window, cx)
10173 });
10174 assert!(cx.read(|cx| editor.is_dirty(cx)));
10175
10176 // Test that save still works when formatting hangs
10177 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
10178 move |params, _| async move {
10179 assert_eq!(
10180 params.text_document.uri,
10181 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10182 );
10183 futures::future::pending::<()>().await;
10184 unreachable!()
10185 },
10186 );
10187 let save = editor
10188 .update_in(cx, |editor, window, cx| {
10189 editor.save(
10190 SaveOptions {
10191 format: true,
10192 autosave: false,
10193 },
10194 project.clone(),
10195 window,
10196 cx,
10197 )
10198 })
10199 .unwrap();
10200 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
10201 cx.executor().start_waiting();
10202 save.await;
10203 assert_eq!(
10204 editor.update(cx, |editor, cx| editor.text(cx)),
10205 "one\ntwo\nthree\n"
10206 );
10207 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10208}
10209
10210#[gpui::test]
10211async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
10212 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
10213
10214 // Buffer starts clean, no formatting should be requested
10215 let save = editor
10216 .update_in(cx, |editor, window, cx| {
10217 editor.save(
10218 SaveOptions {
10219 format: false,
10220 autosave: false,
10221 },
10222 project.clone(),
10223 window,
10224 cx,
10225 )
10226 })
10227 .unwrap();
10228 let _pending_format_request = fake_server
10229 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
10230 panic!("Should not be invoked");
10231 })
10232 .next();
10233 cx.executor().start_waiting();
10234 save.await;
10235 cx.run_until_parked();
10236}
10237
10238#[gpui::test]
10239async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
10240 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
10241
10242 // Set Rust language override and assert overridden tabsize is sent to language server
10243 update_test_language_settings(cx, |settings| {
10244 settings.languages.0.insert(
10245 "Rust".into(),
10246 LanguageSettingsContent {
10247 tab_size: NonZeroU32::new(8),
10248 ..Default::default()
10249 },
10250 );
10251 });
10252
10253 editor.update_in(cx, |editor, window, cx| {
10254 editor.set_text("something_new\n", window, cx)
10255 });
10256 assert!(cx.read(|cx| editor.is_dirty(cx)));
10257 let save = editor
10258 .update_in(cx, |editor, window, cx| {
10259 editor.save(
10260 SaveOptions {
10261 format: true,
10262 autosave: false,
10263 },
10264 project.clone(),
10265 window,
10266 cx,
10267 )
10268 })
10269 .unwrap();
10270 fake_server
10271 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
10272 assert_eq!(
10273 params.text_document.uri,
10274 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10275 );
10276 assert_eq!(params.options.tab_size, 8);
10277 Ok(Some(Vec::new()))
10278 })
10279 .next()
10280 .await;
10281 save.await;
10282}
10283
10284#[gpui::test]
10285async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
10286 init_test(cx, |settings| {
10287 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
10288 Formatter::LanguageServer { name: None },
10289 )))
10290 });
10291
10292 let fs = FakeFs::new(cx.executor());
10293 fs.insert_file(path!("/file.rs"), Default::default()).await;
10294
10295 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10296
10297 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10298 language_registry.add(Arc::new(Language::new(
10299 LanguageConfig {
10300 name: "Rust".into(),
10301 matcher: LanguageMatcher {
10302 path_suffixes: vec!["rs".to_string()],
10303 ..Default::default()
10304 },
10305 ..LanguageConfig::default()
10306 },
10307 Some(tree_sitter_rust::LANGUAGE.into()),
10308 )));
10309 update_test_language_settings(cx, |settings| {
10310 // Enable Prettier formatting for the same buffer, and ensure
10311 // LSP is called instead of Prettier.
10312 settings.defaults.prettier = Some(PrettierSettings {
10313 allowed: true,
10314 ..PrettierSettings::default()
10315 });
10316 });
10317 let mut fake_servers = language_registry.register_fake_lsp(
10318 "Rust",
10319 FakeLspAdapter {
10320 capabilities: lsp::ServerCapabilities {
10321 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10322 ..Default::default()
10323 },
10324 ..Default::default()
10325 },
10326 );
10327
10328 let buffer = project
10329 .update(cx, |project, cx| {
10330 project.open_local_buffer(path!("/file.rs"), cx)
10331 })
10332 .await
10333 .unwrap();
10334
10335 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10336 let (editor, cx) = cx.add_window_view(|window, cx| {
10337 build_editor_with_project(project.clone(), buffer, window, cx)
10338 });
10339 editor.update_in(cx, |editor, window, cx| {
10340 editor.set_text("one\ntwo\nthree\n", window, cx)
10341 });
10342
10343 cx.executor().start_waiting();
10344 let fake_server = fake_servers.next().await.unwrap();
10345
10346 let format = editor
10347 .update_in(cx, |editor, window, cx| {
10348 editor.perform_format(
10349 project.clone(),
10350 FormatTrigger::Manual,
10351 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10352 window,
10353 cx,
10354 )
10355 })
10356 .unwrap();
10357 fake_server
10358 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
10359 assert_eq!(
10360 params.text_document.uri,
10361 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10362 );
10363 assert_eq!(params.options.tab_size, 4);
10364 Ok(Some(vec![lsp::TextEdit::new(
10365 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10366 ", ".to_string(),
10367 )]))
10368 })
10369 .next()
10370 .await;
10371 cx.executor().start_waiting();
10372 format.await;
10373 assert_eq!(
10374 editor.update(cx, |editor, cx| editor.text(cx)),
10375 "one, two\nthree\n"
10376 );
10377
10378 editor.update_in(cx, |editor, window, cx| {
10379 editor.set_text("one\ntwo\nthree\n", window, cx)
10380 });
10381 // Ensure we don't lock if formatting hangs.
10382 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10383 move |params, _| async move {
10384 assert_eq!(
10385 params.text_document.uri,
10386 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10387 );
10388 futures::future::pending::<()>().await;
10389 unreachable!()
10390 },
10391 );
10392 let format = editor
10393 .update_in(cx, |editor, window, cx| {
10394 editor.perform_format(
10395 project,
10396 FormatTrigger::Manual,
10397 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10398 window,
10399 cx,
10400 )
10401 })
10402 .unwrap();
10403 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
10404 cx.executor().start_waiting();
10405 format.await;
10406 assert_eq!(
10407 editor.update(cx, |editor, cx| editor.text(cx)),
10408 "one\ntwo\nthree\n"
10409 );
10410}
10411
10412#[gpui::test]
10413async fn test_multiple_formatters(cx: &mut TestAppContext) {
10414 init_test(cx, |settings| {
10415 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
10416 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
10417 Formatter::LanguageServer { name: None },
10418 Formatter::CodeActions(
10419 [
10420 ("code-action-1".into(), true),
10421 ("code-action-2".into(), true),
10422 ]
10423 .into_iter()
10424 .collect(),
10425 ),
10426 ])))
10427 });
10428
10429 let fs = FakeFs::new(cx.executor());
10430 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
10431 .await;
10432
10433 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10434 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10435 language_registry.add(rust_lang());
10436
10437 let mut fake_servers = language_registry.register_fake_lsp(
10438 "Rust",
10439 FakeLspAdapter {
10440 capabilities: lsp::ServerCapabilities {
10441 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10442 execute_command_provider: Some(lsp::ExecuteCommandOptions {
10443 commands: vec!["the-command-for-code-action-1".into()],
10444 ..Default::default()
10445 }),
10446 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
10447 ..Default::default()
10448 },
10449 ..Default::default()
10450 },
10451 );
10452
10453 let buffer = project
10454 .update(cx, |project, cx| {
10455 project.open_local_buffer(path!("/file.rs"), cx)
10456 })
10457 .await
10458 .unwrap();
10459
10460 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10461 let (editor, cx) = cx.add_window_view(|window, cx| {
10462 build_editor_with_project(project.clone(), buffer, window, cx)
10463 });
10464
10465 cx.executor().start_waiting();
10466
10467 let fake_server = fake_servers.next().await.unwrap();
10468 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10469 move |_params, _| async move {
10470 Ok(Some(vec![lsp::TextEdit::new(
10471 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10472 "applied-formatting\n".to_string(),
10473 )]))
10474 },
10475 );
10476 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
10477 move |params, _| async move {
10478 assert_eq!(
10479 params.context.only,
10480 Some(vec!["code-action-1".into(), "code-action-2".into()])
10481 );
10482 let uri = lsp::Url::from_file_path(path!("/file.rs")).unwrap();
10483 Ok(Some(vec![
10484 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
10485 kind: Some("code-action-1".into()),
10486 edit: Some(lsp::WorkspaceEdit::new(
10487 [(
10488 uri.clone(),
10489 vec![lsp::TextEdit::new(
10490 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10491 "applied-code-action-1-edit\n".to_string(),
10492 )],
10493 )]
10494 .into_iter()
10495 .collect(),
10496 )),
10497 command: Some(lsp::Command {
10498 command: "the-command-for-code-action-1".into(),
10499 ..Default::default()
10500 }),
10501 ..Default::default()
10502 }),
10503 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
10504 kind: Some("code-action-2".into()),
10505 edit: Some(lsp::WorkspaceEdit::new(
10506 [(
10507 uri.clone(),
10508 vec![lsp::TextEdit::new(
10509 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10510 "applied-code-action-2-edit\n".to_string(),
10511 )],
10512 )]
10513 .into_iter()
10514 .collect(),
10515 )),
10516 ..Default::default()
10517 }),
10518 ]))
10519 },
10520 );
10521
10522 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
10523 move |params, _| async move { Ok(params) }
10524 });
10525
10526 let command_lock = Arc::new(futures::lock::Mutex::new(()));
10527 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
10528 let fake = fake_server.clone();
10529 let lock = command_lock.clone();
10530 move |params, _| {
10531 assert_eq!(params.command, "the-command-for-code-action-1");
10532 let fake = fake.clone();
10533 let lock = lock.clone();
10534 async move {
10535 lock.lock().await;
10536 fake.server
10537 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
10538 label: None,
10539 edit: lsp::WorkspaceEdit {
10540 changes: Some(
10541 [(
10542 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
10543 vec![lsp::TextEdit {
10544 range: lsp::Range::new(
10545 lsp::Position::new(0, 0),
10546 lsp::Position::new(0, 0),
10547 ),
10548 new_text: "applied-code-action-1-command\n".into(),
10549 }],
10550 )]
10551 .into_iter()
10552 .collect(),
10553 ),
10554 ..Default::default()
10555 },
10556 })
10557 .await
10558 .into_response()
10559 .unwrap();
10560 Ok(Some(json!(null)))
10561 }
10562 }
10563 });
10564
10565 cx.executor().start_waiting();
10566 editor
10567 .update_in(cx, |editor, window, cx| {
10568 editor.perform_format(
10569 project.clone(),
10570 FormatTrigger::Manual,
10571 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10572 window,
10573 cx,
10574 )
10575 })
10576 .unwrap()
10577 .await;
10578 editor.update(cx, |editor, cx| {
10579 assert_eq!(
10580 editor.text(cx),
10581 r#"
10582 applied-code-action-2-edit
10583 applied-code-action-1-command
10584 applied-code-action-1-edit
10585 applied-formatting
10586 one
10587 two
10588 three
10589 "#
10590 .unindent()
10591 );
10592 });
10593
10594 editor.update_in(cx, |editor, window, cx| {
10595 editor.undo(&Default::default(), window, cx);
10596 assert_eq!(editor.text(cx), "one \ntwo \nthree");
10597 });
10598
10599 // Perform a manual edit while waiting for an LSP command
10600 // that's being run as part of a formatting code action.
10601 let lock_guard = command_lock.lock().await;
10602 let format = editor
10603 .update_in(cx, |editor, window, cx| {
10604 editor.perform_format(
10605 project.clone(),
10606 FormatTrigger::Manual,
10607 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10608 window,
10609 cx,
10610 )
10611 })
10612 .unwrap();
10613 cx.run_until_parked();
10614 editor.update(cx, |editor, cx| {
10615 assert_eq!(
10616 editor.text(cx),
10617 r#"
10618 applied-code-action-1-edit
10619 applied-formatting
10620 one
10621 two
10622 three
10623 "#
10624 .unindent()
10625 );
10626
10627 editor.buffer.update(cx, |buffer, cx| {
10628 let ix = buffer.len(cx);
10629 buffer.edit([(ix..ix, "edited\n")], None, cx);
10630 });
10631 });
10632
10633 // Allow the LSP command to proceed. Because the buffer was edited,
10634 // the second code action will not be run.
10635 drop(lock_guard);
10636 format.await;
10637 editor.update_in(cx, |editor, window, cx| {
10638 assert_eq!(
10639 editor.text(cx),
10640 r#"
10641 applied-code-action-1-command
10642 applied-code-action-1-edit
10643 applied-formatting
10644 one
10645 two
10646 three
10647 edited
10648 "#
10649 .unindent()
10650 );
10651
10652 // The manual edit is undone first, because it is the last thing the user did
10653 // (even though the command completed afterwards).
10654 editor.undo(&Default::default(), window, cx);
10655 assert_eq!(
10656 editor.text(cx),
10657 r#"
10658 applied-code-action-1-command
10659 applied-code-action-1-edit
10660 applied-formatting
10661 one
10662 two
10663 three
10664 "#
10665 .unindent()
10666 );
10667
10668 // All the formatting (including the command, which completed after the manual edit)
10669 // is undone together.
10670 editor.undo(&Default::default(), window, cx);
10671 assert_eq!(editor.text(cx), "one \ntwo \nthree");
10672 });
10673}
10674
10675#[gpui::test]
10676async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
10677 init_test(cx, |settings| {
10678 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
10679 Formatter::LanguageServer { name: None },
10680 ])))
10681 });
10682
10683 let fs = FakeFs::new(cx.executor());
10684 fs.insert_file(path!("/file.ts"), Default::default()).await;
10685
10686 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10687
10688 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10689 language_registry.add(Arc::new(Language::new(
10690 LanguageConfig {
10691 name: "TypeScript".into(),
10692 matcher: LanguageMatcher {
10693 path_suffixes: vec!["ts".to_string()],
10694 ..Default::default()
10695 },
10696 ..LanguageConfig::default()
10697 },
10698 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
10699 )));
10700 update_test_language_settings(cx, |settings| {
10701 settings.defaults.prettier = Some(PrettierSettings {
10702 allowed: true,
10703 ..PrettierSettings::default()
10704 });
10705 });
10706 let mut fake_servers = language_registry.register_fake_lsp(
10707 "TypeScript",
10708 FakeLspAdapter {
10709 capabilities: lsp::ServerCapabilities {
10710 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
10711 ..Default::default()
10712 },
10713 ..Default::default()
10714 },
10715 );
10716
10717 let buffer = project
10718 .update(cx, |project, cx| {
10719 project.open_local_buffer(path!("/file.ts"), cx)
10720 })
10721 .await
10722 .unwrap();
10723
10724 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10725 let (editor, cx) = cx.add_window_view(|window, cx| {
10726 build_editor_with_project(project.clone(), buffer, window, cx)
10727 });
10728 editor.update_in(cx, |editor, window, cx| {
10729 editor.set_text(
10730 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
10731 window,
10732 cx,
10733 )
10734 });
10735
10736 cx.executor().start_waiting();
10737 let fake_server = fake_servers.next().await.unwrap();
10738
10739 let format = editor
10740 .update_in(cx, |editor, window, cx| {
10741 editor.perform_code_action_kind(
10742 project.clone(),
10743 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
10744 window,
10745 cx,
10746 )
10747 })
10748 .unwrap();
10749 fake_server
10750 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
10751 assert_eq!(
10752 params.text_document.uri,
10753 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
10754 );
10755 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
10756 lsp::CodeAction {
10757 title: "Organize Imports".to_string(),
10758 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
10759 edit: Some(lsp::WorkspaceEdit {
10760 changes: Some(
10761 [(
10762 params.text_document.uri.clone(),
10763 vec![lsp::TextEdit::new(
10764 lsp::Range::new(
10765 lsp::Position::new(1, 0),
10766 lsp::Position::new(2, 0),
10767 ),
10768 "".to_string(),
10769 )],
10770 )]
10771 .into_iter()
10772 .collect(),
10773 ),
10774 ..Default::default()
10775 }),
10776 ..Default::default()
10777 },
10778 )]))
10779 })
10780 .next()
10781 .await;
10782 cx.executor().start_waiting();
10783 format.await;
10784 assert_eq!(
10785 editor.update(cx, |editor, cx| editor.text(cx)),
10786 "import { a } from 'module';\n\nconst x = a;\n"
10787 );
10788
10789 editor.update_in(cx, |editor, window, cx| {
10790 editor.set_text(
10791 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
10792 window,
10793 cx,
10794 )
10795 });
10796 // Ensure we don't lock if code action hangs.
10797 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
10798 move |params, _| async move {
10799 assert_eq!(
10800 params.text_document.uri,
10801 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
10802 );
10803 futures::future::pending::<()>().await;
10804 unreachable!()
10805 },
10806 );
10807 let format = editor
10808 .update_in(cx, |editor, window, cx| {
10809 editor.perform_code_action_kind(
10810 project,
10811 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
10812 window,
10813 cx,
10814 )
10815 })
10816 .unwrap();
10817 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
10818 cx.executor().start_waiting();
10819 format.await;
10820 assert_eq!(
10821 editor.update(cx, |editor, cx| editor.text(cx)),
10822 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
10823 );
10824}
10825
10826#[gpui::test]
10827async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
10828 init_test(cx, |_| {});
10829
10830 let mut cx = EditorLspTestContext::new_rust(
10831 lsp::ServerCapabilities {
10832 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10833 ..Default::default()
10834 },
10835 cx,
10836 )
10837 .await;
10838
10839 cx.set_state(indoc! {"
10840 one.twoˇ
10841 "});
10842
10843 // The format request takes a long time. When it completes, it inserts
10844 // a newline and an indent before the `.`
10845 cx.lsp
10846 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
10847 let executor = cx.background_executor().clone();
10848 async move {
10849 executor.timer(Duration::from_millis(100)).await;
10850 Ok(Some(vec![lsp::TextEdit {
10851 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
10852 new_text: "\n ".into(),
10853 }]))
10854 }
10855 });
10856
10857 // Submit a format request.
10858 let format_1 = cx
10859 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10860 .unwrap();
10861 cx.executor().run_until_parked();
10862
10863 // Submit a second format request.
10864 let format_2 = cx
10865 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10866 .unwrap();
10867 cx.executor().run_until_parked();
10868
10869 // Wait for both format requests to complete
10870 cx.executor().advance_clock(Duration::from_millis(200));
10871 cx.executor().start_waiting();
10872 format_1.await.unwrap();
10873 cx.executor().start_waiting();
10874 format_2.await.unwrap();
10875
10876 // The formatting edits only happens once.
10877 cx.assert_editor_state(indoc! {"
10878 one
10879 .twoˇ
10880 "});
10881}
10882
10883#[gpui::test]
10884async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
10885 init_test(cx, |settings| {
10886 settings.defaults.formatter = Some(SelectedFormatter::Auto)
10887 });
10888
10889 let mut cx = EditorLspTestContext::new_rust(
10890 lsp::ServerCapabilities {
10891 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10892 ..Default::default()
10893 },
10894 cx,
10895 )
10896 .await;
10897
10898 // Set up a buffer white some trailing whitespace and no trailing newline.
10899 cx.set_state(
10900 &[
10901 "one ", //
10902 "twoˇ", //
10903 "three ", //
10904 "four", //
10905 ]
10906 .join("\n"),
10907 );
10908
10909 // Submit a format request.
10910 let format = cx
10911 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10912 .unwrap();
10913
10914 // Record which buffer changes have been sent to the language server
10915 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
10916 cx.lsp
10917 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
10918 let buffer_changes = buffer_changes.clone();
10919 move |params, _| {
10920 buffer_changes.lock().extend(
10921 params
10922 .content_changes
10923 .into_iter()
10924 .map(|e| (e.range.unwrap(), e.text)),
10925 );
10926 }
10927 });
10928
10929 // Handle formatting requests to the language server.
10930 cx.lsp
10931 .set_request_handler::<lsp::request::Formatting, _, _>({
10932 let buffer_changes = buffer_changes.clone();
10933 move |_, _| {
10934 // When formatting is requested, trailing whitespace has already been stripped,
10935 // and the trailing newline has already been added.
10936 assert_eq!(
10937 &buffer_changes.lock()[1..],
10938 &[
10939 (
10940 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
10941 "".into()
10942 ),
10943 (
10944 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
10945 "".into()
10946 ),
10947 (
10948 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
10949 "\n".into()
10950 ),
10951 ]
10952 );
10953
10954 // Insert blank lines between each line of the buffer.
10955 async move {
10956 Ok(Some(vec![
10957 lsp::TextEdit {
10958 range: lsp::Range::new(
10959 lsp::Position::new(1, 0),
10960 lsp::Position::new(1, 0),
10961 ),
10962 new_text: "\n".into(),
10963 },
10964 lsp::TextEdit {
10965 range: lsp::Range::new(
10966 lsp::Position::new(2, 0),
10967 lsp::Position::new(2, 0),
10968 ),
10969 new_text: "\n".into(),
10970 },
10971 ]))
10972 }
10973 }
10974 });
10975
10976 // After formatting the buffer, the trailing whitespace is stripped,
10977 // a newline is appended, and the edits provided by the language server
10978 // have been applied.
10979 format.await.unwrap();
10980 cx.assert_editor_state(
10981 &[
10982 "one", //
10983 "", //
10984 "twoˇ", //
10985 "", //
10986 "three", //
10987 "four", //
10988 "", //
10989 ]
10990 .join("\n"),
10991 );
10992
10993 // Undoing the formatting undoes the trailing whitespace removal, the
10994 // trailing newline, and the LSP edits.
10995 cx.update_buffer(|buffer, cx| buffer.undo(cx));
10996 cx.assert_editor_state(
10997 &[
10998 "one ", //
10999 "twoˇ", //
11000 "three ", //
11001 "four", //
11002 ]
11003 .join("\n"),
11004 );
11005}
11006
11007#[gpui::test]
11008async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
11009 cx: &mut TestAppContext,
11010) {
11011 init_test(cx, |_| {});
11012
11013 cx.update(|cx| {
11014 cx.update_global::<SettingsStore, _>(|settings, cx| {
11015 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11016 settings.auto_signature_help = Some(true);
11017 });
11018 });
11019 });
11020
11021 let mut cx = EditorLspTestContext::new_rust(
11022 lsp::ServerCapabilities {
11023 signature_help_provider: Some(lsp::SignatureHelpOptions {
11024 ..Default::default()
11025 }),
11026 ..Default::default()
11027 },
11028 cx,
11029 )
11030 .await;
11031
11032 let language = Language::new(
11033 LanguageConfig {
11034 name: "Rust".into(),
11035 brackets: BracketPairConfig {
11036 pairs: vec![
11037 BracketPair {
11038 start: "{".to_string(),
11039 end: "}".to_string(),
11040 close: true,
11041 surround: true,
11042 newline: true,
11043 },
11044 BracketPair {
11045 start: "(".to_string(),
11046 end: ")".to_string(),
11047 close: true,
11048 surround: true,
11049 newline: true,
11050 },
11051 BracketPair {
11052 start: "/*".to_string(),
11053 end: " */".to_string(),
11054 close: true,
11055 surround: true,
11056 newline: true,
11057 },
11058 BracketPair {
11059 start: "[".to_string(),
11060 end: "]".to_string(),
11061 close: false,
11062 surround: false,
11063 newline: true,
11064 },
11065 BracketPair {
11066 start: "\"".to_string(),
11067 end: "\"".to_string(),
11068 close: true,
11069 surround: true,
11070 newline: false,
11071 },
11072 BracketPair {
11073 start: "<".to_string(),
11074 end: ">".to_string(),
11075 close: false,
11076 surround: true,
11077 newline: true,
11078 },
11079 ],
11080 ..Default::default()
11081 },
11082 autoclose_before: "})]".to_string(),
11083 ..Default::default()
11084 },
11085 Some(tree_sitter_rust::LANGUAGE.into()),
11086 );
11087 let language = Arc::new(language);
11088
11089 cx.language_registry().add(language.clone());
11090 cx.update_buffer(|buffer, cx| {
11091 buffer.set_language(Some(language), cx);
11092 });
11093
11094 cx.set_state(
11095 &r#"
11096 fn main() {
11097 sampleˇ
11098 }
11099 "#
11100 .unindent(),
11101 );
11102
11103 cx.update_editor(|editor, window, cx| {
11104 editor.handle_input("(", window, cx);
11105 });
11106 cx.assert_editor_state(
11107 &"
11108 fn main() {
11109 sample(ˇ)
11110 }
11111 "
11112 .unindent(),
11113 );
11114
11115 let mocked_response = lsp::SignatureHelp {
11116 signatures: vec![lsp::SignatureInformation {
11117 label: "fn sample(param1: u8, param2: u8)".to_string(),
11118 documentation: None,
11119 parameters: Some(vec![
11120 lsp::ParameterInformation {
11121 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11122 documentation: None,
11123 },
11124 lsp::ParameterInformation {
11125 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11126 documentation: None,
11127 },
11128 ]),
11129 active_parameter: None,
11130 }],
11131 active_signature: Some(0),
11132 active_parameter: Some(0),
11133 };
11134 handle_signature_help_request(&mut cx, mocked_response).await;
11135
11136 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11137 .await;
11138
11139 cx.editor(|editor, _, _| {
11140 let signature_help_state = editor.signature_help_state.popover().cloned();
11141 let signature = signature_help_state.unwrap();
11142 assert_eq!(
11143 signature.signatures[signature.current_signature].label,
11144 "fn sample(param1: u8, param2: u8)"
11145 );
11146 });
11147}
11148
11149#[gpui::test]
11150async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
11151 init_test(cx, |_| {});
11152
11153 cx.update(|cx| {
11154 cx.update_global::<SettingsStore, _>(|settings, cx| {
11155 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11156 settings.auto_signature_help = Some(false);
11157 settings.show_signature_help_after_edits = Some(false);
11158 });
11159 });
11160 });
11161
11162 let mut cx = EditorLspTestContext::new_rust(
11163 lsp::ServerCapabilities {
11164 signature_help_provider: Some(lsp::SignatureHelpOptions {
11165 ..Default::default()
11166 }),
11167 ..Default::default()
11168 },
11169 cx,
11170 )
11171 .await;
11172
11173 let language = Language::new(
11174 LanguageConfig {
11175 name: "Rust".into(),
11176 brackets: BracketPairConfig {
11177 pairs: vec![
11178 BracketPair {
11179 start: "{".to_string(),
11180 end: "}".to_string(),
11181 close: true,
11182 surround: true,
11183 newline: true,
11184 },
11185 BracketPair {
11186 start: "(".to_string(),
11187 end: ")".to_string(),
11188 close: true,
11189 surround: true,
11190 newline: true,
11191 },
11192 BracketPair {
11193 start: "/*".to_string(),
11194 end: " */".to_string(),
11195 close: true,
11196 surround: true,
11197 newline: true,
11198 },
11199 BracketPair {
11200 start: "[".to_string(),
11201 end: "]".to_string(),
11202 close: false,
11203 surround: false,
11204 newline: true,
11205 },
11206 BracketPair {
11207 start: "\"".to_string(),
11208 end: "\"".to_string(),
11209 close: true,
11210 surround: true,
11211 newline: false,
11212 },
11213 BracketPair {
11214 start: "<".to_string(),
11215 end: ">".to_string(),
11216 close: false,
11217 surround: true,
11218 newline: true,
11219 },
11220 ],
11221 ..Default::default()
11222 },
11223 autoclose_before: "})]".to_string(),
11224 ..Default::default()
11225 },
11226 Some(tree_sitter_rust::LANGUAGE.into()),
11227 );
11228 let language = Arc::new(language);
11229
11230 cx.language_registry().add(language.clone());
11231 cx.update_buffer(|buffer, cx| {
11232 buffer.set_language(Some(language), cx);
11233 });
11234
11235 // Ensure that signature_help is not called when no signature help is enabled.
11236 cx.set_state(
11237 &r#"
11238 fn main() {
11239 sampleˇ
11240 }
11241 "#
11242 .unindent(),
11243 );
11244 cx.update_editor(|editor, window, cx| {
11245 editor.handle_input("(", window, cx);
11246 });
11247 cx.assert_editor_state(
11248 &"
11249 fn main() {
11250 sample(ˇ)
11251 }
11252 "
11253 .unindent(),
11254 );
11255 cx.editor(|editor, _, _| {
11256 assert!(editor.signature_help_state.task().is_none());
11257 });
11258
11259 let mocked_response = lsp::SignatureHelp {
11260 signatures: vec![lsp::SignatureInformation {
11261 label: "fn sample(param1: u8, param2: u8)".to_string(),
11262 documentation: None,
11263 parameters: Some(vec![
11264 lsp::ParameterInformation {
11265 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11266 documentation: None,
11267 },
11268 lsp::ParameterInformation {
11269 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11270 documentation: None,
11271 },
11272 ]),
11273 active_parameter: None,
11274 }],
11275 active_signature: Some(0),
11276 active_parameter: Some(0),
11277 };
11278
11279 // Ensure that signature_help is called when enabled afte edits
11280 cx.update(|_, cx| {
11281 cx.update_global::<SettingsStore, _>(|settings, cx| {
11282 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11283 settings.auto_signature_help = Some(false);
11284 settings.show_signature_help_after_edits = Some(true);
11285 });
11286 });
11287 });
11288 cx.set_state(
11289 &r#"
11290 fn main() {
11291 sampleˇ
11292 }
11293 "#
11294 .unindent(),
11295 );
11296 cx.update_editor(|editor, window, cx| {
11297 editor.handle_input("(", window, cx);
11298 });
11299 cx.assert_editor_state(
11300 &"
11301 fn main() {
11302 sample(ˇ)
11303 }
11304 "
11305 .unindent(),
11306 );
11307 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11308 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11309 .await;
11310 cx.update_editor(|editor, _, _| {
11311 let signature_help_state = editor.signature_help_state.popover().cloned();
11312 assert!(signature_help_state.is_some());
11313 let signature = signature_help_state.unwrap();
11314 assert_eq!(
11315 signature.signatures[signature.current_signature].label,
11316 "fn sample(param1: u8, param2: u8)"
11317 );
11318 editor.signature_help_state = SignatureHelpState::default();
11319 });
11320
11321 // Ensure that signature_help is called when auto signature help override is enabled
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(true);
11326 settings.show_signature_help_after_edits = Some(false);
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).await;
11350 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11351 .await;
11352 cx.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 });
11361}
11362
11363#[gpui::test]
11364async fn test_signature_help(cx: &mut TestAppContext) {
11365 init_test(cx, |_| {});
11366 cx.update(|cx| {
11367 cx.update_global::<SettingsStore, _>(|settings, cx| {
11368 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11369 settings.auto_signature_help = Some(true);
11370 });
11371 });
11372 });
11373
11374 let mut cx = EditorLspTestContext::new_rust(
11375 lsp::ServerCapabilities {
11376 signature_help_provider: Some(lsp::SignatureHelpOptions {
11377 ..Default::default()
11378 }),
11379 ..Default::default()
11380 },
11381 cx,
11382 )
11383 .await;
11384
11385 // A test that directly calls `show_signature_help`
11386 cx.update_editor(|editor, window, cx| {
11387 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11388 });
11389
11390 let mocked_response = lsp::SignatureHelp {
11391 signatures: vec![lsp::SignatureInformation {
11392 label: "fn sample(param1: u8, param2: u8)".to_string(),
11393 documentation: None,
11394 parameters: Some(vec![
11395 lsp::ParameterInformation {
11396 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11397 documentation: None,
11398 },
11399 lsp::ParameterInformation {
11400 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11401 documentation: None,
11402 },
11403 ]),
11404 active_parameter: None,
11405 }],
11406 active_signature: Some(0),
11407 active_parameter: Some(0),
11408 };
11409 handle_signature_help_request(&mut cx, mocked_response).await;
11410
11411 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11412 .await;
11413
11414 cx.editor(|editor, _, _| {
11415 let signature_help_state = editor.signature_help_state.popover().cloned();
11416 assert!(signature_help_state.is_some());
11417 let signature = signature_help_state.unwrap();
11418 assert_eq!(
11419 signature.signatures[signature.current_signature].label,
11420 "fn sample(param1: u8, param2: u8)"
11421 );
11422 });
11423
11424 // When exiting outside from inside the brackets, `signature_help` is closed.
11425 cx.set_state(indoc! {"
11426 fn main() {
11427 sample(ˇ);
11428 }
11429
11430 fn sample(param1: u8, param2: u8) {}
11431 "});
11432
11433 cx.update_editor(|editor, window, cx| {
11434 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11435 s.select_ranges([0..0])
11436 });
11437 });
11438
11439 let mocked_response = lsp::SignatureHelp {
11440 signatures: Vec::new(),
11441 active_signature: None,
11442 active_parameter: None,
11443 };
11444 handle_signature_help_request(&mut cx, mocked_response).await;
11445
11446 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
11447 .await;
11448
11449 cx.editor(|editor, _, _| {
11450 assert!(!editor.signature_help_state.is_shown());
11451 });
11452
11453 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
11454 cx.set_state(indoc! {"
11455 fn main() {
11456 sample(ˇ);
11457 }
11458
11459 fn sample(param1: u8, param2: u8) {}
11460 "});
11461
11462 let mocked_response = lsp::SignatureHelp {
11463 signatures: vec![lsp::SignatureInformation {
11464 label: "fn sample(param1: u8, param2: u8)".to_string(),
11465 documentation: None,
11466 parameters: Some(vec![
11467 lsp::ParameterInformation {
11468 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11469 documentation: None,
11470 },
11471 lsp::ParameterInformation {
11472 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11473 documentation: None,
11474 },
11475 ]),
11476 active_parameter: None,
11477 }],
11478 active_signature: Some(0),
11479 active_parameter: Some(0),
11480 };
11481 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11482 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11483 .await;
11484 cx.editor(|editor, _, _| {
11485 assert!(editor.signature_help_state.is_shown());
11486 });
11487
11488 // Restore the popover with more parameter input
11489 cx.set_state(indoc! {"
11490 fn main() {
11491 sample(param1, param2ˇ);
11492 }
11493
11494 fn sample(param1: u8, param2: u8) {}
11495 "});
11496
11497 let mocked_response = lsp::SignatureHelp {
11498 signatures: vec![lsp::SignatureInformation {
11499 label: "fn sample(param1: u8, param2: u8)".to_string(),
11500 documentation: None,
11501 parameters: Some(vec![
11502 lsp::ParameterInformation {
11503 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11504 documentation: None,
11505 },
11506 lsp::ParameterInformation {
11507 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11508 documentation: None,
11509 },
11510 ]),
11511 active_parameter: None,
11512 }],
11513 active_signature: Some(0),
11514 active_parameter: Some(1),
11515 };
11516 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11517 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11518 .await;
11519
11520 // When selecting a range, the popover is gone.
11521 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
11522 cx.update_editor(|editor, window, cx| {
11523 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11524 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
11525 })
11526 });
11527 cx.assert_editor_state(indoc! {"
11528 fn main() {
11529 sample(param1, «ˇparam2»);
11530 }
11531
11532 fn sample(param1: u8, param2: u8) {}
11533 "});
11534 cx.editor(|editor, _, _| {
11535 assert!(!editor.signature_help_state.is_shown());
11536 });
11537
11538 // When unselecting again, the popover is back if within the brackets.
11539 cx.update_editor(|editor, window, cx| {
11540 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11541 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11542 })
11543 });
11544 cx.assert_editor_state(indoc! {"
11545 fn main() {
11546 sample(param1, ˇparam2);
11547 }
11548
11549 fn sample(param1: u8, param2: u8) {}
11550 "});
11551 handle_signature_help_request(&mut cx, mocked_response).await;
11552 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11553 .await;
11554 cx.editor(|editor, _, _| {
11555 assert!(editor.signature_help_state.is_shown());
11556 });
11557
11558 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
11559 cx.update_editor(|editor, window, cx| {
11560 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11561 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
11562 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11563 })
11564 });
11565 cx.assert_editor_state(indoc! {"
11566 fn main() {
11567 sample(param1, ˇparam2);
11568 }
11569
11570 fn sample(param1: u8, param2: u8) {}
11571 "});
11572
11573 let mocked_response = lsp::SignatureHelp {
11574 signatures: vec![lsp::SignatureInformation {
11575 label: "fn sample(param1: u8, param2: u8)".to_string(),
11576 documentation: None,
11577 parameters: Some(vec![
11578 lsp::ParameterInformation {
11579 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11580 documentation: None,
11581 },
11582 lsp::ParameterInformation {
11583 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11584 documentation: None,
11585 },
11586 ]),
11587 active_parameter: None,
11588 }],
11589 active_signature: Some(0),
11590 active_parameter: Some(1),
11591 };
11592 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11593 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11594 .await;
11595 cx.update_editor(|editor, _, cx| {
11596 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
11597 });
11598 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
11599 .await;
11600 cx.update_editor(|editor, window, cx| {
11601 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11602 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
11603 })
11604 });
11605 cx.assert_editor_state(indoc! {"
11606 fn main() {
11607 sample(param1, «ˇparam2»);
11608 }
11609
11610 fn sample(param1: u8, param2: u8) {}
11611 "});
11612 cx.update_editor(|editor, window, cx| {
11613 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11614 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11615 })
11616 });
11617 cx.assert_editor_state(indoc! {"
11618 fn main() {
11619 sample(param1, ˇparam2);
11620 }
11621
11622 fn sample(param1: u8, param2: u8) {}
11623 "});
11624 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
11625 .await;
11626}
11627
11628#[gpui::test]
11629async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
11630 init_test(cx, |_| {});
11631
11632 let mut cx = EditorLspTestContext::new_rust(
11633 lsp::ServerCapabilities {
11634 signature_help_provider: Some(lsp::SignatureHelpOptions {
11635 ..Default::default()
11636 }),
11637 ..Default::default()
11638 },
11639 cx,
11640 )
11641 .await;
11642
11643 cx.set_state(indoc! {"
11644 fn main() {
11645 overloadedˇ
11646 }
11647 "});
11648
11649 cx.update_editor(|editor, window, cx| {
11650 editor.handle_input("(", window, cx);
11651 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11652 });
11653
11654 // Mock response with 3 signatures
11655 let mocked_response = lsp::SignatureHelp {
11656 signatures: vec![
11657 lsp::SignatureInformation {
11658 label: "fn overloaded(x: i32)".to_string(),
11659 documentation: None,
11660 parameters: Some(vec![lsp::ParameterInformation {
11661 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11662 documentation: None,
11663 }]),
11664 active_parameter: None,
11665 },
11666 lsp::SignatureInformation {
11667 label: "fn overloaded(x: i32, y: i32)".to_string(),
11668 documentation: None,
11669 parameters: Some(vec![
11670 lsp::ParameterInformation {
11671 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11672 documentation: None,
11673 },
11674 lsp::ParameterInformation {
11675 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
11676 documentation: None,
11677 },
11678 ]),
11679 active_parameter: None,
11680 },
11681 lsp::SignatureInformation {
11682 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
11683 documentation: None,
11684 parameters: Some(vec![
11685 lsp::ParameterInformation {
11686 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11687 documentation: None,
11688 },
11689 lsp::ParameterInformation {
11690 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
11691 documentation: None,
11692 },
11693 lsp::ParameterInformation {
11694 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
11695 documentation: None,
11696 },
11697 ]),
11698 active_parameter: None,
11699 },
11700 ],
11701 active_signature: Some(1),
11702 active_parameter: Some(0),
11703 };
11704 handle_signature_help_request(&mut cx, mocked_response).await;
11705
11706 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11707 .await;
11708
11709 // Verify we have multiple signatures and the right one is selected
11710 cx.editor(|editor, _, _| {
11711 let popover = editor.signature_help_state.popover().cloned().unwrap();
11712 assert_eq!(popover.signatures.len(), 3);
11713 // active_signature was 1, so that should be the current
11714 assert_eq!(popover.current_signature, 1);
11715 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
11716 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
11717 assert_eq!(
11718 popover.signatures[2].label,
11719 "fn overloaded(x: i32, y: i32, z: i32)"
11720 );
11721 });
11722
11723 // Test navigation functionality
11724 cx.update_editor(|editor, window, cx| {
11725 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
11726 });
11727
11728 cx.editor(|editor, _, _| {
11729 let popover = editor.signature_help_state.popover().cloned().unwrap();
11730 assert_eq!(popover.current_signature, 2);
11731 });
11732
11733 // Test wrap around
11734 cx.update_editor(|editor, window, cx| {
11735 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
11736 });
11737
11738 cx.editor(|editor, _, _| {
11739 let popover = editor.signature_help_state.popover().cloned().unwrap();
11740 assert_eq!(popover.current_signature, 0);
11741 });
11742
11743 // Test previous navigation
11744 cx.update_editor(|editor, window, cx| {
11745 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
11746 });
11747
11748 cx.editor(|editor, _, _| {
11749 let popover = editor.signature_help_state.popover().cloned().unwrap();
11750 assert_eq!(popover.current_signature, 2);
11751 });
11752}
11753
11754#[gpui::test]
11755async fn test_completion_mode(cx: &mut TestAppContext) {
11756 init_test(cx, |_| {});
11757 let mut cx = EditorLspTestContext::new_rust(
11758 lsp::ServerCapabilities {
11759 completion_provider: Some(lsp::CompletionOptions {
11760 resolve_provider: Some(true),
11761 ..Default::default()
11762 }),
11763 ..Default::default()
11764 },
11765 cx,
11766 )
11767 .await;
11768
11769 struct Run {
11770 run_description: &'static str,
11771 initial_state: String,
11772 buffer_marked_text: String,
11773 completion_label: &'static str,
11774 completion_text: &'static str,
11775 expected_with_insert_mode: String,
11776 expected_with_replace_mode: String,
11777 expected_with_replace_subsequence_mode: String,
11778 expected_with_replace_suffix_mode: String,
11779 }
11780
11781 let runs = [
11782 Run {
11783 run_description: "Start of word matches completion text",
11784 initial_state: "before ediˇ after".into(),
11785 buffer_marked_text: "before <edi|> after".into(),
11786 completion_label: "editor",
11787 completion_text: "editor",
11788 expected_with_insert_mode: "before editorˇ after".into(),
11789 expected_with_replace_mode: "before editorˇ after".into(),
11790 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11791 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11792 },
11793 Run {
11794 run_description: "Accept same text at the middle of the word",
11795 initial_state: "before ediˇtor after".into(),
11796 buffer_marked_text: "before <edi|tor> after".into(),
11797 completion_label: "editor",
11798 completion_text: "editor",
11799 expected_with_insert_mode: "before editorˇtor after".into(),
11800 expected_with_replace_mode: "before editorˇ after".into(),
11801 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11802 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11803 },
11804 Run {
11805 run_description: "End of word matches completion text -- cursor at end",
11806 initial_state: "before torˇ after".into(),
11807 buffer_marked_text: "before <tor|> after".into(),
11808 completion_label: "editor",
11809 completion_text: "editor",
11810 expected_with_insert_mode: "before editorˇ after".into(),
11811 expected_with_replace_mode: "before editorˇ after".into(),
11812 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11813 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11814 },
11815 Run {
11816 run_description: "End of word matches completion text -- cursor at start",
11817 initial_state: "before ˇtor after".into(),
11818 buffer_marked_text: "before <|tor> after".into(),
11819 completion_label: "editor",
11820 completion_text: "editor",
11821 expected_with_insert_mode: "before editorˇtor after".into(),
11822 expected_with_replace_mode: "before editorˇ after".into(),
11823 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11824 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11825 },
11826 Run {
11827 run_description: "Prepend text containing whitespace",
11828 initial_state: "pˇfield: bool".into(),
11829 buffer_marked_text: "<p|field>: bool".into(),
11830 completion_label: "pub ",
11831 completion_text: "pub ",
11832 expected_with_insert_mode: "pub ˇfield: bool".into(),
11833 expected_with_replace_mode: "pub ˇ: bool".into(),
11834 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
11835 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
11836 },
11837 Run {
11838 run_description: "Add element to start of list",
11839 initial_state: "[element_ˇelement_2]".into(),
11840 buffer_marked_text: "[<element_|element_2>]".into(),
11841 completion_label: "element_1",
11842 completion_text: "element_1",
11843 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
11844 expected_with_replace_mode: "[element_1ˇ]".into(),
11845 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
11846 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
11847 },
11848 Run {
11849 run_description: "Add element to start of list -- first and second elements are equal",
11850 initial_state: "[elˇelement]".into(),
11851 buffer_marked_text: "[<el|element>]".into(),
11852 completion_label: "element",
11853 completion_text: "element",
11854 expected_with_insert_mode: "[elementˇelement]".into(),
11855 expected_with_replace_mode: "[elementˇ]".into(),
11856 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
11857 expected_with_replace_suffix_mode: "[elementˇ]".into(),
11858 },
11859 Run {
11860 run_description: "Ends with matching suffix",
11861 initial_state: "SubˇError".into(),
11862 buffer_marked_text: "<Sub|Error>".into(),
11863 completion_label: "SubscriptionError",
11864 completion_text: "SubscriptionError",
11865 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
11866 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11867 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11868 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
11869 },
11870 Run {
11871 run_description: "Suffix is a subsequence -- contiguous",
11872 initial_state: "SubˇErr".into(),
11873 buffer_marked_text: "<Sub|Err>".into(),
11874 completion_label: "SubscriptionError",
11875 completion_text: "SubscriptionError",
11876 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
11877 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11878 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11879 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
11880 },
11881 Run {
11882 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
11883 initial_state: "Suˇscrirr".into(),
11884 buffer_marked_text: "<Su|scrirr>".into(),
11885 completion_label: "SubscriptionError",
11886 completion_text: "SubscriptionError",
11887 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
11888 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11889 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11890 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
11891 },
11892 Run {
11893 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
11894 initial_state: "foo(indˇix)".into(),
11895 buffer_marked_text: "foo(<ind|ix>)".into(),
11896 completion_label: "node_index",
11897 completion_text: "node_index",
11898 expected_with_insert_mode: "foo(node_indexˇix)".into(),
11899 expected_with_replace_mode: "foo(node_indexˇ)".into(),
11900 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
11901 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
11902 },
11903 Run {
11904 run_description: "Replace range ends before cursor - should extend to cursor",
11905 initial_state: "before editˇo after".into(),
11906 buffer_marked_text: "before <{ed}>it|o after".into(),
11907 completion_label: "editor",
11908 completion_text: "editor",
11909 expected_with_insert_mode: "before editorˇo after".into(),
11910 expected_with_replace_mode: "before editorˇo after".into(),
11911 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
11912 expected_with_replace_suffix_mode: "before editorˇo after".into(),
11913 },
11914 Run {
11915 run_description: "Uses label for suffix matching",
11916 initial_state: "before ediˇtor after".into(),
11917 buffer_marked_text: "before <edi|tor> after".into(),
11918 completion_label: "editor",
11919 completion_text: "editor()",
11920 expected_with_insert_mode: "before editor()ˇtor after".into(),
11921 expected_with_replace_mode: "before editor()ˇ after".into(),
11922 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
11923 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
11924 },
11925 Run {
11926 run_description: "Case insensitive subsequence and suffix matching",
11927 initial_state: "before EDiˇtoR after".into(),
11928 buffer_marked_text: "before <EDi|toR> after".into(),
11929 completion_label: "editor",
11930 completion_text: "editor",
11931 expected_with_insert_mode: "before editorˇtoR after".into(),
11932 expected_with_replace_mode: "before editorˇ after".into(),
11933 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11934 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11935 },
11936 ];
11937
11938 for run in runs {
11939 let run_variations = [
11940 (LspInsertMode::Insert, run.expected_with_insert_mode),
11941 (LspInsertMode::Replace, run.expected_with_replace_mode),
11942 (
11943 LspInsertMode::ReplaceSubsequence,
11944 run.expected_with_replace_subsequence_mode,
11945 ),
11946 (
11947 LspInsertMode::ReplaceSuffix,
11948 run.expected_with_replace_suffix_mode,
11949 ),
11950 ];
11951
11952 for (lsp_insert_mode, expected_text) in run_variations {
11953 eprintln!(
11954 "run = {:?}, mode = {lsp_insert_mode:.?}",
11955 run.run_description,
11956 );
11957
11958 update_test_language_settings(&mut cx, |settings| {
11959 settings.defaults.completions = Some(CompletionSettings {
11960 lsp_insert_mode,
11961 words: WordsCompletionMode::Disabled,
11962 lsp: true,
11963 lsp_fetch_timeout_ms: 0,
11964 });
11965 });
11966
11967 cx.set_state(&run.initial_state);
11968 cx.update_editor(|editor, window, cx| {
11969 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11970 });
11971
11972 let counter = Arc::new(AtomicUsize::new(0));
11973 handle_completion_request_with_insert_and_replace(
11974 &mut cx,
11975 &run.buffer_marked_text,
11976 vec![(run.completion_label, run.completion_text)],
11977 counter.clone(),
11978 )
11979 .await;
11980 cx.condition(|editor, _| editor.context_menu_visible())
11981 .await;
11982 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11983
11984 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11985 editor
11986 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11987 .unwrap()
11988 });
11989 cx.assert_editor_state(&expected_text);
11990 handle_resolve_completion_request(&mut cx, None).await;
11991 apply_additional_edits.await.unwrap();
11992 }
11993 }
11994}
11995
11996#[gpui::test]
11997async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
11998 init_test(cx, |_| {});
11999 let mut cx = EditorLspTestContext::new_rust(
12000 lsp::ServerCapabilities {
12001 completion_provider: Some(lsp::CompletionOptions {
12002 resolve_provider: Some(true),
12003 ..Default::default()
12004 }),
12005 ..Default::default()
12006 },
12007 cx,
12008 )
12009 .await;
12010
12011 let initial_state = "SubˇError";
12012 let buffer_marked_text = "<Sub|Error>";
12013 let completion_text = "SubscriptionError";
12014 let expected_with_insert_mode = "SubscriptionErrorˇError";
12015 let expected_with_replace_mode = "SubscriptionErrorˇ";
12016
12017 update_test_language_settings(&mut cx, |settings| {
12018 settings.defaults.completions = Some(CompletionSettings {
12019 words: WordsCompletionMode::Disabled,
12020 // set the opposite here to ensure that the action is overriding the default behavior
12021 lsp_insert_mode: LspInsertMode::Insert,
12022 lsp: true,
12023 lsp_fetch_timeout_ms: 0,
12024 });
12025 });
12026
12027 cx.set_state(initial_state);
12028 cx.update_editor(|editor, window, cx| {
12029 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12030 });
12031
12032 let counter = Arc::new(AtomicUsize::new(0));
12033 handle_completion_request_with_insert_and_replace(
12034 &mut cx,
12035 &buffer_marked_text,
12036 vec![(completion_text, completion_text)],
12037 counter.clone(),
12038 )
12039 .await;
12040 cx.condition(|editor, _| editor.context_menu_visible())
12041 .await;
12042 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12043
12044 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12045 editor
12046 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12047 .unwrap()
12048 });
12049 cx.assert_editor_state(&expected_with_replace_mode);
12050 handle_resolve_completion_request(&mut cx, None).await;
12051 apply_additional_edits.await.unwrap();
12052
12053 update_test_language_settings(&mut cx, |settings| {
12054 settings.defaults.completions = Some(CompletionSettings {
12055 words: WordsCompletionMode::Disabled,
12056 // set the opposite here to ensure that the action is overriding the default behavior
12057 lsp_insert_mode: LspInsertMode::Replace,
12058 lsp: true,
12059 lsp_fetch_timeout_ms: 0,
12060 });
12061 });
12062
12063 cx.set_state(initial_state);
12064 cx.update_editor(|editor, window, cx| {
12065 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12066 });
12067 handle_completion_request_with_insert_and_replace(
12068 &mut cx,
12069 &buffer_marked_text,
12070 vec![(completion_text, completion_text)],
12071 counter.clone(),
12072 )
12073 .await;
12074 cx.condition(|editor, _| editor.context_menu_visible())
12075 .await;
12076 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12077
12078 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12079 editor
12080 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
12081 .unwrap()
12082 });
12083 cx.assert_editor_state(&expected_with_insert_mode);
12084 handle_resolve_completion_request(&mut cx, None).await;
12085 apply_additional_edits.await.unwrap();
12086}
12087
12088#[gpui::test]
12089async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
12090 init_test(cx, |_| {});
12091 let mut cx = EditorLspTestContext::new_rust(
12092 lsp::ServerCapabilities {
12093 completion_provider: Some(lsp::CompletionOptions {
12094 resolve_provider: Some(true),
12095 ..Default::default()
12096 }),
12097 ..Default::default()
12098 },
12099 cx,
12100 )
12101 .await;
12102
12103 // scenario: surrounding text matches completion text
12104 let completion_text = "to_offset";
12105 let initial_state = indoc! {"
12106 1. buf.to_offˇsuffix
12107 2. buf.to_offˇsuf
12108 3. buf.to_offˇfix
12109 4. buf.to_offˇ
12110 5. into_offˇensive
12111 6. ˇsuffix
12112 7. let ˇ //
12113 8. aaˇzz
12114 9. buf.to_off«zzzzzˇ»suffix
12115 10. buf.«ˇzzzzz»suffix
12116 11. to_off«ˇzzzzz»
12117
12118 buf.to_offˇsuffix // newest cursor
12119 "};
12120 let completion_marked_buffer = indoc! {"
12121 1. buf.to_offsuffix
12122 2. buf.to_offsuf
12123 3. buf.to_offfix
12124 4. buf.to_off
12125 5. into_offensive
12126 6. suffix
12127 7. let //
12128 8. aazz
12129 9. buf.to_offzzzzzsuffix
12130 10. buf.zzzzzsuffix
12131 11. to_offzzzzz
12132
12133 buf.<to_off|suffix> // newest cursor
12134 "};
12135 let expected = indoc! {"
12136 1. buf.to_offsetˇ
12137 2. buf.to_offsetˇsuf
12138 3. buf.to_offsetˇfix
12139 4. buf.to_offsetˇ
12140 5. into_offsetˇensive
12141 6. to_offsetˇsuffix
12142 7. let to_offsetˇ //
12143 8. aato_offsetˇzz
12144 9. buf.to_offsetˇ
12145 10. buf.to_offsetˇsuffix
12146 11. to_offsetˇ
12147
12148 buf.to_offsetˇ // newest cursor
12149 "};
12150 cx.set_state(initial_state);
12151 cx.update_editor(|editor, window, cx| {
12152 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12153 });
12154 handle_completion_request_with_insert_and_replace(
12155 &mut cx,
12156 completion_marked_buffer,
12157 vec![(completion_text, completion_text)],
12158 Arc::new(AtomicUsize::new(0)),
12159 )
12160 .await;
12161 cx.condition(|editor, _| editor.context_menu_visible())
12162 .await;
12163 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12164 editor
12165 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12166 .unwrap()
12167 });
12168 cx.assert_editor_state(expected);
12169 handle_resolve_completion_request(&mut cx, None).await;
12170 apply_additional_edits.await.unwrap();
12171
12172 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
12173 let completion_text = "foo_and_bar";
12174 let initial_state = indoc! {"
12175 1. ooanbˇ
12176 2. zooanbˇ
12177 3. ooanbˇz
12178 4. zooanbˇz
12179 5. ooanˇ
12180 6. oanbˇ
12181
12182 ooanbˇ
12183 "};
12184 let completion_marked_buffer = indoc! {"
12185 1. ooanb
12186 2. zooanb
12187 3. ooanbz
12188 4. zooanbz
12189 5. ooan
12190 6. oanb
12191
12192 <ooanb|>
12193 "};
12194 let expected = indoc! {"
12195 1. foo_and_barˇ
12196 2. zfoo_and_barˇ
12197 3. foo_and_barˇz
12198 4. zfoo_and_barˇz
12199 5. ooanfoo_and_barˇ
12200 6. oanbfoo_and_barˇ
12201
12202 foo_and_barˇ
12203 "};
12204 cx.set_state(initial_state);
12205 cx.update_editor(|editor, window, cx| {
12206 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12207 });
12208 handle_completion_request_with_insert_and_replace(
12209 &mut cx,
12210 completion_marked_buffer,
12211 vec![(completion_text, completion_text)],
12212 Arc::new(AtomicUsize::new(0)),
12213 )
12214 .await;
12215 cx.condition(|editor, _| editor.context_menu_visible())
12216 .await;
12217 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12218 editor
12219 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12220 .unwrap()
12221 });
12222 cx.assert_editor_state(expected);
12223 handle_resolve_completion_request(&mut cx, None).await;
12224 apply_additional_edits.await.unwrap();
12225
12226 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
12227 // (expects the same as if it was inserted at the end)
12228 let completion_text = "foo_and_bar";
12229 let initial_state = indoc! {"
12230 1. ooˇanb
12231 2. zooˇanb
12232 3. ooˇanbz
12233 4. zooˇanbz
12234
12235 ooˇanb
12236 "};
12237 let completion_marked_buffer = indoc! {"
12238 1. ooanb
12239 2. zooanb
12240 3. ooanbz
12241 4. zooanbz
12242
12243 <oo|anb>
12244 "};
12245 let expected = indoc! {"
12246 1. foo_and_barˇ
12247 2. zfoo_and_barˇ
12248 3. foo_and_barˇz
12249 4. zfoo_and_barˇz
12250
12251 foo_and_barˇ
12252 "};
12253 cx.set_state(initial_state);
12254 cx.update_editor(|editor, window, cx| {
12255 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12256 });
12257 handle_completion_request_with_insert_and_replace(
12258 &mut cx,
12259 completion_marked_buffer,
12260 vec![(completion_text, completion_text)],
12261 Arc::new(AtomicUsize::new(0)),
12262 )
12263 .await;
12264 cx.condition(|editor, _| editor.context_menu_visible())
12265 .await;
12266 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12267 editor
12268 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12269 .unwrap()
12270 });
12271 cx.assert_editor_state(expected);
12272 handle_resolve_completion_request(&mut cx, None).await;
12273 apply_additional_edits.await.unwrap();
12274}
12275
12276// This used to crash
12277#[gpui::test]
12278async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
12279 init_test(cx, |_| {});
12280
12281 let buffer_text = indoc! {"
12282 fn main() {
12283 10.satu;
12284
12285 //
12286 // separate cursors so they open in different excerpts (manually reproducible)
12287 //
12288
12289 10.satu20;
12290 }
12291 "};
12292 let multibuffer_text_with_selections = indoc! {"
12293 fn main() {
12294 10.satuˇ;
12295
12296 //
12297
12298 //
12299
12300 10.satuˇ20;
12301 }
12302 "};
12303 let expected_multibuffer = indoc! {"
12304 fn main() {
12305 10.saturating_sub()ˇ;
12306
12307 //
12308
12309 //
12310
12311 10.saturating_sub()ˇ;
12312 }
12313 "};
12314
12315 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
12316 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
12317
12318 let fs = FakeFs::new(cx.executor());
12319 fs.insert_tree(
12320 path!("/a"),
12321 json!({
12322 "main.rs": buffer_text,
12323 }),
12324 )
12325 .await;
12326
12327 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12328 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12329 language_registry.add(rust_lang());
12330 let mut fake_servers = language_registry.register_fake_lsp(
12331 "Rust",
12332 FakeLspAdapter {
12333 capabilities: lsp::ServerCapabilities {
12334 completion_provider: Some(lsp::CompletionOptions {
12335 resolve_provider: None,
12336 ..lsp::CompletionOptions::default()
12337 }),
12338 ..lsp::ServerCapabilities::default()
12339 },
12340 ..FakeLspAdapter::default()
12341 },
12342 );
12343 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12344 let cx = &mut VisualTestContext::from_window(*workspace, cx);
12345 let buffer = project
12346 .update(cx, |project, cx| {
12347 project.open_local_buffer(path!("/a/main.rs"), cx)
12348 })
12349 .await
12350 .unwrap();
12351
12352 let multi_buffer = cx.new(|cx| {
12353 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
12354 multi_buffer.push_excerpts(
12355 buffer.clone(),
12356 [ExcerptRange::new(0..first_excerpt_end)],
12357 cx,
12358 );
12359 multi_buffer.push_excerpts(
12360 buffer.clone(),
12361 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
12362 cx,
12363 );
12364 multi_buffer
12365 });
12366
12367 let editor = workspace
12368 .update(cx, |_, window, cx| {
12369 cx.new(|cx| {
12370 Editor::new(
12371 EditorMode::Full {
12372 scale_ui_elements_with_buffer_font_size: false,
12373 show_active_line_background: false,
12374 sized_by_content: false,
12375 },
12376 multi_buffer.clone(),
12377 Some(project.clone()),
12378 window,
12379 cx,
12380 )
12381 })
12382 })
12383 .unwrap();
12384
12385 let pane = workspace
12386 .update(cx, |workspace, _, _| workspace.active_pane().clone())
12387 .unwrap();
12388 pane.update_in(cx, |pane, window, cx| {
12389 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
12390 });
12391
12392 let fake_server = fake_servers.next().await.unwrap();
12393
12394 editor.update_in(cx, |editor, window, cx| {
12395 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12396 s.select_ranges([
12397 Point::new(1, 11)..Point::new(1, 11),
12398 Point::new(7, 11)..Point::new(7, 11),
12399 ])
12400 });
12401
12402 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
12403 });
12404
12405 editor.update_in(cx, |editor, window, cx| {
12406 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12407 });
12408
12409 fake_server
12410 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
12411 let completion_item = lsp::CompletionItem {
12412 label: "saturating_sub()".into(),
12413 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
12414 lsp::InsertReplaceEdit {
12415 new_text: "saturating_sub()".to_owned(),
12416 insert: lsp::Range::new(
12417 lsp::Position::new(7, 7),
12418 lsp::Position::new(7, 11),
12419 ),
12420 replace: lsp::Range::new(
12421 lsp::Position::new(7, 7),
12422 lsp::Position::new(7, 13),
12423 ),
12424 },
12425 )),
12426 ..lsp::CompletionItem::default()
12427 };
12428
12429 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
12430 })
12431 .next()
12432 .await
12433 .unwrap();
12434
12435 cx.condition(&editor, |editor, _| editor.context_menu_visible())
12436 .await;
12437
12438 editor
12439 .update_in(cx, |editor, window, cx| {
12440 editor
12441 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12442 .unwrap()
12443 })
12444 .await
12445 .unwrap();
12446
12447 editor.update(cx, |editor, cx| {
12448 assert_text_with_selections(editor, expected_multibuffer, cx);
12449 })
12450}
12451
12452#[gpui::test]
12453async fn test_completion(cx: &mut TestAppContext) {
12454 init_test(cx, |_| {});
12455
12456 let mut cx = EditorLspTestContext::new_rust(
12457 lsp::ServerCapabilities {
12458 completion_provider: Some(lsp::CompletionOptions {
12459 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12460 resolve_provider: Some(true),
12461 ..Default::default()
12462 }),
12463 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12464 ..Default::default()
12465 },
12466 cx,
12467 )
12468 .await;
12469 let counter = Arc::new(AtomicUsize::new(0));
12470
12471 cx.set_state(indoc! {"
12472 oneˇ
12473 two
12474 three
12475 "});
12476 cx.simulate_keystroke(".");
12477 handle_completion_request(
12478 indoc! {"
12479 one.|<>
12480 two
12481 three
12482 "},
12483 vec!["first_completion", "second_completion"],
12484 true,
12485 counter.clone(),
12486 &mut cx,
12487 )
12488 .await;
12489 cx.condition(|editor, _| editor.context_menu_visible())
12490 .await;
12491 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12492
12493 let _handler = handle_signature_help_request(
12494 &mut cx,
12495 lsp::SignatureHelp {
12496 signatures: vec![lsp::SignatureInformation {
12497 label: "test signature".to_string(),
12498 documentation: None,
12499 parameters: Some(vec![lsp::ParameterInformation {
12500 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
12501 documentation: None,
12502 }]),
12503 active_parameter: None,
12504 }],
12505 active_signature: None,
12506 active_parameter: None,
12507 },
12508 );
12509 cx.update_editor(|editor, window, cx| {
12510 assert!(
12511 !editor.signature_help_state.is_shown(),
12512 "No signature help was called for"
12513 );
12514 editor.show_signature_help(&ShowSignatureHelp, window, cx);
12515 });
12516 cx.run_until_parked();
12517 cx.update_editor(|editor, _, _| {
12518 assert!(
12519 !editor.signature_help_state.is_shown(),
12520 "No signature help should be shown when completions menu is open"
12521 );
12522 });
12523
12524 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12525 editor.context_menu_next(&Default::default(), window, cx);
12526 editor
12527 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12528 .unwrap()
12529 });
12530 cx.assert_editor_state(indoc! {"
12531 one.second_completionˇ
12532 two
12533 three
12534 "});
12535
12536 handle_resolve_completion_request(
12537 &mut cx,
12538 Some(vec![
12539 (
12540 //This overlaps with the primary completion edit which is
12541 //misbehavior from the LSP spec, test that we filter it out
12542 indoc! {"
12543 one.second_ˇcompletion
12544 two
12545 threeˇ
12546 "},
12547 "overlapping additional edit",
12548 ),
12549 (
12550 indoc! {"
12551 one.second_completion
12552 two
12553 threeˇ
12554 "},
12555 "\nadditional edit",
12556 ),
12557 ]),
12558 )
12559 .await;
12560 apply_additional_edits.await.unwrap();
12561 cx.assert_editor_state(indoc! {"
12562 one.second_completionˇ
12563 two
12564 three
12565 additional edit
12566 "});
12567
12568 cx.set_state(indoc! {"
12569 one.second_completion
12570 twoˇ
12571 threeˇ
12572 additional edit
12573 "});
12574 cx.simulate_keystroke(" ");
12575 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12576 cx.simulate_keystroke("s");
12577 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12578
12579 cx.assert_editor_state(indoc! {"
12580 one.second_completion
12581 two sˇ
12582 three sˇ
12583 additional edit
12584 "});
12585 handle_completion_request(
12586 indoc! {"
12587 one.second_completion
12588 two s
12589 three <s|>
12590 additional edit
12591 "},
12592 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
12593 true,
12594 counter.clone(),
12595 &mut cx,
12596 )
12597 .await;
12598 cx.condition(|editor, _| editor.context_menu_visible())
12599 .await;
12600 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12601
12602 cx.simulate_keystroke("i");
12603
12604 handle_completion_request(
12605 indoc! {"
12606 one.second_completion
12607 two si
12608 three <si|>
12609 additional edit
12610 "},
12611 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
12612 true,
12613 counter.clone(),
12614 &mut cx,
12615 )
12616 .await;
12617 cx.condition(|editor, _| editor.context_menu_visible())
12618 .await;
12619 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12620
12621 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12622 editor
12623 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12624 .unwrap()
12625 });
12626 cx.assert_editor_state(indoc! {"
12627 one.second_completion
12628 two sixth_completionˇ
12629 three sixth_completionˇ
12630 additional edit
12631 "});
12632
12633 apply_additional_edits.await.unwrap();
12634
12635 update_test_language_settings(&mut cx, |settings| {
12636 settings.defaults.show_completions_on_input = Some(false);
12637 });
12638 cx.set_state("editorˇ");
12639 cx.simulate_keystroke(".");
12640 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12641 cx.simulate_keystrokes("c l o");
12642 cx.assert_editor_state("editor.cloˇ");
12643 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12644 cx.update_editor(|editor, window, cx| {
12645 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12646 });
12647 handle_completion_request(
12648 "editor.<clo|>",
12649 vec!["close", "clobber"],
12650 true,
12651 counter.clone(),
12652 &mut cx,
12653 )
12654 .await;
12655 cx.condition(|editor, _| editor.context_menu_visible())
12656 .await;
12657 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
12658
12659 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12660 editor
12661 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12662 .unwrap()
12663 });
12664 cx.assert_editor_state("editor.clobberˇ");
12665 handle_resolve_completion_request(&mut cx, None).await;
12666 apply_additional_edits.await.unwrap();
12667}
12668
12669#[gpui::test]
12670async fn test_completion_reuse(cx: &mut TestAppContext) {
12671 init_test(cx, |_| {});
12672
12673 let mut cx = EditorLspTestContext::new_rust(
12674 lsp::ServerCapabilities {
12675 completion_provider: Some(lsp::CompletionOptions {
12676 trigger_characters: Some(vec![".".to_string()]),
12677 ..Default::default()
12678 }),
12679 ..Default::default()
12680 },
12681 cx,
12682 )
12683 .await;
12684
12685 let counter = Arc::new(AtomicUsize::new(0));
12686 cx.set_state("objˇ");
12687 cx.simulate_keystroke(".");
12688
12689 // Initial completion request returns complete results
12690 let is_incomplete = false;
12691 handle_completion_request(
12692 "obj.|<>",
12693 vec!["a", "ab", "abc"],
12694 is_incomplete,
12695 counter.clone(),
12696 &mut cx,
12697 )
12698 .await;
12699 cx.run_until_parked();
12700 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12701 cx.assert_editor_state("obj.ˇ");
12702 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12703
12704 // Type "a" - filters existing completions
12705 cx.simulate_keystroke("a");
12706 cx.run_until_parked();
12707 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12708 cx.assert_editor_state("obj.aˇ");
12709 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12710
12711 // Type "b" - filters existing completions
12712 cx.simulate_keystroke("b");
12713 cx.run_until_parked();
12714 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12715 cx.assert_editor_state("obj.abˇ");
12716 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12717
12718 // Type "c" - filters existing completions
12719 cx.simulate_keystroke("c");
12720 cx.run_until_parked();
12721 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12722 cx.assert_editor_state("obj.abcˇ");
12723 check_displayed_completions(vec!["abc"], &mut cx);
12724
12725 // Backspace to delete "c" - filters existing completions
12726 cx.update_editor(|editor, window, cx| {
12727 editor.backspace(&Backspace, window, cx);
12728 });
12729 cx.run_until_parked();
12730 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12731 cx.assert_editor_state("obj.abˇ");
12732 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12733
12734 // Moving cursor to the left dismisses menu.
12735 cx.update_editor(|editor, window, cx| {
12736 editor.move_left(&MoveLeft, window, cx);
12737 });
12738 cx.run_until_parked();
12739 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12740 cx.assert_editor_state("obj.aˇb");
12741 cx.update_editor(|editor, _, _| {
12742 assert_eq!(editor.context_menu_visible(), false);
12743 });
12744
12745 // Type "b" - new request
12746 cx.simulate_keystroke("b");
12747 let is_incomplete = false;
12748 handle_completion_request(
12749 "obj.<ab|>a",
12750 vec!["ab", "abc"],
12751 is_incomplete,
12752 counter.clone(),
12753 &mut cx,
12754 )
12755 .await;
12756 cx.run_until_parked();
12757 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12758 cx.assert_editor_state("obj.abˇb");
12759 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12760
12761 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
12762 cx.update_editor(|editor, window, cx| {
12763 editor.backspace(&Backspace, window, cx);
12764 });
12765 let is_incomplete = false;
12766 handle_completion_request(
12767 "obj.<a|>b",
12768 vec!["a", "ab", "abc"],
12769 is_incomplete,
12770 counter.clone(),
12771 &mut cx,
12772 )
12773 .await;
12774 cx.run_until_parked();
12775 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12776 cx.assert_editor_state("obj.aˇb");
12777 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12778
12779 // Backspace to delete "a" - dismisses menu.
12780 cx.update_editor(|editor, window, cx| {
12781 editor.backspace(&Backspace, window, cx);
12782 });
12783 cx.run_until_parked();
12784 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12785 cx.assert_editor_state("obj.ˇb");
12786 cx.update_editor(|editor, _, _| {
12787 assert_eq!(editor.context_menu_visible(), false);
12788 });
12789}
12790
12791#[gpui::test]
12792async fn test_word_completion(cx: &mut TestAppContext) {
12793 let lsp_fetch_timeout_ms = 10;
12794 init_test(cx, |language_settings| {
12795 language_settings.defaults.completions = Some(CompletionSettings {
12796 words: WordsCompletionMode::Fallback,
12797 lsp: true,
12798 lsp_fetch_timeout_ms: 10,
12799 lsp_insert_mode: LspInsertMode::Insert,
12800 });
12801 });
12802
12803 let mut cx = EditorLspTestContext::new_rust(
12804 lsp::ServerCapabilities {
12805 completion_provider: Some(lsp::CompletionOptions {
12806 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12807 ..lsp::CompletionOptions::default()
12808 }),
12809 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12810 ..lsp::ServerCapabilities::default()
12811 },
12812 cx,
12813 )
12814 .await;
12815
12816 let throttle_completions = Arc::new(AtomicBool::new(false));
12817
12818 let lsp_throttle_completions = throttle_completions.clone();
12819 let _completion_requests_handler =
12820 cx.lsp
12821 .server
12822 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
12823 let lsp_throttle_completions = lsp_throttle_completions.clone();
12824 let cx = cx.clone();
12825 async move {
12826 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
12827 cx.background_executor()
12828 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
12829 .await;
12830 }
12831 Ok(Some(lsp::CompletionResponse::Array(vec![
12832 lsp::CompletionItem {
12833 label: "first".into(),
12834 ..lsp::CompletionItem::default()
12835 },
12836 lsp::CompletionItem {
12837 label: "last".into(),
12838 ..lsp::CompletionItem::default()
12839 },
12840 ])))
12841 }
12842 });
12843
12844 cx.set_state(indoc! {"
12845 oneˇ
12846 two
12847 three
12848 "});
12849 cx.simulate_keystroke(".");
12850 cx.executor().run_until_parked();
12851 cx.condition(|editor, _| editor.context_menu_visible())
12852 .await;
12853 cx.update_editor(|editor, window, cx| {
12854 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12855 {
12856 assert_eq!(
12857 completion_menu_entries(&menu),
12858 &["first", "last"],
12859 "When LSP server is fast to reply, no fallback word completions are used"
12860 );
12861 } else {
12862 panic!("expected completion menu to be open");
12863 }
12864 editor.cancel(&Cancel, window, cx);
12865 });
12866 cx.executor().run_until_parked();
12867 cx.condition(|editor, _| !editor.context_menu_visible())
12868 .await;
12869
12870 throttle_completions.store(true, atomic::Ordering::Release);
12871 cx.simulate_keystroke(".");
12872 cx.executor()
12873 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
12874 cx.executor().run_until_parked();
12875 cx.condition(|editor, _| editor.context_menu_visible())
12876 .await;
12877 cx.update_editor(|editor, _, _| {
12878 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12879 {
12880 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
12881 "When LSP server is slow, document words can be shown instead, if configured accordingly");
12882 } else {
12883 panic!("expected completion menu to be open");
12884 }
12885 });
12886}
12887
12888#[gpui::test]
12889async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
12890 init_test(cx, |language_settings| {
12891 language_settings.defaults.completions = Some(CompletionSettings {
12892 words: WordsCompletionMode::Enabled,
12893 lsp: true,
12894 lsp_fetch_timeout_ms: 0,
12895 lsp_insert_mode: LspInsertMode::Insert,
12896 });
12897 });
12898
12899 let mut cx = EditorLspTestContext::new_rust(
12900 lsp::ServerCapabilities {
12901 completion_provider: Some(lsp::CompletionOptions {
12902 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12903 ..lsp::CompletionOptions::default()
12904 }),
12905 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12906 ..lsp::ServerCapabilities::default()
12907 },
12908 cx,
12909 )
12910 .await;
12911
12912 let _completion_requests_handler =
12913 cx.lsp
12914 .server
12915 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
12916 Ok(Some(lsp::CompletionResponse::Array(vec![
12917 lsp::CompletionItem {
12918 label: "first".into(),
12919 ..lsp::CompletionItem::default()
12920 },
12921 lsp::CompletionItem {
12922 label: "last".into(),
12923 ..lsp::CompletionItem::default()
12924 },
12925 ])))
12926 });
12927
12928 cx.set_state(indoc! {"ˇ
12929 first
12930 last
12931 second
12932 "});
12933 cx.simulate_keystroke(".");
12934 cx.executor().run_until_parked();
12935 cx.condition(|editor, _| editor.context_menu_visible())
12936 .await;
12937 cx.update_editor(|editor, _, _| {
12938 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12939 {
12940 assert_eq!(
12941 completion_menu_entries(&menu),
12942 &["first", "last", "second"],
12943 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
12944 );
12945 } else {
12946 panic!("expected completion menu to be open");
12947 }
12948 });
12949}
12950
12951#[gpui::test]
12952async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
12953 init_test(cx, |language_settings| {
12954 language_settings.defaults.completions = Some(CompletionSettings {
12955 words: WordsCompletionMode::Disabled,
12956 lsp: true,
12957 lsp_fetch_timeout_ms: 0,
12958 lsp_insert_mode: LspInsertMode::Insert,
12959 });
12960 });
12961
12962 let mut cx = EditorLspTestContext::new_rust(
12963 lsp::ServerCapabilities {
12964 completion_provider: Some(lsp::CompletionOptions {
12965 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12966 ..lsp::CompletionOptions::default()
12967 }),
12968 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12969 ..lsp::ServerCapabilities::default()
12970 },
12971 cx,
12972 )
12973 .await;
12974
12975 let _completion_requests_handler =
12976 cx.lsp
12977 .server
12978 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
12979 panic!("LSP completions should not be queried when dealing with word completions")
12980 });
12981
12982 cx.set_state(indoc! {"ˇ
12983 first
12984 last
12985 second
12986 "});
12987 cx.update_editor(|editor, window, cx| {
12988 editor.show_word_completions(&ShowWordCompletions, window, cx);
12989 });
12990 cx.executor().run_until_parked();
12991 cx.condition(|editor, _| editor.context_menu_visible())
12992 .await;
12993 cx.update_editor(|editor, _, _| {
12994 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12995 {
12996 assert_eq!(
12997 completion_menu_entries(&menu),
12998 &["first", "last", "second"],
12999 "`ShowWordCompletions` action should show word completions"
13000 );
13001 } else {
13002 panic!("expected completion menu to be open");
13003 }
13004 });
13005
13006 cx.simulate_keystroke("l");
13007 cx.executor().run_until_parked();
13008 cx.condition(|editor, _| editor.context_menu_visible())
13009 .await;
13010 cx.update_editor(|editor, _, _| {
13011 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13012 {
13013 assert_eq!(
13014 completion_menu_entries(&menu),
13015 &["last"],
13016 "After showing word completions, further editing should filter them and not query the LSP"
13017 );
13018 } else {
13019 panic!("expected completion menu to be open");
13020 }
13021 });
13022}
13023
13024#[gpui::test]
13025async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
13026 init_test(cx, |language_settings| {
13027 language_settings.defaults.completions = Some(CompletionSettings {
13028 words: WordsCompletionMode::Fallback,
13029 lsp: false,
13030 lsp_fetch_timeout_ms: 0,
13031 lsp_insert_mode: LspInsertMode::Insert,
13032 });
13033 });
13034
13035 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13036
13037 cx.set_state(indoc! {"ˇ
13038 0_usize
13039 let
13040 33
13041 4.5f32
13042 "});
13043 cx.update_editor(|editor, window, cx| {
13044 editor.show_completions(&ShowCompletions::default(), window, cx);
13045 });
13046 cx.executor().run_until_parked();
13047 cx.condition(|editor, _| editor.context_menu_visible())
13048 .await;
13049 cx.update_editor(|editor, window, cx| {
13050 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13051 {
13052 assert_eq!(
13053 completion_menu_entries(&menu),
13054 &["let"],
13055 "With no digits in the completion query, no digits should be in the word completions"
13056 );
13057 } else {
13058 panic!("expected completion menu to be open");
13059 }
13060 editor.cancel(&Cancel, window, cx);
13061 });
13062
13063 cx.set_state(indoc! {"3ˇ
13064 0_usize
13065 let
13066 3
13067 33.35f32
13068 "});
13069 cx.update_editor(|editor, window, cx| {
13070 editor.show_completions(&ShowCompletions::default(), window, cx);
13071 });
13072 cx.executor().run_until_parked();
13073 cx.condition(|editor, _| editor.context_menu_visible())
13074 .await;
13075 cx.update_editor(|editor, _, _| {
13076 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13077 {
13078 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
13079 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
13080 } else {
13081 panic!("expected completion menu to be open");
13082 }
13083 });
13084}
13085
13086fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
13087 let position = || lsp::Position {
13088 line: params.text_document_position.position.line,
13089 character: params.text_document_position.position.character,
13090 };
13091 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13092 range: lsp::Range {
13093 start: position(),
13094 end: position(),
13095 },
13096 new_text: text.to_string(),
13097 }))
13098}
13099
13100#[gpui::test]
13101async fn test_multiline_completion(cx: &mut TestAppContext) {
13102 init_test(cx, |_| {});
13103
13104 let fs = FakeFs::new(cx.executor());
13105 fs.insert_tree(
13106 path!("/a"),
13107 json!({
13108 "main.ts": "a",
13109 }),
13110 )
13111 .await;
13112
13113 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13114 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13115 let typescript_language = Arc::new(Language::new(
13116 LanguageConfig {
13117 name: "TypeScript".into(),
13118 matcher: LanguageMatcher {
13119 path_suffixes: vec!["ts".to_string()],
13120 ..LanguageMatcher::default()
13121 },
13122 line_comments: vec!["// ".into()],
13123 ..LanguageConfig::default()
13124 },
13125 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
13126 ));
13127 language_registry.add(typescript_language.clone());
13128 let mut fake_servers = language_registry.register_fake_lsp(
13129 "TypeScript",
13130 FakeLspAdapter {
13131 capabilities: lsp::ServerCapabilities {
13132 completion_provider: Some(lsp::CompletionOptions {
13133 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
13134 ..lsp::CompletionOptions::default()
13135 }),
13136 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13137 ..lsp::ServerCapabilities::default()
13138 },
13139 // Emulate vtsls label generation
13140 label_for_completion: Some(Box::new(|item, _| {
13141 let text = if let Some(description) = item
13142 .label_details
13143 .as_ref()
13144 .and_then(|label_details| label_details.description.as_ref())
13145 {
13146 format!("{} {}", item.label, description)
13147 } else if let Some(detail) = &item.detail {
13148 format!("{} {}", item.label, detail)
13149 } else {
13150 item.label.clone()
13151 };
13152 let len = text.len();
13153 Some(language::CodeLabel {
13154 text,
13155 runs: Vec::new(),
13156 filter_range: 0..len,
13157 })
13158 })),
13159 ..FakeLspAdapter::default()
13160 },
13161 );
13162 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13163 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13164 let worktree_id = workspace
13165 .update(cx, |workspace, _window, cx| {
13166 workspace.project().update(cx, |project, cx| {
13167 project.worktrees(cx).next().unwrap().read(cx).id()
13168 })
13169 })
13170 .unwrap();
13171 let _buffer = project
13172 .update(cx, |project, cx| {
13173 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
13174 })
13175 .await
13176 .unwrap();
13177 let editor = workspace
13178 .update(cx, |workspace, window, cx| {
13179 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
13180 })
13181 .unwrap()
13182 .await
13183 .unwrap()
13184 .downcast::<Editor>()
13185 .unwrap();
13186 let fake_server = fake_servers.next().await.unwrap();
13187
13188 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
13189 let multiline_label_2 = "a\nb\nc\n";
13190 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
13191 let multiline_description = "d\ne\nf\n";
13192 let multiline_detail_2 = "g\nh\ni\n";
13193
13194 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
13195 move |params, _| async move {
13196 Ok(Some(lsp::CompletionResponse::Array(vec![
13197 lsp::CompletionItem {
13198 label: multiline_label.to_string(),
13199 text_edit: gen_text_edit(¶ms, "new_text_1"),
13200 ..lsp::CompletionItem::default()
13201 },
13202 lsp::CompletionItem {
13203 label: "single line label 1".to_string(),
13204 detail: Some(multiline_detail.to_string()),
13205 text_edit: gen_text_edit(¶ms, "new_text_2"),
13206 ..lsp::CompletionItem::default()
13207 },
13208 lsp::CompletionItem {
13209 label: "single line label 2".to_string(),
13210 label_details: Some(lsp::CompletionItemLabelDetails {
13211 description: Some(multiline_description.to_string()),
13212 detail: None,
13213 }),
13214 text_edit: gen_text_edit(¶ms, "new_text_2"),
13215 ..lsp::CompletionItem::default()
13216 },
13217 lsp::CompletionItem {
13218 label: multiline_label_2.to_string(),
13219 detail: Some(multiline_detail_2.to_string()),
13220 text_edit: gen_text_edit(¶ms, "new_text_3"),
13221 ..lsp::CompletionItem::default()
13222 },
13223 lsp::CompletionItem {
13224 label: "Label with many spaces and \t but without newlines".to_string(),
13225 detail: Some(
13226 "Details with many spaces and \t but without newlines".to_string(),
13227 ),
13228 text_edit: gen_text_edit(¶ms, "new_text_4"),
13229 ..lsp::CompletionItem::default()
13230 },
13231 ])))
13232 },
13233 );
13234
13235 editor.update_in(cx, |editor, window, cx| {
13236 cx.focus_self(window);
13237 editor.move_to_end(&MoveToEnd, window, cx);
13238 editor.handle_input(".", window, cx);
13239 });
13240 cx.run_until_parked();
13241 completion_handle.next().await.unwrap();
13242
13243 editor.update(cx, |editor, _| {
13244 assert!(editor.context_menu_visible());
13245 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13246 {
13247 let completion_labels = menu
13248 .completions
13249 .borrow()
13250 .iter()
13251 .map(|c| c.label.text.clone())
13252 .collect::<Vec<_>>();
13253 assert_eq!(
13254 completion_labels,
13255 &[
13256 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
13257 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
13258 "single line label 2 d e f ",
13259 "a b c g h i ",
13260 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
13261 ],
13262 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
13263 );
13264
13265 for completion in menu
13266 .completions
13267 .borrow()
13268 .iter() {
13269 assert_eq!(
13270 completion.label.filter_range,
13271 0..completion.label.text.len(),
13272 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
13273 );
13274 }
13275 } else {
13276 panic!("expected completion menu to be open");
13277 }
13278 });
13279}
13280
13281#[gpui::test]
13282async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
13283 init_test(cx, |_| {});
13284 let mut cx = EditorLspTestContext::new_rust(
13285 lsp::ServerCapabilities {
13286 completion_provider: Some(lsp::CompletionOptions {
13287 trigger_characters: Some(vec![".".to_string()]),
13288 ..Default::default()
13289 }),
13290 ..Default::default()
13291 },
13292 cx,
13293 )
13294 .await;
13295 cx.lsp
13296 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13297 Ok(Some(lsp::CompletionResponse::Array(vec![
13298 lsp::CompletionItem {
13299 label: "first".into(),
13300 ..Default::default()
13301 },
13302 lsp::CompletionItem {
13303 label: "last".into(),
13304 ..Default::default()
13305 },
13306 ])))
13307 });
13308 cx.set_state("variableˇ");
13309 cx.simulate_keystroke(".");
13310 cx.executor().run_until_parked();
13311
13312 cx.update_editor(|editor, _, _| {
13313 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13314 {
13315 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
13316 } else {
13317 panic!("expected completion menu to be open");
13318 }
13319 });
13320
13321 cx.update_editor(|editor, window, cx| {
13322 editor.move_page_down(&MovePageDown::default(), window, cx);
13323 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13324 {
13325 assert!(
13326 menu.selected_item == 1,
13327 "expected PageDown to select the last item from the context menu"
13328 );
13329 } else {
13330 panic!("expected completion menu to stay open after PageDown");
13331 }
13332 });
13333
13334 cx.update_editor(|editor, window, cx| {
13335 editor.move_page_up(&MovePageUp::default(), window, cx);
13336 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13337 {
13338 assert!(
13339 menu.selected_item == 0,
13340 "expected PageUp to select the first item from the context menu"
13341 );
13342 } else {
13343 panic!("expected completion menu to stay open after PageUp");
13344 }
13345 });
13346}
13347
13348#[gpui::test]
13349async fn test_as_is_completions(cx: &mut TestAppContext) {
13350 init_test(cx, |_| {});
13351 let mut cx = EditorLspTestContext::new_rust(
13352 lsp::ServerCapabilities {
13353 completion_provider: Some(lsp::CompletionOptions {
13354 ..Default::default()
13355 }),
13356 ..Default::default()
13357 },
13358 cx,
13359 )
13360 .await;
13361 cx.lsp
13362 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13363 Ok(Some(lsp::CompletionResponse::Array(vec![
13364 lsp::CompletionItem {
13365 label: "unsafe".into(),
13366 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13367 range: lsp::Range {
13368 start: lsp::Position {
13369 line: 1,
13370 character: 2,
13371 },
13372 end: lsp::Position {
13373 line: 1,
13374 character: 3,
13375 },
13376 },
13377 new_text: "unsafe".to_string(),
13378 })),
13379 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
13380 ..Default::default()
13381 },
13382 ])))
13383 });
13384 cx.set_state("fn a() {}\n nˇ");
13385 cx.executor().run_until_parked();
13386 cx.update_editor(|editor, window, cx| {
13387 editor.show_completions(
13388 &ShowCompletions {
13389 trigger: Some("\n".into()),
13390 },
13391 window,
13392 cx,
13393 );
13394 });
13395 cx.executor().run_until_parked();
13396
13397 cx.update_editor(|editor, window, cx| {
13398 editor.confirm_completion(&Default::default(), window, cx)
13399 });
13400 cx.executor().run_until_parked();
13401 cx.assert_editor_state("fn a() {}\n unsafeˇ");
13402}
13403
13404#[gpui::test]
13405async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
13406 init_test(cx, |_| {});
13407 let language =
13408 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
13409 let mut cx = EditorLspTestContext::new(
13410 language,
13411 lsp::ServerCapabilities {
13412 completion_provider: Some(lsp::CompletionOptions {
13413 ..lsp::CompletionOptions::default()
13414 }),
13415 ..lsp::ServerCapabilities::default()
13416 },
13417 cx,
13418 )
13419 .await;
13420
13421 cx.set_state(
13422 "#ifndef BAR_H
13423#define BAR_H
13424
13425#include <stdbool.h>
13426
13427int fn_branch(bool do_branch1, bool do_branch2);
13428
13429#endif // BAR_H
13430ˇ",
13431 );
13432 cx.executor().run_until_parked();
13433 cx.update_editor(|editor, window, cx| {
13434 editor.handle_input("#", window, cx);
13435 });
13436 cx.executor().run_until_parked();
13437 cx.update_editor(|editor, window, cx| {
13438 editor.handle_input("i", window, cx);
13439 });
13440 cx.executor().run_until_parked();
13441 cx.update_editor(|editor, window, cx| {
13442 editor.handle_input("n", window, cx);
13443 });
13444 cx.executor().run_until_parked();
13445 cx.assert_editor_state(
13446 "#ifndef BAR_H
13447#define BAR_H
13448
13449#include <stdbool.h>
13450
13451int fn_branch(bool do_branch1, bool do_branch2);
13452
13453#endif // BAR_H
13454#inˇ",
13455 );
13456
13457 cx.lsp
13458 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13459 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
13460 is_incomplete: false,
13461 item_defaults: None,
13462 items: vec![lsp::CompletionItem {
13463 kind: Some(lsp::CompletionItemKind::SNIPPET),
13464 label_details: Some(lsp::CompletionItemLabelDetails {
13465 detail: Some("header".to_string()),
13466 description: None,
13467 }),
13468 label: " include".to_string(),
13469 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13470 range: lsp::Range {
13471 start: lsp::Position {
13472 line: 8,
13473 character: 1,
13474 },
13475 end: lsp::Position {
13476 line: 8,
13477 character: 1,
13478 },
13479 },
13480 new_text: "include \"$0\"".to_string(),
13481 })),
13482 sort_text: Some("40b67681include".to_string()),
13483 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
13484 filter_text: Some("include".to_string()),
13485 insert_text: Some("include \"$0\"".to_string()),
13486 ..lsp::CompletionItem::default()
13487 }],
13488 })))
13489 });
13490 cx.update_editor(|editor, window, cx| {
13491 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13492 });
13493 cx.executor().run_until_parked();
13494 cx.update_editor(|editor, window, cx| {
13495 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
13496 });
13497 cx.executor().run_until_parked();
13498 cx.assert_editor_state(
13499 "#ifndef BAR_H
13500#define BAR_H
13501
13502#include <stdbool.h>
13503
13504int fn_branch(bool do_branch1, bool do_branch2);
13505
13506#endif // BAR_H
13507#include \"ˇ\"",
13508 );
13509
13510 cx.lsp
13511 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13512 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
13513 is_incomplete: true,
13514 item_defaults: None,
13515 items: vec![lsp::CompletionItem {
13516 kind: Some(lsp::CompletionItemKind::FILE),
13517 label: "AGL/".to_string(),
13518 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13519 range: lsp::Range {
13520 start: lsp::Position {
13521 line: 8,
13522 character: 10,
13523 },
13524 end: lsp::Position {
13525 line: 8,
13526 character: 11,
13527 },
13528 },
13529 new_text: "AGL/".to_string(),
13530 })),
13531 sort_text: Some("40b67681AGL/".to_string()),
13532 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
13533 filter_text: Some("AGL/".to_string()),
13534 insert_text: Some("AGL/".to_string()),
13535 ..lsp::CompletionItem::default()
13536 }],
13537 })))
13538 });
13539 cx.update_editor(|editor, window, cx| {
13540 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13541 });
13542 cx.executor().run_until_parked();
13543 cx.update_editor(|editor, window, cx| {
13544 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
13545 });
13546 cx.executor().run_until_parked();
13547 cx.assert_editor_state(
13548 r##"#ifndef BAR_H
13549#define BAR_H
13550
13551#include <stdbool.h>
13552
13553int fn_branch(bool do_branch1, bool do_branch2);
13554
13555#endif // BAR_H
13556#include "AGL/ˇ"##,
13557 );
13558
13559 cx.update_editor(|editor, window, cx| {
13560 editor.handle_input("\"", window, cx);
13561 });
13562 cx.executor().run_until_parked();
13563 cx.assert_editor_state(
13564 r##"#ifndef BAR_H
13565#define BAR_H
13566
13567#include <stdbool.h>
13568
13569int fn_branch(bool do_branch1, bool do_branch2);
13570
13571#endif // BAR_H
13572#include "AGL/"ˇ"##,
13573 );
13574}
13575
13576#[gpui::test]
13577async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
13578 init_test(cx, |_| {});
13579
13580 let mut cx = EditorLspTestContext::new_rust(
13581 lsp::ServerCapabilities {
13582 completion_provider: Some(lsp::CompletionOptions {
13583 trigger_characters: Some(vec![".".to_string()]),
13584 resolve_provider: Some(true),
13585 ..Default::default()
13586 }),
13587 ..Default::default()
13588 },
13589 cx,
13590 )
13591 .await;
13592
13593 cx.set_state("fn main() { let a = 2ˇ; }");
13594 cx.simulate_keystroke(".");
13595 let completion_item = lsp::CompletionItem {
13596 label: "Some".into(),
13597 kind: Some(lsp::CompletionItemKind::SNIPPET),
13598 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
13599 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
13600 kind: lsp::MarkupKind::Markdown,
13601 value: "```rust\nSome(2)\n```".to_string(),
13602 })),
13603 deprecated: Some(false),
13604 sort_text: Some("Some".to_string()),
13605 filter_text: Some("Some".to_string()),
13606 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
13607 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13608 range: lsp::Range {
13609 start: lsp::Position {
13610 line: 0,
13611 character: 22,
13612 },
13613 end: lsp::Position {
13614 line: 0,
13615 character: 22,
13616 },
13617 },
13618 new_text: "Some(2)".to_string(),
13619 })),
13620 additional_text_edits: Some(vec![lsp::TextEdit {
13621 range: lsp::Range {
13622 start: lsp::Position {
13623 line: 0,
13624 character: 20,
13625 },
13626 end: lsp::Position {
13627 line: 0,
13628 character: 22,
13629 },
13630 },
13631 new_text: "".to_string(),
13632 }]),
13633 ..Default::default()
13634 };
13635
13636 let closure_completion_item = completion_item.clone();
13637 let counter = Arc::new(AtomicUsize::new(0));
13638 let counter_clone = counter.clone();
13639 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13640 let task_completion_item = closure_completion_item.clone();
13641 counter_clone.fetch_add(1, atomic::Ordering::Release);
13642 async move {
13643 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
13644 is_incomplete: true,
13645 item_defaults: None,
13646 items: vec![task_completion_item],
13647 })))
13648 }
13649 });
13650
13651 cx.condition(|editor, _| editor.context_menu_visible())
13652 .await;
13653 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
13654 assert!(request.next().await.is_some());
13655 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13656
13657 cx.simulate_keystrokes("S o m");
13658 cx.condition(|editor, _| editor.context_menu_visible())
13659 .await;
13660 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
13661 assert!(request.next().await.is_some());
13662 assert!(request.next().await.is_some());
13663 assert!(request.next().await.is_some());
13664 request.close();
13665 assert!(request.next().await.is_none());
13666 assert_eq!(
13667 counter.load(atomic::Ordering::Acquire),
13668 4,
13669 "With the completions menu open, only one LSP request should happen per input"
13670 );
13671}
13672
13673#[gpui::test]
13674async fn test_toggle_comment(cx: &mut TestAppContext) {
13675 init_test(cx, |_| {});
13676 let mut cx = EditorTestContext::new(cx).await;
13677 let language = Arc::new(Language::new(
13678 LanguageConfig {
13679 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
13680 ..Default::default()
13681 },
13682 Some(tree_sitter_rust::LANGUAGE.into()),
13683 ));
13684 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
13685
13686 // If multiple selections intersect a line, the line is only toggled once.
13687 cx.set_state(indoc! {"
13688 fn a() {
13689 «//b();
13690 ˇ»// «c();
13691 //ˇ» d();
13692 }
13693 "});
13694
13695 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13696
13697 cx.assert_editor_state(indoc! {"
13698 fn a() {
13699 «b();
13700 c();
13701 ˇ» d();
13702 }
13703 "});
13704
13705 // The comment prefix is inserted at the same column for every line in a
13706 // selection.
13707 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13708
13709 cx.assert_editor_state(indoc! {"
13710 fn a() {
13711 // «b();
13712 // c();
13713 ˇ»// d();
13714 }
13715 "});
13716
13717 // If a selection ends at the beginning of a line, that line is not toggled.
13718 cx.set_selections_state(indoc! {"
13719 fn a() {
13720 // b();
13721 «// c();
13722 ˇ» // d();
13723 }
13724 "});
13725
13726 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13727
13728 cx.assert_editor_state(indoc! {"
13729 fn a() {
13730 // b();
13731 «c();
13732 ˇ» // d();
13733 }
13734 "});
13735
13736 // If a selection span a single line and is empty, the line is toggled.
13737 cx.set_state(indoc! {"
13738 fn a() {
13739 a();
13740 b();
13741 ˇ
13742 }
13743 "});
13744
13745 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13746
13747 cx.assert_editor_state(indoc! {"
13748 fn a() {
13749 a();
13750 b();
13751 //•ˇ
13752 }
13753 "});
13754
13755 // If a selection span multiple lines, empty lines are not toggled.
13756 cx.set_state(indoc! {"
13757 fn a() {
13758 «a();
13759
13760 c();ˇ»
13761 }
13762 "});
13763
13764 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13765
13766 cx.assert_editor_state(indoc! {"
13767 fn a() {
13768 // «a();
13769
13770 // c();ˇ»
13771 }
13772 "});
13773
13774 // If a selection includes multiple comment prefixes, all lines are uncommented.
13775 cx.set_state(indoc! {"
13776 fn a() {
13777 «// a();
13778 /// b();
13779 //! c();ˇ»
13780 }
13781 "});
13782
13783 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13784
13785 cx.assert_editor_state(indoc! {"
13786 fn a() {
13787 «a();
13788 b();
13789 c();ˇ»
13790 }
13791 "});
13792}
13793
13794#[gpui::test]
13795async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
13796 init_test(cx, |_| {});
13797 let mut cx = EditorTestContext::new(cx).await;
13798 let language = Arc::new(Language::new(
13799 LanguageConfig {
13800 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
13801 ..Default::default()
13802 },
13803 Some(tree_sitter_rust::LANGUAGE.into()),
13804 ));
13805 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
13806
13807 let toggle_comments = &ToggleComments {
13808 advance_downwards: false,
13809 ignore_indent: true,
13810 };
13811
13812 // If multiple selections intersect a line, the line is only toggled once.
13813 cx.set_state(indoc! {"
13814 fn a() {
13815 // «b();
13816 // c();
13817 // ˇ» d();
13818 }
13819 "});
13820
13821 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13822
13823 cx.assert_editor_state(indoc! {"
13824 fn a() {
13825 «b();
13826 c();
13827 ˇ» d();
13828 }
13829 "});
13830
13831 // The comment prefix is inserted at the beginning of each line
13832 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13833
13834 cx.assert_editor_state(indoc! {"
13835 fn a() {
13836 // «b();
13837 // c();
13838 // ˇ» d();
13839 }
13840 "});
13841
13842 // If a selection ends at the beginning of a line, that line is not toggled.
13843 cx.set_selections_state(indoc! {"
13844 fn a() {
13845 // b();
13846 // «c();
13847 ˇ»// d();
13848 }
13849 "});
13850
13851 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13852
13853 cx.assert_editor_state(indoc! {"
13854 fn a() {
13855 // b();
13856 «c();
13857 ˇ»// d();
13858 }
13859 "});
13860
13861 // If a selection span a single line and is empty, the line is toggled.
13862 cx.set_state(indoc! {"
13863 fn a() {
13864 a();
13865 b();
13866 ˇ
13867 }
13868 "});
13869
13870 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13871
13872 cx.assert_editor_state(indoc! {"
13873 fn a() {
13874 a();
13875 b();
13876 //ˇ
13877 }
13878 "});
13879
13880 // If a selection span multiple lines, empty lines are not toggled.
13881 cx.set_state(indoc! {"
13882 fn a() {
13883 «a();
13884
13885 c();ˇ»
13886 }
13887 "});
13888
13889 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13890
13891 cx.assert_editor_state(indoc! {"
13892 fn a() {
13893 // «a();
13894
13895 // c();ˇ»
13896 }
13897 "});
13898
13899 // If a selection includes multiple comment prefixes, all lines are uncommented.
13900 cx.set_state(indoc! {"
13901 fn a() {
13902 // «a();
13903 /// b();
13904 //! c();ˇ»
13905 }
13906 "});
13907
13908 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13909
13910 cx.assert_editor_state(indoc! {"
13911 fn a() {
13912 «a();
13913 b();
13914 c();ˇ»
13915 }
13916 "});
13917}
13918
13919#[gpui::test]
13920async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
13921 init_test(cx, |_| {});
13922
13923 let language = Arc::new(Language::new(
13924 LanguageConfig {
13925 line_comments: vec!["// ".into()],
13926 ..Default::default()
13927 },
13928 Some(tree_sitter_rust::LANGUAGE.into()),
13929 ));
13930
13931 let mut cx = EditorTestContext::new(cx).await;
13932
13933 cx.language_registry().add(language.clone());
13934 cx.update_buffer(|buffer, cx| {
13935 buffer.set_language(Some(language), cx);
13936 });
13937
13938 let toggle_comments = &ToggleComments {
13939 advance_downwards: true,
13940 ignore_indent: false,
13941 };
13942
13943 // Single cursor on one line -> advance
13944 // Cursor moves horizontally 3 characters as well on non-blank line
13945 cx.set_state(indoc!(
13946 "fn a() {
13947 ˇdog();
13948 cat();
13949 }"
13950 ));
13951 cx.update_editor(|editor, window, cx| {
13952 editor.toggle_comments(toggle_comments, window, cx);
13953 });
13954 cx.assert_editor_state(indoc!(
13955 "fn a() {
13956 // dog();
13957 catˇ();
13958 }"
13959 ));
13960
13961 // Single selection on one line -> don't advance
13962 cx.set_state(indoc!(
13963 "fn a() {
13964 «dog()ˇ»;
13965 cat();
13966 }"
13967 ));
13968 cx.update_editor(|editor, window, cx| {
13969 editor.toggle_comments(toggle_comments, window, cx);
13970 });
13971 cx.assert_editor_state(indoc!(
13972 "fn a() {
13973 // «dog()ˇ»;
13974 cat();
13975 }"
13976 ));
13977
13978 // Multiple cursors on one line -> advance
13979 cx.set_state(indoc!(
13980 "fn a() {
13981 ˇdˇog();
13982 cat();
13983 }"
13984 ));
13985 cx.update_editor(|editor, window, cx| {
13986 editor.toggle_comments(toggle_comments, window, cx);
13987 });
13988 cx.assert_editor_state(indoc!(
13989 "fn a() {
13990 // dog();
13991 catˇ(ˇ);
13992 }"
13993 ));
13994
13995 // Multiple cursors on one line, with selection -> don't advance
13996 cx.set_state(indoc!(
13997 "fn a() {
13998 ˇdˇog«()ˇ»;
13999 cat();
14000 }"
14001 ));
14002 cx.update_editor(|editor, window, cx| {
14003 editor.toggle_comments(toggle_comments, window, cx);
14004 });
14005 cx.assert_editor_state(indoc!(
14006 "fn a() {
14007 // ˇdˇog«()ˇ»;
14008 cat();
14009 }"
14010 ));
14011
14012 // Single cursor on one line -> advance
14013 // Cursor moves to column 0 on blank line
14014 cx.set_state(indoc!(
14015 "fn a() {
14016 ˇdog();
14017
14018 cat();
14019 }"
14020 ));
14021 cx.update_editor(|editor, window, cx| {
14022 editor.toggle_comments(toggle_comments, window, cx);
14023 });
14024 cx.assert_editor_state(indoc!(
14025 "fn a() {
14026 // dog();
14027 ˇ
14028 cat();
14029 }"
14030 ));
14031
14032 // Single cursor on one line -> advance
14033 // Cursor starts and ends at column 0
14034 cx.set_state(indoc!(
14035 "fn a() {
14036 ˇ dog();
14037 cat();
14038 }"
14039 ));
14040 cx.update_editor(|editor, window, cx| {
14041 editor.toggle_comments(toggle_comments, window, cx);
14042 });
14043 cx.assert_editor_state(indoc!(
14044 "fn a() {
14045 // dog();
14046 ˇ cat();
14047 }"
14048 ));
14049}
14050
14051#[gpui::test]
14052async fn test_toggle_block_comment(cx: &mut TestAppContext) {
14053 init_test(cx, |_| {});
14054
14055 let mut cx = EditorTestContext::new(cx).await;
14056
14057 let html_language = Arc::new(
14058 Language::new(
14059 LanguageConfig {
14060 name: "HTML".into(),
14061 block_comment: Some(BlockCommentConfig {
14062 start: "<!-- ".into(),
14063 prefix: "".into(),
14064 end: " -->".into(),
14065 tab_size: 0,
14066 }),
14067 ..Default::default()
14068 },
14069 Some(tree_sitter_html::LANGUAGE.into()),
14070 )
14071 .with_injection_query(
14072 r#"
14073 (script_element
14074 (raw_text) @injection.content
14075 (#set! injection.language "javascript"))
14076 "#,
14077 )
14078 .unwrap(),
14079 );
14080
14081 let javascript_language = Arc::new(Language::new(
14082 LanguageConfig {
14083 name: "JavaScript".into(),
14084 line_comments: vec!["// ".into()],
14085 ..Default::default()
14086 },
14087 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
14088 ));
14089
14090 cx.language_registry().add(html_language.clone());
14091 cx.language_registry().add(javascript_language.clone());
14092 cx.update_buffer(|buffer, cx| {
14093 buffer.set_language(Some(html_language), cx);
14094 });
14095
14096 // Toggle comments for empty selections
14097 cx.set_state(
14098 &r#"
14099 <p>A</p>ˇ
14100 <p>B</p>ˇ
14101 <p>C</p>ˇ
14102 "#
14103 .unindent(),
14104 );
14105 cx.update_editor(|editor, window, cx| {
14106 editor.toggle_comments(&ToggleComments::default(), window, cx)
14107 });
14108 cx.assert_editor_state(
14109 &r#"
14110 <!-- <p>A</p>ˇ -->
14111 <!-- <p>B</p>ˇ -->
14112 <!-- <p>C</p>ˇ -->
14113 "#
14114 .unindent(),
14115 );
14116 cx.update_editor(|editor, window, cx| {
14117 editor.toggle_comments(&ToggleComments::default(), window, cx)
14118 });
14119 cx.assert_editor_state(
14120 &r#"
14121 <p>A</p>ˇ
14122 <p>B</p>ˇ
14123 <p>C</p>ˇ
14124 "#
14125 .unindent(),
14126 );
14127
14128 // Toggle comments for mixture of empty and non-empty selections, where
14129 // multiple selections occupy a given line.
14130 cx.set_state(
14131 &r#"
14132 <p>A«</p>
14133 <p>ˇ»B</p>ˇ
14134 <p>C«</p>
14135 <p>ˇ»D</p>ˇ
14136 "#
14137 .unindent(),
14138 );
14139
14140 cx.update_editor(|editor, window, cx| {
14141 editor.toggle_comments(&ToggleComments::default(), window, cx)
14142 });
14143 cx.assert_editor_state(
14144 &r#"
14145 <!-- <p>A«</p>
14146 <p>ˇ»B</p>ˇ -->
14147 <!-- <p>C«</p>
14148 <p>ˇ»D</p>ˇ -->
14149 "#
14150 .unindent(),
14151 );
14152 cx.update_editor(|editor, window, cx| {
14153 editor.toggle_comments(&ToggleComments::default(), window, cx)
14154 });
14155 cx.assert_editor_state(
14156 &r#"
14157 <p>A«</p>
14158 <p>ˇ»B</p>ˇ
14159 <p>C«</p>
14160 <p>ˇ»D</p>ˇ
14161 "#
14162 .unindent(),
14163 );
14164
14165 // Toggle comments when different languages are active for different
14166 // selections.
14167 cx.set_state(
14168 &r#"
14169 ˇ<script>
14170 ˇvar x = new Y();
14171 ˇ</script>
14172 "#
14173 .unindent(),
14174 );
14175 cx.executor().run_until_parked();
14176 cx.update_editor(|editor, window, cx| {
14177 editor.toggle_comments(&ToggleComments::default(), window, cx)
14178 });
14179 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
14180 // Uncommenting and commenting from this position brings in even more wrong artifacts.
14181 cx.assert_editor_state(
14182 &r#"
14183 <!-- ˇ<script> -->
14184 // ˇvar x = new Y();
14185 <!-- ˇ</script> -->
14186 "#
14187 .unindent(),
14188 );
14189}
14190
14191#[gpui::test]
14192fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
14193 init_test(cx, |_| {});
14194
14195 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
14196 let multibuffer = cx.new(|cx| {
14197 let mut multibuffer = MultiBuffer::new(ReadWrite);
14198 multibuffer.push_excerpts(
14199 buffer.clone(),
14200 [
14201 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
14202 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
14203 ],
14204 cx,
14205 );
14206 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
14207 multibuffer
14208 });
14209
14210 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
14211 editor.update_in(cx, |editor, window, cx| {
14212 assert_eq!(editor.text(cx), "aaaa\nbbbb");
14213 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14214 s.select_ranges([
14215 Point::new(0, 0)..Point::new(0, 0),
14216 Point::new(1, 0)..Point::new(1, 0),
14217 ])
14218 });
14219
14220 editor.handle_input("X", window, cx);
14221 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
14222 assert_eq!(
14223 editor.selections.ranges(cx),
14224 [
14225 Point::new(0, 1)..Point::new(0, 1),
14226 Point::new(1, 1)..Point::new(1, 1),
14227 ]
14228 );
14229
14230 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
14231 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14232 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
14233 });
14234 editor.backspace(&Default::default(), window, cx);
14235 assert_eq!(editor.text(cx), "Xa\nbbb");
14236 assert_eq!(
14237 editor.selections.ranges(cx),
14238 [Point::new(1, 0)..Point::new(1, 0)]
14239 );
14240
14241 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14242 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
14243 });
14244 editor.backspace(&Default::default(), window, cx);
14245 assert_eq!(editor.text(cx), "X\nbb");
14246 assert_eq!(
14247 editor.selections.ranges(cx),
14248 [Point::new(0, 1)..Point::new(0, 1)]
14249 );
14250 });
14251}
14252
14253#[gpui::test]
14254fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
14255 init_test(cx, |_| {});
14256
14257 let markers = vec![('[', ']').into(), ('(', ')').into()];
14258 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
14259 indoc! {"
14260 [aaaa
14261 (bbbb]
14262 cccc)",
14263 },
14264 markers.clone(),
14265 );
14266 let excerpt_ranges = markers.into_iter().map(|marker| {
14267 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
14268 ExcerptRange::new(context.clone())
14269 });
14270 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
14271 let multibuffer = cx.new(|cx| {
14272 let mut multibuffer = MultiBuffer::new(ReadWrite);
14273 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
14274 multibuffer
14275 });
14276
14277 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
14278 editor.update_in(cx, |editor, window, cx| {
14279 let (expected_text, selection_ranges) = marked_text_ranges(
14280 indoc! {"
14281 aaaa
14282 bˇbbb
14283 bˇbbˇb
14284 cccc"
14285 },
14286 true,
14287 );
14288 assert_eq!(editor.text(cx), expected_text);
14289 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14290 s.select_ranges(selection_ranges)
14291 });
14292
14293 editor.handle_input("X", window, cx);
14294
14295 let (expected_text, expected_selections) = marked_text_ranges(
14296 indoc! {"
14297 aaaa
14298 bXˇbbXb
14299 bXˇbbXˇb
14300 cccc"
14301 },
14302 false,
14303 );
14304 assert_eq!(editor.text(cx), expected_text);
14305 assert_eq!(editor.selections.ranges(cx), expected_selections);
14306
14307 editor.newline(&Newline, window, cx);
14308 let (expected_text, expected_selections) = marked_text_ranges(
14309 indoc! {"
14310 aaaa
14311 bX
14312 ˇbbX
14313 b
14314 bX
14315 ˇbbX
14316 ˇb
14317 cccc"
14318 },
14319 false,
14320 );
14321 assert_eq!(editor.text(cx), expected_text);
14322 assert_eq!(editor.selections.ranges(cx), expected_selections);
14323 });
14324}
14325
14326#[gpui::test]
14327fn test_refresh_selections(cx: &mut TestAppContext) {
14328 init_test(cx, |_| {});
14329
14330 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
14331 let mut excerpt1_id = None;
14332 let multibuffer = cx.new(|cx| {
14333 let mut multibuffer = MultiBuffer::new(ReadWrite);
14334 excerpt1_id = multibuffer
14335 .push_excerpts(
14336 buffer.clone(),
14337 [
14338 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
14339 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
14340 ],
14341 cx,
14342 )
14343 .into_iter()
14344 .next();
14345 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
14346 multibuffer
14347 });
14348
14349 let editor = cx.add_window(|window, cx| {
14350 let mut editor = build_editor(multibuffer.clone(), window, cx);
14351 let snapshot = editor.snapshot(window, cx);
14352 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14353 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
14354 });
14355 editor.begin_selection(
14356 Point::new(2, 1).to_display_point(&snapshot),
14357 true,
14358 1,
14359 window,
14360 cx,
14361 );
14362 assert_eq!(
14363 editor.selections.ranges(cx),
14364 [
14365 Point::new(1, 3)..Point::new(1, 3),
14366 Point::new(2, 1)..Point::new(2, 1),
14367 ]
14368 );
14369 editor
14370 });
14371
14372 // Refreshing selections is a no-op when excerpts haven't changed.
14373 _ = editor.update(cx, |editor, window, cx| {
14374 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14375 assert_eq!(
14376 editor.selections.ranges(cx),
14377 [
14378 Point::new(1, 3)..Point::new(1, 3),
14379 Point::new(2, 1)..Point::new(2, 1),
14380 ]
14381 );
14382 });
14383
14384 multibuffer.update(cx, |multibuffer, cx| {
14385 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
14386 });
14387 _ = editor.update(cx, |editor, window, cx| {
14388 // Removing an excerpt causes the first selection to become degenerate.
14389 assert_eq!(
14390 editor.selections.ranges(cx),
14391 [
14392 Point::new(0, 0)..Point::new(0, 0),
14393 Point::new(0, 1)..Point::new(0, 1)
14394 ]
14395 );
14396
14397 // Refreshing selections will relocate the first selection to the original buffer
14398 // location.
14399 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14400 assert_eq!(
14401 editor.selections.ranges(cx),
14402 [
14403 Point::new(0, 1)..Point::new(0, 1),
14404 Point::new(0, 3)..Point::new(0, 3)
14405 ]
14406 );
14407 assert!(editor.selections.pending_anchor().is_some());
14408 });
14409}
14410
14411#[gpui::test]
14412fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
14413 init_test(cx, |_| {});
14414
14415 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
14416 let mut excerpt1_id = None;
14417 let multibuffer = cx.new(|cx| {
14418 let mut multibuffer = MultiBuffer::new(ReadWrite);
14419 excerpt1_id = multibuffer
14420 .push_excerpts(
14421 buffer.clone(),
14422 [
14423 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
14424 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
14425 ],
14426 cx,
14427 )
14428 .into_iter()
14429 .next();
14430 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
14431 multibuffer
14432 });
14433
14434 let editor = cx.add_window(|window, cx| {
14435 let mut editor = build_editor(multibuffer.clone(), window, cx);
14436 let snapshot = editor.snapshot(window, cx);
14437 editor.begin_selection(
14438 Point::new(1, 3).to_display_point(&snapshot),
14439 false,
14440 1,
14441 window,
14442 cx,
14443 );
14444 assert_eq!(
14445 editor.selections.ranges(cx),
14446 [Point::new(1, 3)..Point::new(1, 3)]
14447 );
14448 editor
14449 });
14450
14451 multibuffer.update(cx, |multibuffer, cx| {
14452 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
14453 });
14454 _ = editor.update(cx, |editor, window, cx| {
14455 assert_eq!(
14456 editor.selections.ranges(cx),
14457 [Point::new(0, 0)..Point::new(0, 0)]
14458 );
14459
14460 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
14461 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14462 assert_eq!(
14463 editor.selections.ranges(cx),
14464 [Point::new(0, 3)..Point::new(0, 3)]
14465 );
14466 assert!(editor.selections.pending_anchor().is_some());
14467 });
14468}
14469
14470#[gpui::test]
14471async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
14472 init_test(cx, |_| {});
14473
14474 let language = Arc::new(
14475 Language::new(
14476 LanguageConfig {
14477 brackets: BracketPairConfig {
14478 pairs: vec![
14479 BracketPair {
14480 start: "{".to_string(),
14481 end: "}".to_string(),
14482 close: true,
14483 surround: true,
14484 newline: true,
14485 },
14486 BracketPair {
14487 start: "/* ".to_string(),
14488 end: " */".to_string(),
14489 close: true,
14490 surround: true,
14491 newline: true,
14492 },
14493 ],
14494 ..Default::default()
14495 },
14496 ..Default::default()
14497 },
14498 Some(tree_sitter_rust::LANGUAGE.into()),
14499 )
14500 .with_indents_query("")
14501 .unwrap(),
14502 );
14503
14504 let text = concat!(
14505 "{ }\n", //
14506 " x\n", //
14507 " /* */\n", //
14508 "x\n", //
14509 "{{} }\n", //
14510 );
14511
14512 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
14513 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
14514 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
14515 editor
14516 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
14517 .await;
14518
14519 editor.update_in(cx, |editor, window, cx| {
14520 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14521 s.select_display_ranges([
14522 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
14523 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
14524 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
14525 ])
14526 });
14527 editor.newline(&Newline, window, cx);
14528
14529 assert_eq!(
14530 editor.buffer().read(cx).read(cx).text(),
14531 concat!(
14532 "{ \n", // Suppress rustfmt
14533 "\n", //
14534 "}\n", //
14535 " x\n", //
14536 " /* \n", //
14537 " \n", //
14538 " */\n", //
14539 "x\n", //
14540 "{{} \n", //
14541 "}\n", //
14542 )
14543 );
14544 });
14545}
14546
14547#[gpui::test]
14548fn test_highlighted_ranges(cx: &mut TestAppContext) {
14549 init_test(cx, |_| {});
14550
14551 let editor = cx.add_window(|window, cx| {
14552 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
14553 build_editor(buffer.clone(), window, cx)
14554 });
14555
14556 _ = editor.update(cx, |editor, window, cx| {
14557 struct Type1;
14558 struct Type2;
14559
14560 let buffer = editor.buffer.read(cx).snapshot(cx);
14561
14562 let anchor_range =
14563 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
14564
14565 editor.highlight_background::<Type1>(
14566 &[
14567 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
14568 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
14569 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
14570 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
14571 ],
14572 |_| Hsla::red(),
14573 cx,
14574 );
14575 editor.highlight_background::<Type2>(
14576 &[
14577 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
14578 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
14579 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
14580 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
14581 ],
14582 |_| Hsla::green(),
14583 cx,
14584 );
14585
14586 let snapshot = editor.snapshot(window, cx);
14587 let mut highlighted_ranges = editor.background_highlights_in_range(
14588 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
14589 &snapshot,
14590 cx.theme(),
14591 );
14592 // Enforce a consistent ordering based on color without relying on the ordering of the
14593 // highlight's `TypeId` which is non-executor.
14594 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
14595 assert_eq!(
14596 highlighted_ranges,
14597 &[
14598 (
14599 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
14600 Hsla::red(),
14601 ),
14602 (
14603 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
14604 Hsla::red(),
14605 ),
14606 (
14607 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
14608 Hsla::green(),
14609 ),
14610 (
14611 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
14612 Hsla::green(),
14613 ),
14614 ]
14615 );
14616 assert_eq!(
14617 editor.background_highlights_in_range(
14618 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
14619 &snapshot,
14620 cx.theme(),
14621 ),
14622 &[(
14623 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
14624 Hsla::red(),
14625 )]
14626 );
14627 });
14628}
14629
14630#[gpui::test]
14631async fn test_following(cx: &mut TestAppContext) {
14632 init_test(cx, |_| {});
14633
14634 let fs = FakeFs::new(cx.executor());
14635 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
14636
14637 let buffer = project.update(cx, |project, cx| {
14638 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
14639 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
14640 });
14641 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
14642 let follower = cx.update(|cx| {
14643 cx.open_window(
14644 WindowOptions {
14645 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
14646 gpui::Point::new(px(0.), px(0.)),
14647 gpui::Point::new(px(10.), px(80.)),
14648 ))),
14649 ..Default::default()
14650 },
14651 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
14652 )
14653 .unwrap()
14654 });
14655
14656 let is_still_following = Rc::new(RefCell::new(true));
14657 let follower_edit_event_count = Rc::new(RefCell::new(0));
14658 let pending_update = Rc::new(RefCell::new(None));
14659 let leader_entity = leader.root(cx).unwrap();
14660 let follower_entity = follower.root(cx).unwrap();
14661 _ = follower.update(cx, {
14662 let update = pending_update.clone();
14663 let is_still_following = is_still_following.clone();
14664 let follower_edit_event_count = follower_edit_event_count.clone();
14665 |_, window, cx| {
14666 cx.subscribe_in(
14667 &leader_entity,
14668 window,
14669 move |_, leader, event, window, cx| {
14670 leader.read(cx).add_event_to_update_proto(
14671 event,
14672 &mut update.borrow_mut(),
14673 window,
14674 cx,
14675 );
14676 },
14677 )
14678 .detach();
14679
14680 cx.subscribe_in(
14681 &follower_entity,
14682 window,
14683 move |_, _, event: &EditorEvent, _window, _cx| {
14684 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
14685 *is_still_following.borrow_mut() = false;
14686 }
14687
14688 if let EditorEvent::BufferEdited = event {
14689 *follower_edit_event_count.borrow_mut() += 1;
14690 }
14691 },
14692 )
14693 .detach();
14694 }
14695 });
14696
14697 // Update the selections only
14698 _ = leader.update(cx, |leader, window, cx| {
14699 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14700 s.select_ranges([1..1])
14701 });
14702 });
14703 follower
14704 .update(cx, |follower, window, cx| {
14705 follower.apply_update_proto(
14706 &project,
14707 pending_update.borrow_mut().take().unwrap(),
14708 window,
14709 cx,
14710 )
14711 })
14712 .unwrap()
14713 .await
14714 .unwrap();
14715 _ = follower.update(cx, |follower, _, cx| {
14716 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
14717 });
14718 assert!(*is_still_following.borrow());
14719 assert_eq!(*follower_edit_event_count.borrow(), 0);
14720
14721 // Update the scroll position only
14722 _ = leader.update(cx, |leader, window, cx| {
14723 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
14724 });
14725 follower
14726 .update(cx, |follower, window, cx| {
14727 follower.apply_update_proto(
14728 &project,
14729 pending_update.borrow_mut().take().unwrap(),
14730 window,
14731 cx,
14732 )
14733 })
14734 .unwrap()
14735 .await
14736 .unwrap();
14737 assert_eq!(
14738 follower
14739 .update(cx, |follower, _, cx| follower.scroll_position(cx))
14740 .unwrap(),
14741 gpui::Point::new(1.5, 3.5)
14742 );
14743 assert!(*is_still_following.borrow());
14744 assert_eq!(*follower_edit_event_count.borrow(), 0);
14745
14746 // Update the selections and scroll position. The follower's scroll position is updated
14747 // via autoscroll, not via the leader's exact scroll position.
14748 _ = leader.update(cx, |leader, window, cx| {
14749 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14750 s.select_ranges([0..0])
14751 });
14752 leader.request_autoscroll(Autoscroll::newest(), cx);
14753 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
14754 });
14755 follower
14756 .update(cx, |follower, window, cx| {
14757 follower.apply_update_proto(
14758 &project,
14759 pending_update.borrow_mut().take().unwrap(),
14760 window,
14761 cx,
14762 )
14763 })
14764 .unwrap()
14765 .await
14766 .unwrap();
14767 _ = follower.update(cx, |follower, _, cx| {
14768 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
14769 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
14770 });
14771 assert!(*is_still_following.borrow());
14772
14773 // Creating a pending selection that precedes another selection
14774 _ = leader.update(cx, |leader, window, cx| {
14775 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14776 s.select_ranges([1..1])
14777 });
14778 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
14779 });
14780 follower
14781 .update(cx, |follower, window, cx| {
14782 follower.apply_update_proto(
14783 &project,
14784 pending_update.borrow_mut().take().unwrap(),
14785 window,
14786 cx,
14787 )
14788 })
14789 .unwrap()
14790 .await
14791 .unwrap();
14792 _ = follower.update(cx, |follower, _, cx| {
14793 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
14794 });
14795 assert!(*is_still_following.borrow());
14796
14797 // Extend the pending selection so that it surrounds another selection
14798 _ = leader.update(cx, |leader, window, cx| {
14799 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
14800 });
14801 follower
14802 .update(cx, |follower, window, cx| {
14803 follower.apply_update_proto(
14804 &project,
14805 pending_update.borrow_mut().take().unwrap(),
14806 window,
14807 cx,
14808 )
14809 })
14810 .unwrap()
14811 .await
14812 .unwrap();
14813 _ = follower.update(cx, |follower, _, cx| {
14814 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
14815 });
14816
14817 // Scrolling locally breaks the follow
14818 _ = follower.update(cx, |follower, window, cx| {
14819 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
14820 follower.set_scroll_anchor(
14821 ScrollAnchor {
14822 anchor: top_anchor,
14823 offset: gpui::Point::new(0.0, 0.5),
14824 },
14825 window,
14826 cx,
14827 );
14828 });
14829 assert!(!(*is_still_following.borrow()));
14830}
14831
14832#[gpui::test]
14833async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
14834 init_test(cx, |_| {});
14835
14836 let fs = FakeFs::new(cx.executor());
14837 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
14838 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14839 let pane = workspace
14840 .update(cx, |workspace, _, _| workspace.active_pane().clone())
14841 .unwrap();
14842
14843 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
14844
14845 let leader = pane.update_in(cx, |_, window, cx| {
14846 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
14847 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
14848 });
14849
14850 // Start following the editor when it has no excerpts.
14851 let mut state_message =
14852 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
14853 let workspace_entity = workspace.root(cx).unwrap();
14854 let follower_1 = cx
14855 .update_window(*workspace.deref(), |_, window, cx| {
14856 Editor::from_state_proto(
14857 workspace_entity,
14858 ViewId {
14859 creator: CollaboratorId::PeerId(PeerId::default()),
14860 id: 0,
14861 },
14862 &mut state_message,
14863 window,
14864 cx,
14865 )
14866 })
14867 .unwrap()
14868 .unwrap()
14869 .await
14870 .unwrap();
14871
14872 let update_message = Rc::new(RefCell::new(None));
14873 follower_1.update_in(cx, {
14874 let update = update_message.clone();
14875 |_, window, cx| {
14876 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
14877 leader.read(cx).add_event_to_update_proto(
14878 event,
14879 &mut update.borrow_mut(),
14880 window,
14881 cx,
14882 );
14883 })
14884 .detach();
14885 }
14886 });
14887
14888 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
14889 (
14890 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
14891 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
14892 )
14893 });
14894
14895 // Insert some excerpts.
14896 leader.update(cx, |leader, cx| {
14897 leader.buffer.update(cx, |multibuffer, cx| {
14898 multibuffer.set_excerpts_for_path(
14899 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
14900 buffer_1.clone(),
14901 vec![
14902 Point::row_range(0..3),
14903 Point::row_range(1..6),
14904 Point::row_range(12..15),
14905 ],
14906 0,
14907 cx,
14908 );
14909 multibuffer.set_excerpts_for_path(
14910 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
14911 buffer_2.clone(),
14912 vec![Point::row_range(0..6), Point::row_range(8..12)],
14913 0,
14914 cx,
14915 );
14916 });
14917 });
14918
14919 // Apply the update of adding the excerpts.
14920 follower_1
14921 .update_in(cx, |follower, window, cx| {
14922 follower.apply_update_proto(
14923 &project,
14924 update_message.borrow().clone().unwrap(),
14925 window,
14926 cx,
14927 )
14928 })
14929 .await
14930 .unwrap();
14931 assert_eq!(
14932 follower_1.update(cx, |editor, cx| editor.text(cx)),
14933 leader.update(cx, |editor, cx| editor.text(cx))
14934 );
14935 update_message.borrow_mut().take();
14936
14937 // Start following separately after it already has excerpts.
14938 let mut state_message =
14939 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
14940 let workspace_entity = workspace.root(cx).unwrap();
14941 let follower_2 = cx
14942 .update_window(*workspace.deref(), |_, window, cx| {
14943 Editor::from_state_proto(
14944 workspace_entity,
14945 ViewId {
14946 creator: CollaboratorId::PeerId(PeerId::default()),
14947 id: 0,
14948 },
14949 &mut state_message,
14950 window,
14951 cx,
14952 )
14953 })
14954 .unwrap()
14955 .unwrap()
14956 .await
14957 .unwrap();
14958 assert_eq!(
14959 follower_2.update(cx, |editor, cx| editor.text(cx)),
14960 leader.update(cx, |editor, cx| editor.text(cx))
14961 );
14962
14963 // Remove some excerpts.
14964 leader.update(cx, |leader, cx| {
14965 leader.buffer.update(cx, |multibuffer, cx| {
14966 let excerpt_ids = multibuffer.excerpt_ids();
14967 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
14968 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
14969 });
14970 });
14971
14972 // Apply the update of removing the excerpts.
14973 follower_1
14974 .update_in(cx, |follower, window, cx| {
14975 follower.apply_update_proto(
14976 &project,
14977 update_message.borrow().clone().unwrap(),
14978 window,
14979 cx,
14980 )
14981 })
14982 .await
14983 .unwrap();
14984 follower_2
14985 .update_in(cx, |follower, window, cx| {
14986 follower.apply_update_proto(
14987 &project,
14988 update_message.borrow().clone().unwrap(),
14989 window,
14990 cx,
14991 )
14992 })
14993 .await
14994 .unwrap();
14995 update_message.borrow_mut().take();
14996 assert_eq!(
14997 follower_1.update(cx, |editor, cx| editor.text(cx)),
14998 leader.update(cx, |editor, cx| editor.text(cx))
14999 );
15000}
15001
15002#[gpui::test]
15003async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15004 init_test(cx, |_| {});
15005
15006 let mut cx = EditorTestContext::new(cx).await;
15007 let lsp_store =
15008 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
15009
15010 cx.set_state(indoc! {"
15011 ˇfn func(abc def: i32) -> u32 {
15012 }
15013 "});
15014
15015 cx.update(|_, cx| {
15016 lsp_store.update(cx, |lsp_store, cx| {
15017 lsp_store
15018 .update_diagnostics(
15019 LanguageServerId(0),
15020 lsp::PublishDiagnosticsParams {
15021 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
15022 version: None,
15023 diagnostics: vec![
15024 lsp::Diagnostic {
15025 range: lsp::Range::new(
15026 lsp::Position::new(0, 11),
15027 lsp::Position::new(0, 12),
15028 ),
15029 severity: Some(lsp::DiagnosticSeverity::ERROR),
15030 ..Default::default()
15031 },
15032 lsp::Diagnostic {
15033 range: lsp::Range::new(
15034 lsp::Position::new(0, 12),
15035 lsp::Position::new(0, 15),
15036 ),
15037 severity: Some(lsp::DiagnosticSeverity::ERROR),
15038 ..Default::default()
15039 },
15040 lsp::Diagnostic {
15041 range: lsp::Range::new(
15042 lsp::Position::new(0, 25),
15043 lsp::Position::new(0, 28),
15044 ),
15045 severity: Some(lsp::DiagnosticSeverity::ERROR),
15046 ..Default::default()
15047 },
15048 ],
15049 },
15050 None,
15051 DiagnosticSourceKind::Pushed,
15052 &[],
15053 cx,
15054 )
15055 .unwrap()
15056 });
15057 });
15058
15059 executor.run_until_parked();
15060
15061 cx.update_editor(|editor, window, cx| {
15062 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
15063 });
15064
15065 cx.assert_editor_state(indoc! {"
15066 fn func(abc def: i32) -> ˇu32 {
15067 }
15068 "});
15069
15070 cx.update_editor(|editor, window, cx| {
15071 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
15072 });
15073
15074 cx.assert_editor_state(indoc! {"
15075 fn func(abc ˇdef: i32) -> u32 {
15076 }
15077 "});
15078
15079 cx.update_editor(|editor, window, cx| {
15080 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
15081 });
15082
15083 cx.assert_editor_state(indoc! {"
15084 fn func(abcˇ def: i32) -> u32 {
15085 }
15086 "});
15087
15088 cx.update_editor(|editor, window, cx| {
15089 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
15090 });
15091
15092 cx.assert_editor_state(indoc! {"
15093 fn func(abc def: i32) -> ˇu32 {
15094 }
15095 "});
15096}
15097
15098#[gpui::test]
15099async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15100 init_test(cx, |_| {});
15101
15102 let mut cx = EditorTestContext::new(cx).await;
15103
15104 let diff_base = r#"
15105 use some::mod;
15106
15107 const A: u32 = 42;
15108
15109 fn main() {
15110 println!("hello");
15111
15112 println!("world");
15113 }
15114 "#
15115 .unindent();
15116
15117 // Edits are modified, removed, modified, added
15118 cx.set_state(
15119 &r#"
15120 use some::modified;
15121
15122 ˇ
15123 fn main() {
15124 println!("hello there");
15125
15126 println!("around the");
15127 println!("world");
15128 }
15129 "#
15130 .unindent(),
15131 );
15132
15133 cx.set_head_text(&diff_base);
15134 executor.run_until_parked();
15135
15136 cx.update_editor(|editor, window, cx| {
15137 //Wrap around the bottom of the buffer
15138 for _ in 0..3 {
15139 editor.go_to_next_hunk(&GoToHunk, window, cx);
15140 }
15141 });
15142
15143 cx.assert_editor_state(
15144 &r#"
15145 ˇuse some::modified;
15146
15147
15148 fn main() {
15149 println!("hello there");
15150
15151 println!("around the");
15152 println!("world");
15153 }
15154 "#
15155 .unindent(),
15156 );
15157
15158 cx.update_editor(|editor, window, cx| {
15159 //Wrap around the top of the buffer
15160 for _ in 0..2 {
15161 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
15162 }
15163 });
15164
15165 cx.assert_editor_state(
15166 &r#"
15167 use some::modified;
15168
15169
15170 fn main() {
15171 ˇ println!("hello there");
15172
15173 println!("around the");
15174 println!("world");
15175 }
15176 "#
15177 .unindent(),
15178 );
15179
15180 cx.update_editor(|editor, window, cx| {
15181 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
15182 });
15183
15184 cx.assert_editor_state(
15185 &r#"
15186 use some::modified;
15187
15188 ˇ
15189 fn main() {
15190 println!("hello there");
15191
15192 println!("around the");
15193 println!("world");
15194 }
15195 "#
15196 .unindent(),
15197 );
15198
15199 cx.update_editor(|editor, window, cx| {
15200 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
15201 });
15202
15203 cx.assert_editor_state(
15204 &r#"
15205 ˇuse some::modified;
15206
15207
15208 fn main() {
15209 println!("hello there");
15210
15211 println!("around the");
15212 println!("world");
15213 }
15214 "#
15215 .unindent(),
15216 );
15217
15218 cx.update_editor(|editor, window, cx| {
15219 for _ in 0..2 {
15220 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
15221 }
15222 });
15223
15224 cx.assert_editor_state(
15225 &r#"
15226 use some::modified;
15227
15228
15229 fn main() {
15230 ˇ println!("hello there");
15231
15232 println!("around the");
15233 println!("world");
15234 }
15235 "#
15236 .unindent(),
15237 );
15238
15239 cx.update_editor(|editor, window, cx| {
15240 editor.fold(&Fold, window, cx);
15241 });
15242
15243 cx.update_editor(|editor, window, cx| {
15244 editor.go_to_next_hunk(&GoToHunk, window, cx);
15245 });
15246
15247 cx.assert_editor_state(
15248 &r#"
15249 ˇuse some::modified;
15250
15251
15252 fn main() {
15253 println!("hello there");
15254
15255 println!("around the");
15256 println!("world");
15257 }
15258 "#
15259 .unindent(),
15260 );
15261}
15262
15263#[test]
15264fn test_split_words() {
15265 fn split(text: &str) -> Vec<&str> {
15266 split_words(text).collect()
15267 }
15268
15269 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
15270 assert_eq!(split("hello_world"), &["hello_", "world"]);
15271 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
15272 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
15273 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
15274 assert_eq!(split("helloworld"), &["helloworld"]);
15275
15276 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
15277}
15278
15279#[gpui::test]
15280async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
15281 init_test(cx, |_| {});
15282
15283 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
15284 let mut assert = |before, after| {
15285 let _state_context = cx.set_state(before);
15286 cx.run_until_parked();
15287 cx.update_editor(|editor, window, cx| {
15288 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
15289 });
15290 cx.run_until_parked();
15291 cx.assert_editor_state(after);
15292 };
15293
15294 // Outside bracket jumps to outside of matching bracket
15295 assert("console.logˇ(var);", "console.log(var)ˇ;");
15296 assert("console.log(var)ˇ;", "console.logˇ(var);");
15297
15298 // Inside bracket jumps to inside of matching bracket
15299 assert("console.log(ˇvar);", "console.log(varˇ);");
15300 assert("console.log(varˇ);", "console.log(ˇvar);");
15301
15302 // When outside a bracket and inside, favor jumping to the inside bracket
15303 assert(
15304 "console.log('foo', [1, 2, 3]ˇ);",
15305 "console.log(ˇ'foo', [1, 2, 3]);",
15306 );
15307 assert(
15308 "console.log(ˇ'foo', [1, 2, 3]);",
15309 "console.log('foo', [1, 2, 3]ˇ);",
15310 );
15311
15312 // Bias forward if two options are equally likely
15313 assert(
15314 "let result = curried_fun()ˇ();",
15315 "let result = curried_fun()()ˇ;",
15316 );
15317
15318 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
15319 assert(
15320 indoc! {"
15321 function test() {
15322 console.log('test')ˇ
15323 }"},
15324 indoc! {"
15325 function test() {
15326 console.logˇ('test')
15327 }"},
15328 );
15329}
15330
15331#[gpui::test]
15332async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
15333 init_test(cx, |_| {});
15334
15335 let fs = FakeFs::new(cx.executor());
15336 fs.insert_tree(
15337 path!("/a"),
15338 json!({
15339 "main.rs": "fn main() { let a = 5; }",
15340 "other.rs": "// Test file",
15341 }),
15342 )
15343 .await;
15344 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15345
15346 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15347 language_registry.add(Arc::new(Language::new(
15348 LanguageConfig {
15349 name: "Rust".into(),
15350 matcher: LanguageMatcher {
15351 path_suffixes: vec!["rs".to_string()],
15352 ..Default::default()
15353 },
15354 brackets: BracketPairConfig {
15355 pairs: vec![BracketPair {
15356 start: "{".to_string(),
15357 end: "}".to_string(),
15358 close: true,
15359 surround: true,
15360 newline: true,
15361 }],
15362 disabled_scopes_by_bracket_ix: Vec::new(),
15363 },
15364 ..Default::default()
15365 },
15366 Some(tree_sitter_rust::LANGUAGE.into()),
15367 )));
15368 let mut fake_servers = language_registry.register_fake_lsp(
15369 "Rust",
15370 FakeLspAdapter {
15371 capabilities: lsp::ServerCapabilities {
15372 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
15373 first_trigger_character: "{".to_string(),
15374 more_trigger_character: None,
15375 }),
15376 ..Default::default()
15377 },
15378 ..Default::default()
15379 },
15380 );
15381
15382 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15383
15384 let cx = &mut VisualTestContext::from_window(*workspace, cx);
15385
15386 let worktree_id = workspace
15387 .update(cx, |workspace, _, cx| {
15388 workspace.project().update(cx, |project, cx| {
15389 project.worktrees(cx).next().unwrap().read(cx).id()
15390 })
15391 })
15392 .unwrap();
15393
15394 let buffer = project
15395 .update(cx, |project, cx| {
15396 project.open_local_buffer(path!("/a/main.rs"), cx)
15397 })
15398 .await
15399 .unwrap();
15400 let editor_handle = workspace
15401 .update(cx, |workspace, window, cx| {
15402 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
15403 })
15404 .unwrap()
15405 .await
15406 .unwrap()
15407 .downcast::<Editor>()
15408 .unwrap();
15409
15410 cx.executor().start_waiting();
15411 let fake_server = fake_servers.next().await.unwrap();
15412
15413 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
15414 |params, _| async move {
15415 assert_eq!(
15416 params.text_document_position.text_document.uri,
15417 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
15418 );
15419 assert_eq!(
15420 params.text_document_position.position,
15421 lsp::Position::new(0, 21),
15422 );
15423
15424 Ok(Some(vec![lsp::TextEdit {
15425 new_text: "]".to_string(),
15426 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15427 }]))
15428 },
15429 );
15430
15431 editor_handle.update_in(cx, |editor, window, cx| {
15432 window.focus(&editor.focus_handle(cx));
15433 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15434 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
15435 });
15436 editor.handle_input("{", window, cx);
15437 });
15438
15439 cx.executor().run_until_parked();
15440
15441 buffer.update(cx, |buffer, _| {
15442 assert_eq!(
15443 buffer.text(),
15444 "fn main() { let a = {5}; }",
15445 "No extra braces from on type formatting should appear in the buffer"
15446 )
15447 });
15448}
15449
15450#[gpui::test(iterations = 20, seeds(31))]
15451async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
15452 init_test(cx, |_| {});
15453
15454 let mut cx = EditorLspTestContext::new_rust(
15455 lsp::ServerCapabilities {
15456 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
15457 first_trigger_character: ".".to_string(),
15458 more_trigger_character: None,
15459 }),
15460 ..Default::default()
15461 },
15462 cx,
15463 )
15464 .await;
15465
15466 cx.update_buffer(|buffer, _| {
15467 // This causes autoindent to be async.
15468 buffer.set_sync_parse_timeout(Duration::ZERO)
15469 });
15470
15471 cx.set_state("fn c() {\n d()ˇ\n}\n");
15472 cx.simulate_keystroke("\n");
15473 cx.run_until_parked();
15474
15475 let buffer_cloned =
15476 cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap().clone());
15477 let mut request =
15478 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
15479 let buffer_cloned = buffer_cloned.clone();
15480 async move {
15481 buffer_cloned.update(&mut cx, |buffer, _| {
15482 assert_eq!(
15483 buffer.text(),
15484 "fn c() {\n d()\n .\n}\n",
15485 "OnTypeFormatting should triggered after autoindent applied"
15486 )
15487 })?;
15488
15489 Ok(Some(vec![]))
15490 }
15491 });
15492
15493 cx.simulate_keystroke(".");
15494 cx.run_until_parked();
15495
15496 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
15497 assert!(request.next().await.is_some());
15498 request.close();
15499 assert!(request.next().await.is_none());
15500}
15501
15502#[gpui::test]
15503async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
15504 init_test(cx, |_| {});
15505
15506 let fs = FakeFs::new(cx.executor());
15507 fs.insert_tree(
15508 path!("/a"),
15509 json!({
15510 "main.rs": "fn main() { let a = 5; }",
15511 "other.rs": "// Test file",
15512 }),
15513 )
15514 .await;
15515
15516 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15517
15518 let server_restarts = Arc::new(AtomicUsize::new(0));
15519 let closure_restarts = Arc::clone(&server_restarts);
15520 let language_server_name = "test language server";
15521 let language_name: LanguageName = "Rust".into();
15522
15523 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15524 language_registry.add(Arc::new(Language::new(
15525 LanguageConfig {
15526 name: language_name.clone(),
15527 matcher: LanguageMatcher {
15528 path_suffixes: vec!["rs".to_string()],
15529 ..Default::default()
15530 },
15531 ..Default::default()
15532 },
15533 Some(tree_sitter_rust::LANGUAGE.into()),
15534 )));
15535 let mut fake_servers = language_registry.register_fake_lsp(
15536 "Rust",
15537 FakeLspAdapter {
15538 name: language_server_name,
15539 initialization_options: Some(json!({
15540 "testOptionValue": true
15541 })),
15542 initializer: Some(Box::new(move |fake_server| {
15543 let task_restarts = Arc::clone(&closure_restarts);
15544 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
15545 task_restarts.fetch_add(1, atomic::Ordering::Release);
15546 futures::future::ready(Ok(()))
15547 });
15548 })),
15549 ..Default::default()
15550 },
15551 );
15552
15553 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15554 let _buffer = project
15555 .update(cx, |project, cx| {
15556 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
15557 })
15558 .await
15559 .unwrap();
15560 let _fake_server = fake_servers.next().await.unwrap();
15561 update_test_language_settings(cx, |language_settings| {
15562 language_settings.languages.0.insert(
15563 language_name.clone(),
15564 LanguageSettingsContent {
15565 tab_size: NonZeroU32::new(8),
15566 ..Default::default()
15567 },
15568 );
15569 });
15570 cx.executor().run_until_parked();
15571 assert_eq!(
15572 server_restarts.load(atomic::Ordering::Acquire),
15573 0,
15574 "Should not restart LSP server on an unrelated change"
15575 );
15576
15577 update_test_project_settings(cx, |project_settings| {
15578 project_settings.lsp.insert(
15579 "Some other server name".into(),
15580 LspSettings {
15581 binary: None,
15582 settings: None,
15583 initialization_options: Some(json!({
15584 "some other init value": false
15585 })),
15586 enable_lsp_tasks: false,
15587 },
15588 );
15589 });
15590 cx.executor().run_until_parked();
15591 assert_eq!(
15592 server_restarts.load(atomic::Ordering::Acquire),
15593 0,
15594 "Should not restart LSP server on an unrelated LSP settings change"
15595 );
15596
15597 update_test_project_settings(cx, |project_settings| {
15598 project_settings.lsp.insert(
15599 language_server_name.into(),
15600 LspSettings {
15601 binary: None,
15602 settings: None,
15603 initialization_options: Some(json!({
15604 "anotherInitValue": false
15605 })),
15606 enable_lsp_tasks: false,
15607 },
15608 );
15609 });
15610 cx.executor().run_until_parked();
15611 assert_eq!(
15612 server_restarts.load(atomic::Ordering::Acquire),
15613 1,
15614 "Should restart LSP server on a related LSP settings change"
15615 );
15616
15617 update_test_project_settings(cx, |project_settings| {
15618 project_settings.lsp.insert(
15619 language_server_name.into(),
15620 LspSettings {
15621 binary: None,
15622 settings: None,
15623 initialization_options: Some(json!({
15624 "anotherInitValue": false
15625 })),
15626 enable_lsp_tasks: false,
15627 },
15628 );
15629 });
15630 cx.executor().run_until_parked();
15631 assert_eq!(
15632 server_restarts.load(atomic::Ordering::Acquire),
15633 1,
15634 "Should not restart LSP server on a related LSP settings change that is the same"
15635 );
15636
15637 update_test_project_settings(cx, |project_settings| {
15638 project_settings.lsp.insert(
15639 language_server_name.into(),
15640 LspSettings {
15641 binary: None,
15642 settings: None,
15643 initialization_options: None,
15644 enable_lsp_tasks: false,
15645 },
15646 );
15647 });
15648 cx.executor().run_until_parked();
15649 assert_eq!(
15650 server_restarts.load(atomic::Ordering::Acquire),
15651 2,
15652 "Should restart LSP server on another related LSP settings change"
15653 );
15654}
15655
15656#[gpui::test]
15657async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
15658 init_test(cx, |_| {});
15659
15660 let mut cx = EditorLspTestContext::new_rust(
15661 lsp::ServerCapabilities {
15662 completion_provider: Some(lsp::CompletionOptions {
15663 trigger_characters: Some(vec![".".to_string()]),
15664 resolve_provider: Some(true),
15665 ..Default::default()
15666 }),
15667 ..Default::default()
15668 },
15669 cx,
15670 )
15671 .await;
15672
15673 cx.set_state("fn main() { let a = 2ˇ; }");
15674 cx.simulate_keystroke(".");
15675 let completion_item = lsp::CompletionItem {
15676 label: "some".into(),
15677 kind: Some(lsp::CompletionItemKind::SNIPPET),
15678 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15679 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15680 kind: lsp::MarkupKind::Markdown,
15681 value: "```rust\nSome(2)\n```".to_string(),
15682 })),
15683 deprecated: Some(false),
15684 sort_text: Some("fffffff2".to_string()),
15685 filter_text: Some("some".to_string()),
15686 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15687 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15688 range: lsp::Range {
15689 start: lsp::Position {
15690 line: 0,
15691 character: 22,
15692 },
15693 end: lsp::Position {
15694 line: 0,
15695 character: 22,
15696 },
15697 },
15698 new_text: "Some(2)".to_string(),
15699 })),
15700 additional_text_edits: Some(vec![lsp::TextEdit {
15701 range: lsp::Range {
15702 start: lsp::Position {
15703 line: 0,
15704 character: 20,
15705 },
15706 end: lsp::Position {
15707 line: 0,
15708 character: 22,
15709 },
15710 },
15711 new_text: "".to_string(),
15712 }]),
15713 ..Default::default()
15714 };
15715
15716 let closure_completion_item = completion_item.clone();
15717 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15718 let task_completion_item = closure_completion_item.clone();
15719 async move {
15720 Ok(Some(lsp::CompletionResponse::Array(vec![
15721 task_completion_item,
15722 ])))
15723 }
15724 });
15725
15726 request.next().await;
15727
15728 cx.condition(|editor, _| editor.context_menu_visible())
15729 .await;
15730 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
15731 editor
15732 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15733 .unwrap()
15734 });
15735 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
15736
15737 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
15738 let task_completion_item = completion_item.clone();
15739 async move { Ok(task_completion_item) }
15740 })
15741 .next()
15742 .await
15743 .unwrap();
15744 apply_additional_edits.await.unwrap();
15745 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
15746}
15747
15748#[gpui::test]
15749async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
15750 init_test(cx, |_| {});
15751
15752 let mut cx = EditorLspTestContext::new_rust(
15753 lsp::ServerCapabilities {
15754 completion_provider: Some(lsp::CompletionOptions {
15755 trigger_characters: Some(vec![".".to_string()]),
15756 resolve_provider: Some(true),
15757 ..Default::default()
15758 }),
15759 ..Default::default()
15760 },
15761 cx,
15762 )
15763 .await;
15764
15765 cx.set_state("fn main() { let a = 2ˇ; }");
15766 cx.simulate_keystroke(".");
15767
15768 let item1 = lsp::CompletionItem {
15769 label: "method id()".to_string(),
15770 filter_text: Some("id".to_string()),
15771 detail: None,
15772 documentation: None,
15773 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15774 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15775 new_text: ".id".to_string(),
15776 })),
15777 ..lsp::CompletionItem::default()
15778 };
15779
15780 let item2 = lsp::CompletionItem {
15781 label: "other".to_string(),
15782 filter_text: Some("other".to_string()),
15783 detail: None,
15784 documentation: None,
15785 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15786 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15787 new_text: ".other".to_string(),
15788 })),
15789 ..lsp::CompletionItem::default()
15790 };
15791
15792 let item1 = item1.clone();
15793 cx.set_request_handler::<lsp::request::Completion, _, _>({
15794 let item1 = item1.clone();
15795 move |_, _, _| {
15796 let item1 = item1.clone();
15797 let item2 = item2.clone();
15798 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
15799 }
15800 })
15801 .next()
15802 .await;
15803
15804 cx.condition(|editor, _| editor.context_menu_visible())
15805 .await;
15806 cx.update_editor(|editor, _, _| {
15807 let context_menu = editor.context_menu.borrow_mut();
15808 let context_menu = context_menu
15809 .as_ref()
15810 .expect("Should have the context menu deployed");
15811 match context_menu {
15812 CodeContextMenu::Completions(completions_menu) => {
15813 let completions = completions_menu.completions.borrow_mut();
15814 assert_eq!(
15815 completions
15816 .iter()
15817 .map(|completion| &completion.label.text)
15818 .collect::<Vec<_>>(),
15819 vec!["method id()", "other"]
15820 )
15821 }
15822 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15823 }
15824 });
15825
15826 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
15827 let item1 = item1.clone();
15828 move |_, item_to_resolve, _| {
15829 let item1 = item1.clone();
15830 async move {
15831 if item1 == item_to_resolve {
15832 Ok(lsp::CompletionItem {
15833 label: "method id()".to_string(),
15834 filter_text: Some("id".to_string()),
15835 detail: Some("Now resolved!".to_string()),
15836 documentation: Some(lsp::Documentation::String("Docs".to_string())),
15837 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15838 range: lsp::Range::new(
15839 lsp::Position::new(0, 22),
15840 lsp::Position::new(0, 22),
15841 ),
15842 new_text: ".id".to_string(),
15843 })),
15844 ..lsp::CompletionItem::default()
15845 })
15846 } else {
15847 Ok(item_to_resolve)
15848 }
15849 }
15850 }
15851 })
15852 .next()
15853 .await
15854 .unwrap();
15855 cx.run_until_parked();
15856
15857 cx.update_editor(|editor, window, cx| {
15858 editor.context_menu_next(&Default::default(), window, cx);
15859 });
15860
15861 cx.update_editor(|editor, _, _| {
15862 let context_menu = editor.context_menu.borrow_mut();
15863 let context_menu = context_menu
15864 .as_ref()
15865 .expect("Should have the context menu deployed");
15866 match context_menu {
15867 CodeContextMenu::Completions(completions_menu) => {
15868 let completions = completions_menu.completions.borrow_mut();
15869 assert_eq!(
15870 completions
15871 .iter()
15872 .map(|completion| &completion.label.text)
15873 .collect::<Vec<_>>(),
15874 vec!["method id() Now resolved!", "other"],
15875 "Should update first completion label, but not second as the filter text did not match."
15876 );
15877 }
15878 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15879 }
15880 });
15881}
15882
15883#[gpui::test]
15884async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
15885 init_test(cx, |_| {});
15886 let mut cx = EditorLspTestContext::new_rust(
15887 lsp::ServerCapabilities {
15888 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
15889 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
15890 completion_provider: Some(lsp::CompletionOptions {
15891 resolve_provider: Some(true),
15892 ..Default::default()
15893 }),
15894 ..Default::default()
15895 },
15896 cx,
15897 )
15898 .await;
15899 cx.set_state(indoc! {"
15900 struct TestStruct {
15901 field: i32
15902 }
15903
15904 fn mainˇ() {
15905 let unused_var = 42;
15906 let test_struct = TestStruct { field: 42 };
15907 }
15908 "});
15909 let symbol_range = cx.lsp_range(indoc! {"
15910 struct TestStruct {
15911 field: i32
15912 }
15913
15914 «fn main»() {
15915 let unused_var = 42;
15916 let test_struct = TestStruct { field: 42 };
15917 }
15918 "});
15919 let mut hover_requests =
15920 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
15921 Ok(Some(lsp::Hover {
15922 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
15923 kind: lsp::MarkupKind::Markdown,
15924 value: "Function documentation".to_string(),
15925 }),
15926 range: Some(symbol_range),
15927 }))
15928 });
15929
15930 // Case 1: Test that code action menu hide hover popover
15931 cx.dispatch_action(Hover);
15932 hover_requests.next().await;
15933 cx.condition(|editor, _| editor.hover_state.visible()).await;
15934 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
15935 move |_, _, _| async move {
15936 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
15937 lsp::CodeAction {
15938 title: "Remove unused variable".to_string(),
15939 kind: Some(CodeActionKind::QUICKFIX),
15940 edit: Some(lsp::WorkspaceEdit {
15941 changes: Some(
15942 [(
15943 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
15944 vec![lsp::TextEdit {
15945 range: lsp::Range::new(
15946 lsp::Position::new(5, 4),
15947 lsp::Position::new(5, 27),
15948 ),
15949 new_text: "".to_string(),
15950 }],
15951 )]
15952 .into_iter()
15953 .collect(),
15954 ),
15955 ..Default::default()
15956 }),
15957 ..Default::default()
15958 },
15959 )]))
15960 },
15961 );
15962 cx.update_editor(|editor, window, cx| {
15963 editor.toggle_code_actions(
15964 &ToggleCodeActions {
15965 deployed_from: None,
15966 quick_launch: false,
15967 },
15968 window,
15969 cx,
15970 );
15971 });
15972 code_action_requests.next().await;
15973 cx.run_until_parked();
15974 cx.condition(|editor, _| editor.context_menu_visible())
15975 .await;
15976 cx.update_editor(|editor, _, _| {
15977 assert!(
15978 !editor.hover_state.visible(),
15979 "Hover popover should be hidden when code action menu is shown"
15980 );
15981 // Hide code actions
15982 editor.context_menu.take();
15983 });
15984
15985 // Case 2: Test that code completions hide hover popover
15986 cx.dispatch_action(Hover);
15987 hover_requests.next().await;
15988 cx.condition(|editor, _| editor.hover_state.visible()).await;
15989 let counter = Arc::new(AtomicUsize::new(0));
15990 let mut completion_requests =
15991 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15992 let counter = counter.clone();
15993 async move {
15994 counter.fetch_add(1, atomic::Ordering::Release);
15995 Ok(Some(lsp::CompletionResponse::Array(vec![
15996 lsp::CompletionItem {
15997 label: "main".into(),
15998 kind: Some(lsp::CompletionItemKind::FUNCTION),
15999 detail: Some("() -> ()".to_string()),
16000 ..Default::default()
16001 },
16002 lsp::CompletionItem {
16003 label: "TestStruct".into(),
16004 kind: Some(lsp::CompletionItemKind::STRUCT),
16005 detail: Some("struct TestStruct".to_string()),
16006 ..Default::default()
16007 },
16008 ])))
16009 }
16010 });
16011 cx.update_editor(|editor, window, cx| {
16012 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
16013 });
16014 completion_requests.next().await;
16015 cx.condition(|editor, _| editor.context_menu_visible())
16016 .await;
16017 cx.update_editor(|editor, _, _| {
16018 assert!(
16019 !editor.hover_state.visible(),
16020 "Hover popover should be hidden when completion menu is shown"
16021 );
16022 });
16023}
16024
16025#[gpui::test]
16026async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
16027 init_test(cx, |_| {});
16028
16029 let mut cx = EditorLspTestContext::new_rust(
16030 lsp::ServerCapabilities {
16031 completion_provider: Some(lsp::CompletionOptions {
16032 trigger_characters: Some(vec![".".to_string()]),
16033 resolve_provider: Some(true),
16034 ..Default::default()
16035 }),
16036 ..Default::default()
16037 },
16038 cx,
16039 )
16040 .await;
16041
16042 cx.set_state("fn main() { let a = 2ˇ; }");
16043 cx.simulate_keystroke(".");
16044
16045 let unresolved_item_1 = lsp::CompletionItem {
16046 label: "id".to_string(),
16047 filter_text: Some("id".to_string()),
16048 detail: None,
16049 documentation: None,
16050 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16051 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
16052 new_text: ".id".to_string(),
16053 })),
16054 ..lsp::CompletionItem::default()
16055 };
16056 let resolved_item_1 = lsp::CompletionItem {
16057 additional_text_edits: Some(vec![lsp::TextEdit {
16058 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
16059 new_text: "!!".to_string(),
16060 }]),
16061 ..unresolved_item_1.clone()
16062 };
16063 let unresolved_item_2 = lsp::CompletionItem {
16064 label: "other".to_string(),
16065 filter_text: Some("other".to_string()),
16066 detail: None,
16067 documentation: None,
16068 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16069 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
16070 new_text: ".other".to_string(),
16071 })),
16072 ..lsp::CompletionItem::default()
16073 };
16074 let resolved_item_2 = lsp::CompletionItem {
16075 additional_text_edits: Some(vec![lsp::TextEdit {
16076 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
16077 new_text: "??".to_string(),
16078 }]),
16079 ..unresolved_item_2.clone()
16080 };
16081
16082 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
16083 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
16084 cx.lsp
16085 .server
16086 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
16087 let unresolved_item_1 = unresolved_item_1.clone();
16088 let resolved_item_1 = resolved_item_1.clone();
16089 let unresolved_item_2 = unresolved_item_2.clone();
16090 let resolved_item_2 = resolved_item_2.clone();
16091 let resolve_requests_1 = resolve_requests_1.clone();
16092 let resolve_requests_2 = resolve_requests_2.clone();
16093 move |unresolved_request, _| {
16094 let unresolved_item_1 = unresolved_item_1.clone();
16095 let resolved_item_1 = resolved_item_1.clone();
16096 let unresolved_item_2 = unresolved_item_2.clone();
16097 let resolved_item_2 = resolved_item_2.clone();
16098 let resolve_requests_1 = resolve_requests_1.clone();
16099 let resolve_requests_2 = resolve_requests_2.clone();
16100 async move {
16101 if unresolved_request == unresolved_item_1 {
16102 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
16103 Ok(resolved_item_1.clone())
16104 } else if unresolved_request == unresolved_item_2 {
16105 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
16106 Ok(resolved_item_2.clone())
16107 } else {
16108 panic!("Unexpected completion item {unresolved_request:?}")
16109 }
16110 }
16111 }
16112 })
16113 .detach();
16114
16115 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
16116 let unresolved_item_1 = unresolved_item_1.clone();
16117 let unresolved_item_2 = unresolved_item_2.clone();
16118 async move {
16119 Ok(Some(lsp::CompletionResponse::Array(vec![
16120 unresolved_item_1,
16121 unresolved_item_2,
16122 ])))
16123 }
16124 })
16125 .next()
16126 .await;
16127
16128 cx.condition(|editor, _| editor.context_menu_visible())
16129 .await;
16130 cx.update_editor(|editor, _, _| {
16131 let context_menu = editor.context_menu.borrow_mut();
16132 let context_menu = context_menu
16133 .as_ref()
16134 .expect("Should have the context menu deployed");
16135 match context_menu {
16136 CodeContextMenu::Completions(completions_menu) => {
16137 let completions = completions_menu.completions.borrow_mut();
16138 assert_eq!(
16139 completions
16140 .iter()
16141 .map(|completion| &completion.label.text)
16142 .collect::<Vec<_>>(),
16143 vec!["id", "other"]
16144 )
16145 }
16146 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
16147 }
16148 });
16149 cx.run_until_parked();
16150
16151 cx.update_editor(|editor, window, cx| {
16152 editor.context_menu_next(&ContextMenuNext, window, cx);
16153 });
16154 cx.run_until_parked();
16155 cx.update_editor(|editor, window, cx| {
16156 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
16157 });
16158 cx.run_until_parked();
16159 cx.update_editor(|editor, window, cx| {
16160 editor.context_menu_next(&ContextMenuNext, window, cx);
16161 });
16162 cx.run_until_parked();
16163 cx.update_editor(|editor, window, cx| {
16164 editor
16165 .compose_completion(&ComposeCompletion::default(), window, cx)
16166 .expect("No task returned")
16167 })
16168 .await
16169 .expect("Completion failed");
16170 cx.run_until_parked();
16171
16172 cx.update_editor(|editor, _, cx| {
16173 assert_eq!(
16174 resolve_requests_1.load(atomic::Ordering::Acquire),
16175 1,
16176 "Should always resolve once despite multiple selections"
16177 );
16178 assert_eq!(
16179 resolve_requests_2.load(atomic::Ordering::Acquire),
16180 1,
16181 "Should always resolve once after multiple selections and applying the completion"
16182 );
16183 assert_eq!(
16184 editor.text(cx),
16185 "fn main() { let a = ??.other; }",
16186 "Should use resolved data when applying the completion"
16187 );
16188 });
16189}
16190
16191#[gpui::test]
16192async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
16193 init_test(cx, |_| {});
16194
16195 let item_0 = lsp::CompletionItem {
16196 label: "abs".into(),
16197 insert_text: Some("abs".into()),
16198 data: Some(json!({ "very": "special"})),
16199 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
16200 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
16201 lsp::InsertReplaceEdit {
16202 new_text: "abs".to_string(),
16203 insert: lsp::Range::default(),
16204 replace: lsp::Range::default(),
16205 },
16206 )),
16207 ..lsp::CompletionItem::default()
16208 };
16209 let items = iter::once(item_0.clone())
16210 .chain((11..51).map(|i| lsp::CompletionItem {
16211 label: format!("item_{}", i),
16212 insert_text: Some(format!("item_{}", i)),
16213 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
16214 ..lsp::CompletionItem::default()
16215 }))
16216 .collect::<Vec<_>>();
16217
16218 let default_commit_characters = vec!["?".to_string()];
16219 let default_data = json!({ "default": "data"});
16220 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
16221 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
16222 let default_edit_range = lsp::Range {
16223 start: lsp::Position {
16224 line: 0,
16225 character: 5,
16226 },
16227 end: lsp::Position {
16228 line: 0,
16229 character: 5,
16230 },
16231 };
16232
16233 let mut cx = EditorLspTestContext::new_rust(
16234 lsp::ServerCapabilities {
16235 completion_provider: Some(lsp::CompletionOptions {
16236 trigger_characters: Some(vec![".".to_string()]),
16237 resolve_provider: Some(true),
16238 ..Default::default()
16239 }),
16240 ..Default::default()
16241 },
16242 cx,
16243 )
16244 .await;
16245
16246 cx.set_state("fn main() { let a = 2ˇ; }");
16247 cx.simulate_keystroke(".");
16248
16249 let completion_data = default_data.clone();
16250 let completion_characters = default_commit_characters.clone();
16251 let completion_items = items.clone();
16252 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
16253 let default_data = completion_data.clone();
16254 let default_commit_characters = completion_characters.clone();
16255 let items = completion_items.clone();
16256 async move {
16257 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
16258 items,
16259 item_defaults: Some(lsp::CompletionListItemDefaults {
16260 data: Some(default_data.clone()),
16261 commit_characters: Some(default_commit_characters.clone()),
16262 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
16263 default_edit_range,
16264 )),
16265 insert_text_format: Some(default_insert_text_format),
16266 insert_text_mode: Some(default_insert_text_mode),
16267 }),
16268 ..lsp::CompletionList::default()
16269 })))
16270 }
16271 })
16272 .next()
16273 .await;
16274
16275 let resolved_items = Arc::new(Mutex::new(Vec::new()));
16276 cx.lsp
16277 .server
16278 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
16279 let closure_resolved_items = resolved_items.clone();
16280 move |item_to_resolve, _| {
16281 let closure_resolved_items = closure_resolved_items.clone();
16282 async move {
16283 closure_resolved_items.lock().push(item_to_resolve.clone());
16284 Ok(item_to_resolve)
16285 }
16286 }
16287 })
16288 .detach();
16289
16290 cx.condition(|editor, _| editor.context_menu_visible())
16291 .await;
16292 cx.run_until_parked();
16293 cx.update_editor(|editor, _, _| {
16294 let menu = editor.context_menu.borrow_mut();
16295 match menu.as_ref().expect("should have the completions menu") {
16296 CodeContextMenu::Completions(completions_menu) => {
16297 assert_eq!(
16298 completions_menu
16299 .entries
16300 .borrow()
16301 .iter()
16302 .map(|mat| mat.string.clone())
16303 .collect::<Vec<String>>(),
16304 items
16305 .iter()
16306 .map(|completion| completion.label.clone())
16307 .collect::<Vec<String>>()
16308 );
16309 }
16310 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
16311 }
16312 });
16313 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
16314 // with 4 from the end.
16315 assert_eq!(
16316 *resolved_items.lock(),
16317 [&items[0..16], &items[items.len() - 4..items.len()]]
16318 .concat()
16319 .iter()
16320 .cloned()
16321 .map(|mut item| {
16322 if item.data.is_none() {
16323 item.data = Some(default_data.clone());
16324 }
16325 item
16326 })
16327 .collect::<Vec<lsp::CompletionItem>>(),
16328 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
16329 );
16330 resolved_items.lock().clear();
16331
16332 cx.update_editor(|editor, window, cx| {
16333 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
16334 });
16335 cx.run_until_parked();
16336 // Completions that have already been resolved are skipped.
16337 assert_eq!(
16338 *resolved_items.lock(),
16339 items[items.len() - 17..items.len() - 4]
16340 .iter()
16341 .cloned()
16342 .map(|mut item| {
16343 if item.data.is_none() {
16344 item.data = Some(default_data.clone());
16345 }
16346 item
16347 })
16348 .collect::<Vec<lsp::CompletionItem>>()
16349 );
16350 resolved_items.lock().clear();
16351}
16352
16353#[gpui::test]
16354async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
16355 init_test(cx, |_| {});
16356
16357 let mut cx = EditorLspTestContext::new(
16358 Language::new(
16359 LanguageConfig {
16360 matcher: LanguageMatcher {
16361 path_suffixes: vec!["jsx".into()],
16362 ..Default::default()
16363 },
16364 overrides: [(
16365 "element".into(),
16366 LanguageConfigOverride {
16367 completion_query_characters: Override::Set(['-'].into_iter().collect()),
16368 ..Default::default()
16369 },
16370 )]
16371 .into_iter()
16372 .collect(),
16373 ..Default::default()
16374 },
16375 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
16376 )
16377 .with_override_query("(jsx_self_closing_element) @element")
16378 .unwrap(),
16379 lsp::ServerCapabilities {
16380 completion_provider: Some(lsp::CompletionOptions {
16381 trigger_characters: Some(vec![":".to_string()]),
16382 ..Default::default()
16383 }),
16384 ..Default::default()
16385 },
16386 cx,
16387 )
16388 .await;
16389
16390 cx.lsp
16391 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
16392 Ok(Some(lsp::CompletionResponse::Array(vec![
16393 lsp::CompletionItem {
16394 label: "bg-blue".into(),
16395 ..Default::default()
16396 },
16397 lsp::CompletionItem {
16398 label: "bg-red".into(),
16399 ..Default::default()
16400 },
16401 lsp::CompletionItem {
16402 label: "bg-yellow".into(),
16403 ..Default::default()
16404 },
16405 ])))
16406 });
16407
16408 cx.set_state(r#"<p class="bgˇ" />"#);
16409
16410 // Trigger completion when typing a dash, because the dash is an extra
16411 // word character in the 'element' scope, which contains the cursor.
16412 cx.simulate_keystroke("-");
16413 cx.executor().run_until_parked();
16414 cx.update_editor(|editor, _, _| {
16415 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16416 {
16417 assert_eq!(
16418 completion_menu_entries(&menu),
16419 &["bg-blue", "bg-red", "bg-yellow"]
16420 );
16421 } else {
16422 panic!("expected completion menu to be open");
16423 }
16424 });
16425
16426 cx.simulate_keystroke("l");
16427 cx.executor().run_until_parked();
16428 cx.update_editor(|editor, _, _| {
16429 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16430 {
16431 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
16432 } else {
16433 panic!("expected completion menu to be open");
16434 }
16435 });
16436
16437 // When filtering completions, consider the character after the '-' to
16438 // be the start of a subword.
16439 cx.set_state(r#"<p class="yelˇ" />"#);
16440 cx.simulate_keystroke("l");
16441 cx.executor().run_until_parked();
16442 cx.update_editor(|editor, _, _| {
16443 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16444 {
16445 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
16446 } else {
16447 panic!("expected completion menu to be open");
16448 }
16449 });
16450}
16451
16452fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
16453 let entries = menu.entries.borrow();
16454 entries.iter().map(|mat| mat.string.clone()).collect()
16455}
16456
16457#[gpui::test]
16458async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
16459 init_test(cx, |settings| {
16460 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
16461 Formatter::Prettier,
16462 )))
16463 });
16464
16465 let fs = FakeFs::new(cx.executor());
16466 fs.insert_file(path!("/file.ts"), Default::default()).await;
16467
16468 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
16469 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
16470
16471 language_registry.add(Arc::new(Language::new(
16472 LanguageConfig {
16473 name: "TypeScript".into(),
16474 matcher: LanguageMatcher {
16475 path_suffixes: vec!["ts".to_string()],
16476 ..Default::default()
16477 },
16478 ..Default::default()
16479 },
16480 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
16481 )));
16482 update_test_language_settings(cx, |settings| {
16483 settings.defaults.prettier = Some(PrettierSettings {
16484 allowed: true,
16485 ..PrettierSettings::default()
16486 });
16487 });
16488
16489 let test_plugin = "test_plugin";
16490 let _ = language_registry.register_fake_lsp(
16491 "TypeScript",
16492 FakeLspAdapter {
16493 prettier_plugins: vec![test_plugin],
16494 ..Default::default()
16495 },
16496 );
16497
16498 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
16499 let buffer = project
16500 .update(cx, |project, cx| {
16501 project.open_local_buffer(path!("/file.ts"), cx)
16502 })
16503 .await
16504 .unwrap();
16505
16506 let buffer_text = "one\ntwo\nthree\n";
16507 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16508 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16509 editor.update_in(cx, |editor, window, cx| {
16510 editor.set_text(buffer_text, window, cx)
16511 });
16512
16513 editor
16514 .update_in(cx, |editor, window, cx| {
16515 editor.perform_format(
16516 project.clone(),
16517 FormatTrigger::Manual,
16518 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
16519 window,
16520 cx,
16521 )
16522 })
16523 .unwrap()
16524 .await;
16525 assert_eq!(
16526 editor.update(cx, |editor, cx| editor.text(cx)),
16527 buffer_text.to_string() + prettier_format_suffix,
16528 "Test prettier formatting was not applied to the original buffer text",
16529 );
16530
16531 update_test_language_settings(cx, |settings| {
16532 settings.defaults.formatter = Some(SelectedFormatter::Auto)
16533 });
16534 let format = editor.update_in(cx, |editor, window, cx| {
16535 editor.perform_format(
16536 project.clone(),
16537 FormatTrigger::Manual,
16538 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
16539 window,
16540 cx,
16541 )
16542 });
16543 format.await.unwrap();
16544 assert_eq!(
16545 editor.update(cx, |editor, cx| editor.text(cx)),
16546 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
16547 "Autoformatting (via test prettier) was not applied to the original buffer text",
16548 );
16549}
16550
16551#[gpui::test]
16552async fn test_addition_reverts(cx: &mut TestAppContext) {
16553 init_test(cx, |_| {});
16554 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16555 let base_text = indoc! {r#"
16556 struct Row;
16557 struct Row1;
16558 struct Row2;
16559
16560 struct Row4;
16561 struct Row5;
16562 struct Row6;
16563
16564 struct Row8;
16565 struct Row9;
16566 struct Row10;"#};
16567
16568 // When addition hunks are not adjacent to carets, no hunk revert is performed
16569 assert_hunk_revert(
16570 indoc! {r#"struct Row;
16571 struct Row1;
16572 struct Row1.1;
16573 struct Row1.2;
16574 struct Row2;ˇ
16575
16576 struct Row4;
16577 struct Row5;
16578 struct Row6;
16579
16580 struct Row8;
16581 ˇstruct Row9;
16582 struct Row9.1;
16583 struct Row9.2;
16584 struct Row9.3;
16585 struct Row10;"#},
16586 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
16587 indoc! {r#"struct Row;
16588 struct Row1;
16589 struct Row1.1;
16590 struct Row1.2;
16591 struct Row2;ˇ
16592
16593 struct Row4;
16594 struct Row5;
16595 struct Row6;
16596
16597 struct Row8;
16598 ˇstruct Row9;
16599 struct Row9.1;
16600 struct Row9.2;
16601 struct Row9.3;
16602 struct Row10;"#},
16603 base_text,
16604 &mut cx,
16605 );
16606 // Same for selections
16607 assert_hunk_revert(
16608 indoc! {r#"struct Row;
16609 struct Row1;
16610 struct Row2;
16611 struct Row2.1;
16612 struct Row2.2;
16613 «ˇ
16614 struct Row4;
16615 struct» Row5;
16616 «struct Row6;
16617 ˇ»
16618 struct Row9.1;
16619 struct Row9.2;
16620 struct Row9.3;
16621 struct Row8;
16622 struct Row9;
16623 struct Row10;"#},
16624 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
16625 indoc! {r#"struct Row;
16626 struct Row1;
16627 struct Row2;
16628 struct Row2.1;
16629 struct Row2.2;
16630 «ˇ
16631 struct Row4;
16632 struct» Row5;
16633 «struct Row6;
16634 ˇ»
16635 struct Row9.1;
16636 struct Row9.2;
16637 struct Row9.3;
16638 struct Row8;
16639 struct Row9;
16640 struct Row10;"#},
16641 base_text,
16642 &mut cx,
16643 );
16644
16645 // When carets and selections intersect the addition hunks, those are reverted.
16646 // Adjacent carets got merged.
16647 assert_hunk_revert(
16648 indoc! {r#"struct Row;
16649 ˇ// something on the top
16650 struct Row1;
16651 struct Row2;
16652 struct Roˇw3.1;
16653 struct Row2.2;
16654 struct Row2.3;ˇ
16655
16656 struct Row4;
16657 struct ˇRow5.1;
16658 struct Row5.2;
16659 struct «Rowˇ»5.3;
16660 struct Row5;
16661 struct Row6;
16662 ˇ
16663 struct Row9.1;
16664 struct «Rowˇ»9.2;
16665 struct «ˇRow»9.3;
16666 struct Row8;
16667 struct Row9;
16668 «ˇ// something on bottom»
16669 struct Row10;"#},
16670 vec![
16671 DiffHunkStatusKind::Added,
16672 DiffHunkStatusKind::Added,
16673 DiffHunkStatusKind::Added,
16674 DiffHunkStatusKind::Added,
16675 DiffHunkStatusKind::Added,
16676 ],
16677 indoc! {r#"struct Row;
16678 ˇstruct Row1;
16679 struct Row2;
16680 ˇ
16681 struct Row4;
16682 ˇstruct Row5;
16683 struct Row6;
16684 ˇ
16685 ˇstruct Row8;
16686 struct Row9;
16687 ˇstruct Row10;"#},
16688 base_text,
16689 &mut cx,
16690 );
16691}
16692
16693#[gpui::test]
16694async fn test_modification_reverts(cx: &mut TestAppContext) {
16695 init_test(cx, |_| {});
16696 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16697 let base_text = indoc! {r#"
16698 struct Row;
16699 struct Row1;
16700 struct Row2;
16701
16702 struct Row4;
16703 struct Row5;
16704 struct Row6;
16705
16706 struct Row8;
16707 struct Row9;
16708 struct Row10;"#};
16709
16710 // Modification hunks behave the same as the addition ones.
16711 assert_hunk_revert(
16712 indoc! {r#"struct Row;
16713 struct Row1;
16714 struct Row33;
16715 ˇ
16716 struct Row4;
16717 struct Row5;
16718 struct Row6;
16719 ˇ
16720 struct Row99;
16721 struct Row9;
16722 struct Row10;"#},
16723 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
16724 indoc! {r#"struct Row;
16725 struct Row1;
16726 struct Row33;
16727 ˇ
16728 struct Row4;
16729 struct Row5;
16730 struct Row6;
16731 ˇ
16732 struct Row99;
16733 struct Row9;
16734 struct Row10;"#},
16735 base_text,
16736 &mut cx,
16737 );
16738 assert_hunk_revert(
16739 indoc! {r#"struct Row;
16740 struct Row1;
16741 struct Row33;
16742 «ˇ
16743 struct Row4;
16744 struct» Row5;
16745 «struct Row6;
16746 ˇ»
16747 struct Row99;
16748 struct Row9;
16749 struct Row10;"#},
16750 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
16751 indoc! {r#"struct Row;
16752 struct Row1;
16753 struct Row33;
16754 «ˇ
16755 struct Row4;
16756 struct» Row5;
16757 «struct Row6;
16758 ˇ»
16759 struct Row99;
16760 struct Row9;
16761 struct Row10;"#},
16762 base_text,
16763 &mut cx,
16764 );
16765
16766 assert_hunk_revert(
16767 indoc! {r#"ˇstruct Row1.1;
16768 struct Row1;
16769 «ˇstr»uct Row22;
16770
16771 struct ˇRow44;
16772 struct Row5;
16773 struct «Rˇ»ow66;ˇ
16774
16775 «struˇ»ct Row88;
16776 struct Row9;
16777 struct Row1011;ˇ"#},
16778 vec![
16779 DiffHunkStatusKind::Modified,
16780 DiffHunkStatusKind::Modified,
16781 DiffHunkStatusKind::Modified,
16782 DiffHunkStatusKind::Modified,
16783 DiffHunkStatusKind::Modified,
16784 DiffHunkStatusKind::Modified,
16785 ],
16786 indoc! {r#"struct Row;
16787 ˇstruct Row1;
16788 struct Row2;
16789 ˇ
16790 struct Row4;
16791 ˇstruct Row5;
16792 struct Row6;
16793 ˇ
16794 struct Row8;
16795 ˇstruct Row9;
16796 struct Row10;ˇ"#},
16797 base_text,
16798 &mut cx,
16799 );
16800}
16801
16802#[gpui::test]
16803async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
16804 init_test(cx, |_| {});
16805 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16806 let base_text = indoc! {r#"
16807 one
16808
16809 two
16810 three
16811 "#};
16812
16813 cx.set_head_text(base_text);
16814 cx.set_state("\nˇ\n");
16815 cx.executor().run_until_parked();
16816 cx.update_editor(|editor, _window, cx| {
16817 editor.expand_selected_diff_hunks(cx);
16818 });
16819 cx.executor().run_until_parked();
16820 cx.update_editor(|editor, window, cx| {
16821 editor.backspace(&Default::default(), window, cx);
16822 });
16823 cx.run_until_parked();
16824 cx.assert_state_with_diff(
16825 indoc! {r#"
16826
16827 - two
16828 - threeˇ
16829 +
16830 "#}
16831 .to_string(),
16832 );
16833}
16834
16835#[gpui::test]
16836async fn test_deletion_reverts(cx: &mut TestAppContext) {
16837 init_test(cx, |_| {});
16838 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16839 let base_text = indoc! {r#"struct Row;
16840struct Row1;
16841struct Row2;
16842
16843struct Row4;
16844struct Row5;
16845struct Row6;
16846
16847struct Row8;
16848struct Row9;
16849struct Row10;"#};
16850
16851 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
16852 assert_hunk_revert(
16853 indoc! {r#"struct Row;
16854 struct Row2;
16855
16856 ˇstruct Row4;
16857 struct Row5;
16858 struct Row6;
16859 ˇ
16860 struct Row8;
16861 struct Row10;"#},
16862 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16863 indoc! {r#"struct Row;
16864 struct Row2;
16865
16866 ˇstruct Row4;
16867 struct Row5;
16868 struct Row6;
16869 ˇ
16870 struct Row8;
16871 struct Row10;"#},
16872 base_text,
16873 &mut cx,
16874 );
16875 assert_hunk_revert(
16876 indoc! {r#"struct Row;
16877 struct Row2;
16878
16879 «ˇstruct Row4;
16880 struct» Row5;
16881 «struct Row6;
16882 ˇ»
16883 struct Row8;
16884 struct Row10;"#},
16885 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16886 indoc! {r#"struct Row;
16887 struct Row2;
16888
16889 «ˇstruct Row4;
16890 struct» Row5;
16891 «struct Row6;
16892 ˇ»
16893 struct Row8;
16894 struct Row10;"#},
16895 base_text,
16896 &mut cx,
16897 );
16898
16899 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
16900 assert_hunk_revert(
16901 indoc! {r#"struct Row;
16902 ˇstruct Row2;
16903
16904 struct Row4;
16905 struct Row5;
16906 struct Row6;
16907
16908 struct Row8;ˇ
16909 struct Row10;"#},
16910 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16911 indoc! {r#"struct Row;
16912 struct Row1;
16913 ˇstruct Row2;
16914
16915 struct Row4;
16916 struct Row5;
16917 struct Row6;
16918
16919 struct Row8;ˇ
16920 struct Row9;
16921 struct Row10;"#},
16922 base_text,
16923 &mut cx,
16924 );
16925 assert_hunk_revert(
16926 indoc! {r#"struct Row;
16927 struct Row2«ˇ;
16928 struct Row4;
16929 struct» Row5;
16930 «struct Row6;
16931
16932 struct Row8;ˇ»
16933 struct Row10;"#},
16934 vec![
16935 DiffHunkStatusKind::Deleted,
16936 DiffHunkStatusKind::Deleted,
16937 DiffHunkStatusKind::Deleted,
16938 ],
16939 indoc! {r#"struct Row;
16940 struct Row1;
16941 struct Row2«ˇ;
16942
16943 struct Row4;
16944 struct» Row5;
16945 «struct Row6;
16946
16947 struct Row8;ˇ»
16948 struct Row9;
16949 struct Row10;"#},
16950 base_text,
16951 &mut cx,
16952 );
16953}
16954
16955#[gpui::test]
16956async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
16957 init_test(cx, |_| {});
16958
16959 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
16960 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
16961 let base_text_3 =
16962 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
16963
16964 let text_1 = edit_first_char_of_every_line(base_text_1);
16965 let text_2 = edit_first_char_of_every_line(base_text_2);
16966 let text_3 = edit_first_char_of_every_line(base_text_3);
16967
16968 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
16969 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
16970 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
16971
16972 let multibuffer = cx.new(|cx| {
16973 let mut multibuffer = MultiBuffer::new(ReadWrite);
16974 multibuffer.push_excerpts(
16975 buffer_1.clone(),
16976 [
16977 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16978 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16979 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16980 ],
16981 cx,
16982 );
16983 multibuffer.push_excerpts(
16984 buffer_2.clone(),
16985 [
16986 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16987 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16988 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16989 ],
16990 cx,
16991 );
16992 multibuffer.push_excerpts(
16993 buffer_3.clone(),
16994 [
16995 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16996 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16997 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16998 ],
16999 cx,
17000 );
17001 multibuffer
17002 });
17003
17004 let fs = FakeFs::new(cx.executor());
17005 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
17006 let (editor, cx) = cx
17007 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
17008 editor.update_in(cx, |editor, _window, cx| {
17009 for (buffer, diff_base) in [
17010 (buffer_1.clone(), base_text_1),
17011 (buffer_2.clone(), base_text_2),
17012 (buffer_3.clone(), base_text_3),
17013 ] {
17014 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
17015 editor
17016 .buffer
17017 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
17018 }
17019 });
17020 cx.executor().run_until_parked();
17021
17022 editor.update_in(cx, |editor, window, cx| {
17023 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}");
17024 editor.select_all(&SelectAll, window, cx);
17025 editor.git_restore(&Default::default(), window, cx);
17026 });
17027 cx.executor().run_until_parked();
17028
17029 // When all ranges are selected, all buffer hunks are reverted.
17030 editor.update(cx, |editor, cx| {
17031 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");
17032 });
17033 buffer_1.update(cx, |buffer, _| {
17034 assert_eq!(buffer.text(), base_text_1);
17035 });
17036 buffer_2.update(cx, |buffer, _| {
17037 assert_eq!(buffer.text(), base_text_2);
17038 });
17039 buffer_3.update(cx, |buffer, _| {
17040 assert_eq!(buffer.text(), base_text_3);
17041 });
17042
17043 editor.update_in(cx, |editor, window, cx| {
17044 editor.undo(&Default::default(), window, cx);
17045 });
17046
17047 editor.update_in(cx, |editor, window, cx| {
17048 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17049 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
17050 });
17051 editor.git_restore(&Default::default(), window, cx);
17052 });
17053
17054 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
17055 // but not affect buffer_2 and its related excerpts.
17056 editor.update(cx, |editor, cx| {
17057 assert_eq!(
17058 editor.text(cx),
17059 "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}"
17060 );
17061 });
17062 buffer_1.update(cx, |buffer, _| {
17063 assert_eq!(buffer.text(), base_text_1);
17064 });
17065 buffer_2.update(cx, |buffer, _| {
17066 assert_eq!(
17067 buffer.text(),
17068 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
17069 );
17070 });
17071 buffer_3.update(cx, |buffer, _| {
17072 assert_eq!(
17073 buffer.text(),
17074 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
17075 );
17076 });
17077
17078 fn edit_first_char_of_every_line(text: &str) -> String {
17079 text.split('\n')
17080 .map(|line| format!("X{}", &line[1..]))
17081 .collect::<Vec<_>>()
17082 .join("\n")
17083 }
17084}
17085
17086#[gpui::test]
17087async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
17088 init_test(cx, |_| {});
17089
17090 let cols = 4;
17091 let rows = 10;
17092 let sample_text_1 = sample_text(rows, cols, 'a');
17093 assert_eq!(
17094 sample_text_1,
17095 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
17096 );
17097 let sample_text_2 = sample_text(rows, cols, 'l');
17098 assert_eq!(
17099 sample_text_2,
17100 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
17101 );
17102 let sample_text_3 = sample_text(rows, cols, 'v');
17103 assert_eq!(
17104 sample_text_3,
17105 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
17106 );
17107
17108 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
17109 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
17110 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
17111
17112 let multi_buffer = cx.new(|cx| {
17113 let mut multibuffer = MultiBuffer::new(ReadWrite);
17114 multibuffer.push_excerpts(
17115 buffer_1.clone(),
17116 [
17117 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17118 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17119 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17120 ],
17121 cx,
17122 );
17123 multibuffer.push_excerpts(
17124 buffer_2.clone(),
17125 [
17126 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17127 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17128 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17129 ],
17130 cx,
17131 );
17132 multibuffer.push_excerpts(
17133 buffer_3.clone(),
17134 [
17135 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17136 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17137 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17138 ],
17139 cx,
17140 );
17141 multibuffer
17142 });
17143
17144 let fs = FakeFs::new(cx.executor());
17145 fs.insert_tree(
17146 "/a",
17147 json!({
17148 "main.rs": sample_text_1,
17149 "other.rs": sample_text_2,
17150 "lib.rs": sample_text_3,
17151 }),
17152 )
17153 .await;
17154 let project = Project::test(fs, ["/a".as_ref()], cx).await;
17155 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17156 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17157 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
17158 Editor::new(
17159 EditorMode::full(),
17160 multi_buffer,
17161 Some(project.clone()),
17162 window,
17163 cx,
17164 )
17165 });
17166 let multibuffer_item_id = workspace
17167 .update(cx, |workspace, window, cx| {
17168 assert!(
17169 workspace.active_item(cx).is_none(),
17170 "active item should be None before the first item is added"
17171 );
17172 workspace.add_item_to_active_pane(
17173 Box::new(multi_buffer_editor.clone()),
17174 None,
17175 true,
17176 window,
17177 cx,
17178 );
17179 let active_item = workspace
17180 .active_item(cx)
17181 .expect("should have an active item after adding the multi buffer");
17182 assert!(
17183 !active_item.is_singleton(cx),
17184 "A multi buffer was expected to active after adding"
17185 );
17186 active_item.item_id()
17187 })
17188 .unwrap();
17189 cx.executor().run_until_parked();
17190
17191 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17192 editor.change_selections(
17193 SelectionEffects::scroll(Autoscroll::Next),
17194 window,
17195 cx,
17196 |s| s.select_ranges(Some(1..2)),
17197 );
17198 editor.open_excerpts(&OpenExcerpts, window, cx);
17199 });
17200 cx.executor().run_until_parked();
17201 let first_item_id = workspace
17202 .update(cx, |workspace, window, cx| {
17203 let active_item = workspace
17204 .active_item(cx)
17205 .expect("should have an active item after navigating into the 1st buffer");
17206 let first_item_id = active_item.item_id();
17207 assert_ne!(
17208 first_item_id, multibuffer_item_id,
17209 "Should navigate into the 1st buffer and activate it"
17210 );
17211 assert!(
17212 active_item.is_singleton(cx),
17213 "New active item should be a singleton buffer"
17214 );
17215 assert_eq!(
17216 active_item
17217 .act_as::<Editor>(cx)
17218 .expect("should have navigated into an editor for the 1st buffer")
17219 .read(cx)
17220 .text(cx),
17221 sample_text_1
17222 );
17223
17224 workspace
17225 .go_back(workspace.active_pane().downgrade(), window, cx)
17226 .detach_and_log_err(cx);
17227
17228 first_item_id
17229 })
17230 .unwrap();
17231 cx.executor().run_until_parked();
17232 workspace
17233 .update(cx, |workspace, _, cx| {
17234 let active_item = workspace
17235 .active_item(cx)
17236 .expect("should have an active item after navigating back");
17237 assert_eq!(
17238 active_item.item_id(),
17239 multibuffer_item_id,
17240 "Should navigate back to the multi buffer"
17241 );
17242 assert!(!active_item.is_singleton(cx));
17243 })
17244 .unwrap();
17245
17246 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17247 editor.change_selections(
17248 SelectionEffects::scroll(Autoscroll::Next),
17249 window,
17250 cx,
17251 |s| s.select_ranges(Some(39..40)),
17252 );
17253 editor.open_excerpts(&OpenExcerpts, window, cx);
17254 });
17255 cx.executor().run_until_parked();
17256 let second_item_id = workspace
17257 .update(cx, |workspace, window, cx| {
17258 let active_item = workspace
17259 .active_item(cx)
17260 .expect("should have an active item after navigating into the 2nd buffer");
17261 let second_item_id = active_item.item_id();
17262 assert_ne!(
17263 second_item_id, multibuffer_item_id,
17264 "Should navigate away from the multibuffer"
17265 );
17266 assert_ne!(
17267 second_item_id, first_item_id,
17268 "Should navigate into the 2nd buffer and activate it"
17269 );
17270 assert!(
17271 active_item.is_singleton(cx),
17272 "New active item should be a singleton buffer"
17273 );
17274 assert_eq!(
17275 active_item
17276 .act_as::<Editor>(cx)
17277 .expect("should have navigated into an editor")
17278 .read(cx)
17279 .text(cx),
17280 sample_text_2
17281 );
17282
17283 workspace
17284 .go_back(workspace.active_pane().downgrade(), window, cx)
17285 .detach_and_log_err(cx);
17286
17287 second_item_id
17288 })
17289 .unwrap();
17290 cx.executor().run_until_parked();
17291 workspace
17292 .update(cx, |workspace, _, cx| {
17293 let active_item = workspace
17294 .active_item(cx)
17295 .expect("should have an active item after navigating back from the 2nd buffer");
17296 assert_eq!(
17297 active_item.item_id(),
17298 multibuffer_item_id,
17299 "Should navigate back from the 2nd buffer to the multi buffer"
17300 );
17301 assert!(!active_item.is_singleton(cx));
17302 })
17303 .unwrap();
17304
17305 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17306 editor.change_selections(
17307 SelectionEffects::scroll(Autoscroll::Next),
17308 window,
17309 cx,
17310 |s| s.select_ranges(Some(70..70)),
17311 );
17312 editor.open_excerpts(&OpenExcerpts, window, cx);
17313 });
17314 cx.executor().run_until_parked();
17315 workspace
17316 .update(cx, |workspace, window, cx| {
17317 let active_item = workspace
17318 .active_item(cx)
17319 .expect("should have an active item after navigating into the 3rd buffer");
17320 let third_item_id = active_item.item_id();
17321 assert_ne!(
17322 third_item_id, multibuffer_item_id,
17323 "Should navigate into the 3rd buffer and activate it"
17324 );
17325 assert_ne!(third_item_id, first_item_id);
17326 assert_ne!(third_item_id, second_item_id);
17327 assert!(
17328 active_item.is_singleton(cx),
17329 "New active item should be a singleton buffer"
17330 );
17331 assert_eq!(
17332 active_item
17333 .act_as::<Editor>(cx)
17334 .expect("should have navigated into an editor")
17335 .read(cx)
17336 .text(cx),
17337 sample_text_3
17338 );
17339
17340 workspace
17341 .go_back(workspace.active_pane().downgrade(), window, cx)
17342 .detach_and_log_err(cx);
17343 })
17344 .unwrap();
17345 cx.executor().run_until_parked();
17346 workspace
17347 .update(cx, |workspace, _, cx| {
17348 let active_item = workspace
17349 .active_item(cx)
17350 .expect("should have an active item after navigating back from the 3rd buffer");
17351 assert_eq!(
17352 active_item.item_id(),
17353 multibuffer_item_id,
17354 "Should navigate back from the 3rd buffer to the multi buffer"
17355 );
17356 assert!(!active_item.is_singleton(cx));
17357 })
17358 .unwrap();
17359}
17360
17361#[gpui::test]
17362async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17363 init_test(cx, |_| {});
17364
17365 let mut cx = EditorTestContext::new(cx).await;
17366
17367 let diff_base = r#"
17368 use some::mod;
17369
17370 const A: u32 = 42;
17371
17372 fn main() {
17373 println!("hello");
17374
17375 println!("world");
17376 }
17377 "#
17378 .unindent();
17379
17380 cx.set_state(
17381 &r#"
17382 use some::modified;
17383
17384 ˇ
17385 fn main() {
17386 println!("hello there");
17387
17388 println!("around the");
17389 println!("world");
17390 }
17391 "#
17392 .unindent(),
17393 );
17394
17395 cx.set_head_text(&diff_base);
17396 executor.run_until_parked();
17397
17398 cx.update_editor(|editor, window, cx| {
17399 editor.go_to_next_hunk(&GoToHunk, window, cx);
17400 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17401 });
17402 executor.run_until_parked();
17403 cx.assert_state_with_diff(
17404 r#"
17405 use some::modified;
17406
17407
17408 fn main() {
17409 - println!("hello");
17410 + ˇ println!("hello there");
17411
17412 println!("around the");
17413 println!("world");
17414 }
17415 "#
17416 .unindent(),
17417 );
17418
17419 cx.update_editor(|editor, window, cx| {
17420 for _ in 0..2 {
17421 editor.go_to_next_hunk(&GoToHunk, window, cx);
17422 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17423 }
17424 });
17425 executor.run_until_parked();
17426 cx.assert_state_with_diff(
17427 r#"
17428 - use some::mod;
17429 + ˇuse some::modified;
17430
17431
17432 fn main() {
17433 - println!("hello");
17434 + println!("hello there");
17435
17436 + println!("around the");
17437 println!("world");
17438 }
17439 "#
17440 .unindent(),
17441 );
17442
17443 cx.update_editor(|editor, window, cx| {
17444 editor.go_to_next_hunk(&GoToHunk, window, cx);
17445 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17446 });
17447 executor.run_until_parked();
17448 cx.assert_state_with_diff(
17449 r#"
17450 - use some::mod;
17451 + use some::modified;
17452
17453 - const A: u32 = 42;
17454 ˇ
17455 fn main() {
17456 - println!("hello");
17457 + println!("hello there");
17458
17459 + println!("around the");
17460 println!("world");
17461 }
17462 "#
17463 .unindent(),
17464 );
17465
17466 cx.update_editor(|editor, window, cx| {
17467 editor.cancel(&Cancel, window, cx);
17468 });
17469
17470 cx.assert_state_with_diff(
17471 r#"
17472 use some::modified;
17473
17474 ˇ
17475 fn main() {
17476 println!("hello there");
17477
17478 println!("around the");
17479 println!("world");
17480 }
17481 "#
17482 .unindent(),
17483 );
17484}
17485
17486#[gpui::test]
17487async fn test_diff_base_change_with_expanded_diff_hunks(
17488 executor: BackgroundExecutor,
17489 cx: &mut TestAppContext,
17490) {
17491 init_test(cx, |_| {});
17492
17493 let mut cx = EditorTestContext::new(cx).await;
17494
17495 let diff_base = r#"
17496 use some::mod1;
17497 use some::mod2;
17498
17499 const A: u32 = 42;
17500 const B: u32 = 42;
17501 const C: u32 = 42;
17502
17503 fn main() {
17504 println!("hello");
17505
17506 println!("world");
17507 }
17508 "#
17509 .unindent();
17510
17511 cx.set_state(
17512 &r#"
17513 use some::mod2;
17514
17515 const A: u32 = 42;
17516 const C: u32 = 42;
17517
17518 fn main(ˇ) {
17519 //println!("hello");
17520
17521 println!("world");
17522 //
17523 //
17524 }
17525 "#
17526 .unindent(),
17527 );
17528
17529 cx.set_head_text(&diff_base);
17530 executor.run_until_parked();
17531
17532 cx.update_editor(|editor, window, cx| {
17533 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17534 });
17535 executor.run_until_parked();
17536 cx.assert_state_with_diff(
17537 r#"
17538 - use some::mod1;
17539 use some::mod2;
17540
17541 const A: u32 = 42;
17542 - const B: u32 = 42;
17543 const C: u32 = 42;
17544
17545 fn main(ˇ) {
17546 - println!("hello");
17547 + //println!("hello");
17548
17549 println!("world");
17550 + //
17551 + //
17552 }
17553 "#
17554 .unindent(),
17555 );
17556
17557 cx.set_head_text("new diff base!");
17558 executor.run_until_parked();
17559 cx.assert_state_with_diff(
17560 r#"
17561 - new diff base!
17562 + use some::mod2;
17563 +
17564 + const A: u32 = 42;
17565 + const C: u32 = 42;
17566 +
17567 + fn main(ˇ) {
17568 + //println!("hello");
17569 +
17570 + println!("world");
17571 + //
17572 + //
17573 + }
17574 "#
17575 .unindent(),
17576 );
17577}
17578
17579#[gpui::test]
17580async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
17581 init_test(cx, |_| {});
17582
17583 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
17584 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
17585 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
17586 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
17587 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
17588 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
17589
17590 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
17591 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
17592 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
17593
17594 let multi_buffer = cx.new(|cx| {
17595 let mut multibuffer = MultiBuffer::new(ReadWrite);
17596 multibuffer.push_excerpts(
17597 buffer_1.clone(),
17598 [
17599 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17600 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17601 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17602 ],
17603 cx,
17604 );
17605 multibuffer.push_excerpts(
17606 buffer_2.clone(),
17607 [
17608 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17609 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17610 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17611 ],
17612 cx,
17613 );
17614 multibuffer.push_excerpts(
17615 buffer_3.clone(),
17616 [
17617 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17618 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17619 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17620 ],
17621 cx,
17622 );
17623 multibuffer
17624 });
17625
17626 let editor =
17627 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
17628 editor
17629 .update(cx, |editor, _window, cx| {
17630 for (buffer, diff_base) in [
17631 (buffer_1.clone(), file_1_old),
17632 (buffer_2.clone(), file_2_old),
17633 (buffer_3.clone(), file_3_old),
17634 ] {
17635 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
17636 editor
17637 .buffer
17638 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
17639 }
17640 })
17641 .unwrap();
17642
17643 let mut cx = EditorTestContext::for_editor(editor, cx).await;
17644 cx.run_until_parked();
17645
17646 cx.assert_editor_state(
17647 &"
17648 ˇaaa
17649 ccc
17650 ddd
17651
17652 ggg
17653 hhh
17654
17655
17656 lll
17657 mmm
17658 NNN
17659
17660 qqq
17661 rrr
17662
17663 uuu
17664 111
17665 222
17666 333
17667
17668 666
17669 777
17670
17671 000
17672 !!!"
17673 .unindent(),
17674 );
17675
17676 cx.update_editor(|editor, window, cx| {
17677 editor.select_all(&SelectAll, window, cx);
17678 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17679 });
17680 cx.executor().run_until_parked();
17681
17682 cx.assert_state_with_diff(
17683 "
17684 «aaa
17685 - bbb
17686 ccc
17687 ddd
17688
17689 ggg
17690 hhh
17691
17692
17693 lll
17694 mmm
17695 - nnn
17696 + NNN
17697
17698 qqq
17699 rrr
17700
17701 uuu
17702 111
17703 222
17704 333
17705
17706 + 666
17707 777
17708
17709 000
17710 !!!ˇ»"
17711 .unindent(),
17712 );
17713}
17714
17715#[gpui::test]
17716async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
17717 init_test(cx, |_| {});
17718
17719 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
17720 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
17721
17722 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
17723 let multi_buffer = cx.new(|cx| {
17724 let mut multibuffer = MultiBuffer::new(ReadWrite);
17725 multibuffer.push_excerpts(
17726 buffer.clone(),
17727 [
17728 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
17729 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
17730 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
17731 ],
17732 cx,
17733 );
17734 multibuffer
17735 });
17736
17737 let editor =
17738 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
17739 editor
17740 .update(cx, |editor, _window, cx| {
17741 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
17742 editor
17743 .buffer
17744 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
17745 })
17746 .unwrap();
17747
17748 let mut cx = EditorTestContext::for_editor(editor, cx).await;
17749 cx.run_until_parked();
17750
17751 cx.update_editor(|editor, window, cx| {
17752 editor.expand_all_diff_hunks(&Default::default(), window, cx)
17753 });
17754 cx.executor().run_until_parked();
17755
17756 // When the start of a hunk coincides with the start of its excerpt,
17757 // the hunk is expanded. When the start of a a hunk is earlier than
17758 // the start of its excerpt, the hunk is not expanded.
17759 cx.assert_state_with_diff(
17760 "
17761 ˇaaa
17762 - bbb
17763 + BBB
17764
17765 - ddd
17766 - eee
17767 + DDD
17768 + EEE
17769 fff
17770
17771 iii
17772 "
17773 .unindent(),
17774 );
17775}
17776
17777#[gpui::test]
17778async fn test_edits_around_expanded_insertion_hunks(
17779 executor: BackgroundExecutor,
17780 cx: &mut TestAppContext,
17781) {
17782 init_test(cx, |_| {});
17783
17784 let mut cx = EditorTestContext::new(cx).await;
17785
17786 let diff_base = r#"
17787 use some::mod1;
17788 use some::mod2;
17789
17790 const A: u32 = 42;
17791
17792 fn main() {
17793 println!("hello");
17794
17795 println!("world");
17796 }
17797 "#
17798 .unindent();
17799 executor.run_until_parked();
17800 cx.set_state(
17801 &r#"
17802 use some::mod1;
17803 use some::mod2;
17804
17805 const A: u32 = 42;
17806 const B: u32 = 42;
17807 const C: u32 = 42;
17808 ˇ
17809
17810 fn main() {
17811 println!("hello");
17812
17813 println!("world");
17814 }
17815 "#
17816 .unindent(),
17817 );
17818
17819 cx.set_head_text(&diff_base);
17820 executor.run_until_parked();
17821
17822 cx.update_editor(|editor, window, cx| {
17823 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17824 });
17825 executor.run_until_parked();
17826
17827 cx.assert_state_with_diff(
17828 r#"
17829 use some::mod1;
17830 use some::mod2;
17831
17832 const A: u32 = 42;
17833 + const B: u32 = 42;
17834 + const C: u32 = 42;
17835 + ˇ
17836
17837 fn main() {
17838 println!("hello");
17839
17840 println!("world");
17841 }
17842 "#
17843 .unindent(),
17844 );
17845
17846 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
17847 executor.run_until_parked();
17848
17849 cx.assert_state_with_diff(
17850 r#"
17851 use some::mod1;
17852 use some::mod2;
17853
17854 const A: u32 = 42;
17855 + const B: u32 = 42;
17856 + const C: u32 = 42;
17857 + const D: u32 = 42;
17858 + ˇ
17859
17860 fn main() {
17861 println!("hello");
17862
17863 println!("world");
17864 }
17865 "#
17866 .unindent(),
17867 );
17868
17869 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
17870 executor.run_until_parked();
17871
17872 cx.assert_state_with_diff(
17873 r#"
17874 use some::mod1;
17875 use some::mod2;
17876
17877 const A: u32 = 42;
17878 + const B: u32 = 42;
17879 + const C: u32 = 42;
17880 + const D: u32 = 42;
17881 + const E: u32 = 42;
17882 + ˇ
17883
17884 fn main() {
17885 println!("hello");
17886
17887 println!("world");
17888 }
17889 "#
17890 .unindent(),
17891 );
17892
17893 cx.update_editor(|editor, window, cx| {
17894 editor.delete_line(&DeleteLine, window, cx);
17895 });
17896 executor.run_until_parked();
17897
17898 cx.assert_state_with_diff(
17899 r#"
17900 use some::mod1;
17901 use some::mod2;
17902
17903 const A: u32 = 42;
17904 + const B: u32 = 42;
17905 + const C: u32 = 42;
17906 + const D: u32 = 42;
17907 + const E: u32 = 42;
17908 ˇ
17909 fn main() {
17910 println!("hello");
17911
17912 println!("world");
17913 }
17914 "#
17915 .unindent(),
17916 );
17917
17918 cx.update_editor(|editor, window, cx| {
17919 editor.move_up(&MoveUp, window, cx);
17920 editor.delete_line(&DeleteLine, window, cx);
17921 editor.move_up(&MoveUp, window, cx);
17922 editor.delete_line(&DeleteLine, window, cx);
17923 editor.move_up(&MoveUp, window, cx);
17924 editor.delete_line(&DeleteLine, window, cx);
17925 });
17926 executor.run_until_parked();
17927 cx.assert_state_with_diff(
17928 r#"
17929 use some::mod1;
17930 use some::mod2;
17931
17932 const A: u32 = 42;
17933 + const B: u32 = 42;
17934 ˇ
17935 fn main() {
17936 println!("hello");
17937
17938 println!("world");
17939 }
17940 "#
17941 .unindent(),
17942 );
17943
17944 cx.update_editor(|editor, window, cx| {
17945 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
17946 editor.delete_line(&DeleteLine, window, cx);
17947 });
17948 executor.run_until_parked();
17949 cx.assert_state_with_diff(
17950 r#"
17951 ˇ
17952 fn main() {
17953 println!("hello");
17954
17955 println!("world");
17956 }
17957 "#
17958 .unindent(),
17959 );
17960}
17961
17962#[gpui::test]
17963async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
17964 init_test(cx, |_| {});
17965
17966 let mut cx = EditorTestContext::new(cx).await;
17967 cx.set_head_text(indoc! { "
17968 one
17969 two
17970 three
17971 four
17972 five
17973 "
17974 });
17975 cx.set_state(indoc! { "
17976 one
17977 ˇthree
17978 five
17979 "});
17980 cx.run_until_parked();
17981 cx.update_editor(|editor, window, cx| {
17982 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17983 });
17984 cx.assert_state_with_diff(
17985 indoc! { "
17986 one
17987 - two
17988 ˇthree
17989 - four
17990 five
17991 "}
17992 .to_string(),
17993 );
17994 cx.update_editor(|editor, window, cx| {
17995 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17996 });
17997
17998 cx.assert_state_with_diff(
17999 indoc! { "
18000 one
18001 ˇthree
18002 five
18003 "}
18004 .to_string(),
18005 );
18006
18007 cx.set_state(indoc! { "
18008 one
18009 ˇTWO
18010 three
18011 four
18012 five
18013 "});
18014 cx.run_until_parked();
18015 cx.update_editor(|editor, window, cx| {
18016 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
18017 });
18018
18019 cx.assert_state_with_diff(
18020 indoc! { "
18021 one
18022 - two
18023 + ˇTWO
18024 three
18025 four
18026 five
18027 "}
18028 .to_string(),
18029 );
18030 cx.update_editor(|editor, window, cx| {
18031 editor.move_up(&Default::default(), window, cx);
18032 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
18033 });
18034 cx.assert_state_with_diff(
18035 indoc! { "
18036 one
18037 ˇTWO
18038 three
18039 four
18040 five
18041 "}
18042 .to_string(),
18043 );
18044}
18045
18046#[gpui::test]
18047async fn test_edits_around_expanded_deletion_hunks(
18048 executor: BackgroundExecutor,
18049 cx: &mut TestAppContext,
18050) {
18051 init_test(cx, |_| {});
18052
18053 let mut cx = EditorTestContext::new(cx).await;
18054
18055 let diff_base = r#"
18056 use some::mod1;
18057 use some::mod2;
18058
18059 const A: u32 = 42;
18060 const B: u32 = 42;
18061 const C: u32 = 42;
18062
18063
18064 fn main() {
18065 println!("hello");
18066
18067 println!("world");
18068 }
18069 "#
18070 .unindent();
18071 executor.run_until_parked();
18072 cx.set_state(
18073 &r#"
18074 use some::mod1;
18075 use some::mod2;
18076
18077 ˇconst B: u32 = 42;
18078 const C: u32 = 42;
18079
18080
18081 fn main() {
18082 println!("hello");
18083
18084 println!("world");
18085 }
18086 "#
18087 .unindent(),
18088 );
18089
18090 cx.set_head_text(&diff_base);
18091 executor.run_until_parked();
18092
18093 cx.update_editor(|editor, window, cx| {
18094 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18095 });
18096 executor.run_until_parked();
18097
18098 cx.assert_state_with_diff(
18099 r#"
18100 use some::mod1;
18101 use some::mod2;
18102
18103 - const A: u32 = 42;
18104 ˇconst B: u32 = 42;
18105 const C: u32 = 42;
18106
18107
18108 fn main() {
18109 println!("hello");
18110
18111 println!("world");
18112 }
18113 "#
18114 .unindent(),
18115 );
18116
18117 cx.update_editor(|editor, window, cx| {
18118 editor.delete_line(&DeleteLine, window, cx);
18119 });
18120 executor.run_until_parked();
18121 cx.assert_state_with_diff(
18122 r#"
18123 use some::mod1;
18124 use some::mod2;
18125
18126 - const A: u32 = 42;
18127 - const B: u32 = 42;
18128 ˇconst C: u32 = 42;
18129
18130
18131 fn main() {
18132 println!("hello");
18133
18134 println!("world");
18135 }
18136 "#
18137 .unindent(),
18138 );
18139
18140 cx.update_editor(|editor, window, cx| {
18141 editor.delete_line(&DeleteLine, window, cx);
18142 });
18143 executor.run_until_parked();
18144 cx.assert_state_with_diff(
18145 r#"
18146 use some::mod1;
18147 use some::mod2;
18148
18149 - const A: u32 = 42;
18150 - const B: u32 = 42;
18151 - const C: u32 = 42;
18152 ˇ
18153
18154 fn main() {
18155 println!("hello");
18156
18157 println!("world");
18158 }
18159 "#
18160 .unindent(),
18161 );
18162
18163 cx.update_editor(|editor, window, cx| {
18164 editor.handle_input("replacement", window, cx);
18165 });
18166 executor.run_until_parked();
18167 cx.assert_state_with_diff(
18168 r#"
18169 use some::mod1;
18170 use some::mod2;
18171
18172 - const A: u32 = 42;
18173 - const B: u32 = 42;
18174 - const C: u32 = 42;
18175 -
18176 + replacementˇ
18177
18178 fn main() {
18179 println!("hello");
18180
18181 println!("world");
18182 }
18183 "#
18184 .unindent(),
18185 );
18186}
18187
18188#[gpui::test]
18189async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
18190 init_test(cx, |_| {});
18191
18192 let mut cx = EditorTestContext::new(cx).await;
18193
18194 let base_text = r#"
18195 one
18196 two
18197 three
18198 four
18199 five
18200 "#
18201 .unindent();
18202 executor.run_until_parked();
18203 cx.set_state(
18204 &r#"
18205 one
18206 two
18207 fˇour
18208 five
18209 "#
18210 .unindent(),
18211 );
18212
18213 cx.set_head_text(&base_text);
18214 executor.run_until_parked();
18215
18216 cx.update_editor(|editor, window, cx| {
18217 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18218 });
18219 executor.run_until_parked();
18220
18221 cx.assert_state_with_diff(
18222 r#"
18223 one
18224 two
18225 - three
18226 fˇour
18227 five
18228 "#
18229 .unindent(),
18230 );
18231
18232 cx.update_editor(|editor, window, cx| {
18233 editor.backspace(&Backspace, window, cx);
18234 editor.backspace(&Backspace, window, cx);
18235 });
18236 executor.run_until_parked();
18237 cx.assert_state_with_diff(
18238 r#"
18239 one
18240 two
18241 - threeˇ
18242 - four
18243 + our
18244 five
18245 "#
18246 .unindent(),
18247 );
18248}
18249
18250#[gpui::test]
18251async fn test_edit_after_expanded_modification_hunk(
18252 executor: BackgroundExecutor,
18253 cx: &mut TestAppContext,
18254) {
18255 init_test(cx, |_| {});
18256
18257 let mut cx = EditorTestContext::new(cx).await;
18258
18259 let diff_base = r#"
18260 use some::mod1;
18261 use some::mod2;
18262
18263 const A: u32 = 42;
18264 const B: u32 = 42;
18265 const C: u32 = 42;
18266 const D: u32 = 42;
18267
18268
18269 fn main() {
18270 println!("hello");
18271
18272 println!("world");
18273 }"#
18274 .unindent();
18275
18276 cx.set_state(
18277 &r#"
18278 use some::mod1;
18279 use some::mod2;
18280
18281 const A: u32 = 42;
18282 const B: u32 = 42;
18283 const C: u32 = 43ˇ
18284 const D: u32 = 42;
18285
18286
18287 fn main() {
18288 println!("hello");
18289
18290 println!("world");
18291 }"#
18292 .unindent(),
18293 );
18294
18295 cx.set_head_text(&diff_base);
18296 executor.run_until_parked();
18297 cx.update_editor(|editor, window, cx| {
18298 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18299 });
18300 executor.run_until_parked();
18301
18302 cx.assert_state_with_diff(
18303 r#"
18304 use some::mod1;
18305 use some::mod2;
18306
18307 const A: u32 = 42;
18308 const B: u32 = 42;
18309 - const C: u32 = 42;
18310 + const C: u32 = 43ˇ
18311 const D: u32 = 42;
18312
18313
18314 fn main() {
18315 println!("hello");
18316
18317 println!("world");
18318 }"#
18319 .unindent(),
18320 );
18321
18322 cx.update_editor(|editor, window, cx| {
18323 editor.handle_input("\nnew_line\n", window, cx);
18324 });
18325 executor.run_until_parked();
18326
18327 cx.assert_state_with_diff(
18328 r#"
18329 use some::mod1;
18330 use some::mod2;
18331
18332 const A: u32 = 42;
18333 const B: u32 = 42;
18334 - const C: u32 = 42;
18335 + const C: u32 = 43
18336 + new_line
18337 + ˇ
18338 const D: u32 = 42;
18339
18340
18341 fn main() {
18342 println!("hello");
18343
18344 println!("world");
18345 }"#
18346 .unindent(),
18347 );
18348}
18349
18350#[gpui::test]
18351async fn test_stage_and_unstage_added_file_hunk(
18352 executor: BackgroundExecutor,
18353 cx: &mut TestAppContext,
18354) {
18355 init_test(cx, |_| {});
18356
18357 let mut cx = EditorTestContext::new(cx).await;
18358 cx.update_editor(|editor, _, cx| {
18359 editor.set_expand_all_diff_hunks(cx);
18360 });
18361
18362 let working_copy = r#"
18363 ˇfn main() {
18364 println!("hello, world!");
18365 }
18366 "#
18367 .unindent();
18368
18369 cx.set_state(&working_copy);
18370 executor.run_until_parked();
18371
18372 cx.assert_state_with_diff(
18373 r#"
18374 + ˇfn main() {
18375 + println!("hello, world!");
18376 + }
18377 "#
18378 .unindent(),
18379 );
18380 cx.assert_index_text(None);
18381
18382 cx.update_editor(|editor, window, cx| {
18383 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18384 });
18385 executor.run_until_parked();
18386 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
18387 cx.assert_state_with_diff(
18388 r#"
18389 + ˇfn main() {
18390 + println!("hello, world!");
18391 + }
18392 "#
18393 .unindent(),
18394 );
18395
18396 cx.update_editor(|editor, window, cx| {
18397 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18398 });
18399 executor.run_until_parked();
18400 cx.assert_index_text(None);
18401}
18402
18403async fn setup_indent_guides_editor(
18404 text: &str,
18405 cx: &mut TestAppContext,
18406) -> (BufferId, EditorTestContext) {
18407 init_test(cx, |_| {});
18408
18409 let mut cx = EditorTestContext::new(cx).await;
18410
18411 let buffer_id = cx.update_editor(|editor, window, cx| {
18412 editor.set_text(text, window, cx);
18413 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
18414
18415 buffer_ids[0]
18416 });
18417
18418 (buffer_id, cx)
18419}
18420
18421fn assert_indent_guides(
18422 range: Range<u32>,
18423 expected: Vec<IndentGuide>,
18424 active_indices: Option<Vec<usize>>,
18425 cx: &mut EditorTestContext,
18426) {
18427 let indent_guides = cx.update_editor(|editor, window, cx| {
18428 let snapshot = editor.snapshot(window, cx).display_snapshot;
18429 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
18430 editor,
18431 MultiBufferRow(range.start)..MultiBufferRow(range.end),
18432 true,
18433 &snapshot,
18434 cx,
18435 );
18436
18437 indent_guides.sort_by(|a, b| {
18438 a.depth.cmp(&b.depth).then(
18439 a.start_row
18440 .cmp(&b.start_row)
18441 .then(a.end_row.cmp(&b.end_row)),
18442 )
18443 });
18444 indent_guides
18445 });
18446
18447 if let Some(expected) = active_indices {
18448 let active_indices = cx.update_editor(|editor, window, cx| {
18449 let snapshot = editor.snapshot(window, cx).display_snapshot;
18450 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
18451 });
18452
18453 assert_eq!(
18454 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
18455 expected,
18456 "Active indent guide indices do not match"
18457 );
18458 }
18459
18460 assert_eq!(indent_guides, expected, "Indent guides do not match");
18461}
18462
18463fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
18464 IndentGuide {
18465 buffer_id,
18466 start_row: MultiBufferRow(start_row),
18467 end_row: MultiBufferRow(end_row),
18468 depth,
18469 tab_size: 4,
18470 settings: IndentGuideSettings {
18471 enabled: true,
18472 line_width: 1,
18473 active_line_width: 1,
18474 ..Default::default()
18475 },
18476 }
18477}
18478
18479#[gpui::test]
18480async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
18481 let (buffer_id, mut cx) = setup_indent_guides_editor(
18482 &"
18483 fn main() {
18484 let a = 1;
18485 }"
18486 .unindent(),
18487 cx,
18488 )
18489 .await;
18490
18491 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
18492}
18493
18494#[gpui::test]
18495async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
18496 let (buffer_id, mut cx) = setup_indent_guides_editor(
18497 &"
18498 fn main() {
18499 let a = 1;
18500 let b = 2;
18501 }"
18502 .unindent(),
18503 cx,
18504 )
18505 .await;
18506
18507 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
18508}
18509
18510#[gpui::test]
18511async fn test_indent_guide_nested(cx: &mut TestAppContext) {
18512 let (buffer_id, mut cx) = setup_indent_guides_editor(
18513 &"
18514 fn main() {
18515 let a = 1;
18516 if a == 3 {
18517 let b = 2;
18518 } else {
18519 let c = 3;
18520 }
18521 }"
18522 .unindent(),
18523 cx,
18524 )
18525 .await;
18526
18527 assert_indent_guides(
18528 0..8,
18529 vec![
18530 indent_guide(buffer_id, 1, 6, 0),
18531 indent_guide(buffer_id, 3, 3, 1),
18532 indent_guide(buffer_id, 5, 5, 1),
18533 ],
18534 None,
18535 &mut cx,
18536 );
18537}
18538
18539#[gpui::test]
18540async fn test_indent_guide_tab(cx: &mut TestAppContext) {
18541 let (buffer_id, mut cx) = setup_indent_guides_editor(
18542 &"
18543 fn main() {
18544 let a = 1;
18545 let b = 2;
18546 let c = 3;
18547 }"
18548 .unindent(),
18549 cx,
18550 )
18551 .await;
18552
18553 assert_indent_guides(
18554 0..5,
18555 vec![
18556 indent_guide(buffer_id, 1, 3, 0),
18557 indent_guide(buffer_id, 2, 2, 1),
18558 ],
18559 None,
18560 &mut cx,
18561 );
18562}
18563
18564#[gpui::test]
18565async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
18566 let (buffer_id, mut cx) = setup_indent_guides_editor(
18567 &"
18568 fn main() {
18569 let a = 1;
18570
18571 let c = 3;
18572 }"
18573 .unindent(),
18574 cx,
18575 )
18576 .await;
18577
18578 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
18579}
18580
18581#[gpui::test]
18582async fn test_indent_guide_complex(cx: &mut TestAppContext) {
18583 let (buffer_id, mut cx) = setup_indent_guides_editor(
18584 &"
18585 fn main() {
18586 let a = 1;
18587
18588 let c = 3;
18589
18590 if a == 3 {
18591 let b = 2;
18592 } else {
18593 let c = 3;
18594 }
18595 }"
18596 .unindent(),
18597 cx,
18598 )
18599 .await;
18600
18601 assert_indent_guides(
18602 0..11,
18603 vec![
18604 indent_guide(buffer_id, 1, 9, 0),
18605 indent_guide(buffer_id, 6, 6, 1),
18606 indent_guide(buffer_id, 8, 8, 1),
18607 ],
18608 None,
18609 &mut cx,
18610 );
18611}
18612
18613#[gpui::test]
18614async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
18615 let (buffer_id, mut cx) = setup_indent_guides_editor(
18616 &"
18617 fn main() {
18618 let a = 1;
18619
18620 let c = 3;
18621
18622 if a == 3 {
18623 let b = 2;
18624 } else {
18625 let c = 3;
18626 }
18627 }"
18628 .unindent(),
18629 cx,
18630 )
18631 .await;
18632
18633 assert_indent_guides(
18634 1..11,
18635 vec![
18636 indent_guide(buffer_id, 1, 9, 0),
18637 indent_guide(buffer_id, 6, 6, 1),
18638 indent_guide(buffer_id, 8, 8, 1),
18639 ],
18640 None,
18641 &mut cx,
18642 );
18643}
18644
18645#[gpui::test]
18646async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
18647 let (buffer_id, mut cx) = setup_indent_guides_editor(
18648 &"
18649 fn main() {
18650 let a = 1;
18651
18652 let c = 3;
18653
18654 if a == 3 {
18655 let b = 2;
18656 } else {
18657 let c = 3;
18658 }
18659 }"
18660 .unindent(),
18661 cx,
18662 )
18663 .await;
18664
18665 assert_indent_guides(
18666 1..10,
18667 vec![
18668 indent_guide(buffer_id, 1, 9, 0),
18669 indent_guide(buffer_id, 6, 6, 1),
18670 indent_guide(buffer_id, 8, 8, 1),
18671 ],
18672 None,
18673 &mut cx,
18674 );
18675}
18676
18677#[gpui::test]
18678async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
18679 let (buffer_id, mut cx) = setup_indent_guides_editor(
18680 &"
18681 fn main() {
18682 if a {
18683 b(
18684 c,
18685 d,
18686 )
18687 } else {
18688 e(
18689 f
18690 )
18691 }
18692 }"
18693 .unindent(),
18694 cx,
18695 )
18696 .await;
18697
18698 assert_indent_guides(
18699 0..11,
18700 vec![
18701 indent_guide(buffer_id, 1, 10, 0),
18702 indent_guide(buffer_id, 2, 5, 1),
18703 indent_guide(buffer_id, 7, 9, 1),
18704 indent_guide(buffer_id, 3, 4, 2),
18705 indent_guide(buffer_id, 8, 8, 2),
18706 ],
18707 None,
18708 &mut cx,
18709 );
18710
18711 cx.update_editor(|editor, window, cx| {
18712 editor.fold_at(MultiBufferRow(2), window, cx);
18713 assert_eq!(
18714 editor.display_text(cx),
18715 "
18716 fn main() {
18717 if a {
18718 b(⋯
18719 )
18720 } else {
18721 e(
18722 f
18723 )
18724 }
18725 }"
18726 .unindent()
18727 );
18728 });
18729
18730 assert_indent_guides(
18731 0..11,
18732 vec![
18733 indent_guide(buffer_id, 1, 10, 0),
18734 indent_guide(buffer_id, 2, 5, 1),
18735 indent_guide(buffer_id, 7, 9, 1),
18736 indent_guide(buffer_id, 8, 8, 2),
18737 ],
18738 None,
18739 &mut cx,
18740 );
18741}
18742
18743#[gpui::test]
18744async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
18745 let (buffer_id, mut cx) = setup_indent_guides_editor(
18746 &"
18747 block1
18748 block2
18749 block3
18750 block4
18751 block2
18752 block1
18753 block1"
18754 .unindent(),
18755 cx,
18756 )
18757 .await;
18758
18759 assert_indent_guides(
18760 1..10,
18761 vec![
18762 indent_guide(buffer_id, 1, 4, 0),
18763 indent_guide(buffer_id, 2, 3, 1),
18764 indent_guide(buffer_id, 3, 3, 2),
18765 ],
18766 None,
18767 &mut cx,
18768 );
18769}
18770
18771#[gpui::test]
18772async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
18773 let (buffer_id, mut cx) = setup_indent_guides_editor(
18774 &"
18775 block1
18776 block2
18777 block3
18778
18779 block1
18780 block1"
18781 .unindent(),
18782 cx,
18783 )
18784 .await;
18785
18786 assert_indent_guides(
18787 0..6,
18788 vec![
18789 indent_guide(buffer_id, 1, 2, 0),
18790 indent_guide(buffer_id, 2, 2, 1),
18791 ],
18792 None,
18793 &mut cx,
18794 );
18795}
18796
18797#[gpui::test]
18798async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
18799 let (buffer_id, mut cx) = setup_indent_guides_editor(
18800 &"
18801 function component() {
18802 \treturn (
18803 \t\t\t
18804 \t\t<div>
18805 \t\t\t<abc></abc>
18806 \t\t</div>
18807 \t)
18808 }"
18809 .unindent(),
18810 cx,
18811 )
18812 .await;
18813
18814 assert_indent_guides(
18815 0..8,
18816 vec![
18817 indent_guide(buffer_id, 1, 6, 0),
18818 indent_guide(buffer_id, 2, 5, 1),
18819 indent_guide(buffer_id, 4, 4, 2),
18820 ],
18821 None,
18822 &mut cx,
18823 );
18824}
18825
18826#[gpui::test]
18827async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
18828 let (buffer_id, mut cx) = setup_indent_guides_editor(
18829 &"
18830 function component() {
18831 \treturn (
18832 \t
18833 \t\t<div>
18834 \t\t\t<abc></abc>
18835 \t\t</div>
18836 \t)
18837 }"
18838 .unindent(),
18839 cx,
18840 )
18841 .await;
18842
18843 assert_indent_guides(
18844 0..8,
18845 vec![
18846 indent_guide(buffer_id, 1, 6, 0),
18847 indent_guide(buffer_id, 2, 5, 1),
18848 indent_guide(buffer_id, 4, 4, 2),
18849 ],
18850 None,
18851 &mut cx,
18852 );
18853}
18854
18855#[gpui::test]
18856async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
18857 let (buffer_id, mut cx) = setup_indent_guides_editor(
18858 &"
18859 block1
18860
18861
18862
18863 block2
18864 "
18865 .unindent(),
18866 cx,
18867 )
18868 .await;
18869
18870 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
18871}
18872
18873#[gpui::test]
18874async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
18875 let (buffer_id, mut cx) = setup_indent_guides_editor(
18876 &"
18877 def a:
18878 \tb = 3
18879 \tif True:
18880 \t\tc = 4
18881 \t\td = 5
18882 \tprint(b)
18883 "
18884 .unindent(),
18885 cx,
18886 )
18887 .await;
18888
18889 assert_indent_guides(
18890 0..6,
18891 vec![
18892 indent_guide(buffer_id, 1, 5, 0),
18893 indent_guide(buffer_id, 3, 4, 1),
18894 ],
18895 None,
18896 &mut cx,
18897 );
18898}
18899
18900#[gpui::test]
18901async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
18902 let (buffer_id, mut cx) = setup_indent_guides_editor(
18903 &"
18904 fn main() {
18905 let a = 1;
18906 }"
18907 .unindent(),
18908 cx,
18909 )
18910 .await;
18911
18912 cx.update_editor(|editor, window, cx| {
18913 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18914 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18915 });
18916 });
18917
18918 assert_indent_guides(
18919 0..3,
18920 vec![indent_guide(buffer_id, 1, 1, 0)],
18921 Some(vec![0]),
18922 &mut cx,
18923 );
18924}
18925
18926#[gpui::test]
18927async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
18928 let (buffer_id, mut cx) = setup_indent_guides_editor(
18929 &"
18930 fn main() {
18931 if 1 == 2 {
18932 let a = 1;
18933 }
18934 }"
18935 .unindent(),
18936 cx,
18937 )
18938 .await;
18939
18940 cx.update_editor(|editor, window, cx| {
18941 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18942 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18943 });
18944 });
18945
18946 assert_indent_guides(
18947 0..4,
18948 vec![
18949 indent_guide(buffer_id, 1, 3, 0),
18950 indent_guide(buffer_id, 2, 2, 1),
18951 ],
18952 Some(vec![1]),
18953 &mut cx,
18954 );
18955
18956 cx.update_editor(|editor, window, cx| {
18957 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18958 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
18959 });
18960 });
18961
18962 assert_indent_guides(
18963 0..4,
18964 vec![
18965 indent_guide(buffer_id, 1, 3, 0),
18966 indent_guide(buffer_id, 2, 2, 1),
18967 ],
18968 Some(vec![1]),
18969 &mut cx,
18970 );
18971
18972 cx.update_editor(|editor, window, cx| {
18973 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18974 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
18975 });
18976 });
18977
18978 assert_indent_guides(
18979 0..4,
18980 vec![
18981 indent_guide(buffer_id, 1, 3, 0),
18982 indent_guide(buffer_id, 2, 2, 1),
18983 ],
18984 Some(vec![0]),
18985 &mut cx,
18986 );
18987}
18988
18989#[gpui::test]
18990async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
18991 let (buffer_id, mut cx) = setup_indent_guides_editor(
18992 &"
18993 fn main() {
18994 let a = 1;
18995
18996 let b = 2;
18997 }"
18998 .unindent(),
18999 cx,
19000 )
19001 .await;
19002
19003 cx.update_editor(|editor, window, cx| {
19004 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19005 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
19006 });
19007 });
19008
19009 assert_indent_guides(
19010 0..5,
19011 vec![indent_guide(buffer_id, 1, 3, 0)],
19012 Some(vec![0]),
19013 &mut cx,
19014 );
19015}
19016
19017#[gpui::test]
19018async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
19019 let (buffer_id, mut cx) = setup_indent_guides_editor(
19020 &"
19021 def m:
19022 a = 1
19023 pass"
19024 .unindent(),
19025 cx,
19026 )
19027 .await;
19028
19029 cx.update_editor(|editor, window, cx| {
19030 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19031 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
19032 });
19033 });
19034
19035 assert_indent_guides(
19036 0..3,
19037 vec![indent_guide(buffer_id, 1, 2, 0)],
19038 Some(vec![0]),
19039 &mut cx,
19040 );
19041}
19042
19043#[gpui::test]
19044async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
19045 init_test(cx, |_| {});
19046 let mut cx = EditorTestContext::new(cx).await;
19047 let text = indoc! {
19048 "
19049 impl A {
19050 fn b() {
19051 0;
19052 3;
19053 5;
19054 6;
19055 7;
19056 }
19057 }
19058 "
19059 };
19060 let base_text = indoc! {
19061 "
19062 impl A {
19063 fn b() {
19064 0;
19065 1;
19066 2;
19067 3;
19068 4;
19069 }
19070 fn c() {
19071 5;
19072 6;
19073 7;
19074 }
19075 }
19076 "
19077 };
19078
19079 cx.update_editor(|editor, window, cx| {
19080 editor.set_text(text, window, cx);
19081
19082 editor.buffer().update(cx, |multibuffer, cx| {
19083 let buffer = multibuffer.as_singleton().unwrap();
19084 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
19085
19086 multibuffer.set_all_diff_hunks_expanded(cx);
19087 multibuffer.add_diff(diff, cx);
19088
19089 buffer.read(cx).remote_id()
19090 })
19091 });
19092 cx.run_until_parked();
19093
19094 cx.assert_state_with_diff(
19095 indoc! { "
19096 impl A {
19097 fn b() {
19098 0;
19099 - 1;
19100 - 2;
19101 3;
19102 - 4;
19103 - }
19104 - fn c() {
19105 5;
19106 6;
19107 7;
19108 }
19109 }
19110 ˇ"
19111 }
19112 .to_string(),
19113 );
19114
19115 let mut actual_guides = cx.update_editor(|editor, window, cx| {
19116 editor
19117 .snapshot(window, cx)
19118 .buffer_snapshot
19119 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
19120 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
19121 .collect::<Vec<_>>()
19122 });
19123 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
19124 assert_eq!(
19125 actual_guides,
19126 vec![
19127 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
19128 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
19129 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
19130 ]
19131 );
19132}
19133
19134#[gpui::test]
19135async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19136 init_test(cx, |_| {});
19137 let mut cx = EditorTestContext::new(cx).await;
19138
19139 let diff_base = r#"
19140 a
19141 b
19142 c
19143 "#
19144 .unindent();
19145
19146 cx.set_state(
19147 &r#"
19148 ˇA
19149 b
19150 C
19151 "#
19152 .unindent(),
19153 );
19154 cx.set_head_text(&diff_base);
19155 cx.update_editor(|editor, window, cx| {
19156 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19157 });
19158 executor.run_until_parked();
19159
19160 let both_hunks_expanded = r#"
19161 - a
19162 + ˇA
19163 b
19164 - c
19165 + C
19166 "#
19167 .unindent();
19168
19169 cx.assert_state_with_diff(both_hunks_expanded.clone());
19170
19171 let hunk_ranges = cx.update_editor(|editor, window, cx| {
19172 let snapshot = editor.snapshot(window, cx);
19173 let hunks = editor
19174 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19175 .collect::<Vec<_>>();
19176 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
19177 let buffer_id = hunks[0].buffer_id;
19178 hunks
19179 .into_iter()
19180 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
19181 .collect::<Vec<_>>()
19182 });
19183 assert_eq!(hunk_ranges.len(), 2);
19184
19185 cx.update_editor(|editor, _, cx| {
19186 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
19187 });
19188 executor.run_until_parked();
19189
19190 let second_hunk_expanded = r#"
19191 ˇA
19192 b
19193 - c
19194 + C
19195 "#
19196 .unindent();
19197
19198 cx.assert_state_with_diff(second_hunk_expanded);
19199
19200 cx.update_editor(|editor, _, cx| {
19201 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
19202 });
19203 executor.run_until_parked();
19204
19205 cx.assert_state_with_diff(both_hunks_expanded.clone());
19206
19207 cx.update_editor(|editor, _, cx| {
19208 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
19209 });
19210 executor.run_until_parked();
19211
19212 let first_hunk_expanded = r#"
19213 - a
19214 + ˇA
19215 b
19216 C
19217 "#
19218 .unindent();
19219
19220 cx.assert_state_with_diff(first_hunk_expanded);
19221
19222 cx.update_editor(|editor, _, cx| {
19223 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
19224 });
19225 executor.run_until_parked();
19226
19227 cx.assert_state_with_diff(both_hunks_expanded);
19228
19229 cx.set_state(
19230 &r#"
19231 ˇA
19232 b
19233 "#
19234 .unindent(),
19235 );
19236 cx.run_until_parked();
19237
19238 // TODO this cursor position seems bad
19239 cx.assert_state_with_diff(
19240 r#"
19241 - ˇa
19242 + A
19243 b
19244 "#
19245 .unindent(),
19246 );
19247
19248 cx.update_editor(|editor, window, cx| {
19249 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19250 });
19251
19252 cx.assert_state_with_diff(
19253 r#"
19254 - ˇa
19255 + A
19256 b
19257 - c
19258 "#
19259 .unindent(),
19260 );
19261
19262 let hunk_ranges = cx.update_editor(|editor, window, cx| {
19263 let snapshot = editor.snapshot(window, cx);
19264 let hunks = editor
19265 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19266 .collect::<Vec<_>>();
19267 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
19268 let buffer_id = hunks[0].buffer_id;
19269 hunks
19270 .into_iter()
19271 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
19272 .collect::<Vec<_>>()
19273 });
19274 assert_eq!(hunk_ranges.len(), 2);
19275
19276 cx.update_editor(|editor, _, cx| {
19277 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
19278 });
19279 executor.run_until_parked();
19280
19281 cx.assert_state_with_diff(
19282 r#"
19283 - ˇa
19284 + A
19285 b
19286 "#
19287 .unindent(),
19288 );
19289}
19290
19291#[gpui::test]
19292async fn test_toggle_deletion_hunk_at_start_of_file(
19293 executor: BackgroundExecutor,
19294 cx: &mut TestAppContext,
19295) {
19296 init_test(cx, |_| {});
19297 let mut cx = EditorTestContext::new(cx).await;
19298
19299 let diff_base = r#"
19300 a
19301 b
19302 c
19303 "#
19304 .unindent();
19305
19306 cx.set_state(
19307 &r#"
19308 ˇb
19309 c
19310 "#
19311 .unindent(),
19312 );
19313 cx.set_head_text(&diff_base);
19314 cx.update_editor(|editor, window, cx| {
19315 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19316 });
19317 executor.run_until_parked();
19318
19319 let hunk_expanded = r#"
19320 - a
19321 ˇb
19322 c
19323 "#
19324 .unindent();
19325
19326 cx.assert_state_with_diff(hunk_expanded.clone());
19327
19328 let hunk_ranges = cx.update_editor(|editor, window, cx| {
19329 let snapshot = editor.snapshot(window, cx);
19330 let hunks = editor
19331 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19332 .collect::<Vec<_>>();
19333 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
19334 let buffer_id = hunks[0].buffer_id;
19335 hunks
19336 .into_iter()
19337 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
19338 .collect::<Vec<_>>()
19339 });
19340 assert_eq!(hunk_ranges.len(), 1);
19341
19342 cx.update_editor(|editor, _, cx| {
19343 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
19344 });
19345 executor.run_until_parked();
19346
19347 let hunk_collapsed = r#"
19348 ˇb
19349 c
19350 "#
19351 .unindent();
19352
19353 cx.assert_state_with_diff(hunk_collapsed);
19354
19355 cx.update_editor(|editor, _, cx| {
19356 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
19357 });
19358 executor.run_until_parked();
19359
19360 cx.assert_state_with_diff(hunk_expanded.clone());
19361}
19362
19363#[gpui::test]
19364async fn test_display_diff_hunks(cx: &mut TestAppContext) {
19365 init_test(cx, |_| {});
19366
19367 let fs = FakeFs::new(cx.executor());
19368 fs.insert_tree(
19369 path!("/test"),
19370 json!({
19371 ".git": {},
19372 "file-1": "ONE\n",
19373 "file-2": "TWO\n",
19374 "file-3": "THREE\n",
19375 }),
19376 )
19377 .await;
19378
19379 fs.set_head_for_repo(
19380 path!("/test/.git").as_ref(),
19381 &[
19382 ("file-1".into(), "one\n".into()),
19383 ("file-2".into(), "two\n".into()),
19384 ("file-3".into(), "three\n".into()),
19385 ],
19386 "deadbeef",
19387 );
19388
19389 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
19390 let mut buffers = vec![];
19391 for i in 1..=3 {
19392 let buffer = project
19393 .update(cx, |project, cx| {
19394 let path = format!(path!("/test/file-{}"), i);
19395 project.open_local_buffer(path, cx)
19396 })
19397 .await
19398 .unwrap();
19399 buffers.push(buffer);
19400 }
19401
19402 let multibuffer = cx.new(|cx| {
19403 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
19404 multibuffer.set_all_diff_hunks_expanded(cx);
19405 for buffer in &buffers {
19406 let snapshot = buffer.read(cx).snapshot();
19407 multibuffer.set_excerpts_for_path(
19408 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
19409 buffer.clone(),
19410 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
19411 DEFAULT_MULTIBUFFER_CONTEXT,
19412 cx,
19413 );
19414 }
19415 multibuffer
19416 });
19417
19418 let editor = cx.add_window(|window, cx| {
19419 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
19420 });
19421 cx.run_until_parked();
19422
19423 let snapshot = editor
19424 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19425 .unwrap();
19426 let hunks = snapshot
19427 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
19428 .map(|hunk| match hunk {
19429 DisplayDiffHunk::Unfolded {
19430 display_row_range, ..
19431 } => display_row_range,
19432 DisplayDiffHunk::Folded { .. } => unreachable!(),
19433 })
19434 .collect::<Vec<_>>();
19435 assert_eq!(
19436 hunks,
19437 [
19438 DisplayRow(2)..DisplayRow(4),
19439 DisplayRow(7)..DisplayRow(9),
19440 DisplayRow(12)..DisplayRow(14),
19441 ]
19442 );
19443}
19444
19445#[gpui::test]
19446async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
19447 init_test(cx, |_| {});
19448
19449 let mut cx = EditorTestContext::new(cx).await;
19450 cx.set_head_text(indoc! { "
19451 one
19452 two
19453 three
19454 four
19455 five
19456 "
19457 });
19458 cx.set_index_text(indoc! { "
19459 one
19460 two
19461 three
19462 four
19463 five
19464 "
19465 });
19466 cx.set_state(indoc! {"
19467 one
19468 TWO
19469 ˇTHREE
19470 FOUR
19471 five
19472 "});
19473 cx.run_until_parked();
19474 cx.update_editor(|editor, window, cx| {
19475 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19476 });
19477 cx.run_until_parked();
19478 cx.assert_index_text(Some(indoc! {"
19479 one
19480 TWO
19481 THREE
19482 FOUR
19483 five
19484 "}));
19485 cx.set_state(indoc! { "
19486 one
19487 TWO
19488 ˇTHREE-HUNDRED
19489 FOUR
19490 five
19491 "});
19492 cx.run_until_parked();
19493 cx.update_editor(|editor, window, cx| {
19494 let snapshot = editor.snapshot(window, cx);
19495 let hunks = editor
19496 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19497 .collect::<Vec<_>>();
19498 assert_eq!(hunks.len(), 1);
19499 assert_eq!(
19500 hunks[0].status(),
19501 DiffHunkStatus {
19502 kind: DiffHunkStatusKind::Modified,
19503 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
19504 }
19505 );
19506
19507 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19508 });
19509 cx.run_until_parked();
19510 cx.assert_index_text(Some(indoc! {"
19511 one
19512 TWO
19513 THREE-HUNDRED
19514 FOUR
19515 five
19516 "}));
19517}
19518
19519#[gpui::test]
19520fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
19521 init_test(cx, |_| {});
19522
19523 let editor = cx.add_window(|window, cx| {
19524 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
19525 build_editor(buffer, window, cx)
19526 });
19527
19528 let render_args = Arc::new(Mutex::new(None));
19529 let snapshot = editor
19530 .update(cx, |editor, window, cx| {
19531 let snapshot = editor.buffer().read(cx).snapshot(cx);
19532 let range =
19533 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
19534
19535 struct RenderArgs {
19536 row: MultiBufferRow,
19537 folded: bool,
19538 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
19539 }
19540
19541 let crease = Crease::inline(
19542 range,
19543 FoldPlaceholder::test(),
19544 {
19545 let toggle_callback = render_args.clone();
19546 move |row, folded, callback, _window, _cx| {
19547 *toggle_callback.lock() = Some(RenderArgs {
19548 row,
19549 folded,
19550 callback,
19551 });
19552 div()
19553 }
19554 },
19555 |_row, _folded, _window, _cx| div(),
19556 );
19557
19558 editor.insert_creases(Some(crease), cx);
19559 let snapshot = editor.snapshot(window, cx);
19560 let _div = snapshot.render_crease_toggle(
19561 MultiBufferRow(1),
19562 false,
19563 cx.entity().clone(),
19564 window,
19565 cx,
19566 );
19567 snapshot
19568 })
19569 .unwrap();
19570
19571 let render_args = render_args.lock().take().unwrap();
19572 assert_eq!(render_args.row, MultiBufferRow(1));
19573 assert!(!render_args.folded);
19574 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
19575
19576 cx.update_window(*editor, |_, window, cx| {
19577 (render_args.callback)(true, window, cx)
19578 })
19579 .unwrap();
19580 let snapshot = editor
19581 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19582 .unwrap();
19583 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
19584
19585 cx.update_window(*editor, |_, window, cx| {
19586 (render_args.callback)(false, window, cx)
19587 })
19588 .unwrap();
19589 let snapshot = editor
19590 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19591 .unwrap();
19592 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
19593}
19594
19595#[gpui::test]
19596async fn test_input_text(cx: &mut TestAppContext) {
19597 init_test(cx, |_| {});
19598 let mut cx = EditorTestContext::new(cx).await;
19599
19600 cx.set_state(
19601 &r#"ˇone
19602 two
19603
19604 three
19605 fourˇ
19606 five
19607
19608 siˇx"#
19609 .unindent(),
19610 );
19611
19612 cx.dispatch_action(HandleInput(String::new()));
19613 cx.assert_editor_state(
19614 &r#"ˇone
19615 two
19616
19617 three
19618 fourˇ
19619 five
19620
19621 siˇx"#
19622 .unindent(),
19623 );
19624
19625 cx.dispatch_action(HandleInput("AAAA".to_string()));
19626 cx.assert_editor_state(
19627 &r#"AAAAˇone
19628 two
19629
19630 three
19631 fourAAAAˇ
19632 five
19633
19634 siAAAAˇx"#
19635 .unindent(),
19636 );
19637}
19638
19639#[gpui::test]
19640async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
19641 init_test(cx, |_| {});
19642
19643 let mut cx = EditorTestContext::new(cx).await;
19644 cx.set_state(
19645 r#"let foo = 1;
19646let foo = 2;
19647let foo = 3;
19648let fooˇ = 4;
19649let foo = 5;
19650let foo = 6;
19651let foo = 7;
19652let foo = 8;
19653let foo = 9;
19654let foo = 10;
19655let foo = 11;
19656let foo = 12;
19657let foo = 13;
19658let foo = 14;
19659let foo = 15;"#,
19660 );
19661
19662 cx.update_editor(|e, window, cx| {
19663 assert_eq!(
19664 e.next_scroll_position,
19665 NextScrollCursorCenterTopBottom::Center,
19666 "Default next scroll direction is center",
19667 );
19668
19669 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19670 assert_eq!(
19671 e.next_scroll_position,
19672 NextScrollCursorCenterTopBottom::Top,
19673 "After center, next scroll direction should be top",
19674 );
19675
19676 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19677 assert_eq!(
19678 e.next_scroll_position,
19679 NextScrollCursorCenterTopBottom::Bottom,
19680 "After top, next scroll direction should be bottom",
19681 );
19682
19683 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19684 assert_eq!(
19685 e.next_scroll_position,
19686 NextScrollCursorCenterTopBottom::Center,
19687 "After bottom, scrolling should start over",
19688 );
19689
19690 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19691 assert_eq!(
19692 e.next_scroll_position,
19693 NextScrollCursorCenterTopBottom::Top,
19694 "Scrolling continues if retriggered fast enough"
19695 );
19696 });
19697
19698 cx.executor()
19699 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
19700 cx.executor().run_until_parked();
19701 cx.update_editor(|e, _, _| {
19702 assert_eq!(
19703 e.next_scroll_position,
19704 NextScrollCursorCenterTopBottom::Center,
19705 "If scrolling is not triggered fast enough, it should reset"
19706 );
19707 });
19708}
19709
19710#[gpui::test]
19711async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
19712 init_test(cx, |_| {});
19713 let mut cx = EditorLspTestContext::new_rust(
19714 lsp::ServerCapabilities {
19715 definition_provider: Some(lsp::OneOf::Left(true)),
19716 references_provider: Some(lsp::OneOf::Left(true)),
19717 ..lsp::ServerCapabilities::default()
19718 },
19719 cx,
19720 )
19721 .await;
19722
19723 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
19724 let go_to_definition = cx
19725 .lsp
19726 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
19727 move |params, _| async move {
19728 if empty_go_to_definition {
19729 Ok(None)
19730 } else {
19731 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
19732 uri: params.text_document_position_params.text_document.uri,
19733 range: lsp::Range::new(
19734 lsp::Position::new(4, 3),
19735 lsp::Position::new(4, 6),
19736 ),
19737 })))
19738 }
19739 },
19740 );
19741 let references = cx
19742 .lsp
19743 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
19744 Ok(Some(vec![lsp::Location {
19745 uri: params.text_document_position.text_document.uri,
19746 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
19747 }]))
19748 });
19749 (go_to_definition, references)
19750 };
19751
19752 cx.set_state(
19753 &r#"fn one() {
19754 let mut a = ˇtwo();
19755 }
19756
19757 fn two() {}"#
19758 .unindent(),
19759 );
19760 set_up_lsp_handlers(false, &mut cx);
19761 let navigated = cx
19762 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19763 .await
19764 .expect("Failed to navigate to definition");
19765 assert_eq!(
19766 navigated,
19767 Navigated::Yes,
19768 "Should have navigated to definition from the GetDefinition response"
19769 );
19770 cx.assert_editor_state(
19771 &r#"fn one() {
19772 let mut a = two();
19773 }
19774
19775 fn «twoˇ»() {}"#
19776 .unindent(),
19777 );
19778
19779 let editors = cx.update_workspace(|workspace, _, cx| {
19780 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19781 });
19782 cx.update_editor(|_, _, test_editor_cx| {
19783 assert_eq!(
19784 editors.len(),
19785 1,
19786 "Initially, only one, test, editor should be open in the workspace"
19787 );
19788 assert_eq!(
19789 test_editor_cx.entity(),
19790 editors.last().expect("Asserted len is 1").clone()
19791 );
19792 });
19793
19794 set_up_lsp_handlers(true, &mut cx);
19795 let navigated = cx
19796 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19797 .await
19798 .expect("Failed to navigate to lookup references");
19799 assert_eq!(
19800 navigated,
19801 Navigated::Yes,
19802 "Should have navigated to references as a fallback after empty GoToDefinition response"
19803 );
19804 // We should not change the selections in the existing file,
19805 // if opening another milti buffer with the references
19806 cx.assert_editor_state(
19807 &r#"fn one() {
19808 let mut a = two();
19809 }
19810
19811 fn «twoˇ»() {}"#
19812 .unindent(),
19813 );
19814 let editors = cx.update_workspace(|workspace, _, cx| {
19815 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19816 });
19817 cx.update_editor(|_, _, test_editor_cx| {
19818 assert_eq!(
19819 editors.len(),
19820 2,
19821 "After falling back to references search, we open a new editor with the results"
19822 );
19823 let references_fallback_text = editors
19824 .into_iter()
19825 .find(|new_editor| *new_editor != test_editor_cx.entity())
19826 .expect("Should have one non-test editor now")
19827 .read(test_editor_cx)
19828 .text(test_editor_cx);
19829 assert_eq!(
19830 references_fallback_text, "fn one() {\n let mut a = two();\n}",
19831 "Should use the range from the references response and not the GoToDefinition one"
19832 );
19833 });
19834}
19835
19836#[gpui::test]
19837async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
19838 init_test(cx, |_| {});
19839 cx.update(|cx| {
19840 let mut editor_settings = EditorSettings::get_global(cx).clone();
19841 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
19842 EditorSettings::override_global(editor_settings, cx);
19843 });
19844 let mut cx = EditorLspTestContext::new_rust(
19845 lsp::ServerCapabilities {
19846 definition_provider: Some(lsp::OneOf::Left(true)),
19847 references_provider: Some(lsp::OneOf::Left(true)),
19848 ..lsp::ServerCapabilities::default()
19849 },
19850 cx,
19851 )
19852 .await;
19853 let original_state = r#"fn one() {
19854 let mut a = ˇtwo();
19855 }
19856
19857 fn two() {}"#
19858 .unindent();
19859 cx.set_state(&original_state);
19860
19861 let mut go_to_definition = cx
19862 .lsp
19863 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
19864 move |_, _| async move { Ok(None) },
19865 );
19866 let _references = cx
19867 .lsp
19868 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
19869 panic!("Should not call for references with no go to definition fallback")
19870 });
19871
19872 let navigated = cx
19873 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19874 .await
19875 .expect("Failed to navigate to lookup references");
19876 go_to_definition
19877 .next()
19878 .await
19879 .expect("Should have called the go_to_definition handler");
19880
19881 assert_eq!(
19882 navigated,
19883 Navigated::No,
19884 "Should have navigated to references as a fallback after empty GoToDefinition response"
19885 );
19886 cx.assert_editor_state(&original_state);
19887 let editors = cx.update_workspace(|workspace, _, cx| {
19888 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19889 });
19890 cx.update_editor(|_, _, _| {
19891 assert_eq!(
19892 editors.len(),
19893 1,
19894 "After unsuccessful fallback, no other editor should have been opened"
19895 );
19896 });
19897}
19898
19899#[gpui::test]
19900async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
19901 init_test(cx, |_| {});
19902
19903 let language = Arc::new(Language::new(
19904 LanguageConfig::default(),
19905 Some(tree_sitter_rust::LANGUAGE.into()),
19906 ));
19907
19908 let text = r#"
19909 #[cfg(test)]
19910 mod tests() {
19911 #[test]
19912 fn runnable_1() {
19913 let a = 1;
19914 }
19915
19916 #[test]
19917 fn runnable_2() {
19918 let a = 1;
19919 let b = 2;
19920 }
19921 }
19922 "#
19923 .unindent();
19924
19925 let fs = FakeFs::new(cx.executor());
19926 fs.insert_file("/file.rs", Default::default()).await;
19927
19928 let project = Project::test(fs, ["/a".as_ref()], cx).await;
19929 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19930 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19931 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
19932 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
19933
19934 let editor = cx.new_window_entity(|window, cx| {
19935 Editor::new(
19936 EditorMode::full(),
19937 multi_buffer,
19938 Some(project.clone()),
19939 window,
19940 cx,
19941 )
19942 });
19943
19944 editor.update_in(cx, |editor, window, cx| {
19945 let snapshot = editor.buffer().read(cx).snapshot(cx);
19946 editor.tasks.insert(
19947 (buffer.read(cx).remote_id(), 3),
19948 RunnableTasks {
19949 templates: vec![],
19950 offset: snapshot.anchor_before(43),
19951 column: 0,
19952 extra_variables: HashMap::default(),
19953 context_range: BufferOffset(43)..BufferOffset(85),
19954 },
19955 );
19956 editor.tasks.insert(
19957 (buffer.read(cx).remote_id(), 8),
19958 RunnableTasks {
19959 templates: vec![],
19960 offset: snapshot.anchor_before(86),
19961 column: 0,
19962 extra_variables: HashMap::default(),
19963 context_range: BufferOffset(86)..BufferOffset(191),
19964 },
19965 );
19966
19967 // Test finding task when cursor is inside function body
19968 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19969 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
19970 });
19971 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
19972 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
19973
19974 // Test finding task when cursor is on function name
19975 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19976 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
19977 });
19978 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
19979 assert_eq!(row, 8, "Should find task when cursor is on function name");
19980 });
19981}
19982
19983#[gpui::test]
19984async fn test_folding_buffers(cx: &mut TestAppContext) {
19985 init_test(cx, |_| {});
19986
19987 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
19988 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
19989 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
19990
19991 let fs = FakeFs::new(cx.executor());
19992 fs.insert_tree(
19993 path!("/a"),
19994 json!({
19995 "first.rs": sample_text_1,
19996 "second.rs": sample_text_2,
19997 "third.rs": sample_text_3,
19998 }),
19999 )
20000 .await;
20001 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20002 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20003 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20004 let worktree = project.update(cx, |project, cx| {
20005 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
20006 assert_eq!(worktrees.len(), 1);
20007 worktrees.pop().unwrap()
20008 });
20009 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
20010
20011 let buffer_1 = project
20012 .update(cx, |project, cx| {
20013 project.open_buffer((worktree_id, "first.rs"), cx)
20014 })
20015 .await
20016 .unwrap();
20017 let buffer_2 = project
20018 .update(cx, |project, cx| {
20019 project.open_buffer((worktree_id, "second.rs"), cx)
20020 })
20021 .await
20022 .unwrap();
20023 let buffer_3 = project
20024 .update(cx, |project, cx| {
20025 project.open_buffer((worktree_id, "third.rs"), cx)
20026 })
20027 .await
20028 .unwrap();
20029
20030 let multi_buffer = cx.new(|cx| {
20031 let mut multi_buffer = MultiBuffer::new(ReadWrite);
20032 multi_buffer.push_excerpts(
20033 buffer_1.clone(),
20034 [
20035 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20036 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20037 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20038 ],
20039 cx,
20040 );
20041 multi_buffer.push_excerpts(
20042 buffer_2.clone(),
20043 [
20044 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20045 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20046 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20047 ],
20048 cx,
20049 );
20050 multi_buffer.push_excerpts(
20051 buffer_3.clone(),
20052 [
20053 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20054 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20055 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20056 ],
20057 cx,
20058 );
20059 multi_buffer
20060 });
20061 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
20062 Editor::new(
20063 EditorMode::full(),
20064 multi_buffer.clone(),
20065 Some(project.clone()),
20066 window,
20067 cx,
20068 )
20069 });
20070
20071 assert_eq!(
20072 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20073 "\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",
20074 );
20075
20076 multi_buffer_editor.update(cx, |editor, cx| {
20077 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
20078 });
20079 assert_eq!(
20080 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20081 "\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",
20082 "After folding the first buffer, its text should not be displayed"
20083 );
20084
20085 multi_buffer_editor.update(cx, |editor, cx| {
20086 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
20087 });
20088 assert_eq!(
20089 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20090 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
20091 "After folding the second buffer, its text should not be displayed"
20092 );
20093
20094 multi_buffer_editor.update(cx, |editor, cx| {
20095 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
20096 });
20097 assert_eq!(
20098 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20099 "\n\n\n\n\n",
20100 "After folding the third buffer, its text should not be displayed"
20101 );
20102
20103 // Emulate selection inside the fold logic, that should work
20104 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20105 editor
20106 .snapshot(window, cx)
20107 .next_line_boundary(Point::new(0, 4));
20108 });
20109
20110 multi_buffer_editor.update(cx, |editor, cx| {
20111 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
20112 });
20113 assert_eq!(
20114 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20115 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
20116 "After unfolding the second buffer, its text should be displayed"
20117 );
20118
20119 // Typing inside of buffer 1 causes that buffer to be unfolded.
20120 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20121 assert_eq!(
20122 multi_buffer
20123 .read(cx)
20124 .snapshot(cx)
20125 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
20126 .collect::<String>(),
20127 "bbbb"
20128 );
20129 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
20130 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
20131 });
20132 editor.handle_input("B", window, cx);
20133 });
20134
20135 assert_eq!(
20136 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20137 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
20138 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
20139 );
20140
20141 multi_buffer_editor.update(cx, |editor, cx| {
20142 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
20143 });
20144 assert_eq!(
20145 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20146 "\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",
20147 "After unfolding the all buffers, all original text should be displayed"
20148 );
20149}
20150
20151#[gpui::test]
20152async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
20153 init_test(cx, |_| {});
20154
20155 let sample_text_1 = "1111\n2222\n3333".to_string();
20156 let sample_text_2 = "4444\n5555\n6666".to_string();
20157 let sample_text_3 = "7777\n8888\n9999".to_string();
20158
20159 let fs = FakeFs::new(cx.executor());
20160 fs.insert_tree(
20161 path!("/a"),
20162 json!({
20163 "first.rs": sample_text_1,
20164 "second.rs": sample_text_2,
20165 "third.rs": sample_text_3,
20166 }),
20167 )
20168 .await;
20169 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20170 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20171 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20172 let worktree = project.update(cx, |project, cx| {
20173 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
20174 assert_eq!(worktrees.len(), 1);
20175 worktrees.pop().unwrap()
20176 });
20177 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
20178
20179 let buffer_1 = project
20180 .update(cx, |project, cx| {
20181 project.open_buffer((worktree_id, "first.rs"), cx)
20182 })
20183 .await
20184 .unwrap();
20185 let buffer_2 = project
20186 .update(cx, |project, cx| {
20187 project.open_buffer((worktree_id, "second.rs"), cx)
20188 })
20189 .await
20190 .unwrap();
20191 let buffer_3 = project
20192 .update(cx, |project, cx| {
20193 project.open_buffer((worktree_id, "third.rs"), cx)
20194 })
20195 .await
20196 .unwrap();
20197
20198 let multi_buffer = cx.new(|cx| {
20199 let mut multi_buffer = MultiBuffer::new(ReadWrite);
20200 multi_buffer.push_excerpts(
20201 buffer_1.clone(),
20202 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
20203 cx,
20204 );
20205 multi_buffer.push_excerpts(
20206 buffer_2.clone(),
20207 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
20208 cx,
20209 );
20210 multi_buffer.push_excerpts(
20211 buffer_3.clone(),
20212 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
20213 cx,
20214 );
20215 multi_buffer
20216 });
20217
20218 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
20219 Editor::new(
20220 EditorMode::full(),
20221 multi_buffer,
20222 Some(project.clone()),
20223 window,
20224 cx,
20225 )
20226 });
20227
20228 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
20229 assert_eq!(
20230 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20231 full_text,
20232 );
20233
20234 multi_buffer_editor.update(cx, |editor, cx| {
20235 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
20236 });
20237 assert_eq!(
20238 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20239 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
20240 "After folding the first buffer, its text should not be displayed"
20241 );
20242
20243 multi_buffer_editor.update(cx, |editor, cx| {
20244 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
20245 });
20246
20247 assert_eq!(
20248 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20249 "\n\n\n\n\n\n7777\n8888\n9999",
20250 "After folding the second buffer, its text should not be displayed"
20251 );
20252
20253 multi_buffer_editor.update(cx, |editor, cx| {
20254 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
20255 });
20256 assert_eq!(
20257 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20258 "\n\n\n\n\n",
20259 "After folding the third buffer, its text should not be displayed"
20260 );
20261
20262 multi_buffer_editor.update(cx, |editor, cx| {
20263 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
20264 });
20265 assert_eq!(
20266 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20267 "\n\n\n\n4444\n5555\n6666\n\n",
20268 "After unfolding the second buffer, its text should be displayed"
20269 );
20270
20271 multi_buffer_editor.update(cx, |editor, cx| {
20272 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
20273 });
20274 assert_eq!(
20275 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20276 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
20277 "After unfolding the first buffer, its text should be displayed"
20278 );
20279
20280 multi_buffer_editor.update(cx, |editor, cx| {
20281 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
20282 });
20283 assert_eq!(
20284 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20285 full_text,
20286 "After unfolding all buffers, all original text should be displayed"
20287 );
20288}
20289
20290#[gpui::test]
20291async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
20292 init_test(cx, |_| {});
20293
20294 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
20295
20296 let fs = FakeFs::new(cx.executor());
20297 fs.insert_tree(
20298 path!("/a"),
20299 json!({
20300 "main.rs": sample_text,
20301 }),
20302 )
20303 .await;
20304 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20305 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20306 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20307 let worktree = project.update(cx, |project, cx| {
20308 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
20309 assert_eq!(worktrees.len(), 1);
20310 worktrees.pop().unwrap()
20311 });
20312 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
20313
20314 let buffer_1 = project
20315 .update(cx, |project, cx| {
20316 project.open_buffer((worktree_id, "main.rs"), cx)
20317 })
20318 .await
20319 .unwrap();
20320
20321 let multi_buffer = cx.new(|cx| {
20322 let mut multi_buffer = MultiBuffer::new(ReadWrite);
20323 multi_buffer.push_excerpts(
20324 buffer_1.clone(),
20325 [ExcerptRange::new(
20326 Point::new(0, 0)
20327 ..Point::new(
20328 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
20329 0,
20330 ),
20331 )],
20332 cx,
20333 );
20334 multi_buffer
20335 });
20336 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
20337 Editor::new(
20338 EditorMode::full(),
20339 multi_buffer,
20340 Some(project.clone()),
20341 window,
20342 cx,
20343 )
20344 });
20345
20346 let selection_range = Point::new(1, 0)..Point::new(2, 0);
20347 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20348 enum TestHighlight {}
20349 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
20350 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
20351 editor.highlight_text::<TestHighlight>(
20352 vec![highlight_range.clone()],
20353 HighlightStyle::color(Hsla::green()),
20354 cx,
20355 );
20356 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20357 s.select_ranges(Some(highlight_range))
20358 });
20359 });
20360
20361 let full_text = format!("\n\n{sample_text}");
20362 assert_eq!(
20363 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20364 full_text,
20365 );
20366}
20367
20368#[gpui::test]
20369async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
20370 init_test(cx, |_| {});
20371 cx.update(|cx| {
20372 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
20373 "keymaps/default-linux.json",
20374 cx,
20375 )
20376 .unwrap();
20377 cx.bind_keys(default_key_bindings);
20378 });
20379
20380 let (editor, cx) = cx.add_window_view(|window, cx| {
20381 let multi_buffer = MultiBuffer::build_multi(
20382 [
20383 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
20384 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
20385 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
20386 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
20387 ],
20388 cx,
20389 );
20390 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
20391
20392 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
20393 // fold all but the second buffer, so that we test navigating between two
20394 // adjacent folded buffers, as well as folded buffers at the start and
20395 // end the multibuffer
20396 editor.fold_buffer(buffer_ids[0], cx);
20397 editor.fold_buffer(buffer_ids[2], cx);
20398 editor.fold_buffer(buffer_ids[3], cx);
20399
20400 editor
20401 });
20402 cx.simulate_resize(size(px(1000.), px(1000.)));
20403
20404 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
20405 cx.assert_excerpts_with_selections(indoc! {"
20406 [EXCERPT]
20407 ˇ[FOLDED]
20408 [EXCERPT]
20409 a1
20410 b1
20411 [EXCERPT]
20412 [FOLDED]
20413 [EXCERPT]
20414 [FOLDED]
20415 "
20416 });
20417 cx.simulate_keystroke("down");
20418 cx.assert_excerpts_with_selections(indoc! {"
20419 [EXCERPT]
20420 [FOLDED]
20421 [EXCERPT]
20422 ˇa1
20423 b1
20424 [EXCERPT]
20425 [FOLDED]
20426 [EXCERPT]
20427 [FOLDED]
20428 "
20429 });
20430 cx.simulate_keystroke("down");
20431 cx.assert_excerpts_with_selections(indoc! {"
20432 [EXCERPT]
20433 [FOLDED]
20434 [EXCERPT]
20435 a1
20436 ˇb1
20437 [EXCERPT]
20438 [FOLDED]
20439 [EXCERPT]
20440 [FOLDED]
20441 "
20442 });
20443 cx.simulate_keystroke("down");
20444 cx.assert_excerpts_with_selections(indoc! {"
20445 [EXCERPT]
20446 [FOLDED]
20447 [EXCERPT]
20448 a1
20449 b1
20450 ˇ[EXCERPT]
20451 [FOLDED]
20452 [EXCERPT]
20453 [FOLDED]
20454 "
20455 });
20456 cx.simulate_keystroke("down");
20457 cx.assert_excerpts_with_selections(indoc! {"
20458 [EXCERPT]
20459 [FOLDED]
20460 [EXCERPT]
20461 a1
20462 b1
20463 [EXCERPT]
20464 ˇ[FOLDED]
20465 [EXCERPT]
20466 [FOLDED]
20467 "
20468 });
20469 for _ in 0..5 {
20470 cx.simulate_keystroke("down");
20471 cx.assert_excerpts_with_selections(indoc! {"
20472 [EXCERPT]
20473 [FOLDED]
20474 [EXCERPT]
20475 a1
20476 b1
20477 [EXCERPT]
20478 [FOLDED]
20479 [EXCERPT]
20480 ˇ[FOLDED]
20481 "
20482 });
20483 }
20484
20485 cx.simulate_keystroke("up");
20486 cx.assert_excerpts_with_selections(indoc! {"
20487 [EXCERPT]
20488 [FOLDED]
20489 [EXCERPT]
20490 a1
20491 b1
20492 [EXCERPT]
20493 ˇ[FOLDED]
20494 [EXCERPT]
20495 [FOLDED]
20496 "
20497 });
20498 cx.simulate_keystroke("up");
20499 cx.assert_excerpts_with_selections(indoc! {"
20500 [EXCERPT]
20501 [FOLDED]
20502 [EXCERPT]
20503 a1
20504 b1
20505 ˇ[EXCERPT]
20506 [FOLDED]
20507 [EXCERPT]
20508 [FOLDED]
20509 "
20510 });
20511 cx.simulate_keystroke("up");
20512 cx.assert_excerpts_with_selections(indoc! {"
20513 [EXCERPT]
20514 [FOLDED]
20515 [EXCERPT]
20516 a1
20517 ˇb1
20518 [EXCERPT]
20519 [FOLDED]
20520 [EXCERPT]
20521 [FOLDED]
20522 "
20523 });
20524 cx.simulate_keystroke("up");
20525 cx.assert_excerpts_with_selections(indoc! {"
20526 [EXCERPT]
20527 [FOLDED]
20528 [EXCERPT]
20529 ˇa1
20530 b1
20531 [EXCERPT]
20532 [FOLDED]
20533 [EXCERPT]
20534 [FOLDED]
20535 "
20536 });
20537 for _ in 0..5 {
20538 cx.simulate_keystroke("up");
20539 cx.assert_excerpts_with_selections(indoc! {"
20540 [EXCERPT]
20541 ˇ[FOLDED]
20542 [EXCERPT]
20543 a1
20544 b1
20545 [EXCERPT]
20546 [FOLDED]
20547 [EXCERPT]
20548 [FOLDED]
20549 "
20550 });
20551 }
20552}
20553
20554#[gpui::test]
20555async fn test_edit_prediction_text(cx: &mut TestAppContext) {
20556 init_test(cx, |_| {});
20557
20558 // Simple insertion
20559 assert_highlighted_edits(
20560 "Hello, world!",
20561 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
20562 true,
20563 cx,
20564 |highlighted_edits, cx| {
20565 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
20566 assert_eq!(highlighted_edits.highlights.len(), 1);
20567 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
20568 assert_eq!(
20569 highlighted_edits.highlights[0].1.background_color,
20570 Some(cx.theme().status().created_background)
20571 );
20572 },
20573 )
20574 .await;
20575
20576 // Replacement
20577 assert_highlighted_edits(
20578 "This is a test.",
20579 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
20580 false,
20581 cx,
20582 |highlighted_edits, cx| {
20583 assert_eq!(highlighted_edits.text, "That is a test.");
20584 assert_eq!(highlighted_edits.highlights.len(), 1);
20585 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
20586 assert_eq!(
20587 highlighted_edits.highlights[0].1.background_color,
20588 Some(cx.theme().status().created_background)
20589 );
20590 },
20591 )
20592 .await;
20593
20594 // Multiple edits
20595 assert_highlighted_edits(
20596 "Hello, world!",
20597 vec![
20598 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
20599 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
20600 ],
20601 false,
20602 cx,
20603 |highlighted_edits, cx| {
20604 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
20605 assert_eq!(highlighted_edits.highlights.len(), 2);
20606 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
20607 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
20608 assert_eq!(
20609 highlighted_edits.highlights[0].1.background_color,
20610 Some(cx.theme().status().created_background)
20611 );
20612 assert_eq!(
20613 highlighted_edits.highlights[1].1.background_color,
20614 Some(cx.theme().status().created_background)
20615 );
20616 },
20617 )
20618 .await;
20619
20620 // Multiple lines with edits
20621 assert_highlighted_edits(
20622 "First line\nSecond line\nThird line\nFourth line",
20623 vec![
20624 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
20625 (
20626 Point::new(2, 0)..Point::new(2, 10),
20627 "New third line".to_string(),
20628 ),
20629 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
20630 ],
20631 false,
20632 cx,
20633 |highlighted_edits, cx| {
20634 assert_eq!(
20635 highlighted_edits.text,
20636 "Second modified\nNew third line\nFourth updated line"
20637 );
20638 assert_eq!(highlighted_edits.highlights.len(), 3);
20639 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
20640 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
20641 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
20642 for highlight in &highlighted_edits.highlights {
20643 assert_eq!(
20644 highlight.1.background_color,
20645 Some(cx.theme().status().created_background)
20646 );
20647 }
20648 },
20649 )
20650 .await;
20651}
20652
20653#[gpui::test]
20654async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
20655 init_test(cx, |_| {});
20656
20657 // Deletion
20658 assert_highlighted_edits(
20659 "Hello, world!",
20660 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
20661 true,
20662 cx,
20663 |highlighted_edits, cx| {
20664 assert_eq!(highlighted_edits.text, "Hello, world!");
20665 assert_eq!(highlighted_edits.highlights.len(), 1);
20666 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
20667 assert_eq!(
20668 highlighted_edits.highlights[0].1.background_color,
20669 Some(cx.theme().status().deleted_background)
20670 );
20671 },
20672 )
20673 .await;
20674
20675 // Insertion
20676 assert_highlighted_edits(
20677 "Hello, world!",
20678 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
20679 true,
20680 cx,
20681 |highlighted_edits, cx| {
20682 assert_eq!(highlighted_edits.highlights.len(), 1);
20683 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
20684 assert_eq!(
20685 highlighted_edits.highlights[0].1.background_color,
20686 Some(cx.theme().status().created_background)
20687 );
20688 },
20689 )
20690 .await;
20691}
20692
20693async fn assert_highlighted_edits(
20694 text: &str,
20695 edits: Vec<(Range<Point>, String)>,
20696 include_deletions: bool,
20697 cx: &mut TestAppContext,
20698 assertion_fn: impl Fn(HighlightedText, &App),
20699) {
20700 let window = cx.add_window(|window, cx| {
20701 let buffer = MultiBuffer::build_simple(text, cx);
20702 Editor::new(EditorMode::full(), buffer, None, window, cx)
20703 });
20704 let cx = &mut VisualTestContext::from_window(*window, cx);
20705
20706 let (buffer, snapshot) = window
20707 .update(cx, |editor, _window, cx| {
20708 (
20709 editor.buffer().clone(),
20710 editor.buffer().read(cx).snapshot(cx),
20711 )
20712 })
20713 .unwrap();
20714
20715 let edits = edits
20716 .into_iter()
20717 .map(|(range, edit)| {
20718 (
20719 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
20720 edit,
20721 )
20722 })
20723 .collect::<Vec<_>>();
20724
20725 let text_anchor_edits = edits
20726 .clone()
20727 .into_iter()
20728 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
20729 .collect::<Vec<_>>();
20730
20731 let edit_preview = window
20732 .update(cx, |_, _window, cx| {
20733 buffer
20734 .read(cx)
20735 .as_singleton()
20736 .unwrap()
20737 .read(cx)
20738 .preview_edits(text_anchor_edits.into(), cx)
20739 })
20740 .unwrap()
20741 .await;
20742
20743 cx.update(|_window, cx| {
20744 let highlighted_edits = edit_prediction_edit_text(
20745 &snapshot.as_singleton().unwrap().2,
20746 &edits,
20747 &edit_preview,
20748 include_deletions,
20749 cx,
20750 );
20751 assertion_fn(highlighted_edits, cx)
20752 });
20753}
20754
20755#[track_caller]
20756fn assert_breakpoint(
20757 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
20758 path: &Arc<Path>,
20759 expected: Vec<(u32, Breakpoint)>,
20760) {
20761 if expected.len() == 0usize {
20762 assert!(!breakpoints.contains_key(path), "{}", path.display());
20763 } else {
20764 let mut breakpoint = breakpoints
20765 .get(path)
20766 .unwrap()
20767 .into_iter()
20768 .map(|breakpoint| {
20769 (
20770 breakpoint.row,
20771 Breakpoint {
20772 message: breakpoint.message.clone(),
20773 state: breakpoint.state,
20774 condition: breakpoint.condition.clone(),
20775 hit_condition: breakpoint.hit_condition.clone(),
20776 },
20777 )
20778 })
20779 .collect::<Vec<_>>();
20780
20781 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
20782
20783 assert_eq!(expected, breakpoint);
20784 }
20785}
20786
20787fn add_log_breakpoint_at_cursor(
20788 editor: &mut Editor,
20789 log_message: &str,
20790 window: &mut Window,
20791 cx: &mut Context<Editor>,
20792) {
20793 let (anchor, bp) = editor
20794 .breakpoints_at_cursors(window, cx)
20795 .first()
20796 .and_then(|(anchor, bp)| {
20797 if let Some(bp) = bp {
20798 Some((*anchor, bp.clone()))
20799 } else {
20800 None
20801 }
20802 })
20803 .unwrap_or_else(|| {
20804 let cursor_position: Point = editor.selections.newest(cx).head();
20805
20806 let breakpoint_position = editor
20807 .snapshot(window, cx)
20808 .display_snapshot
20809 .buffer_snapshot
20810 .anchor_before(Point::new(cursor_position.row, 0));
20811
20812 (breakpoint_position, Breakpoint::new_log(&log_message))
20813 });
20814
20815 editor.edit_breakpoint_at_anchor(
20816 anchor,
20817 bp,
20818 BreakpointEditAction::EditLogMessage(log_message.into()),
20819 cx,
20820 );
20821}
20822
20823#[gpui::test]
20824async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
20825 init_test(cx, |_| {});
20826
20827 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
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.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20838 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20839
20840 let fs = FakeFs::new(cx.executor());
20841 fs.insert_tree(
20842 path!("/a"),
20843 json!({
20844 "main.rs": sample_text,
20845 }),
20846 )
20847 .await;
20848 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20849 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20850 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20851 let worktree_id = workspace
20852 .update(cx, |workspace, _window, cx| {
20853 workspace.project().update(cx, |project, cx| {
20854 project.worktrees(cx).next().unwrap().read(cx).id()
20855 })
20856 })
20857 .unwrap();
20858
20859 let buffer = project
20860 .update(cx, |project, cx| {
20861 project.open_buffer((worktree_id, "main.rs"), cx)
20862 })
20863 .await
20864 .unwrap();
20865
20866 let (editor, cx) = cx.add_window_view(|window, cx| {
20867 Editor::new(
20868 EditorMode::full(),
20869 MultiBuffer::build_from_buffer(buffer, cx),
20870 Some(project.clone()),
20871 window,
20872 cx,
20873 )
20874 });
20875
20876 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20877 let abs_path = project.read_with(cx, |project, cx| {
20878 project
20879 .absolute_path(&project_path, cx)
20880 .map(|path_buf| Arc::from(path_buf.to_owned()))
20881 .unwrap()
20882 });
20883
20884 // assert we can add breakpoint on the first line
20885 editor.update_in(cx, |editor, window, cx| {
20886 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20887 editor.move_to_end(&MoveToEnd, window, cx);
20888 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20889 });
20890
20891 let breakpoints = editor.update(cx, |editor, cx| {
20892 editor
20893 .breakpoint_store()
20894 .as_ref()
20895 .unwrap()
20896 .read(cx)
20897 .all_source_breakpoints(cx)
20898 .clone()
20899 });
20900
20901 assert_eq!(1, breakpoints.len());
20902 assert_breakpoint(
20903 &breakpoints,
20904 &abs_path,
20905 vec![
20906 (0, Breakpoint::new_standard()),
20907 (3, Breakpoint::new_standard()),
20908 ],
20909 );
20910
20911 editor.update_in(cx, |editor, window, cx| {
20912 editor.move_to_beginning(&MoveToBeginning, window, cx);
20913 editor.toggle_breakpoint(&actions::ToggleBreakpoint, 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_eq!(1, breakpoints.len());
20927 assert_breakpoint(
20928 &breakpoints,
20929 &abs_path,
20930 vec![(3, Breakpoint::new_standard())],
20931 );
20932
20933 editor.update_in(cx, |editor, window, cx| {
20934 editor.move_to_end(&MoveToEnd, window, cx);
20935 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20936 });
20937
20938 let breakpoints = editor.update(cx, |editor, cx| {
20939 editor
20940 .breakpoint_store()
20941 .as_ref()
20942 .unwrap()
20943 .read(cx)
20944 .all_source_breakpoints(cx)
20945 .clone()
20946 });
20947
20948 assert_eq!(0, breakpoints.len());
20949 assert_breakpoint(&breakpoints, &abs_path, vec![]);
20950}
20951
20952#[gpui::test]
20953async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
20954 init_test(cx, |_| {});
20955
20956 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20957
20958 let fs = FakeFs::new(cx.executor());
20959 fs.insert_tree(
20960 path!("/a"),
20961 json!({
20962 "main.rs": sample_text,
20963 }),
20964 )
20965 .await;
20966 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20967 let (workspace, cx) =
20968 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20969
20970 let worktree_id = workspace.update(cx, |workspace, cx| {
20971 workspace.project().update(cx, |project, cx| {
20972 project.worktrees(cx).next().unwrap().read(cx).id()
20973 })
20974 });
20975
20976 let buffer = project
20977 .update(cx, |project, cx| {
20978 project.open_buffer((worktree_id, "main.rs"), cx)
20979 })
20980 .await
20981 .unwrap();
20982
20983 let (editor, cx) = cx.add_window_view(|window, cx| {
20984 Editor::new(
20985 EditorMode::full(),
20986 MultiBuffer::build_from_buffer(buffer, cx),
20987 Some(project.clone()),
20988 window,
20989 cx,
20990 )
20991 });
20992
20993 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20994 let abs_path = project.read_with(cx, |project, cx| {
20995 project
20996 .absolute_path(&project_path, cx)
20997 .map(|path_buf| Arc::from(path_buf.to_owned()))
20998 .unwrap()
20999 });
21000
21001 editor.update_in(cx, |editor, window, cx| {
21002 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
21003 });
21004
21005 let breakpoints = editor.update(cx, |editor, cx| {
21006 editor
21007 .breakpoint_store()
21008 .as_ref()
21009 .unwrap()
21010 .read(cx)
21011 .all_source_breakpoints(cx)
21012 .clone()
21013 });
21014
21015 assert_breakpoint(
21016 &breakpoints,
21017 &abs_path,
21018 vec![(0, Breakpoint::new_log("hello world"))],
21019 );
21020
21021 // Removing a log message from a log breakpoint should remove it
21022 editor.update_in(cx, |editor, window, cx| {
21023 add_log_breakpoint_at_cursor(editor, "", window, cx);
21024 });
21025
21026 let breakpoints = editor.update(cx, |editor, cx| {
21027 editor
21028 .breakpoint_store()
21029 .as_ref()
21030 .unwrap()
21031 .read(cx)
21032 .all_source_breakpoints(cx)
21033 .clone()
21034 });
21035
21036 assert_breakpoint(&breakpoints, &abs_path, vec![]);
21037
21038 editor.update_in(cx, |editor, window, cx| {
21039 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21040 editor.move_to_end(&MoveToEnd, window, cx);
21041 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21042 // Not adding a log message to a standard breakpoint shouldn't remove it
21043 add_log_breakpoint_at_cursor(editor, "", window, cx);
21044 });
21045
21046 let breakpoints = editor.update(cx, |editor, cx| {
21047 editor
21048 .breakpoint_store()
21049 .as_ref()
21050 .unwrap()
21051 .read(cx)
21052 .all_source_breakpoints(cx)
21053 .clone()
21054 });
21055
21056 assert_breakpoint(
21057 &breakpoints,
21058 &abs_path,
21059 vec![
21060 (0, Breakpoint::new_standard()),
21061 (3, Breakpoint::new_standard()),
21062 ],
21063 );
21064
21065 editor.update_in(cx, |editor, window, cx| {
21066 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
21067 });
21068
21069 let breakpoints = editor.update(cx, |editor, cx| {
21070 editor
21071 .breakpoint_store()
21072 .as_ref()
21073 .unwrap()
21074 .read(cx)
21075 .all_source_breakpoints(cx)
21076 .clone()
21077 });
21078
21079 assert_breakpoint(
21080 &breakpoints,
21081 &abs_path,
21082 vec![
21083 (0, Breakpoint::new_standard()),
21084 (3, Breakpoint::new_log("hello world")),
21085 ],
21086 );
21087
21088 editor.update_in(cx, |editor, window, cx| {
21089 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
21090 });
21091
21092 let breakpoints = editor.update(cx, |editor, cx| {
21093 editor
21094 .breakpoint_store()
21095 .as_ref()
21096 .unwrap()
21097 .read(cx)
21098 .all_source_breakpoints(cx)
21099 .clone()
21100 });
21101
21102 assert_breakpoint(
21103 &breakpoints,
21104 &abs_path,
21105 vec![
21106 (0, Breakpoint::new_standard()),
21107 (3, Breakpoint::new_log("hello Earth!!")),
21108 ],
21109 );
21110}
21111
21112/// This also tests that Editor::breakpoint_at_cursor_head is working properly
21113/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
21114/// or when breakpoints were placed out of order. This tests for a regression too
21115#[gpui::test]
21116async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
21117 init_test(cx, |_| {});
21118
21119 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
21120 let fs = FakeFs::new(cx.executor());
21121 fs.insert_tree(
21122 path!("/a"),
21123 json!({
21124 "main.rs": sample_text,
21125 }),
21126 )
21127 .await;
21128 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21129 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21130 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21131
21132 let fs = FakeFs::new(cx.executor());
21133 fs.insert_tree(
21134 path!("/a"),
21135 json!({
21136 "main.rs": sample_text,
21137 }),
21138 )
21139 .await;
21140 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21141 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21142 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21143 let worktree_id = workspace
21144 .update(cx, |workspace, _window, cx| {
21145 workspace.project().update(cx, |project, cx| {
21146 project.worktrees(cx).next().unwrap().read(cx).id()
21147 })
21148 })
21149 .unwrap();
21150
21151 let buffer = project
21152 .update(cx, |project, cx| {
21153 project.open_buffer((worktree_id, "main.rs"), cx)
21154 })
21155 .await
21156 .unwrap();
21157
21158 let (editor, cx) = cx.add_window_view(|window, cx| {
21159 Editor::new(
21160 EditorMode::full(),
21161 MultiBuffer::build_from_buffer(buffer, cx),
21162 Some(project.clone()),
21163 window,
21164 cx,
21165 )
21166 });
21167
21168 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
21169 let abs_path = project.read_with(cx, |project, cx| {
21170 project
21171 .absolute_path(&project_path, cx)
21172 .map(|path_buf| Arc::from(path_buf.to_owned()))
21173 .unwrap()
21174 });
21175
21176 // assert we can add breakpoint on the first line
21177 editor.update_in(cx, |editor, window, cx| {
21178 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21179 editor.move_to_end(&MoveToEnd, window, cx);
21180 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21181 editor.move_up(&MoveUp, window, cx);
21182 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21183 });
21184
21185 let breakpoints = editor.update(cx, |editor, cx| {
21186 editor
21187 .breakpoint_store()
21188 .as_ref()
21189 .unwrap()
21190 .read(cx)
21191 .all_source_breakpoints(cx)
21192 .clone()
21193 });
21194
21195 assert_eq!(1, breakpoints.len());
21196 assert_breakpoint(
21197 &breakpoints,
21198 &abs_path,
21199 vec![
21200 (0, Breakpoint::new_standard()),
21201 (2, Breakpoint::new_standard()),
21202 (3, Breakpoint::new_standard()),
21203 ],
21204 );
21205
21206 editor.update_in(cx, |editor, window, cx| {
21207 editor.move_to_beginning(&MoveToBeginning, window, cx);
21208 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
21209 editor.move_to_end(&MoveToEnd, window, cx);
21210 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
21211 // Disabling a breakpoint that doesn't exist should do nothing
21212 editor.move_up(&MoveUp, window, cx);
21213 editor.move_up(&MoveUp, window, cx);
21214 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
21215 });
21216
21217 let breakpoints = editor.update(cx, |editor, cx| {
21218 editor
21219 .breakpoint_store()
21220 .as_ref()
21221 .unwrap()
21222 .read(cx)
21223 .all_source_breakpoints(cx)
21224 .clone()
21225 });
21226
21227 let disable_breakpoint = {
21228 let mut bp = Breakpoint::new_standard();
21229 bp.state = BreakpointState::Disabled;
21230 bp
21231 };
21232
21233 assert_eq!(1, breakpoints.len());
21234 assert_breakpoint(
21235 &breakpoints,
21236 &abs_path,
21237 vec![
21238 (0, disable_breakpoint.clone()),
21239 (2, Breakpoint::new_standard()),
21240 (3, disable_breakpoint.clone()),
21241 ],
21242 );
21243
21244 editor.update_in(cx, |editor, window, cx| {
21245 editor.move_to_beginning(&MoveToBeginning, window, cx);
21246 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
21247 editor.move_to_end(&MoveToEnd, window, cx);
21248 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
21249 editor.move_up(&MoveUp, window, cx);
21250 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
21251 });
21252
21253 let breakpoints = editor.update(cx, |editor, cx| {
21254 editor
21255 .breakpoint_store()
21256 .as_ref()
21257 .unwrap()
21258 .read(cx)
21259 .all_source_breakpoints(cx)
21260 .clone()
21261 });
21262
21263 assert_eq!(1, breakpoints.len());
21264 assert_breakpoint(
21265 &breakpoints,
21266 &abs_path,
21267 vec![
21268 (0, Breakpoint::new_standard()),
21269 (2, disable_breakpoint),
21270 (3, Breakpoint::new_standard()),
21271 ],
21272 );
21273}
21274
21275#[gpui::test]
21276async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
21277 init_test(cx, |_| {});
21278 let capabilities = lsp::ServerCapabilities {
21279 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
21280 prepare_provider: Some(true),
21281 work_done_progress_options: Default::default(),
21282 })),
21283 ..Default::default()
21284 };
21285 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
21286
21287 cx.set_state(indoc! {"
21288 struct Fˇoo {}
21289 "});
21290
21291 cx.update_editor(|editor, _, cx| {
21292 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
21293 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
21294 editor.highlight_background::<DocumentHighlightRead>(
21295 &[highlight_range],
21296 |theme| theme.colors().editor_document_highlight_read_background,
21297 cx,
21298 );
21299 });
21300
21301 let mut prepare_rename_handler = cx
21302 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
21303 move |_, _, _| async move {
21304 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
21305 start: lsp::Position {
21306 line: 0,
21307 character: 7,
21308 },
21309 end: lsp::Position {
21310 line: 0,
21311 character: 10,
21312 },
21313 })))
21314 },
21315 );
21316 let prepare_rename_task = cx
21317 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
21318 .expect("Prepare rename was not started");
21319 prepare_rename_handler.next().await.unwrap();
21320 prepare_rename_task.await.expect("Prepare rename failed");
21321
21322 let mut rename_handler =
21323 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
21324 let edit = lsp::TextEdit {
21325 range: lsp::Range {
21326 start: lsp::Position {
21327 line: 0,
21328 character: 7,
21329 },
21330 end: lsp::Position {
21331 line: 0,
21332 character: 10,
21333 },
21334 },
21335 new_text: "FooRenamed".to_string(),
21336 };
21337 Ok(Some(lsp::WorkspaceEdit::new(
21338 // Specify the same edit twice
21339 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
21340 )))
21341 });
21342 let rename_task = cx
21343 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
21344 .expect("Confirm rename was not started");
21345 rename_handler.next().await.unwrap();
21346 rename_task.await.expect("Confirm rename failed");
21347 cx.run_until_parked();
21348
21349 // Despite two edits, only one is actually applied as those are identical
21350 cx.assert_editor_state(indoc! {"
21351 struct FooRenamedˇ {}
21352 "});
21353}
21354
21355#[gpui::test]
21356async fn test_rename_without_prepare(cx: &mut TestAppContext) {
21357 init_test(cx, |_| {});
21358 // These capabilities indicate that the server does not support prepare rename.
21359 let capabilities = lsp::ServerCapabilities {
21360 rename_provider: Some(lsp::OneOf::Left(true)),
21361 ..Default::default()
21362 };
21363 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
21364
21365 cx.set_state(indoc! {"
21366 struct Fˇoo {}
21367 "});
21368
21369 cx.update_editor(|editor, _window, cx| {
21370 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
21371 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
21372 editor.highlight_background::<DocumentHighlightRead>(
21373 &[highlight_range],
21374 |theme| theme.colors().editor_document_highlight_read_background,
21375 cx,
21376 );
21377 });
21378
21379 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
21380 .expect("Prepare rename was not started")
21381 .await
21382 .expect("Prepare rename failed");
21383
21384 let mut rename_handler =
21385 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
21386 let edit = lsp::TextEdit {
21387 range: lsp::Range {
21388 start: lsp::Position {
21389 line: 0,
21390 character: 7,
21391 },
21392 end: lsp::Position {
21393 line: 0,
21394 character: 10,
21395 },
21396 },
21397 new_text: "FooRenamed".to_string(),
21398 };
21399 Ok(Some(lsp::WorkspaceEdit::new(
21400 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
21401 )))
21402 });
21403 let rename_task = cx
21404 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
21405 .expect("Confirm rename was not started");
21406 rename_handler.next().await.unwrap();
21407 rename_task.await.expect("Confirm rename failed");
21408 cx.run_until_parked();
21409
21410 // Correct range is renamed, as `surrounding_word` is used to find it.
21411 cx.assert_editor_state(indoc! {"
21412 struct FooRenamedˇ {}
21413 "});
21414}
21415
21416#[gpui::test]
21417async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
21418 init_test(cx, |_| {});
21419 let mut cx = EditorTestContext::new(cx).await;
21420
21421 let language = Arc::new(
21422 Language::new(
21423 LanguageConfig::default(),
21424 Some(tree_sitter_html::LANGUAGE.into()),
21425 )
21426 .with_brackets_query(
21427 r#"
21428 ("<" @open "/>" @close)
21429 ("</" @open ">" @close)
21430 ("<" @open ">" @close)
21431 ("\"" @open "\"" @close)
21432 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
21433 "#,
21434 )
21435 .unwrap(),
21436 );
21437 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
21438
21439 cx.set_state(indoc! {"
21440 <span>ˇ</span>
21441 "});
21442 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21443 cx.assert_editor_state(indoc! {"
21444 <span>
21445 ˇ
21446 </span>
21447 "});
21448
21449 cx.set_state(indoc! {"
21450 <span><span></span>ˇ</span>
21451 "});
21452 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21453 cx.assert_editor_state(indoc! {"
21454 <span><span></span>
21455 ˇ</span>
21456 "});
21457
21458 cx.set_state(indoc! {"
21459 <span>ˇ
21460 </span>
21461 "});
21462 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21463 cx.assert_editor_state(indoc! {"
21464 <span>
21465 ˇ
21466 </span>
21467 "});
21468}
21469
21470#[gpui::test(iterations = 10)]
21471async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
21472 init_test(cx, |_| {});
21473
21474 let fs = FakeFs::new(cx.executor());
21475 fs.insert_tree(
21476 path!("/dir"),
21477 json!({
21478 "a.ts": "a",
21479 }),
21480 )
21481 .await;
21482
21483 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
21484 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21485 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21486
21487 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21488 language_registry.add(Arc::new(Language::new(
21489 LanguageConfig {
21490 name: "TypeScript".into(),
21491 matcher: LanguageMatcher {
21492 path_suffixes: vec!["ts".to_string()],
21493 ..Default::default()
21494 },
21495 ..Default::default()
21496 },
21497 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
21498 )));
21499 let mut fake_language_servers = language_registry.register_fake_lsp(
21500 "TypeScript",
21501 FakeLspAdapter {
21502 capabilities: lsp::ServerCapabilities {
21503 code_lens_provider: Some(lsp::CodeLensOptions {
21504 resolve_provider: Some(true),
21505 }),
21506 execute_command_provider: Some(lsp::ExecuteCommandOptions {
21507 commands: vec!["_the/command".to_string()],
21508 ..lsp::ExecuteCommandOptions::default()
21509 }),
21510 ..lsp::ServerCapabilities::default()
21511 },
21512 ..FakeLspAdapter::default()
21513 },
21514 );
21515
21516 let editor = workspace
21517 .update(cx, |workspace, window, cx| {
21518 workspace.open_abs_path(
21519 PathBuf::from(path!("/dir/a.ts")),
21520 OpenOptions::default(),
21521 window,
21522 cx,
21523 )
21524 })
21525 .unwrap()
21526 .await
21527 .unwrap()
21528 .downcast::<Editor>()
21529 .unwrap();
21530 cx.executor().run_until_parked();
21531
21532 let fake_server = fake_language_servers.next().await.unwrap();
21533
21534 let buffer = editor.update(cx, |editor, cx| {
21535 editor
21536 .buffer()
21537 .read(cx)
21538 .as_singleton()
21539 .expect("have opened a single file by path")
21540 });
21541
21542 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
21543 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
21544 drop(buffer_snapshot);
21545 let actions = cx
21546 .update_window(*workspace, |_, window, cx| {
21547 project.code_actions(&buffer, anchor..anchor, window, cx)
21548 })
21549 .unwrap();
21550
21551 fake_server
21552 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
21553 Ok(Some(vec![
21554 lsp::CodeLens {
21555 range: lsp::Range::default(),
21556 command: Some(lsp::Command {
21557 title: "Code lens command".to_owned(),
21558 command: "_the/command".to_owned(),
21559 arguments: None,
21560 }),
21561 data: None,
21562 },
21563 lsp::CodeLens {
21564 range: lsp::Range::default(),
21565 command: Some(lsp::Command {
21566 title: "Command not in capabilities".to_owned(),
21567 command: "not in capabilities".to_owned(),
21568 arguments: None,
21569 }),
21570 data: None,
21571 },
21572 lsp::CodeLens {
21573 range: lsp::Range {
21574 start: lsp::Position {
21575 line: 1,
21576 character: 1,
21577 },
21578 end: lsp::Position {
21579 line: 1,
21580 character: 1,
21581 },
21582 },
21583 command: Some(lsp::Command {
21584 title: "Command not in range".to_owned(),
21585 command: "_the/command".to_owned(),
21586 arguments: None,
21587 }),
21588 data: None,
21589 },
21590 ]))
21591 })
21592 .next()
21593 .await;
21594
21595 let actions = actions.await.unwrap();
21596 assert_eq!(
21597 actions.len(),
21598 1,
21599 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
21600 );
21601 let action = actions[0].clone();
21602 let apply = project.update(cx, |project, cx| {
21603 project.apply_code_action(buffer.clone(), action, true, cx)
21604 });
21605
21606 // Resolving the code action does not populate its edits. In absence of
21607 // edits, we must execute the given command.
21608 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
21609 |mut lens, _| async move {
21610 let lens_command = lens.command.as_mut().expect("should have a command");
21611 assert_eq!(lens_command.title, "Code lens command");
21612 lens_command.arguments = Some(vec![json!("the-argument")]);
21613 Ok(lens)
21614 },
21615 );
21616
21617 // While executing the command, the language server sends the editor
21618 // a `workspaceEdit` request.
21619 fake_server
21620 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
21621 let fake = fake_server.clone();
21622 move |params, _| {
21623 assert_eq!(params.command, "_the/command");
21624 let fake = fake.clone();
21625 async move {
21626 fake.server
21627 .request::<lsp::request::ApplyWorkspaceEdit>(
21628 lsp::ApplyWorkspaceEditParams {
21629 label: None,
21630 edit: lsp::WorkspaceEdit {
21631 changes: Some(
21632 [(
21633 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
21634 vec![lsp::TextEdit {
21635 range: lsp::Range::new(
21636 lsp::Position::new(0, 0),
21637 lsp::Position::new(0, 0),
21638 ),
21639 new_text: "X".into(),
21640 }],
21641 )]
21642 .into_iter()
21643 .collect(),
21644 ),
21645 ..lsp::WorkspaceEdit::default()
21646 },
21647 },
21648 )
21649 .await
21650 .into_response()
21651 .unwrap();
21652 Ok(Some(json!(null)))
21653 }
21654 }
21655 })
21656 .next()
21657 .await;
21658
21659 // Applying the code lens command returns a project transaction containing the edits
21660 // sent by the language server in its `workspaceEdit` request.
21661 let transaction = apply.await.unwrap();
21662 assert!(transaction.0.contains_key(&buffer));
21663 buffer.update(cx, |buffer, cx| {
21664 assert_eq!(buffer.text(), "Xa");
21665 buffer.undo(cx);
21666 assert_eq!(buffer.text(), "a");
21667 });
21668
21669 let actions_after_edits = cx
21670 .update_window(*workspace, |_, window, cx| {
21671 project.code_actions(&buffer, anchor..anchor, window, cx)
21672 })
21673 .unwrap()
21674 .await
21675 .unwrap();
21676 assert_eq!(
21677 actions, actions_after_edits,
21678 "For the same selection, same code lens actions should be returned"
21679 );
21680
21681 let _responses =
21682 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
21683 panic!("No more code lens requests are expected");
21684 });
21685 editor.update_in(cx, |editor, window, cx| {
21686 editor.select_all(&SelectAll, window, cx);
21687 });
21688 cx.executor().run_until_parked();
21689 let new_actions = cx
21690 .update_window(*workspace, |_, window, cx| {
21691 project.code_actions(&buffer, anchor..anchor, window, cx)
21692 })
21693 .unwrap()
21694 .await
21695 .unwrap();
21696 assert_eq!(
21697 actions, new_actions,
21698 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
21699 );
21700}
21701
21702#[gpui::test]
21703async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
21704 init_test(cx, |_| {});
21705
21706 let fs = FakeFs::new(cx.executor());
21707 let main_text = r#"fn main() {
21708println!("1");
21709println!("2");
21710println!("3");
21711println!("4");
21712println!("5");
21713}"#;
21714 let lib_text = "mod foo {}";
21715 fs.insert_tree(
21716 path!("/a"),
21717 json!({
21718 "lib.rs": lib_text,
21719 "main.rs": main_text,
21720 }),
21721 )
21722 .await;
21723
21724 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21725 let (workspace, cx) =
21726 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21727 let worktree_id = workspace.update(cx, |workspace, cx| {
21728 workspace.project().update(cx, |project, cx| {
21729 project.worktrees(cx).next().unwrap().read(cx).id()
21730 })
21731 });
21732
21733 let expected_ranges = vec![
21734 Point::new(0, 0)..Point::new(0, 0),
21735 Point::new(1, 0)..Point::new(1, 1),
21736 Point::new(2, 0)..Point::new(2, 2),
21737 Point::new(3, 0)..Point::new(3, 3),
21738 ];
21739
21740 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
21741 let editor_1 = workspace
21742 .update_in(cx, |workspace, window, cx| {
21743 workspace.open_path(
21744 (worktree_id, "main.rs"),
21745 Some(pane_1.downgrade()),
21746 true,
21747 window,
21748 cx,
21749 )
21750 })
21751 .unwrap()
21752 .await
21753 .downcast::<Editor>()
21754 .unwrap();
21755 pane_1.update(cx, |pane, cx| {
21756 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21757 open_editor.update(cx, |editor, cx| {
21758 assert_eq!(
21759 editor.display_text(cx),
21760 main_text,
21761 "Original main.rs text on initial open",
21762 );
21763 assert_eq!(
21764 editor
21765 .selections
21766 .all::<Point>(cx)
21767 .into_iter()
21768 .map(|s| s.range())
21769 .collect::<Vec<_>>(),
21770 vec![Point::zero()..Point::zero()],
21771 "Default selections on initial open",
21772 );
21773 })
21774 });
21775 editor_1.update_in(cx, |editor, window, cx| {
21776 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21777 s.select_ranges(expected_ranges.clone());
21778 });
21779 });
21780
21781 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
21782 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
21783 });
21784 let editor_2 = workspace
21785 .update_in(cx, |workspace, window, cx| {
21786 workspace.open_path(
21787 (worktree_id, "main.rs"),
21788 Some(pane_2.downgrade()),
21789 true,
21790 window,
21791 cx,
21792 )
21793 })
21794 .unwrap()
21795 .await
21796 .downcast::<Editor>()
21797 .unwrap();
21798 pane_2.update(cx, |pane, cx| {
21799 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21800 open_editor.update(cx, |editor, cx| {
21801 assert_eq!(
21802 editor.display_text(cx),
21803 main_text,
21804 "Original main.rs text on initial open in another panel",
21805 );
21806 assert_eq!(
21807 editor
21808 .selections
21809 .all::<Point>(cx)
21810 .into_iter()
21811 .map(|s| s.range())
21812 .collect::<Vec<_>>(),
21813 vec![Point::zero()..Point::zero()],
21814 "Default selections on initial open in another panel",
21815 );
21816 })
21817 });
21818
21819 editor_2.update_in(cx, |editor, window, cx| {
21820 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
21821 });
21822
21823 let _other_editor_1 = workspace
21824 .update_in(cx, |workspace, window, cx| {
21825 workspace.open_path(
21826 (worktree_id, "lib.rs"),
21827 Some(pane_1.downgrade()),
21828 true,
21829 window,
21830 cx,
21831 )
21832 })
21833 .unwrap()
21834 .await
21835 .downcast::<Editor>()
21836 .unwrap();
21837 pane_1
21838 .update_in(cx, |pane, window, cx| {
21839 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
21840 })
21841 .await
21842 .unwrap();
21843 drop(editor_1);
21844 pane_1.update(cx, |pane, cx| {
21845 pane.active_item()
21846 .unwrap()
21847 .downcast::<Editor>()
21848 .unwrap()
21849 .update(cx, |editor, cx| {
21850 assert_eq!(
21851 editor.display_text(cx),
21852 lib_text,
21853 "Other file should be open and active",
21854 );
21855 });
21856 assert_eq!(pane.items().count(), 1, "No other editors should be open");
21857 });
21858
21859 let _other_editor_2 = workspace
21860 .update_in(cx, |workspace, window, cx| {
21861 workspace.open_path(
21862 (worktree_id, "lib.rs"),
21863 Some(pane_2.downgrade()),
21864 true,
21865 window,
21866 cx,
21867 )
21868 })
21869 .unwrap()
21870 .await
21871 .downcast::<Editor>()
21872 .unwrap();
21873 pane_2
21874 .update_in(cx, |pane, window, cx| {
21875 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
21876 })
21877 .await
21878 .unwrap();
21879 drop(editor_2);
21880 pane_2.update(cx, |pane, cx| {
21881 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21882 open_editor.update(cx, |editor, cx| {
21883 assert_eq!(
21884 editor.display_text(cx),
21885 lib_text,
21886 "Other file should be open and active in another panel too",
21887 );
21888 });
21889 assert_eq!(
21890 pane.items().count(),
21891 1,
21892 "No other editors should be open in another pane",
21893 );
21894 });
21895
21896 let _editor_1_reopened = workspace
21897 .update_in(cx, |workspace, window, cx| {
21898 workspace.open_path(
21899 (worktree_id, "main.rs"),
21900 Some(pane_1.downgrade()),
21901 true,
21902 window,
21903 cx,
21904 )
21905 })
21906 .unwrap()
21907 .await
21908 .downcast::<Editor>()
21909 .unwrap();
21910 let _editor_2_reopened = workspace
21911 .update_in(cx, |workspace, window, cx| {
21912 workspace.open_path(
21913 (worktree_id, "main.rs"),
21914 Some(pane_2.downgrade()),
21915 true,
21916 window,
21917 cx,
21918 )
21919 })
21920 .unwrap()
21921 .await
21922 .downcast::<Editor>()
21923 .unwrap();
21924 pane_1.update(cx, |pane, cx| {
21925 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21926 open_editor.update(cx, |editor, cx| {
21927 assert_eq!(
21928 editor.display_text(cx),
21929 main_text,
21930 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
21931 );
21932 assert_eq!(
21933 editor
21934 .selections
21935 .all::<Point>(cx)
21936 .into_iter()
21937 .map(|s| s.range())
21938 .collect::<Vec<_>>(),
21939 expected_ranges,
21940 "Previous editor in the 1st panel had selections and should get them restored on reopen",
21941 );
21942 })
21943 });
21944 pane_2.update(cx, |pane, cx| {
21945 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21946 open_editor.update(cx, |editor, cx| {
21947 assert_eq!(
21948 editor.display_text(cx),
21949 r#"fn main() {
21950⋯rintln!("1");
21951⋯intln!("2");
21952⋯ntln!("3");
21953println!("4");
21954println!("5");
21955}"#,
21956 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
21957 );
21958 assert_eq!(
21959 editor
21960 .selections
21961 .all::<Point>(cx)
21962 .into_iter()
21963 .map(|s| s.range())
21964 .collect::<Vec<_>>(),
21965 vec![Point::zero()..Point::zero()],
21966 "Previous editor in the 2nd pane had no selections changed hence should restore none",
21967 );
21968 })
21969 });
21970}
21971
21972#[gpui::test]
21973async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
21974 init_test(cx, |_| {});
21975
21976 let fs = FakeFs::new(cx.executor());
21977 let main_text = r#"fn main() {
21978println!("1");
21979println!("2");
21980println!("3");
21981println!("4");
21982println!("5");
21983}"#;
21984 let lib_text = "mod foo {}";
21985 fs.insert_tree(
21986 path!("/a"),
21987 json!({
21988 "lib.rs": lib_text,
21989 "main.rs": main_text,
21990 }),
21991 )
21992 .await;
21993
21994 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21995 let (workspace, cx) =
21996 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21997 let worktree_id = workspace.update(cx, |workspace, cx| {
21998 workspace.project().update(cx, |project, cx| {
21999 project.worktrees(cx).next().unwrap().read(cx).id()
22000 })
22001 });
22002
22003 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
22004 let editor = workspace
22005 .update_in(cx, |workspace, window, cx| {
22006 workspace.open_path(
22007 (worktree_id, "main.rs"),
22008 Some(pane.downgrade()),
22009 true,
22010 window,
22011 cx,
22012 )
22013 })
22014 .unwrap()
22015 .await
22016 .downcast::<Editor>()
22017 .unwrap();
22018 pane.update(cx, |pane, cx| {
22019 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22020 open_editor.update(cx, |editor, cx| {
22021 assert_eq!(
22022 editor.display_text(cx),
22023 main_text,
22024 "Original main.rs text on initial open",
22025 );
22026 })
22027 });
22028 editor.update_in(cx, |editor, window, cx| {
22029 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
22030 });
22031
22032 cx.update_global(|store: &mut SettingsStore, cx| {
22033 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
22034 s.restore_on_file_reopen = Some(false);
22035 });
22036 });
22037 editor.update_in(cx, |editor, window, cx| {
22038 editor.fold_ranges(
22039 vec![
22040 Point::new(1, 0)..Point::new(1, 1),
22041 Point::new(2, 0)..Point::new(2, 2),
22042 Point::new(3, 0)..Point::new(3, 3),
22043 ],
22044 false,
22045 window,
22046 cx,
22047 );
22048 });
22049 pane.update_in(cx, |pane, window, cx| {
22050 pane.close_all_items(&CloseAllItems::default(), window, cx)
22051 })
22052 .await
22053 .unwrap();
22054 pane.update(cx, |pane, _| {
22055 assert!(pane.active_item().is_none());
22056 });
22057 cx.update_global(|store: &mut SettingsStore, cx| {
22058 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
22059 s.restore_on_file_reopen = Some(true);
22060 });
22061 });
22062
22063 let _editor_reopened = workspace
22064 .update_in(cx, |workspace, window, cx| {
22065 workspace.open_path(
22066 (worktree_id, "main.rs"),
22067 Some(pane.downgrade()),
22068 true,
22069 window,
22070 cx,
22071 )
22072 })
22073 .unwrap()
22074 .await
22075 .downcast::<Editor>()
22076 .unwrap();
22077 pane.update(cx, |pane, cx| {
22078 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22079 open_editor.update(cx, |editor, cx| {
22080 assert_eq!(
22081 editor.display_text(cx),
22082 main_text,
22083 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
22084 );
22085 })
22086 });
22087}
22088
22089#[gpui::test]
22090async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
22091 struct EmptyModalView {
22092 focus_handle: gpui::FocusHandle,
22093 }
22094 impl EventEmitter<DismissEvent> for EmptyModalView {}
22095 impl Render for EmptyModalView {
22096 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
22097 div()
22098 }
22099 }
22100 impl Focusable for EmptyModalView {
22101 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
22102 self.focus_handle.clone()
22103 }
22104 }
22105 impl workspace::ModalView for EmptyModalView {}
22106 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
22107 EmptyModalView {
22108 focus_handle: cx.focus_handle(),
22109 }
22110 }
22111
22112 init_test(cx, |_| {});
22113
22114 let fs = FakeFs::new(cx.executor());
22115 let project = Project::test(fs, [], cx).await;
22116 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22117 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
22118 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22119 let editor = cx.new_window_entity(|window, cx| {
22120 Editor::new(
22121 EditorMode::full(),
22122 buffer,
22123 Some(project.clone()),
22124 window,
22125 cx,
22126 )
22127 });
22128 workspace
22129 .update(cx, |workspace, window, cx| {
22130 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
22131 })
22132 .unwrap();
22133 editor.update_in(cx, |editor, window, cx| {
22134 editor.open_context_menu(&OpenContextMenu, window, cx);
22135 assert!(editor.mouse_context_menu.is_some());
22136 });
22137 workspace
22138 .update(cx, |workspace, window, cx| {
22139 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
22140 })
22141 .unwrap();
22142 cx.read(|cx| {
22143 assert!(editor.read(cx).mouse_context_menu.is_none());
22144 });
22145}
22146
22147#[gpui::test]
22148async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
22149 init_test(cx, |_| {});
22150
22151 let fs = FakeFs::new(cx.executor());
22152 fs.insert_file(path!("/file.html"), Default::default())
22153 .await;
22154
22155 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
22156
22157 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22158 let html_language = Arc::new(Language::new(
22159 LanguageConfig {
22160 name: "HTML".into(),
22161 matcher: LanguageMatcher {
22162 path_suffixes: vec!["html".to_string()],
22163 ..LanguageMatcher::default()
22164 },
22165 brackets: BracketPairConfig {
22166 pairs: vec![BracketPair {
22167 start: "<".into(),
22168 end: ">".into(),
22169 close: true,
22170 ..Default::default()
22171 }],
22172 ..Default::default()
22173 },
22174 ..Default::default()
22175 },
22176 Some(tree_sitter_html::LANGUAGE.into()),
22177 ));
22178 language_registry.add(html_language);
22179 let mut fake_servers = language_registry.register_fake_lsp(
22180 "HTML",
22181 FakeLspAdapter {
22182 capabilities: lsp::ServerCapabilities {
22183 completion_provider: Some(lsp::CompletionOptions {
22184 resolve_provider: Some(true),
22185 ..Default::default()
22186 }),
22187 ..Default::default()
22188 },
22189 ..Default::default()
22190 },
22191 );
22192
22193 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22194 let cx = &mut VisualTestContext::from_window(*workspace, cx);
22195
22196 let worktree_id = workspace
22197 .update(cx, |workspace, _window, cx| {
22198 workspace.project().update(cx, |project, cx| {
22199 project.worktrees(cx).next().unwrap().read(cx).id()
22200 })
22201 })
22202 .unwrap();
22203 project
22204 .update(cx, |project, cx| {
22205 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
22206 })
22207 .await
22208 .unwrap();
22209 let editor = workspace
22210 .update(cx, |workspace, window, cx| {
22211 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
22212 })
22213 .unwrap()
22214 .await
22215 .unwrap()
22216 .downcast::<Editor>()
22217 .unwrap();
22218
22219 let fake_server = fake_servers.next().await.unwrap();
22220 editor.update_in(cx, |editor, window, cx| {
22221 editor.set_text("<ad></ad>", window, cx);
22222 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
22223 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
22224 });
22225 let Some((buffer, _)) = editor
22226 .buffer
22227 .read(cx)
22228 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
22229 else {
22230 panic!("Failed to get buffer for selection position");
22231 };
22232 let buffer = buffer.read(cx);
22233 let buffer_id = buffer.remote_id();
22234 let opening_range =
22235 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
22236 let closing_range =
22237 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
22238 let mut linked_ranges = HashMap::default();
22239 linked_ranges.insert(
22240 buffer_id,
22241 vec![(opening_range.clone(), vec![closing_range.clone()])],
22242 );
22243 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
22244 });
22245 let mut completion_handle =
22246 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
22247 Ok(Some(lsp::CompletionResponse::Array(vec![
22248 lsp::CompletionItem {
22249 label: "head".to_string(),
22250 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
22251 lsp::InsertReplaceEdit {
22252 new_text: "head".to_string(),
22253 insert: lsp::Range::new(
22254 lsp::Position::new(0, 1),
22255 lsp::Position::new(0, 3),
22256 ),
22257 replace: lsp::Range::new(
22258 lsp::Position::new(0, 1),
22259 lsp::Position::new(0, 3),
22260 ),
22261 },
22262 )),
22263 ..Default::default()
22264 },
22265 ])))
22266 });
22267 editor.update_in(cx, |editor, window, cx| {
22268 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
22269 });
22270 cx.run_until_parked();
22271 completion_handle.next().await.unwrap();
22272 editor.update(cx, |editor, _| {
22273 assert!(
22274 editor.context_menu_visible(),
22275 "Completion menu should be visible"
22276 );
22277 });
22278 editor.update_in(cx, |editor, window, cx| {
22279 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
22280 });
22281 cx.executor().run_until_parked();
22282 editor.update(cx, |editor, cx| {
22283 assert_eq!(editor.text(cx), "<head></head>");
22284 });
22285}
22286
22287#[gpui::test]
22288async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
22289 init_test(cx, |_| {});
22290
22291 let fs = FakeFs::new(cx.executor());
22292 fs.insert_tree(
22293 path!("/root"),
22294 json!({
22295 "a": {
22296 "main.rs": "fn main() {}",
22297 },
22298 "foo": {
22299 "bar": {
22300 "external_file.rs": "pub mod external {}",
22301 }
22302 }
22303 }),
22304 )
22305 .await;
22306
22307 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
22308 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22309 language_registry.add(rust_lang());
22310 let _fake_servers = language_registry.register_fake_lsp(
22311 "Rust",
22312 FakeLspAdapter {
22313 ..FakeLspAdapter::default()
22314 },
22315 );
22316 let (workspace, cx) =
22317 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22318 let worktree_id = workspace.update(cx, |workspace, cx| {
22319 workspace.project().update(cx, |project, cx| {
22320 project.worktrees(cx).next().unwrap().read(cx).id()
22321 })
22322 });
22323
22324 let assert_language_servers_count =
22325 |expected: usize, context: &str, cx: &mut VisualTestContext| {
22326 project.update(cx, |project, cx| {
22327 let current = project
22328 .lsp_store()
22329 .read(cx)
22330 .as_local()
22331 .unwrap()
22332 .language_servers
22333 .len();
22334 assert_eq!(expected, current, "{context}");
22335 });
22336 };
22337
22338 assert_language_servers_count(
22339 0,
22340 "No servers should be running before any file is open",
22341 cx,
22342 );
22343 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
22344 let main_editor = workspace
22345 .update_in(cx, |workspace, window, cx| {
22346 workspace.open_path(
22347 (worktree_id, "main.rs"),
22348 Some(pane.downgrade()),
22349 true,
22350 window,
22351 cx,
22352 )
22353 })
22354 .unwrap()
22355 .await
22356 .downcast::<Editor>()
22357 .unwrap();
22358 pane.update(cx, |pane, cx| {
22359 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22360 open_editor.update(cx, |editor, cx| {
22361 assert_eq!(
22362 editor.display_text(cx),
22363 "fn main() {}",
22364 "Original main.rs text on initial open",
22365 );
22366 });
22367 assert_eq!(open_editor, main_editor);
22368 });
22369 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
22370
22371 let external_editor = workspace
22372 .update_in(cx, |workspace, window, cx| {
22373 workspace.open_abs_path(
22374 PathBuf::from("/root/foo/bar/external_file.rs"),
22375 OpenOptions::default(),
22376 window,
22377 cx,
22378 )
22379 })
22380 .await
22381 .expect("opening external file")
22382 .downcast::<Editor>()
22383 .expect("downcasted external file's open element to editor");
22384 pane.update(cx, |pane, cx| {
22385 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22386 open_editor.update(cx, |editor, cx| {
22387 assert_eq!(
22388 editor.display_text(cx),
22389 "pub mod external {}",
22390 "External file is open now",
22391 );
22392 });
22393 assert_eq!(open_editor, external_editor);
22394 });
22395 assert_language_servers_count(
22396 1,
22397 "Second, external, *.rs file should join the existing server",
22398 cx,
22399 );
22400
22401 pane.update_in(cx, |pane, window, cx| {
22402 pane.close_active_item(&CloseActiveItem::default(), window, cx)
22403 })
22404 .await
22405 .unwrap();
22406 pane.update_in(cx, |pane, window, cx| {
22407 pane.navigate_backward(window, cx);
22408 });
22409 cx.run_until_parked();
22410 pane.update(cx, |pane, cx| {
22411 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22412 open_editor.update(cx, |editor, cx| {
22413 assert_eq!(
22414 editor.display_text(cx),
22415 "pub mod external {}",
22416 "External file is open now",
22417 );
22418 });
22419 });
22420 assert_language_servers_count(
22421 1,
22422 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
22423 cx,
22424 );
22425
22426 cx.update(|_, cx| {
22427 workspace::reload(&workspace::Reload::default(), cx);
22428 });
22429 assert_language_servers_count(
22430 1,
22431 "After reloading the worktree with local and external files opened, only one project should be started",
22432 cx,
22433 );
22434}
22435
22436#[gpui::test]
22437async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
22438 init_test(cx, |_| {});
22439
22440 let mut cx = EditorTestContext::new(cx).await;
22441 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22442 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22443
22444 // test cursor move to start of each line on tab
22445 // for `if`, `elif`, `else`, `while`, `with` and `for`
22446 cx.set_state(indoc! {"
22447 def main():
22448 ˇ for item in items:
22449 ˇ while item.active:
22450 ˇ if item.value > 10:
22451 ˇ continue
22452 ˇ elif item.value < 0:
22453 ˇ break
22454 ˇ else:
22455 ˇ with item.context() as ctx:
22456 ˇ yield count
22457 ˇ else:
22458 ˇ log('while else')
22459 ˇ else:
22460 ˇ log('for else')
22461 "});
22462 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22463 cx.assert_editor_state(indoc! {"
22464 def main():
22465 ˇfor item in items:
22466 ˇwhile item.active:
22467 ˇif item.value > 10:
22468 ˇcontinue
22469 ˇelif item.value < 0:
22470 ˇbreak
22471 ˇelse:
22472 ˇwith item.context() as ctx:
22473 ˇyield count
22474 ˇelse:
22475 ˇlog('while else')
22476 ˇelse:
22477 ˇlog('for else')
22478 "});
22479 // test relative indent is preserved when tab
22480 // for `if`, `elif`, `else`, `while`, `with` and `for`
22481 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22482 cx.assert_editor_state(indoc! {"
22483 def main():
22484 ˇfor item in items:
22485 ˇwhile item.active:
22486 ˇif item.value > 10:
22487 ˇcontinue
22488 ˇelif item.value < 0:
22489 ˇbreak
22490 ˇelse:
22491 ˇwith item.context() as ctx:
22492 ˇyield count
22493 ˇelse:
22494 ˇlog('while else')
22495 ˇelse:
22496 ˇlog('for else')
22497 "});
22498
22499 // test cursor move to start of each line on tab
22500 // for `try`, `except`, `else`, `finally`, `match` and `def`
22501 cx.set_state(indoc! {"
22502 def main():
22503 ˇ try:
22504 ˇ fetch()
22505 ˇ except ValueError:
22506 ˇ handle_error()
22507 ˇ else:
22508 ˇ match value:
22509 ˇ case _:
22510 ˇ finally:
22511 ˇ def status():
22512 ˇ return 0
22513 "});
22514 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22515 cx.assert_editor_state(indoc! {"
22516 def main():
22517 ˇtry:
22518 ˇfetch()
22519 ˇexcept ValueError:
22520 ˇhandle_error()
22521 ˇelse:
22522 ˇmatch value:
22523 ˇcase _:
22524 ˇfinally:
22525 ˇdef status():
22526 ˇreturn 0
22527 "});
22528 // test relative indent is preserved when tab
22529 // for `try`, `except`, `else`, `finally`, `match` and `def`
22530 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22531 cx.assert_editor_state(indoc! {"
22532 def main():
22533 ˇtry:
22534 ˇfetch()
22535 ˇexcept ValueError:
22536 ˇhandle_error()
22537 ˇelse:
22538 ˇmatch value:
22539 ˇcase _:
22540 ˇfinally:
22541 ˇdef status():
22542 ˇreturn 0
22543 "});
22544}
22545
22546#[gpui::test]
22547async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
22548 init_test(cx, |_| {});
22549
22550 let mut cx = EditorTestContext::new(cx).await;
22551 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22552 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22553
22554 // test `else` auto outdents when typed inside `if` block
22555 cx.set_state(indoc! {"
22556 def main():
22557 if i == 2:
22558 return
22559 ˇ
22560 "});
22561 cx.update_editor(|editor, window, cx| {
22562 editor.handle_input("else:", window, cx);
22563 });
22564 cx.assert_editor_state(indoc! {"
22565 def main():
22566 if i == 2:
22567 return
22568 else:ˇ
22569 "});
22570
22571 // test `except` auto outdents when typed inside `try` block
22572 cx.set_state(indoc! {"
22573 def main():
22574 try:
22575 i = 2
22576 ˇ
22577 "});
22578 cx.update_editor(|editor, window, cx| {
22579 editor.handle_input("except:", window, cx);
22580 });
22581 cx.assert_editor_state(indoc! {"
22582 def main():
22583 try:
22584 i = 2
22585 except:ˇ
22586 "});
22587
22588 // test `else` auto outdents when typed inside `except` block
22589 cx.set_state(indoc! {"
22590 def main():
22591 try:
22592 i = 2
22593 except:
22594 j = 2
22595 ˇ
22596 "});
22597 cx.update_editor(|editor, window, cx| {
22598 editor.handle_input("else:", window, cx);
22599 });
22600 cx.assert_editor_state(indoc! {"
22601 def main():
22602 try:
22603 i = 2
22604 except:
22605 j = 2
22606 else:ˇ
22607 "});
22608
22609 // test `finally` auto outdents when typed inside `else` block
22610 cx.set_state(indoc! {"
22611 def main():
22612 try:
22613 i = 2
22614 except:
22615 j = 2
22616 else:
22617 k = 2
22618 ˇ
22619 "});
22620 cx.update_editor(|editor, window, cx| {
22621 editor.handle_input("finally:", window, cx);
22622 });
22623 cx.assert_editor_state(indoc! {"
22624 def main():
22625 try:
22626 i = 2
22627 except:
22628 j = 2
22629 else:
22630 k = 2
22631 finally:ˇ
22632 "});
22633
22634 // test `else` does not outdents when typed inside `except` block right after for block
22635 cx.set_state(indoc! {"
22636 def main():
22637 try:
22638 i = 2
22639 except:
22640 for i in range(n):
22641 pass
22642 ˇ
22643 "});
22644 cx.update_editor(|editor, window, cx| {
22645 editor.handle_input("else:", window, cx);
22646 });
22647 cx.assert_editor_state(indoc! {"
22648 def main():
22649 try:
22650 i = 2
22651 except:
22652 for i in range(n):
22653 pass
22654 else:ˇ
22655 "});
22656
22657 // test `finally` auto outdents when typed inside `else` block right after for block
22658 cx.set_state(indoc! {"
22659 def main():
22660 try:
22661 i = 2
22662 except:
22663 j = 2
22664 else:
22665 for i in range(n):
22666 pass
22667 ˇ
22668 "});
22669 cx.update_editor(|editor, window, cx| {
22670 editor.handle_input("finally:", window, cx);
22671 });
22672 cx.assert_editor_state(indoc! {"
22673 def main():
22674 try:
22675 i = 2
22676 except:
22677 j = 2
22678 else:
22679 for i in range(n):
22680 pass
22681 finally:ˇ
22682 "});
22683
22684 // test `except` outdents to inner "try" block
22685 cx.set_state(indoc! {"
22686 def main():
22687 try:
22688 i = 2
22689 if i == 2:
22690 try:
22691 i = 3
22692 ˇ
22693 "});
22694 cx.update_editor(|editor, window, cx| {
22695 editor.handle_input("except:", window, cx);
22696 });
22697 cx.assert_editor_state(indoc! {"
22698 def main():
22699 try:
22700 i = 2
22701 if i == 2:
22702 try:
22703 i = 3
22704 except:ˇ
22705 "});
22706
22707 // test `except` outdents to outer "try" block
22708 cx.set_state(indoc! {"
22709 def main():
22710 try:
22711 i = 2
22712 if i == 2:
22713 try:
22714 i = 3
22715 ˇ
22716 "});
22717 cx.update_editor(|editor, window, cx| {
22718 editor.handle_input("except:", window, cx);
22719 });
22720 cx.assert_editor_state(indoc! {"
22721 def main():
22722 try:
22723 i = 2
22724 if i == 2:
22725 try:
22726 i = 3
22727 except:ˇ
22728 "});
22729
22730 // test `else` stays at correct indent when typed after `for` block
22731 cx.set_state(indoc! {"
22732 def main():
22733 for i in range(10):
22734 if i == 3:
22735 break
22736 ˇ
22737 "});
22738 cx.update_editor(|editor, window, cx| {
22739 editor.handle_input("else:", window, cx);
22740 });
22741 cx.assert_editor_state(indoc! {"
22742 def main():
22743 for i in range(10):
22744 if i == 3:
22745 break
22746 else:ˇ
22747 "});
22748
22749 // test does not outdent on typing after line with square brackets
22750 cx.set_state(indoc! {"
22751 def f() -> list[str]:
22752 ˇ
22753 "});
22754 cx.update_editor(|editor, window, cx| {
22755 editor.handle_input("a", window, cx);
22756 });
22757 cx.assert_editor_state(indoc! {"
22758 def f() -> list[str]:
22759 aˇ
22760 "});
22761
22762 // test does not outdent on typing : after case keyword
22763 cx.set_state(indoc! {"
22764 match 1:
22765 caseˇ
22766 "});
22767 cx.update_editor(|editor, window, cx| {
22768 editor.handle_input(":", window, cx);
22769 });
22770 cx.assert_editor_state(indoc! {"
22771 match 1:
22772 case:ˇ
22773 "});
22774}
22775
22776#[gpui::test]
22777async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
22778 init_test(cx, |_| {});
22779 update_test_language_settings(cx, |settings| {
22780 settings.defaults.extend_comment_on_newline = Some(false);
22781 });
22782 let mut cx = EditorTestContext::new(cx).await;
22783 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22784 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22785
22786 // test correct indent after newline on comment
22787 cx.set_state(indoc! {"
22788 # COMMENT:ˇ
22789 "});
22790 cx.update_editor(|editor, window, cx| {
22791 editor.newline(&Newline, window, cx);
22792 });
22793 cx.assert_editor_state(indoc! {"
22794 # COMMENT:
22795 ˇ
22796 "});
22797
22798 // test correct indent after newline in brackets
22799 cx.set_state(indoc! {"
22800 {ˇ}
22801 "});
22802 cx.update_editor(|editor, window, cx| {
22803 editor.newline(&Newline, window, cx);
22804 });
22805 cx.run_until_parked();
22806 cx.assert_editor_state(indoc! {"
22807 {
22808 ˇ
22809 }
22810 "});
22811
22812 cx.set_state(indoc! {"
22813 (ˇ)
22814 "});
22815 cx.update_editor(|editor, window, cx| {
22816 editor.newline(&Newline, window, cx);
22817 });
22818 cx.run_until_parked();
22819 cx.assert_editor_state(indoc! {"
22820 (
22821 ˇ
22822 )
22823 "});
22824
22825 // do not indent after empty lists or dictionaries
22826 cx.set_state(indoc! {"
22827 a = []ˇ
22828 "});
22829 cx.update_editor(|editor, window, cx| {
22830 editor.newline(&Newline, window, cx);
22831 });
22832 cx.run_until_parked();
22833 cx.assert_editor_state(indoc! {"
22834 a = []
22835 ˇ
22836 "});
22837}
22838
22839#[gpui::test]
22840async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
22841 init_test(cx, |_| {});
22842
22843 let mut cx = EditorTestContext::new(cx).await;
22844 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
22845 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22846
22847 // test cursor move to start of each line on tab
22848 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
22849 cx.set_state(indoc! {"
22850 function main() {
22851 ˇ for item in $items; do
22852 ˇ while [ -n \"$item\" ]; do
22853 ˇ if [ \"$value\" -gt 10 ]; then
22854 ˇ continue
22855 ˇ elif [ \"$value\" -lt 0 ]; then
22856 ˇ break
22857 ˇ else
22858 ˇ echo \"$item\"
22859 ˇ fi
22860 ˇ done
22861 ˇ done
22862 ˇ}
22863 "});
22864 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22865 cx.assert_editor_state(indoc! {"
22866 function main() {
22867 ˇfor item in $items; do
22868 ˇwhile [ -n \"$item\" ]; do
22869 ˇif [ \"$value\" -gt 10 ]; then
22870 ˇcontinue
22871 ˇelif [ \"$value\" -lt 0 ]; then
22872 ˇbreak
22873 ˇelse
22874 ˇecho \"$item\"
22875 ˇfi
22876 ˇdone
22877 ˇdone
22878 ˇ}
22879 "});
22880 // test relative indent is preserved when tab
22881 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22882 cx.assert_editor_state(indoc! {"
22883 function main() {
22884 ˇfor item in $items; do
22885 ˇwhile [ -n \"$item\" ]; do
22886 ˇif [ \"$value\" -gt 10 ]; then
22887 ˇcontinue
22888 ˇelif [ \"$value\" -lt 0 ]; then
22889 ˇbreak
22890 ˇelse
22891 ˇecho \"$item\"
22892 ˇfi
22893 ˇdone
22894 ˇdone
22895 ˇ}
22896 "});
22897
22898 // test cursor move to start of each line on tab
22899 // for `case` statement with patterns
22900 cx.set_state(indoc! {"
22901 function handle() {
22902 ˇ case \"$1\" in
22903 ˇ start)
22904 ˇ echo \"a\"
22905 ˇ ;;
22906 ˇ stop)
22907 ˇ echo \"b\"
22908 ˇ ;;
22909 ˇ *)
22910 ˇ echo \"c\"
22911 ˇ ;;
22912 ˇ esac
22913 ˇ}
22914 "});
22915 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22916 cx.assert_editor_state(indoc! {"
22917 function handle() {
22918 ˇcase \"$1\" in
22919 ˇstart)
22920 ˇecho \"a\"
22921 ˇ;;
22922 ˇstop)
22923 ˇecho \"b\"
22924 ˇ;;
22925 ˇ*)
22926 ˇecho \"c\"
22927 ˇ;;
22928 ˇesac
22929 ˇ}
22930 "});
22931}
22932
22933#[gpui::test]
22934async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
22935 init_test(cx, |_| {});
22936
22937 let mut cx = EditorTestContext::new(cx).await;
22938 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
22939 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22940
22941 // test indents on comment insert
22942 cx.set_state(indoc! {"
22943 function main() {
22944 ˇ for item in $items; do
22945 ˇ while [ -n \"$item\" ]; do
22946 ˇ if [ \"$value\" -gt 10 ]; then
22947 ˇ continue
22948 ˇ elif [ \"$value\" -lt 0 ]; then
22949 ˇ break
22950 ˇ else
22951 ˇ echo \"$item\"
22952 ˇ fi
22953 ˇ done
22954 ˇ done
22955 ˇ}
22956 "});
22957 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
22958 cx.assert_editor_state(indoc! {"
22959 function main() {
22960 #ˇ for item in $items; do
22961 #ˇ while [ -n \"$item\" ]; do
22962 #ˇ if [ \"$value\" -gt 10 ]; then
22963 #ˇ continue
22964 #ˇ elif [ \"$value\" -lt 0 ]; then
22965 #ˇ break
22966 #ˇ else
22967 #ˇ echo \"$item\"
22968 #ˇ fi
22969 #ˇ done
22970 #ˇ done
22971 #ˇ}
22972 "});
22973}
22974
22975#[gpui::test]
22976async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
22977 init_test(cx, |_| {});
22978
22979 let mut cx = EditorTestContext::new(cx).await;
22980 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
22981 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22982
22983 // test `else` auto outdents when typed inside `if` block
22984 cx.set_state(indoc! {"
22985 if [ \"$1\" = \"test\" ]; then
22986 echo \"foo bar\"
22987 ˇ
22988 "});
22989 cx.update_editor(|editor, window, cx| {
22990 editor.handle_input("else", window, cx);
22991 });
22992 cx.assert_editor_state(indoc! {"
22993 if [ \"$1\" = \"test\" ]; then
22994 echo \"foo bar\"
22995 elseˇ
22996 "});
22997
22998 // test `elif` auto outdents when typed inside `if` block
22999 cx.set_state(indoc! {"
23000 if [ \"$1\" = \"test\" ]; then
23001 echo \"foo bar\"
23002 ˇ
23003 "});
23004 cx.update_editor(|editor, window, cx| {
23005 editor.handle_input("elif", window, cx);
23006 });
23007 cx.assert_editor_state(indoc! {"
23008 if [ \"$1\" = \"test\" ]; then
23009 echo \"foo bar\"
23010 elifˇ
23011 "});
23012
23013 // test `fi` auto outdents when typed inside `else` block
23014 cx.set_state(indoc! {"
23015 if [ \"$1\" = \"test\" ]; then
23016 echo \"foo bar\"
23017 else
23018 echo \"bar baz\"
23019 ˇ
23020 "});
23021 cx.update_editor(|editor, window, cx| {
23022 editor.handle_input("fi", window, cx);
23023 });
23024 cx.assert_editor_state(indoc! {"
23025 if [ \"$1\" = \"test\" ]; then
23026 echo \"foo bar\"
23027 else
23028 echo \"bar baz\"
23029 fiˇ
23030 "});
23031
23032 // test `done` auto outdents when typed inside `while` block
23033 cx.set_state(indoc! {"
23034 while read line; do
23035 echo \"$line\"
23036 ˇ
23037 "});
23038 cx.update_editor(|editor, window, cx| {
23039 editor.handle_input("done", window, cx);
23040 });
23041 cx.assert_editor_state(indoc! {"
23042 while read line; do
23043 echo \"$line\"
23044 doneˇ
23045 "});
23046
23047 // test `done` auto outdents when typed inside `for` block
23048 cx.set_state(indoc! {"
23049 for file in *.txt; do
23050 cat \"$file\"
23051 ˇ
23052 "});
23053 cx.update_editor(|editor, window, cx| {
23054 editor.handle_input("done", window, cx);
23055 });
23056 cx.assert_editor_state(indoc! {"
23057 for file in *.txt; do
23058 cat \"$file\"
23059 doneˇ
23060 "});
23061
23062 // test `esac` auto outdents when typed inside `case` block
23063 cx.set_state(indoc! {"
23064 case \"$1\" in
23065 start)
23066 echo \"foo bar\"
23067 ;;
23068 stop)
23069 echo \"bar baz\"
23070 ;;
23071 ˇ
23072 "});
23073 cx.update_editor(|editor, window, cx| {
23074 editor.handle_input("esac", window, cx);
23075 });
23076 cx.assert_editor_state(indoc! {"
23077 case \"$1\" in
23078 start)
23079 echo \"foo bar\"
23080 ;;
23081 stop)
23082 echo \"bar baz\"
23083 ;;
23084 esacˇ
23085 "});
23086
23087 // test `*)` auto outdents when typed inside `case` block
23088 cx.set_state(indoc! {"
23089 case \"$1\" in
23090 start)
23091 echo \"foo bar\"
23092 ;;
23093 ˇ
23094 "});
23095 cx.update_editor(|editor, window, cx| {
23096 editor.handle_input("*)", window, cx);
23097 });
23098 cx.assert_editor_state(indoc! {"
23099 case \"$1\" in
23100 start)
23101 echo \"foo bar\"
23102 ;;
23103 *)ˇ
23104 "});
23105
23106 // test `fi` outdents to correct level with nested if blocks
23107 cx.set_state(indoc! {"
23108 if [ \"$1\" = \"test\" ]; then
23109 echo \"outer if\"
23110 if [ \"$2\" = \"debug\" ]; then
23111 echo \"inner if\"
23112 ˇ
23113 "});
23114 cx.update_editor(|editor, window, cx| {
23115 editor.handle_input("fi", window, cx);
23116 });
23117 cx.assert_editor_state(indoc! {"
23118 if [ \"$1\" = \"test\" ]; then
23119 echo \"outer if\"
23120 if [ \"$2\" = \"debug\" ]; then
23121 echo \"inner if\"
23122 fiˇ
23123 "});
23124}
23125
23126#[gpui::test]
23127async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
23128 init_test(cx, |_| {});
23129 update_test_language_settings(cx, |settings| {
23130 settings.defaults.extend_comment_on_newline = Some(false);
23131 });
23132 let mut cx = EditorTestContext::new(cx).await;
23133 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
23134 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23135
23136 // test correct indent after newline on comment
23137 cx.set_state(indoc! {"
23138 # COMMENT:ˇ
23139 "});
23140 cx.update_editor(|editor, window, cx| {
23141 editor.newline(&Newline, window, cx);
23142 });
23143 cx.assert_editor_state(indoc! {"
23144 # COMMENT:
23145 ˇ
23146 "});
23147
23148 // test correct indent after newline after `then`
23149 cx.set_state(indoc! {"
23150
23151 if [ \"$1\" = \"test\" ]; thenˇ
23152 "});
23153 cx.update_editor(|editor, window, cx| {
23154 editor.newline(&Newline, window, cx);
23155 });
23156 cx.run_until_parked();
23157 cx.assert_editor_state(indoc! {"
23158
23159 if [ \"$1\" = \"test\" ]; then
23160 ˇ
23161 "});
23162
23163 // test correct indent after newline after `else`
23164 cx.set_state(indoc! {"
23165 if [ \"$1\" = \"test\" ]; then
23166 elseˇ
23167 "});
23168 cx.update_editor(|editor, window, cx| {
23169 editor.newline(&Newline, window, cx);
23170 });
23171 cx.run_until_parked();
23172 cx.assert_editor_state(indoc! {"
23173 if [ \"$1\" = \"test\" ]; then
23174 else
23175 ˇ
23176 "});
23177
23178 // test correct indent after newline after `elif`
23179 cx.set_state(indoc! {"
23180 if [ \"$1\" = \"test\" ]; then
23181 elifˇ
23182 "});
23183 cx.update_editor(|editor, window, cx| {
23184 editor.newline(&Newline, window, cx);
23185 });
23186 cx.run_until_parked();
23187 cx.assert_editor_state(indoc! {"
23188 if [ \"$1\" = \"test\" ]; then
23189 elif
23190 ˇ
23191 "});
23192
23193 // test correct indent after newline after `do`
23194 cx.set_state(indoc! {"
23195 for file in *.txt; doˇ
23196 "});
23197 cx.update_editor(|editor, window, cx| {
23198 editor.newline(&Newline, window, cx);
23199 });
23200 cx.run_until_parked();
23201 cx.assert_editor_state(indoc! {"
23202 for file in *.txt; do
23203 ˇ
23204 "});
23205
23206 // test correct indent after newline after case pattern
23207 cx.set_state(indoc! {"
23208 case \"$1\" in
23209 start)ˇ
23210 "});
23211 cx.update_editor(|editor, window, cx| {
23212 editor.newline(&Newline, window, cx);
23213 });
23214 cx.run_until_parked();
23215 cx.assert_editor_state(indoc! {"
23216 case \"$1\" in
23217 start)
23218 ˇ
23219 "});
23220
23221 // test correct indent after newline after case pattern
23222 cx.set_state(indoc! {"
23223 case \"$1\" in
23224 start)
23225 ;;
23226 *)ˇ
23227 "});
23228 cx.update_editor(|editor, window, cx| {
23229 editor.newline(&Newline, window, cx);
23230 });
23231 cx.run_until_parked();
23232 cx.assert_editor_state(indoc! {"
23233 case \"$1\" in
23234 start)
23235 ;;
23236 *)
23237 ˇ
23238 "});
23239
23240 // test correct indent after newline after function opening brace
23241 cx.set_state(indoc! {"
23242 function test() {ˇ}
23243 "});
23244 cx.update_editor(|editor, window, cx| {
23245 editor.newline(&Newline, window, cx);
23246 });
23247 cx.run_until_parked();
23248 cx.assert_editor_state(indoc! {"
23249 function test() {
23250 ˇ
23251 }
23252 "});
23253
23254 // test no extra indent after semicolon on same line
23255 cx.set_state(indoc! {"
23256 echo \"test\";ˇ
23257 "});
23258 cx.update_editor(|editor, window, cx| {
23259 editor.newline(&Newline, window, cx);
23260 });
23261 cx.run_until_parked();
23262 cx.assert_editor_state(indoc! {"
23263 echo \"test\";
23264 ˇ
23265 "});
23266}
23267
23268fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
23269 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
23270 point..point
23271}
23272
23273#[track_caller]
23274fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
23275 let (text, ranges) = marked_text_ranges(marked_text, true);
23276 assert_eq!(editor.text(cx), text);
23277 assert_eq!(
23278 editor.selections.ranges(cx),
23279 ranges,
23280 "Assert selections are {}",
23281 marked_text
23282 );
23283}
23284
23285pub fn handle_signature_help_request(
23286 cx: &mut EditorLspTestContext,
23287 mocked_response: lsp::SignatureHelp,
23288) -> impl Future<Output = ()> + use<> {
23289 let mut request =
23290 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
23291 let mocked_response = mocked_response.clone();
23292 async move { Ok(Some(mocked_response)) }
23293 });
23294
23295 async move {
23296 request.next().await;
23297 }
23298}
23299
23300#[track_caller]
23301pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
23302 cx.update_editor(|editor, _, _| {
23303 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
23304 let entries = menu.entries.borrow();
23305 let entries = entries
23306 .iter()
23307 .map(|entry| entry.string.as_str())
23308 .collect::<Vec<_>>();
23309 assert_eq!(entries, expected);
23310 } else {
23311 panic!("Expected completions menu");
23312 }
23313 });
23314}
23315
23316/// Handle completion request passing a marked string specifying where the completion
23317/// should be triggered from using '|' character, what range should be replaced, and what completions
23318/// should be returned using '<' and '>' to delimit the range.
23319///
23320/// Also see `handle_completion_request_with_insert_and_replace`.
23321#[track_caller]
23322pub fn handle_completion_request(
23323 marked_string: &str,
23324 completions: Vec<&'static str>,
23325 is_incomplete: bool,
23326 counter: Arc<AtomicUsize>,
23327 cx: &mut EditorLspTestContext,
23328) -> impl Future<Output = ()> {
23329 let complete_from_marker: TextRangeMarker = '|'.into();
23330 let replace_range_marker: TextRangeMarker = ('<', '>').into();
23331 let (_, mut marked_ranges) = marked_text_ranges_by(
23332 marked_string,
23333 vec![complete_from_marker.clone(), replace_range_marker.clone()],
23334 );
23335
23336 let complete_from_position =
23337 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
23338 let replace_range =
23339 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
23340
23341 let mut request =
23342 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
23343 let completions = completions.clone();
23344 counter.fetch_add(1, atomic::Ordering::Release);
23345 async move {
23346 assert_eq!(params.text_document_position.text_document.uri, url.clone());
23347 assert_eq!(
23348 params.text_document_position.position,
23349 complete_from_position
23350 );
23351 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
23352 is_incomplete: is_incomplete,
23353 item_defaults: None,
23354 items: completions
23355 .iter()
23356 .map(|completion_text| lsp::CompletionItem {
23357 label: completion_text.to_string(),
23358 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
23359 range: replace_range,
23360 new_text: completion_text.to_string(),
23361 })),
23362 ..Default::default()
23363 })
23364 .collect(),
23365 })))
23366 }
23367 });
23368
23369 async move {
23370 request.next().await;
23371 }
23372}
23373
23374/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
23375/// given instead, which also contains an `insert` range.
23376///
23377/// This function uses markers to define ranges:
23378/// - `|` marks the cursor position
23379/// - `<>` marks the replace range
23380/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
23381pub fn handle_completion_request_with_insert_and_replace(
23382 cx: &mut EditorLspTestContext,
23383 marked_string: &str,
23384 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
23385 counter: Arc<AtomicUsize>,
23386) -> impl Future<Output = ()> {
23387 let complete_from_marker: TextRangeMarker = '|'.into();
23388 let replace_range_marker: TextRangeMarker = ('<', '>').into();
23389 let insert_range_marker: TextRangeMarker = ('{', '}').into();
23390
23391 let (_, mut marked_ranges) = marked_text_ranges_by(
23392 marked_string,
23393 vec![
23394 complete_from_marker.clone(),
23395 replace_range_marker.clone(),
23396 insert_range_marker.clone(),
23397 ],
23398 );
23399
23400 let complete_from_position =
23401 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
23402 let replace_range =
23403 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
23404
23405 let insert_range = match marked_ranges.remove(&insert_range_marker) {
23406 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
23407 _ => lsp::Range {
23408 start: replace_range.start,
23409 end: complete_from_position,
23410 },
23411 };
23412
23413 let mut request =
23414 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
23415 let completions = completions.clone();
23416 counter.fetch_add(1, atomic::Ordering::Release);
23417 async move {
23418 assert_eq!(params.text_document_position.text_document.uri, url.clone());
23419 assert_eq!(
23420 params.text_document_position.position, complete_from_position,
23421 "marker `|` position doesn't match",
23422 );
23423 Ok(Some(lsp::CompletionResponse::Array(
23424 completions
23425 .iter()
23426 .map(|(label, new_text)| lsp::CompletionItem {
23427 label: label.to_string(),
23428 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
23429 lsp::InsertReplaceEdit {
23430 insert: insert_range,
23431 replace: replace_range,
23432 new_text: new_text.to_string(),
23433 },
23434 )),
23435 ..Default::default()
23436 })
23437 .collect(),
23438 )))
23439 }
23440 });
23441
23442 async move {
23443 request.next().await;
23444 }
23445}
23446
23447fn handle_resolve_completion_request(
23448 cx: &mut EditorLspTestContext,
23449 edits: Option<Vec<(&'static str, &'static str)>>,
23450) -> impl Future<Output = ()> {
23451 let edits = edits.map(|edits| {
23452 edits
23453 .iter()
23454 .map(|(marked_string, new_text)| {
23455 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
23456 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
23457 lsp::TextEdit::new(replace_range, new_text.to_string())
23458 })
23459 .collect::<Vec<_>>()
23460 });
23461
23462 let mut request =
23463 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
23464 let edits = edits.clone();
23465 async move {
23466 Ok(lsp::CompletionItem {
23467 additional_text_edits: edits,
23468 ..Default::default()
23469 })
23470 }
23471 });
23472
23473 async move {
23474 request.next().await;
23475 }
23476}
23477
23478pub(crate) fn update_test_language_settings(
23479 cx: &mut TestAppContext,
23480 f: impl Fn(&mut AllLanguageSettingsContent),
23481) {
23482 cx.update(|cx| {
23483 SettingsStore::update_global(cx, |store, cx| {
23484 store.update_user_settings::<AllLanguageSettings>(cx, f);
23485 });
23486 });
23487}
23488
23489pub(crate) fn update_test_project_settings(
23490 cx: &mut TestAppContext,
23491 f: impl Fn(&mut ProjectSettings),
23492) {
23493 cx.update(|cx| {
23494 SettingsStore::update_global(cx, |store, cx| {
23495 store.update_user_settings::<ProjectSettings>(cx, f);
23496 });
23497 });
23498}
23499
23500pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
23501 cx.update(|cx| {
23502 assets::Assets.load_test_fonts(cx);
23503 let store = SettingsStore::test(cx);
23504 cx.set_global(store);
23505 theme::init(theme::LoadThemes::JustBase, cx);
23506 release_channel::init(SemanticVersion::default(), cx);
23507 client::init_settings(cx);
23508 language::init(cx);
23509 Project::init_settings(cx);
23510 workspace::init_settings(cx);
23511 crate::init(cx);
23512 });
23513 zlog::init_test();
23514 update_test_language_settings(cx, f);
23515}
23516
23517#[track_caller]
23518fn assert_hunk_revert(
23519 not_reverted_text_with_selections: &str,
23520 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
23521 expected_reverted_text_with_selections: &str,
23522 base_text: &str,
23523 cx: &mut EditorLspTestContext,
23524) {
23525 cx.set_state(not_reverted_text_with_selections);
23526 cx.set_head_text(base_text);
23527 cx.executor().run_until_parked();
23528
23529 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
23530 let snapshot = editor.snapshot(window, cx);
23531 let reverted_hunk_statuses = snapshot
23532 .buffer_snapshot
23533 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
23534 .map(|hunk| hunk.status().kind)
23535 .collect::<Vec<_>>();
23536
23537 editor.git_restore(&Default::default(), window, cx);
23538 reverted_hunk_statuses
23539 });
23540 cx.executor().run_until_parked();
23541 cx.assert_editor_state(expected_reverted_text_with_selections);
23542 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
23543}
23544
23545#[gpui::test(iterations = 10)]
23546async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
23547 init_test(cx, |_| {});
23548
23549 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
23550 let counter = diagnostic_requests.clone();
23551
23552 let fs = FakeFs::new(cx.executor());
23553 fs.insert_tree(
23554 path!("/a"),
23555 json!({
23556 "first.rs": "fn main() { let a = 5; }",
23557 "second.rs": "// Test file",
23558 }),
23559 )
23560 .await;
23561
23562 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23563 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23564 let cx = &mut VisualTestContext::from_window(*workspace, cx);
23565
23566 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23567 language_registry.add(rust_lang());
23568 let mut fake_servers = language_registry.register_fake_lsp(
23569 "Rust",
23570 FakeLspAdapter {
23571 capabilities: lsp::ServerCapabilities {
23572 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
23573 lsp::DiagnosticOptions {
23574 identifier: None,
23575 inter_file_dependencies: true,
23576 workspace_diagnostics: true,
23577 work_done_progress_options: Default::default(),
23578 },
23579 )),
23580 ..Default::default()
23581 },
23582 ..Default::default()
23583 },
23584 );
23585
23586 let editor = workspace
23587 .update(cx, |workspace, window, cx| {
23588 workspace.open_abs_path(
23589 PathBuf::from(path!("/a/first.rs")),
23590 OpenOptions::default(),
23591 window,
23592 cx,
23593 )
23594 })
23595 .unwrap()
23596 .await
23597 .unwrap()
23598 .downcast::<Editor>()
23599 .unwrap();
23600 let fake_server = fake_servers.next().await.unwrap();
23601 let server_id = fake_server.server.server_id();
23602 let mut first_request = fake_server
23603 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
23604 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
23605 let result_id = Some(new_result_id.to_string());
23606 assert_eq!(
23607 params.text_document.uri,
23608 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
23609 );
23610 async move {
23611 Ok(lsp::DocumentDiagnosticReportResult::Report(
23612 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
23613 related_documents: None,
23614 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
23615 items: Vec::new(),
23616 result_id,
23617 },
23618 }),
23619 ))
23620 }
23621 });
23622
23623 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
23624 project.update(cx, |project, cx| {
23625 let buffer_id = editor
23626 .read(cx)
23627 .buffer()
23628 .read(cx)
23629 .as_singleton()
23630 .expect("created a singleton buffer")
23631 .read(cx)
23632 .remote_id();
23633 let buffer_result_id = project
23634 .lsp_store()
23635 .read(cx)
23636 .result_id(server_id, buffer_id, cx);
23637 assert_eq!(expected, buffer_result_id);
23638 });
23639 };
23640
23641 ensure_result_id(None, cx);
23642 cx.executor().advance_clock(Duration::from_millis(60));
23643 cx.executor().run_until_parked();
23644 assert_eq!(
23645 diagnostic_requests.load(atomic::Ordering::Acquire),
23646 1,
23647 "Opening file should trigger diagnostic request"
23648 );
23649 first_request
23650 .next()
23651 .await
23652 .expect("should have sent the first diagnostics pull request");
23653 ensure_result_id(Some("1".to_string()), cx);
23654
23655 // Editing should trigger diagnostics
23656 editor.update_in(cx, |editor, window, cx| {
23657 editor.handle_input("2", window, cx)
23658 });
23659 cx.executor().advance_clock(Duration::from_millis(60));
23660 cx.executor().run_until_parked();
23661 assert_eq!(
23662 diagnostic_requests.load(atomic::Ordering::Acquire),
23663 2,
23664 "Editing should trigger diagnostic request"
23665 );
23666 ensure_result_id(Some("2".to_string()), cx);
23667
23668 // Moving cursor should not trigger diagnostic request
23669 editor.update_in(cx, |editor, window, cx| {
23670 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23671 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
23672 });
23673 });
23674 cx.executor().advance_clock(Duration::from_millis(60));
23675 cx.executor().run_until_parked();
23676 assert_eq!(
23677 diagnostic_requests.load(atomic::Ordering::Acquire),
23678 2,
23679 "Cursor movement should not trigger diagnostic request"
23680 );
23681 ensure_result_id(Some("2".to_string()), cx);
23682 // Multiple rapid edits should be debounced
23683 for _ in 0..5 {
23684 editor.update_in(cx, |editor, window, cx| {
23685 editor.handle_input("x", window, cx)
23686 });
23687 }
23688 cx.executor().advance_clock(Duration::from_millis(60));
23689 cx.executor().run_until_parked();
23690
23691 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
23692 assert!(
23693 final_requests <= 4,
23694 "Multiple rapid edits should be debounced (got {final_requests} requests)",
23695 );
23696 ensure_result_id(Some(final_requests.to_string()), cx);
23697}
23698
23699#[gpui::test]
23700async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
23701 // Regression test for issue #11671
23702 // Previously, adding a cursor after moving multiple cursors would reset
23703 // the cursor count instead of adding to the existing cursors.
23704 init_test(cx, |_| {});
23705 let mut cx = EditorTestContext::new(cx).await;
23706
23707 // Create a simple buffer with cursor at start
23708 cx.set_state(indoc! {"
23709 ˇaaaa
23710 bbbb
23711 cccc
23712 dddd
23713 eeee
23714 ffff
23715 gggg
23716 hhhh"});
23717
23718 // Add 2 cursors below (so we have 3 total)
23719 cx.update_editor(|editor, window, cx| {
23720 editor.add_selection_below(&Default::default(), window, cx);
23721 editor.add_selection_below(&Default::default(), window, cx);
23722 });
23723
23724 // Verify we have 3 cursors
23725 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
23726 assert_eq!(
23727 initial_count, 3,
23728 "Should have 3 cursors after adding 2 below"
23729 );
23730
23731 // Move down one line
23732 cx.update_editor(|editor, window, cx| {
23733 editor.move_down(&MoveDown, window, cx);
23734 });
23735
23736 // Add another cursor below
23737 cx.update_editor(|editor, window, cx| {
23738 editor.add_selection_below(&Default::default(), window, cx);
23739 });
23740
23741 // Should now have 4 cursors (3 original + 1 new)
23742 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
23743 assert_eq!(
23744 final_count, 4,
23745 "Should have 4 cursors after moving and adding another"
23746 );
23747}
23748
23749#[gpui::test(iterations = 10)]
23750async fn test_document_colors(cx: &mut TestAppContext) {
23751 let expected_color = Rgba {
23752 r: 0.33,
23753 g: 0.33,
23754 b: 0.33,
23755 a: 0.33,
23756 };
23757
23758 init_test(cx, |_| {});
23759
23760 let fs = FakeFs::new(cx.executor());
23761 fs.insert_tree(
23762 path!("/a"),
23763 json!({
23764 "first.rs": "fn main() { let a = 5; }",
23765 }),
23766 )
23767 .await;
23768
23769 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23770 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23771 let cx = &mut VisualTestContext::from_window(*workspace, cx);
23772
23773 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23774 language_registry.add(rust_lang());
23775 let mut fake_servers = language_registry.register_fake_lsp(
23776 "Rust",
23777 FakeLspAdapter {
23778 capabilities: lsp::ServerCapabilities {
23779 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
23780 ..lsp::ServerCapabilities::default()
23781 },
23782 name: "rust-analyzer",
23783 ..FakeLspAdapter::default()
23784 },
23785 );
23786 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
23787 "Rust",
23788 FakeLspAdapter {
23789 capabilities: lsp::ServerCapabilities {
23790 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
23791 ..lsp::ServerCapabilities::default()
23792 },
23793 name: "not-rust-analyzer",
23794 ..FakeLspAdapter::default()
23795 },
23796 );
23797
23798 let editor = workspace
23799 .update(cx, |workspace, window, cx| {
23800 workspace.open_abs_path(
23801 PathBuf::from(path!("/a/first.rs")),
23802 OpenOptions::default(),
23803 window,
23804 cx,
23805 )
23806 })
23807 .unwrap()
23808 .await
23809 .unwrap()
23810 .downcast::<Editor>()
23811 .unwrap();
23812 let fake_language_server = fake_servers.next().await.unwrap();
23813 let fake_language_server_without_capabilities =
23814 fake_servers_without_capabilities.next().await.unwrap();
23815 let requests_made = Arc::new(AtomicUsize::new(0));
23816 let closure_requests_made = Arc::clone(&requests_made);
23817 let mut color_request_handle = fake_language_server
23818 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
23819 let requests_made = Arc::clone(&closure_requests_made);
23820 async move {
23821 assert_eq!(
23822 params.text_document.uri,
23823 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
23824 );
23825 requests_made.fetch_add(1, atomic::Ordering::Release);
23826 Ok(vec![
23827 lsp::ColorInformation {
23828 range: lsp::Range {
23829 start: lsp::Position {
23830 line: 0,
23831 character: 0,
23832 },
23833 end: lsp::Position {
23834 line: 0,
23835 character: 1,
23836 },
23837 },
23838 color: lsp::Color {
23839 red: 0.33,
23840 green: 0.33,
23841 blue: 0.33,
23842 alpha: 0.33,
23843 },
23844 },
23845 lsp::ColorInformation {
23846 range: lsp::Range {
23847 start: lsp::Position {
23848 line: 0,
23849 character: 0,
23850 },
23851 end: lsp::Position {
23852 line: 0,
23853 character: 1,
23854 },
23855 },
23856 color: lsp::Color {
23857 red: 0.33,
23858 green: 0.33,
23859 blue: 0.33,
23860 alpha: 0.33,
23861 },
23862 },
23863 ])
23864 }
23865 });
23866
23867 let _handle = fake_language_server_without_capabilities
23868 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
23869 panic!("Should not be called");
23870 });
23871 cx.executor().advance_clock(Duration::from_millis(100));
23872 color_request_handle.next().await.unwrap();
23873 cx.run_until_parked();
23874 assert_eq!(
23875 1,
23876 requests_made.load(atomic::Ordering::Acquire),
23877 "Should query for colors once per editor open"
23878 );
23879 editor.update_in(cx, |editor, _, cx| {
23880 assert_eq!(
23881 vec![expected_color],
23882 extract_color_inlays(editor, cx),
23883 "Should have an initial inlay"
23884 );
23885 });
23886
23887 // opening another file in a split should not influence the LSP query counter
23888 workspace
23889 .update(cx, |workspace, window, cx| {
23890 assert_eq!(
23891 workspace.panes().len(),
23892 1,
23893 "Should have one pane with one editor"
23894 );
23895 workspace.move_item_to_pane_in_direction(
23896 &MoveItemToPaneInDirection {
23897 direction: SplitDirection::Right,
23898 focus: false,
23899 clone: true,
23900 },
23901 window,
23902 cx,
23903 );
23904 })
23905 .unwrap();
23906 cx.run_until_parked();
23907 workspace
23908 .update(cx, |workspace, _, cx| {
23909 let panes = workspace.panes();
23910 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
23911 for pane in panes {
23912 let editor = pane
23913 .read(cx)
23914 .active_item()
23915 .and_then(|item| item.downcast::<Editor>())
23916 .expect("Should have opened an editor in each split");
23917 let editor_file = editor
23918 .read(cx)
23919 .buffer()
23920 .read(cx)
23921 .as_singleton()
23922 .expect("test deals with singleton buffers")
23923 .read(cx)
23924 .file()
23925 .expect("test buffese should have a file")
23926 .path();
23927 assert_eq!(
23928 editor_file.as_ref(),
23929 Path::new("first.rs"),
23930 "Both editors should be opened for the same file"
23931 )
23932 }
23933 })
23934 .unwrap();
23935
23936 cx.executor().advance_clock(Duration::from_millis(500));
23937 let save = editor.update_in(cx, |editor, window, cx| {
23938 editor.move_to_end(&MoveToEnd, window, cx);
23939 editor.handle_input("dirty", window, cx);
23940 editor.save(
23941 SaveOptions {
23942 format: true,
23943 autosave: true,
23944 },
23945 project.clone(),
23946 window,
23947 cx,
23948 )
23949 });
23950 save.await.unwrap();
23951
23952 color_request_handle.next().await.unwrap();
23953 cx.run_until_parked();
23954 assert_eq!(
23955 3,
23956 requests_made.load(atomic::Ordering::Acquire),
23957 "Should query for colors once per save and once per formatting after save"
23958 );
23959
23960 drop(editor);
23961 let close = workspace
23962 .update(cx, |workspace, window, cx| {
23963 workspace.active_pane().update(cx, |pane, cx| {
23964 pane.close_active_item(&CloseActiveItem::default(), window, cx)
23965 })
23966 })
23967 .unwrap();
23968 close.await.unwrap();
23969 let close = workspace
23970 .update(cx, |workspace, window, cx| {
23971 workspace.active_pane().update(cx, |pane, cx| {
23972 pane.close_active_item(&CloseActiveItem::default(), window, cx)
23973 })
23974 })
23975 .unwrap();
23976 close.await.unwrap();
23977 assert_eq!(
23978 3,
23979 requests_made.load(atomic::Ordering::Acquire),
23980 "After saving and closing all editors, no extra requests should be made"
23981 );
23982 workspace
23983 .update(cx, |workspace, _, cx| {
23984 assert!(
23985 workspace.active_item(cx).is_none(),
23986 "Should close all editors"
23987 )
23988 })
23989 .unwrap();
23990
23991 workspace
23992 .update(cx, |workspace, window, cx| {
23993 workspace.active_pane().update(cx, |pane, cx| {
23994 pane.navigate_backward(window, cx);
23995 })
23996 })
23997 .unwrap();
23998 cx.executor().advance_clock(Duration::from_millis(100));
23999 cx.run_until_parked();
24000 let editor = workspace
24001 .update(cx, |workspace, _, cx| {
24002 workspace
24003 .active_item(cx)
24004 .expect("Should have reopened the editor again after navigating back")
24005 .downcast::<Editor>()
24006 .expect("Should be an editor")
24007 })
24008 .unwrap();
24009 color_request_handle.next().await.unwrap();
24010 assert_eq!(
24011 3,
24012 requests_made.load(atomic::Ordering::Acquire),
24013 "Cache should be reused on buffer close and reopen"
24014 );
24015 editor.update(cx, |editor, cx| {
24016 assert_eq!(
24017 vec![expected_color],
24018 extract_color_inlays(editor, cx),
24019 "Should have an initial inlay"
24020 );
24021 });
24022}
24023
24024#[gpui::test]
24025async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
24026 init_test(cx, |_| {});
24027 let (editor, cx) = cx.add_window_view(Editor::single_line);
24028 editor.update_in(cx, |editor, window, cx| {
24029 editor.set_text("oops\n\nwow\n", window, cx)
24030 });
24031 cx.run_until_parked();
24032 editor.update(cx, |editor, cx| {
24033 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
24034 });
24035 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
24036 cx.run_until_parked();
24037 editor.update(cx, |editor, cx| {
24038 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
24039 });
24040}
24041
24042#[track_caller]
24043fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
24044 editor
24045 .all_inlays(cx)
24046 .into_iter()
24047 .filter_map(|inlay| inlay.get_color())
24048 .map(Rgba::from)
24049 .collect()
24050}