repeat.rs

  1use std::{cell::RefCell, rc::Rc};
  2
  3use crate::{
  4    insert::NormalBefore,
  5    motion::Motion,
  6    normal::InsertBefore,
  7    state::{Mode, Operator, RecordedSelection, ReplayableAction, VimGlobals},
  8    Vim,
  9};
 10use editor::Editor;
 11use gpui::{actions, Action, ViewContext, WindowContext};
 12use util::ResultExt;
 13use workspace::Workspace;
 14
 15actions!(vim, [Repeat, EndRepeat, ToggleRecord, ReplayLastRecording]);
 16
 17fn should_replay(action: &dyn Action) -> bool {
 18    // skip so that we don't leave the character palette open
 19    if editor::actions::ShowCharacterPalette.partial_eq(action) {
 20        return false;
 21    }
 22    true
 23}
 24
 25fn repeatable_insert(action: &ReplayableAction) -> Option<Box<dyn Action>> {
 26    match action {
 27        ReplayableAction::Action(action) => {
 28            if super::InsertBefore.partial_eq(&**action)
 29                || super::InsertAfter.partial_eq(&**action)
 30                || super::InsertFirstNonWhitespace.partial_eq(&**action)
 31                || super::InsertEndOfLine.partial_eq(&**action)
 32            {
 33                Some(super::InsertBefore.boxed_clone())
 34            } else if super::InsertLineAbove.partial_eq(&**action)
 35                || super::InsertLineBelow.partial_eq(&**action)
 36            {
 37                Some(super::InsertLineBelow.boxed_clone())
 38            } else if crate::replace::ToggleReplace.partial_eq(&**action) {
 39                Some(crate::replace::ToggleReplace.boxed_clone())
 40            } else {
 41                None
 42            }
 43        }
 44        ReplayableAction::Insertion { .. } => None,
 45    }
 46}
 47
 48pub(crate) fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
 49    Vim::action(editor, cx, |vim, _: &EndRepeat, cx| {
 50        Vim::globals(cx).dot_replaying = false;
 51        vim.switch_mode(Mode::Normal, false, cx)
 52    });
 53
 54    Vim::action(editor, cx, |vim, _: &Repeat, cx| vim.repeat(false, cx));
 55
 56    Vim::action(editor, cx, |vim, _: &ToggleRecord, cx| {
 57        let globals = Vim::globals(cx);
 58        if let Some(char) = globals.recording_register.take() {
 59            globals.last_recorded_register = Some(char)
 60        } else {
 61            vim.push_operator(Operator::RecordRegister, cx);
 62        }
 63    });
 64
 65    Vim::action(editor, cx, |vim, _: &ReplayLastRecording, cx| {
 66        let Some(register) = Vim::globals(cx).last_recorded_register else {
 67            return;
 68        };
 69        vim.replay_register(register, cx)
 70    });
 71}
 72
 73pub struct ReplayerState {
 74    actions: Vec<ReplayableAction>,
 75    running: bool,
 76    ix: usize,
 77}
 78
 79#[derive(Clone)]
 80pub struct Replayer(Rc<RefCell<ReplayerState>>);
 81
 82impl Replayer {
 83    pub fn new() -> Self {
 84        Self(Rc::new(RefCell::new(ReplayerState {
 85            actions: vec![],
 86            running: false,
 87            ix: 0,
 88        })))
 89    }
 90
 91    pub fn replay(&mut self, actions: Vec<ReplayableAction>, cx: &mut WindowContext) {
 92        let mut lock = self.0.borrow_mut();
 93        let range = lock.ix..lock.ix;
 94        lock.actions.splice(range, actions);
 95        if lock.running {
 96            return;
 97        }
 98        lock.running = true;
 99        let this = self.clone();
100        cx.defer(move |cx| this.next(cx))
101    }
102
103    pub fn stop(self) {
104        self.0.borrow_mut().actions.clear()
105    }
106
107    pub fn next(self, cx: &mut WindowContext) {
108        let mut lock = self.0.borrow_mut();
109        let action = if lock.ix < 10000 {
110            lock.actions.get(lock.ix).cloned()
111        } else {
112            log::error!("Aborting replay after 10000 actions");
113            None
114        };
115        lock.ix += 1;
116        drop(lock);
117        let Some(action) = action else {
118            Vim::globals(cx).replayer.take();
119            return;
120        };
121        match action {
122            ReplayableAction::Action(action) => {
123                if should_replay(&*action) {
124                    cx.dispatch_action(action.boxed_clone());
125                    cx.defer(move |cx| Vim::globals(cx).observe_action(action.boxed_clone()));
126                }
127            }
128            ReplayableAction::Insertion {
129                text,
130                utf16_range_to_replace,
131            } => {
132                cx.window_handle()
133                    .update(cx, |handle, cx| {
134                        let Ok(workspace) = handle.downcast::<Workspace>() else {
135                            return;
136                        };
137                        let Some(editor) = workspace
138                            .read(cx)
139                            .active_item(cx)
140                            .and_then(|item| item.act_as::<Editor>(cx))
141                        else {
142                            return;
143                        };
144                        editor.update(cx, |editor, cx| {
145                            editor.replay_insert_event(&text, utf16_range_to_replace.clone(), cx)
146                        })
147                    })
148                    .log_err();
149            }
150        }
151        cx.defer(move |cx| self.next(cx));
152    }
153}
154
155impl Vim {
156    pub(crate) fn record_register(&mut self, register: char, cx: &mut ViewContext<Self>) {
157        let globals = Vim::globals(cx);
158        globals.recording_register = Some(register);
159        globals.recordings.remove(&register);
160        globals.ignore_current_insertion = true;
161        self.clear_operator(cx)
162    }
163
164    pub(crate) fn replay_register(&mut self, mut register: char, cx: &mut ViewContext<Self>) {
165        let mut count = Vim::take_count(cx).unwrap_or(1);
166        self.clear_operator(cx);
167
168        let globals = Vim::globals(cx);
169        if register == '@' {
170            let Some(last) = globals.last_replayed_register else {
171                return;
172            };
173            register = last;
174        }
175        let Some(actions) = globals.recordings.get(&register) else {
176            return;
177        };
178
179        let mut repeated_actions = vec![];
180        while count > 0 {
181            repeated_actions.extend(actions.iter().cloned());
182            count -= 1
183        }
184
185        globals.last_replayed_register = Some(register);
186        let mut replayer = globals.replayer.get_or_insert_with(Replayer::new).clone();
187        replayer.replay(repeated_actions, cx);
188    }
189
190    pub(crate) fn repeat(&mut self, from_insert_mode: bool, cx: &mut ViewContext<Self>) {
191        let count = Vim::take_count(cx);
192        let Some((mut actions, selection, mode)) = Vim::update_globals(cx, |globals, _| {
193            let actions = globals.recorded_actions.clone();
194            if actions.is_empty() {
195                return None;
196            }
197            if globals.replayer.is_none() {
198                if let Some(recording_register) = globals.recording_register {
199                    globals
200                        .recordings
201                        .entry(recording_register)
202                        .or_default()
203                        .push(ReplayableAction::Action(Repeat.boxed_clone()));
204                }
205            }
206
207            let mut mode = None;
208            let selection = globals.recorded_selection.clone();
209            match selection {
210                RecordedSelection::SingleLine { .. } | RecordedSelection::Visual { .. } => {
211                    globals.recorded_count = None;
212                    mode = Some(Mode::Visual);
213                }
214                RecordedSelection::VisualLine { .. } => {
215                    globals.recorded_count = None;
216                    mode = Some(Mode::VisualLine)
217                }
218                RecordedSelection::VisualBlock { .. } => {
219                    globals.recorded_count = None;
220                    mode = Some(Mode::VisualBlock)
221                }
222                RecordedSelection::None => {
223                    if let Some(count) = count {
224                        globals.recorded_count = Some(count);
225                    }
226                }
227            }
228
229            Some((actions, selection, mode))
230        }) else {
231            return;
232        };
233        if let Some(mode) = mode {
234            self.switch_mode(mode, false, cx)
235        }
236
237        match selection {
238            RecordedSelection::SingleLine { cols } => {
239                if cols > 1 {
240                    self.visual_motion(Motion::Right, Some(cols as usize - 1), cx)
241                }
242            }
243            RecordedSelection::Visual { rows, cols } => {
244                self.visual_motion(
245                    Motion::Down {
246                        display_lines: false,
247                    },
248                    Some(rows as usize),
249                    cx,
250                );
251                self.visual_motion(
252                    Motion::StartOfLine {
253                        display_lines: false,
254                    },
255                    None,
256                    cx,
257                );
258                if cols > 1 {
259                    self.visual_motion(Motion::Right, Some(cols as usize - 1), cx)
260                }
261            }
262            RecordedSelection::VisualBlock { rows, cols } => {
263                self.visual_motion(
264                    Motion::Down {
265                        display_lines: false,
266                    },
267                    Some(rows as usize),
268                    cx,
269                );
270                if cols > 1 {
271                    self.visual_motion(Motion::Right, Some(cols as usize - 1), cx);
272                }
273            }
274            RecordedSelection::VisualLine { rows } => {
275                self.visual_motion(
276                    Motion::Down {
277                        display_lines: false,
278                    },
279                    Some(rows as usize),
280                    cx,
281                );
282            }
283            RecordedSelection::None => {}
284        }
285
286        // insert internally uses repeat to handle counts
287        // vim doesn't treat 3a1 as though you literally repeated a1
288        // 3 times, instead it inserts the content thrice at the insert position.
289        if let Some(to_repeat) = repeatable_insert(&actions[0]) {
290            if let Some(ReplayableAction::Action(action)) = actions.last() {
291                if NormalBefore.partial_eq(&**action) {
292                    actions.pop();
293                }
294            }
295
296            let mut new_actions = actions.clone();
297            actions[0] = ReplayableAction::Action(to_repeat.boxed_clone());
298
299            let mut count = cx.global::<VimGlobals>().recorded_count.unwrap_or(1);
300
301            // if we came from insert mode we're just doing repetitions 2 onwards.
302            if from_insert_mode {
303                count -= 1;
304                new_actions[0] = actions[0].clone();
305            }
306
307            for _ in 1..count {
308                new_actions.append(actions.clone().as_mut());
309            }
310            new_actions.push(ReplayableAction::Action(NormalBefore.boxed_clone()));
311            actions = new_actions;
312        }
313
314        actions.push(ReplayableAction::Action(EndRepeat.boxed_clone()));
315
316        if self.temp_mode {
317            self.temp_mode = false;
318            actions.push(ReplayableAction::Action(InsertBefore.boxed_clone()));
319        }
320
321        let globals = Vim::globals(cx);
322        globals.dot_replaying = true;
323        let mut replayer = globals.replayer.get_or_insert_with(Replayer::new).clone();
324        replayer.replay(actions, cx);
325    }
326}
327
328#[cfg(test)]
329mod test {
330    use editor::test::editor_lsp_test_context::EditorLspTestContext;
331    use futures::StreamExt;
332    use indoc::indoc;
333
334    use gpui::ViewInputHandler;
335
336    use crate::{
337        state::Mode,
338        test::{NeovimBackedTestContext, VimTestContext},
339    };
340
341    #[gpui::test]
342    async fn test_dot_repeat(cx: &mut gpui::TestAppContext) {
343        let mut cx = NeovimBackedTestContext::new(cx).await;
344
345        // "o"
346        cx.set_shared_state("ˇhello").await;
347        cx.simulate_shared_keystrokes("o w o r l d escape").await;
348        cx.shared_state().await.assert_eq("hello\nworlˇd");
349        cx.simulate_shared_keystrokes(".").await;
350        cx.shared_state().await.assert_eq("hello\nworld\nworlˇd");
351
352        // "d"
353        cx.simulate_shared_keystrokes("^ d f o").await;
354        cx.simulate_shared_keystrokes("g g .").await;
355        cx.shared_state().await.assert_eq("ˇ\nworld\nrld");
356
357        // "p" (note that it pastes the current clipboard)
358        cx.simulate_shared_keystrokes("j y y p").await;
359        cx.simulate_shared_keystrokes("shift-g y y .").await;
360        cx.shared_state()
361            .await
362            .assert_eq("\nworld\nworld\nrld\nˇrld");
363
364        // "~" (note that counts apply to the action taken, not . itself)
365        cx.set_shared_state("ˇthe quick brown fox").await;
366        cx.simulate_shared_keystrokes("2 ~ .").await;
367        cx.set_shared_state("THE ˇquick brown fox").await;
368        cx.simulate_shared_keystrokes("3 .").await;
369        cx.set_shared_state("THE QUIˇck brown fox").await;
370        cx.run_until_parked();
371        cx.simulate_shared_keystrokes(".").await;
372        cx.shared_state().await.assert_eq("THE QUICK ˇbrown fox");
373    }
374
375    #[gpui::test]
376    async fn test_repeat_ime(cx: &mut gpui::TestAppContext) {
377        let mut cx = VimTestContext::new(cx, true).await;
378
379        cx.set_state("hˇllo", Mode::Normal);
380        cx.simulate_keystrokes("i");
381
382        // simulate brazilian input for ä.
383        cx.update_editor(|editor, cx| {
384            editor.replace_and_mark_text_in_range(None, "\"", Some(1..1), cx);
385            editor.replace_text_in_range(None, "ä", cx);
386        });
387        cx.simulate_keystrokes("escape");
388        cx.assert_state("hˇällo", Mode::Normal);
389        cx.simulate_keystrokes(".");
390        cx.assert_state("hˇäällo", Mode::Normal);
391    }
392
393    #[gpui::test]
394    async fn test_repeat_completion(cx: &mut gpui::TestAppContext) {
395        VimTestContext::init(cx);
396        let cx = EditorLspTestContext::new_rust(
397            lsp::ServerCapabilities {
398                completion_provider: Some(lsp::CompletionOptions {
399                    trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
400                    resolve_provider: Some(true),
401                    ..Default::default()
402                }),
403                ..Default::default()
404            },
405            cx,
406        )
407        .await;
408        let mut cx = VimTestContext::new_with_lsp(cx, true);
409
410        cx.set_state(
411            indoc! {"
412            onˇe
413            two
414            three
415        "},
416            Mode::Normal,
417        );
418
419        let mut request =
420            cx.handle_request::<lsp::request::Completion, _, _>(move |_, params, _| async move {
421                let position = params.text_document_position.position;
422                Ok(Some(lsp::CompletionResponse::Array(vec![
423                    lsp::CompletionItem {
424                        label: "first".to_string(),
425                        text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
426                            range: lsp::Range::new(position, position),
427                            new_text: "first".to_string(),
428                        })),
429                        ..Default::default()
430                    },
431                    lsp::CompletionItem {
432                        label: "second".to_string(),
433                        text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
434                            range: lsp::Range::new(position, position),
435                            new_text: "second".to_string(),
436                        })),
437                        ..Default::default()
438                    },
439                ])))
440            });
441        cx.simulate_keystrokes("a .");
442        request.next().await;
443        cx.condition(|editor, _| editor.context_menu_visible())
444            .await;
445        cx.simulate_keystrokes("down enter ! escape");
446
447        cx.assert_state(
448            indoc! {"
449                one.secondˇ!
450                two
451                three
452            "},
453            Mode::Normal,
454        );
455        cx.simulate_keystrokes("j .");
456        cx.assert_state(
457            indoc! {"
458                one.second!
459                two.secondˇ!
460                three
461            "},
462            Mode::Normal,
463        );
464    }
465
466    #[gpui::test]
467    async fn test_repeat_visual(cx: &mut gpui::TestAppContext) {
468        let mut cx = NeovimBackedTestContext::new(cx).await;
469
470        // single-line (3 columns)
471        cx.set_shared_state(indoc! {
472            "ˇthe quick brown
473            fox jumps over
474            the lazy dog"
475        })
476        .await;
477        cx.simulate_shared_keystrokes("v i w s o escape").await;
478        cx.shared_state().await.assert_eq(indoc! {
479            "ˇo quick brown
480            fox jumps over
481            the lazy dog"
482        });
483        cx.simulate_shared_keystrokes("j w .").await;
484        cx.shared_state().await.assert_eq(indoc! {
485            "o quick brown
486            fox ˇops over
487            the lazy dog"
488        });
489        cx.simulate_shared_keystrokes("f r .").await;
490        cx.shared_state().await.assert_eq(indoc! {
491            "o quick brown
492            fox ops oveˇothe lazy dog"
493        });
494
495        // visual
496        cx.set_shared_state(indoc! {
497            "the ˇquick brown
498            fox jumps over
499            fox jumps over
500            fox jumps over
501            the lazy dog"
502        })
503        .await;
504        cx.simulate_shared_keystrokes("v j x").await;
505        cx.shared_state().await.assert_eq(indoc! {
506            "the ˇumps over
507            fox jumps over
508            fox jumps over
509            the lazy dog"
510        });
511        cx.simulate_shared_keystrokes(".").await;
512        cx.shared_state().await.assert_eq(indoc! {
513            "the ˇumps over
514            fox jumps over
515            the lazy dog"
516        });
517        cx.simulate_shared_keystrokes("w .").await;
518        cx.shared_state().await.assert_eq(indoc! {
519            "the umps ˇumps over
520            the lazy dog"
521        });
522        cx.simulate_shared_keystrokes("j .").await;
523        cx.shared_state().await.assert_eq(indoc! {
524            "the umps umps over
525            the ˇog"
526        });
527
528        // block mode (3 rows)
529        cx.set_shared_state(indoc! {
530            "ˇthe quick brown
531            fox jumps over
532            the lazy dog"
533        })
534        .await;
535        cx.simulate_shared_keystrokes("ctrl-v j j shift-i o escape")
536            .await;
537        cx.shared_state().await.assert_eq(indoc! {
538            "ˇothe quick brown
539            ofox jumps over
540            othe lazy dog"
541        });
542        cx.simulate_shared_keystrokes("j 4 l .").await;
543        cx.shared_state().await.assert_eq(indoc! {
544            "othe quick brown
545            ofoxˇo jumps over
546            otheo lazy dog"
547        });
548
549        // line mode
550        cx.set_shared_state(indoc! {
551            "ˇthe quick brown
552            fox jumps over
553            the lazy dog"
554        })
555        .await;
556        cx.simulate_shared_keystrokes("shift-v shift-r o escape")
557            .await;
558        cx.shared_state().await.assert_eq(indoc! {
559            "ˇo
560            fox jumps over
561            the lazy dog"
562        });
563        cx.simulate_shared_keystrokes("j .").await;
564        cx.shared_state().await.assert_eq(indoc! {
565            "o
566            ˇo
567            the lazy dog"
568        });
569    }
570
571    #[gpui::test]
572    async fn test_repeat_motion_counts(cx: &mut gpui::TestAppContext) {
573        let mut cx = NeovimBackedTestContext::new(cx).await;
574
575        cx.set_shared_state(indoc! {
576            "ˇthe quick brown
577            fox jumps over
578            the lazy dog"
579        })
580        .await;
581        cx.simulate_shared_keystrokes("3 d 3 l").await;
582        cx.shared_state().await.assert_eq(indoc! {
583            "ˇ brown
584            fox jumps over
585            the lazy dog"
586        });
587        cx.simulate_shared_keystrokes("j .").await;
588        cx.shared_state().await.assert_eq(indoc! {
589            " brown
590            ˇ over
591            the lazy dog"
592        });
593        cx.simulate_shared_keystrokes("j 2 .").await;
594        cx.shared_state().await.assert_eq(indoc! {
595            " brown
596             over
597            ˇe lazy dog"
598        });
599    }
600
601    #[gpui::test]
602    async fn test_record_interrupted(cx: &mut gpui::TestAppContext) {
603        let mut cx = VimTestContext::new(cx, true).await;
604
605        cx.set_state("ˇhello\n", Mode::Normal);
606        cx.simulate_keystrokes("4 i j cmd-shift-p escape");
607        cx.simulate_keystrokes("escape");
608        cx.assert_state("ˇjhello\n", Mode::Normal);
609    }
610
611    #[gpui::test]
612    async fn test_repeat_over_blur(cx: &mut gpui::TestAppContext) {
613        let mut cx = NeovimBackedTestContext::new(cx).await;
614
615        cx.set_shared_state("ˇhello hello hello\n").await;
616        cx.simulate_shared_keystrokes("c f o x escape").await;
617        cx.shared_state().await.assert_eq("ˇx hello hello\n");
618        cx.simulate_shared_keystrokes(": escape").await;
619        cx.simulate_shared_keystrokes(".").await;
620        cx.shared_state().await.assert_eq("ˇx hello\n");
621    }
622
623    #[gpui::test]
624    async fn test_undo_repeated_insert(cx: &mut gpui::TestAppContext) {
625        let mut cx = NeovimBackedTestContext::new(cx).await;
626
627        cx.set_shared_state("hellˇo").await;
628        cx.simulate_shared_keystrokes("3 a . escape").await;
629        cx.shared_state().await.assert_eq("hello..ˇ.");
630        cx.simulate_shared_keystrokes("u").await;
631        cx.shared_state().await.assert_eq("hellˇo");
632    }
633
634    #[gpui::test]
635    async fn test_record_replay(cx: &mut gpui::TestAppContext) {
636        let mut cx = NeovimBackedTestContext::new(cx).await;
637
638        cx.set_shared_state("ˇhello world").await;
639        cx.simulate_shared_keystrokes("q w c w j escape q").await;
640        cx.shared_state().await.assert_eq("ˇj world");
641        cx.simulate_shared_keystrokes("2 l @ w").await;
642        cx.shared_state().await.assert_eq("j ˇj");
643    }
644
645    #[gpui::test]
646    async fn test_record_replay_count(cx: &mut gpui::TestAppContext) {
647        let mut cx = NeovimBackedTestContext::new(cx).await;
648
649        cx.set_shared_state("ˇhello world!!").await;
650        cx.simulate_shared_keystrokes("q a v 3 l s 0 escape l q")
651            .await;
652        cx.shared_state().await.assert_eq("0ˇo world!!");
653        cx.simulate_shared_keystrokes("2 @ a").await;
654        cx.shared_state().await.assert_eq("000ˇ!");
655    }
656
657    #[gpui::test]
658    async fn test_record_replay_dot(cx: &mut gpui::TestAppContext) {
659        let mut cx = NeovimBackedTestContext::new(cx).await;
660
661        cx.set_shared_state("ˇhello world").await;
662        cx.simulate_shared_keystrokes("q a r a l r b l q").await;
663        cx.shared_state().await.assert_eq("abˇllo world");
664        cx.simulate_shared_keystrokes(".").await;
665        cx.shared_state().await.assert_eq("abˇblo world");
666        cx.simulate_shared_keystrokes("shift-q").await;
667        cx.shared_state().await.assert_eq("ababˇo world");
668        cx.simulate_shared_keystrokes(".").await;
669        cx.shared_state().await.assert_eq("ababˇb world");
670    }
671
672    #[gpui::test]
673    async fn test_record_replay_of_dot(cx: &mut gpui::TestAppContext) {
674        let mut cx = NeovimBackedTestContext::new(cx).await;
675
676        cx.set_shared_state("ˇhello world").await;
677        cx.simulate_shared_keystrokes("r o q w . q").await;
678        cx.shared_state().await.assert_eq("ˇoello world");
679        cx.simulate_shared_keystrokes("d l").await;
680        cx.shared_state().await.assert_eq("ˇello world");
681        cx.simulate_shared_keystrokes("@ w").await;
682        cx.shared_state().await.assert_eq("ˇllo world");
683    }
684
685    #[gpui::test]
686    async fn test_record_replay_interleaved(cx: &mut gpui::TestAppContext) {
687        let mut cx = NeovimBackedTestContext::new(cx).await;
688
689        cx.set_shared_state("ˇhello world").await;
690        cx.simulate_shared_keystrokes("q z r a l q").await;
691        cx.shared_state().await.assert_eq("aˇello world");
692        cx.simulate_shared_keystrokes("q b @ z @ z q").await;
693        cx.shared_state().await.assert_eq("aaaˇlo world");
694        cx.simulate_shared_keystrokes("@ @").await;
695        cx.shared_state().await.assert_eq("aaaaˇo world");
696        cx.simulate_shared_keystrokes("@ b").await;
697        cx.shared_state().await.assert_eq("aaaaaaˇworld");
698        cx.simulate_shared_keystrokes("@ @").await;
699        cx.shared_state().await.assert_eq("aaaaaaaˇorld");
700        cx.simulate_shared_keystrokes("q z r b l q").await;
701        cx.shared_state().await.assert_eq("aaaaaaabˇrld");
702        cx.simulate_shared_keystrokes("@ b").await;
703        cx.shared_state().await.assert_eq("aaaaaaabbbˇd");
704    }
705}