1use std::cmp;
2
3use editor::{display_map::ToDisplayPoint, movement, scroll::Autoscroll, DisplayPoint, RowExt};
4use gpui::{impl_actions, ViewContext};
5use language::{Bias, SelectionGoal};
6use serde::Deserialize;
7use workspace::Workspace;
8
9use crate::{
10 normal::yank::copy_selections_content,
11 state::{Mode, Register},
12 Vim,
13};
14
15#[derive(Clone, Deserialize, PartialEq)]
16#[serde(rename_all = "camelCase")]
17struct Paste {
18 #[serde(default)]
19 before: bool,
20 #[serde(default)]
21 preserve_clipboard: bool,
22}
23
24impl_actions!(vim, [Paste]);
25
26pub(crate) fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
27 workspace.register_action(paste);
28}
29
30fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext<Workspace>) {
31 Vim::update(cx, |vim, cx| {
32 vim.record_current_action(cx);
33 vim.store_visual_marks(cx);
34 let count = vim.take_count(cx).unwrap_or(1);
35
36 vim.update_active_editor(cx, |vim, editor, cx| {
37 let text_layout_details = editor.text_layout_details(cx);
38 editor.transact(cx, |editor, cx| {
39 editor.set_clip_at_line_ends(false, cx);
40
41 let selected_register = vim.update_state(|state| state.selected_register.take());
42
43 let Some(Register {
44 text,
45 clipboard_selections,
46 }) = vim
47 .read_register(selected_register, Some(editor), cx)
48 .filter(|reg| !reg.text.is_empty())
49 else {
50 return;
51 };
52 let clipboard_selections = clipboard_selections
53 .filter(|sel| sel.len() > 1 && vim.state().mode != Mode::VisualLine);
54
55 if !action.preserve_clipboard && vim.state().mode.is_visual() {
56 copy_selections_content(vim, editor, vim.state().mode == Mode::VisualLine, cx);
57 }
58
59 let (display_map, current_selections) = editor.selections.all_adjusted_display(cx);
60
61 // unlike zed, if you have a multi-cursor selection from vim block mode,
62 // pasting it will paste it on subsequent lines, even if you don't yet
63 // have a cursor there.
64 let mut selections_to_process = Vec::new();
65 let mut i = 0;
66 while i < current_selections.len() {
67 selections_to_process
68 .push((current_selections[i].start..current_selections[i].end, true));
69 i += 1;
70 }
71 if let Some(clipboard_selections) = clipboard_selections.as_ref() {
72 let left = current_selections
73 .iter()
74 .map(|selection| cmp::min(selection.start.column(), selection.end.column()))
75 .min()
76 .unwrap();
77 let mut row = current_selections.last().unwrap().end.row().next_row();
78 while i < clipboard_selections.len() {
79 let cursor =
80 display_map.clip_point(DisplayPoint::new(row, left), Bias::Left);
81 selections_to_process.push((cursor..cursor, false));
82 i += 1;
83 row.0 += 1;
84 }
85 }
86
87 let first_selection_indent_column =
88 clipboard_selections.as_ref().and_then(|zed_selections| {
89 zed_selections
90 .first()
91 .map(|selection| selection.first_line_indent)
92 });
93 let before = action.before || vim.state().mode == Mode::VisualLine;
94
95 let mut edits = Vec::new();
96 let mut new_selections = Vec::new();
97 let mut original_indent_columns = Vec::new();
98 let mut start_offset = 0;
99
100 for (ix, (selection, preserve)) in selections_to_process.iter().enumerate() {
101 let (mut to_insert, original_indent_column) =
102 if let Some(clipboard_selections) = &clipboard_selections {
103 if let Some(clipboard_selection) = clipboard_selections.get(ix) {
104 let end_offset = start_offset + clipboard_selection.len;
105 let text = text[start_offset..end_offset].to_string();
106 start_offset = end_offset + 1;
107 (text, Some(clipboard_selection.first_line_indent))
108 } else {
109 ("".to_string(), first_selection_indent_column)
110 }
111 } else {
112 (text.to_string(), first_selection_indent_column)
113 };
114 let line_mode = to_insert.ends_with('\n');
115 let is_multiline = to_insert.contains('\n');
116
117 if line_mode && !before {
118 if selection.is_empty() {
119 to_insert =
120 "\n".to_owned() + &to_insert[..to_insert.len() - "\n".len()];
121 } else {
122 to_insert = "\n".to_owned() + &to_insert;
123 }
124 } else if !line_mode && vim.state().mode == Mode::VisualLine {
125 to_insert = to_insert + "\n";
126 }
127
128 let display_range = if !selection.is_empty() {
129 selection.start..selection.end
130 } else if line_mode {
131 let point = if before {
132 movement::line_beginning(&display_map, selection.start, false)
133 } else {
134 movement::line_end(&display_map, selection.start, false)
135 };
136 point..point
137 } else {
138 let point = if before {
139 selection.start
140 } else {
141 movement::saturating_right(&display_map, selection.start)
142 };
143 point..point
144 };
145
146 let point_range = display_range.start.to_point(&display_map)
147 ..display_range.end.to_point(&display_map);
148 let anchor = if is_multiline || vim.state().mode == Mode::VisualLine {
149 display_map.buffer_snapshot.anchor_before(point_range.start)
150 } else {
151 display_map.buffer_snapshot.anchor_after(point_range.end)
152 };
153
154 if *preserve {
155 new_selections.push((anchor, line_mode, is_multiline));
156 }
157 edits.push((point_range, to_insert.repeat(count)));
158 original_indent_columns.extend(original_indent_column);
159 }
160
161 editor.edit_with_block_indent(edits, original_indent_columns, cx);
162
163 // in line_mode vim will insert the new text on the next (or previous if before) line
164 // and put the cursor on the first non-blank character of the first inserted line (or at the end if the first line is blank).
165 // otherwise vim will insert the next text at (or before) the current cursor position,
166 // the cursor will go to the last (or first, if is_multiline) inserted character.
167 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
168 s.replace_cursors_with(|map| {
169 let mut cursors = Vec::new();
170 for (anchor, line_mode, is_multiline) in &new_selections {
171 let mut cursor = anchor.to_display_point(map);
172 if *line_mode {
173 if !before {
174 cursor = movement::down(
175 map,
176 cursor,
177 SelectionGoal::None,
178 false,
179 &text_layout_details,
180 )
181 .0;
182 }
183 cursor = movement::indented_line_beginning(map, cursor, true);
184 } else if !is_multiline {
185 cursor = movement::saturating_left(map, cursor)
186 }
187 cursors.push(cursor);
188 if vim.state().mode == Mode::VisualBlock {
189 break;
190 }
191 }
192
193 cursors
194 });
195 })
196 });
197 });
198 vim.switch_mode(Mode::Normal, true, cx);
199 });
200}
201
202#[cfg(test)]
203mod test {
204 use crate::{
205 state::Mode,
206 test::{NeovimBackedTestContext, VimTestContext},
207 UseSystemClipboard, VimSettings,
208 };
209 use gpui::ClipboardItem;
210 use indoc::indoc;
211 use settings::SettingsStore;
212
213 #[gpui::test]
214 async fn test_paste(cx: &mut gpui::TestAppContext) {
215 let mut cx = NeovimBackedTestContext::new(cx).await;
216
217 // single line
218 cx.set_shared_state(indoc! {"
219 The quick brown
220 fox ˇjumps over
221 the lazy dog"})
222 .await;
223 cx.simulate_shared_keystrokes("v w y").await;
224 cx.shared_clipboard().await.assert_eq("jumps o");
225 cx.set_shared_state(indoc! {"
226 The quick brown
227 fox jumps oveˇr
228 the lazy dog"})
229 .await;
230 cx.simulate_shared_keystrokes("p").await;
231 cx.shared_state().await.assert_eq(indoc! {"
232 The quick brown
233 fox jumps overjumps ˇo
234 the lazy dog"});
235
236 cx.set_shared_state(indoc! {"
237 The quick brown
238 fox jumps oveˇr
239 the lazy dog"})
240 .await;
241 cx.simulate_shared_keystrokes("shift-p").await;
242 cx.shared_state().await.assert_eq(indoc! {"
243 The quick brown
244 fox jumps ovejumps ˇor
245 the lazy dog"});
246
247 // line mode
248 cx.set_shared_state(indoc! {"
249 The quick brown
250 fox juˇmps over
251 the lazy dog"})
252 .await;
253 cx.simulate_shared_keystrokes("d d").await;
254 cx.shared_clipboard().await.assert_eq("fox jumps over\n");
255 cx.shared_state().await.assert_eq(indoc! {"
256 The quick brown
257 the laˇzy dog"});
258 cx.simulate_shared_keystrokes("p").await;
259 cx.shared_state().await.assert_eq(indoc! {"
260 The quick brown
261 the lazy dog
262 ˇfox jumps over"});
263 cx.simulate_shared_keystrokes("k shift-p").await;
264 cx.shared_state().await.assert_eq(indoc! {"
265 The quick brown
266 ˇfox jumps over
267 the lazy dog
268 fox jumps over"});
269
270 // multiline, cursor to first character of pasted text.
271 cx.set_shared_state(indoc! {"
272 The quick brown
273 fox jumps ˇover
274 the lazy dog"})
275 .await;
276 cx.simulate_shared_keystrokes("v j y").await;
277 cx.shared_clipboard().await.assert_eq("over\nthe lazy do");
278
279 cx.simulate_shared_keystrokes("p").await;
280 cx.shared_state().await.assert_eq(indoc! {"
281 The quick brown
282 fox jumps oˇover
283 the lazy dover
284 the lazy dog"});
285 cx.simulate_shared_keystrokes("u shift-p").await;
286 cx.shared_state().await.assert_eq(indoc! {"
287 The quick brown
288 fox jumps ˇover
289 the lazy doover
290 the lazy dog"});
291 }
292
293 #[gpui::test]
294 async fn test_yank_system_clipboard_never(cx: &mut gpui::TestAppContext) {
295 let mut cx = VimTestContext::new(cx, true).await;
296
297 cx.update_global(|store: &mut SettingsStore, cx| {
298 store.update_user_settings::<VimSettings>(cx, |s| {
299 s.use_system_clipboard = Some(UseSystemClipboard::Never)
300 });
301 });
302
303 cx.set_state(
304 indoc! {"
305 The quick brown
306 fox jˇumps over
307 the lazy dog"},
308 Mode::Normal,
309 );
310 cx.simulate_keystrokes("v i w y");
311 cx.assert_state(
312 indoc! {"
313 The quick brown
314 fox ˇjumps over
315 the lazy dog"},
316 Mode::Normal,
317 );
318 cx.simulate_keystrokes("p");
319 cx.assert_state(
320 indoc! {"
321 The quick brown
322 fox jjumpˇsumps over
323 the lazy dog"},
324 Mode::Normal,
325 );
326 assert_eq!(cx.read_from_clipboard(), None);
327 }
328
329 #[gpui::test]
330 async fn test_yank_system_clipboard_on_yank(cx: &mut gpui::TestAppContext) {
331 let mut cx = VimTestContext::new(cx, true).await;
332
333 cx.update_global(|store: &mut SettingsStore, cx| {
334 store.update_user_settings::<VimSettings>(cx, |s| {
335 s.use_system_clipboard = Some(UseSystemClipboard::OnYank)
336 });
337 });
338
339 // copy in visual mode
340 cx.set_state(
341 indoc! {"
342 The quick brown
343 fox jˇumps over
344 the lazy dog"},
345 Mode::Normal,
346 );
347 cx.simulate_keystrokes("v i w y");
348 cx.assert_state(
349 indoc! {"
350 The quick brown
351 fox ˇjumps over
352 the lazy dog"},
353 Mode::Normal,
354 );
355 cx.simulate_keystrokes("p");
356 cx.assert_state(
357 indoc! {"
358 The quick brown
359 fox jjumpˇsumps over
360 the lazy dog"},
361 Mode::Normal,
362 );
363 assert_eq!(
364 cx.read_from_clipboard()
365 .map(|item| item.text().unwrap().to_string()),
366 Some("jumps".into())
367 );
368 cx.simulate_keystrokes("d d p");
369 cx.assert_state(
370 indoc! {"
371 The quick brown
372 the lazy dog
373 ˇfox jjumpsumps over"},
374 Mode::Normal,
375 );
376 assert_eq!(
377 cx.read_from_clipboard()
378 .map(|item| item.text().unwrap().to_string()),
379 Some("jumps".into())
380 );
381 cx.write_to_clipboard(ClipboardItem::new_string("test-copy".to_string()));
382 cx.simulate_keystrokes("shift-p");
383 cx.assert_state(
384 indoc! {"
385 The quick brown
386 the lazy dog
387 test-copˇyfox jjumpsumps over"},
388 Mode::Normal,
389 );
390 }
391
392 #[gpui::test]
393 async fn test_paste_visual(cx: &mut gpui::TestAppContext) {
394 let mut cx = NeovimBackedTestContext::new(cx).await;
395
396 // copy in visual mode
397 cx.set_shared_state(indoc! {"
398 The quick brown
399 fox jˇumps over
400 the lazy dog"})
401 .await;
402 cx.simulate_shared_keystrokes("v i w y").await;
403 cx.shared_state().await.assert_eq(indoc! {"
404 The quick brown
405 fox ˇjumps over
406 the lazy dog"});
407 // paste in visual mode
408 cx.simulate_shared_keystrokes("w v i w p").await;
409 cx.shared_state().await.assert_eq(indoc! {"
410 The quick brown
411 fox jumps jumpˇs
412 the lazy dog"});
413 cx.shared_clipboard().await.assert_eq("over");
414 // paste in visual line mode
415 cx.simulate_shared_keystrokes("up shift-v shift-p").await;
416 cx.shared_state().await.assert_eq(indoc! {"
417 ˇover
418 fox jumps jumps
419 the lazy dog"});
420 cx.shared_clipboard().await.assert_eq("over");
421 // paste in visual block mode
422 cx.simulate_shared_keystrokes("ctrl-v down down p").await;
423 cx.shared_state().await.assert_eq(indoc! {"
424 oveˇrver
425 overox jumps jumps
426 overhe lazy dog"});
427
428 // copy in visual line mode
429 cx.set_shared_state(indoc! {"
430 The quick brown
431 fox juˇmps over
432 the lazy dog"})
433 .await;
434 cx.simulate_shared_keystrokes("shift-v d").await;
435 cx.shared_state().await.assert_eq(indoc! {"
436 The quick brown
437 the laˇzy dog"});
438 // paste in visual mode
439 cx.simulate_shared_keystrokes("v i w p").await;
440 cx.shared_state().await.assert_eq(&indoc! {"
441 The quick brown
442 the•
443 ˇfox jumps over
444 dog"});
445 cx.shared_clipboard().await.assert_eq("lazy");
446 cx.set_shared_state(indoc! {"
447 The quick brown
448 fox juˇmps over
449 the lazy dog"})
450 .await;
451 cx.simulate_shared_keystrokes("shift-v d").await;
452 cx.shared_state().await.assert_eq(indoc! {"
453 The quick brown
454 the laˇzy dog"});
455 // paste in visual line mode
456 cx.simulate_shared_keystrokes("k shift-v p").await;
457 cx.shared_state().await.assert_eq(indoc! {"
458 ˇfox jumps over
459 the lazy dog"});
460 cx.shared_clipboard().await.assert_eq("The quick brown\n");
461 }
462
463 #[gpui::test]
464 async fn test_paste_visual_block(cx: &mut gpui::TestAppContext) {
465 let mut cx = NeovimBackedTestContext::new(cx).await;
466 // copy in visual block mode
467 cx.set_shared_state(indoc! {"
468 The ˇquick brown
469 fox jumps over
470 the lazy dog"})
471 .await;
472 cx.simulate_shared_keystrokes("ctrl-v 2 j y").await;
473 cx.shared_clipboard().await.assert_eq("q\nj\nl");
474 cx.simulate_shared_keystrokes("p").await;
475 cx.shared_state().await.assert_eq(indoc! {"
476 The qˇquick brown
477 fox jjumps over
478 the llazy dog"});
479 cx.simulate_shared_keystrokes("v i w shift-p").await;
480 cx.shared_state().await.assert_eq(indoc! {"
481 The ˇq brown
482 fox jjjumps over
483 the lllazy dog"});
484 cx.simulate_shared_keystrokes("v i w shift-p").await;
485
486 cx.set_shared_state(indoc! {"
487 The ˇquick brown
488 fox jumps over
489 the lazy dog"})
490 .await;
491 cx.simulate_shared_keystrokes("ctrl-v j y").await;
492 cx.shared_clipboard().await.assert_eq("q\nj");
493 cx.simulate_shared_keystrokes("l ctrl-v 2 j shift-p").await;
494 cx.shared_state().await.assert_eq(indoc! {"
495 The qˇqick brown
496 fox jjmps over
497 the lzy dog"});
498
499 cx.simulate_shared_keystrokes("shift-v p").await;
500 cx.shared_state().await.assert_eq(indoc! {"
501 ˇq
502 j
503 fox jjmps over
504 the lzy dog"});
505 }
506
507 #[gpui::test]
508 async fn test_paste_indent(cx: &mut gpui::TestAppContext) {
509 let mut cx = VimTestContext::new_typescript(cx).await;
510
511 cx.set_state(
512 indoc! {"
513 class A {ˇ
514 }
515 "},
516 Mode::Normal,
517 );
518 cx.simulate_keystrokes("o a ( ) { escape");
519 cx.assert_state(
520 indoc! {"
521 class A {
522 a()ˇ{}
523 }
524 "},
525 Mode::Normal,
526 );
527 // cursor goes to the first non-blank character in the line;
528 cx.simulate_keystrokes("y y p");
529 cx.assert_state(
530 indoc! {"
531 class A {
532 a(){}
533 ˇa(){}
534 }
535 "},
536 Mode::Normal,
537 );
538 // indentation is preserved when pasting
539 cx.simulate_keystrokes("u shift-v up y shift-p");
540 cx.assert_state(
541 indoc! {"
542 ˇclass A {
543 a(){}
544 class A {
545 a(){}
546 }
547 "},
548 Mode::Normal,
549 );
550 }
551
552 #[gpui::test]
553 async fn test_paste_count(cx: &mut gpui::TestAppContext) {
554 let mut cx = NeovimBackedTestContext::new(cx).await;
555
556 cx.set_shared_state(indoc! {"
557 onˇe
558 two
559 three
560 "})
561 .await;
562 cx.simulate_shared_keystrokes("y y 3 p").await;
563 cx.shared_state().await.assert_eq(indoc! {"
564 one
565 ˇone
566 one
567 one
568 two
569 three
570 "});
571
572 cx.set_shared_state(indoc! {"
573 one
574 ˇtwo
575 three
576 "})
577 .await;
578 cx.simulate_shared_keystrokes("y $ $ 3 p").await;
579 cx.shared_state().await.assert_eq(indoc! {"
580 one
581 twotwotwotwˇo
582 three
583 "});
584 }
585
586 #[gpui::test]
587 async fn test_numbered_registers(cx: &mut gpui::TestAppContext) {
588 let mut cx = NeovimBackedTestContext::new(cx).await;
589
590 cx.update_global(|store: &mut SettingsStore, cx| {
591 store.update_user_settings::<VimSettings>(cx, |s| {
592 s.use_system_clipboard = Some(UseSystemClipboard::Never)
593 });
594 });
595
596 cx.set_shared_state(indoc! {"
597 The quick brown
598 fox jˇumps over
599 the lazy dog"})
600 .await;
601 cx.simulate_shared_keystrokes("y y \" 0 p").await;
602 cx.shared_register('0').await.assert_eq("fox jumps over\n");
603 cx.shared_register('"').await.assert_eq("fox jumps over\n");
604
605 cx.shared_state().await.assert_eq(indoc! {"
606 The quick brown
607 fox jumps over
608 ˇfox jumps over
609 the lazy dog"});
610 cx.simulate_shared_keystrokes("k k d d").await;
611 cx.shared_register('0').await.assert_eq("fox jumps over\n");
612 cx.shared_register('1').await.assert_eq("The quick brown\n");
613 cx.shared_register('"').await.assert_eq("The quick brown\n");
614
615 cx.simulate_shared_keystrokes("d d shift-g d d").await;
616 cx.shared_register('0').await.assert_eq("fox jumps over\n");
617 cx.shared_register('3').await.assert_eq("The quick brown\n");
618 cx.shared_register('2').await.assert_eq("fox jumps over\n");
619 cx.shared_register('1').await.assert_eq("the lazy dog\n");
620
621 cx.shared_state().await.assert_eq(indoc! {"
622 ˇfox jumps over"});
623
624 cx.simulate_shared_keystrokes("d d \" 3 p p \" 1 p").await;
625 cx.set_shared_state(indoc! {"
626 The quick brown
627 fox jumps over
628 ˇthe lazy dog"})
629 .await;
630 }
631
632 #[gpui::test]
633 async fn test_named_registers(cx: &mut gpui::TestAppContext) {
634 let mut cx = NeovimBackedTestContext::new(cx).await;
635
636 cx.update_global(|store: &mut SettingsStore, cx| {
637 store.update_user_settings::<VimSettings>(cx, |s| {
638 s.use_system_clipboard = Some(UseSystemClipboard::Never)
639 });
640 });
641
642 cx.set_shared_state(indoc! {"
643 The quick brown
644 fox jˇumps over
645 the lazy dog"})
646 .await;
647 cx.simulate_shared_keystrokes("\" a d a w").await;
648 cx.shared_register('a').await.assert_eq("jumps ");
649 cx.simulate_shared_keystrokes("\" shift-a d i w").await;
650 cx.shared_register('a').await.assert_eq("jumps over");
651 cx.shared_register('"').await.assert_eq("jumps over");
652 cx.simulate_shared_keystrokes("\" a p").await;
653 cx.shared_state().await.assert_eq(indoc! {"
654 The quick brown
655 fox jumps oveˇr
656 the lazy dog"});
657 cx.simulate_shared_keystrokes("\" a d a w").await;
658 cx.shared_register('a').await.assert_eq(" over");
659 }
660
661 #[gpui::test]
662 async fn test_special_registers(cx: &mut gpui::TestAppContext) {
663 let mut cx = NeovimBackedTestContext::new(cx).await;
664
665 cx.update_global(|store: &mut SettingsStore, cx| {
666 store.update_user_settings::<VimSettings>(cx, |s| {
667 s.use_system_clipboard = Some(UseSystemClipboard::Never)
668 });
669 });
670
671 cx.set_shared_state(indoc! {"
672 The quick brown
673 fox jˇumps over
674 the lazy dog"})
675 .await;
676 cx.simulate_shared_keystrokes("d i w").await;
677 cx.shared_register('-').await.assert_eq("jumps");
678 cx.simulate_shared_keystrokes("\" _ d d").await;
679 cx.shared_register('_').await.assert_eq("");
680
681 cx.shared_state().await.assert_eq(indoc! {"
682 The quick brown
683 the ˇlazy dog"});
684 cx.simulate_shared_keystrokes("\" \" d ^").await;
685 cx.shared_register('0').await.assert_eq("the ");
686 cx.shared_register('"').await.assert_eq("the ");
687
688 cx.simulate_shared_keystrokes("^ \" + d $").await;
689 cx.shared_clipboard().await.assert_eq("lazy dog");
690 cx.shared_register('"').await.assert_eq("lazy dog");
691
692 cx.simulate_shared_keystrokes("/ d o g enter").await;
693 cx.shared_register('/').await.assert_eq("dog");
694 cx.simulate_shared_keystrokes("\" / shift-p").await;
695 cx.shared_state().await.assert_eq(indoc! {"
696 The quick brown
697 doˇg"});
698
699 // not testing nvim as it doesn't have a filename
700 cx.simulate_keystrokes("\" % p");
701 cx.assert_state(
702 indoc! {"
703 The quick brown
704 dogdir/file.rˇs"},
705 Mode::Normal,
706 );
707 }
708
709 #[gpui::test]
710 async fn test_multicursor_paste(cx: &mut gpui::TestAppContext) {
711 let mut cx = VimTestContext::new(cx, true).await;
712
713 cx.update_global(|store: &mut SettingsStore, cx| {
714 store.update_user_settings::<VimSettings>(cx, |s| {
715 s.use_system_clipboard = Some(UseSystemClipboard::Never)
716 });
717 });
718
719 cx.set_state(
720 indoc! {"
721 ˇfish one
722 fish two
723 fish red
724 fish blue
725 "},
726 Mode::Normal,
727 );
728 cx.simulate_keystrokes("4 g l w escape d i w 0 shift-p");
729 cx.assert_state(
730 indoc! {"
731 onˇefish•
732 twˇofish•
733 reˇdfish•
734 bluˇefish•
735 "},
736 Mode::Normal,
737 );
738 }
739}