reedline 0.47.0

A readline-like crate for CLI text input
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
use super::{motion::Motion, motion::ViCharSearch, parser::ReedlineOption, ViMode};
use crate::enums::{TextObject, TextObjectScope, TextObjectType};
use crate::{EditCommand, ReedlineEvent, Vi};
use std::iter::Peekable;

pub fn parse_command<'iter, I>(input: &mut Peekable<I>) -> Option<Command>
where
    I: Iterator<Item = &'iter char>,
{
    match input.peek() {
        Some('d') => {
            let _ = input.next();
            // Checking for "di(" or "diw" etc.
            if let Some('i') = input.peek() {
                let _ = input.next();
                input.next().and_then(|c| {
                    bracket_pair_for(*c)
                        .map(|(left, right)| Command::DeleteInsidePair { left, right })
                        .or_else(|| {
                            char_to_text_object(*c, TextObjectScope::Inner)
                                .map(|text_object| Command::DeleteTextObject { text_object })
                        })
                })
            } else if let Some('a') = input.peek() {
                let _ = input.next();
                input.next().and_then(|c| {
                    bracket_pair_for(*c)
                        .map(|(left, right)| Command::DeleteAroundPair { left, right })
                        .or_else(|| {
                            char_to_text_object(*c, TextObjectScope::Around)
                                .map(|text_object| Command::DeleteTextObject { text_object })
                        })
                })
            } else {
                Some(Command::Delete)
            }
        }
        // Checking for "yi(" or "yiw" etc.
        Some('y') => {
            let _ = input.next();
            if let Some('i') = input.peek() {
                let _ = input.next();
                input.next().and_then(|c| {
                    bracket_pair_for(*c)
                        .map(|(left, right)| Command::YankInsidePair { left, right })
                        .or_else(|| {
                            char_to_text_object(*c, TextObjectScope::Inner)
                                .map(|text_object| Command::YankTextObject { text_object })
                        })
                })
            } else if let Some('a') = input.peek() {
                let _ = input.next();
                input.next().and_then(|c| {
                    bracket_pair_for(*c)
                        .map(|(left, right)| Command::YankAroundPair { left, right })
                        .or_else(|| {
                            char_to_text_object(*c, TextObjectScope::Around)
                                .map(|text_object| Command::YankTextObject { text_object })
                        })
                })
            } else {
                Some(Command::Yank)
            }
        }
        Some('p') => {
            let _ = input.next();
            Some(Command::PasteAfter)
        }
        Some('P') => {
            let _ = input.next();
            Some(Command::PasteBefore)
        }
        Some('i') => {
            let _ = input.next();
            Some(Command::EnterViInsert)
        }
        Some('a') => {
            let _ = input.next();
            Some(Command::EnterViAppend)
        }
        Some('u') => {
            let _ = input.next();
            Some(Command::Undo)
        }
        // Checking for "ci(" or "ciw" etc.
        Some('c') => {
            let _ = input.next();
            if let Some('i') = input.peek() {
                let _ = input.next();
                input.next().and_then(|c| {
                    bracket_pair_for(*c)
                        .map(|(left, right)| Command::ChangeInsidePair { left, right })
                        .or_else(|| {
                            char_to_text_object(*c, TextObjectScope::Inner)
                                .map(|text_object| Command::ChangeTextObject { text_object })
                        })
                })
            } else if let Some('a') = input.peek() {
                let _ = input.next();
                input.next().and_then(|c| {
                    char_to_text_object(*c, TextObjectScope::Around)
                        .map(|text_object| Command::ChangeTextObject { text_object })
                })
            } else {
                Some(Command::Change)
            }
        }
        Some('x') => {
            let _ = input.next();
            Some(Command::DeleteChar)
        }
        Some('r') => {
            let _ = input.next();
            input
                .next()
                .map(|c| Command::ReplaceChar(*c))
                .or(Some(Command::Incomplete))
        }
        Some('s') => {
            let _ = input.next();
            Some(Command::SubstituteCharWithInsert)
        }
        Some('?') => {
            let _ = input.next();
            Some(Command::HistorySearch)
        }
        Some('C') => {
            let _ = input.next();
            Some(Command::ChangeToLineEnd)
        }
        Some('D') => {
            let _ = input.next();
            Some(Command::DeleteToEnd)
        }
        Some('I') => {
            let _ = input.next();
            Some(Command::PrependToStart)
        }
        Some('A') => {
            let _ = input.next();
            Some(Command::AppendToEnd)
        }
        Some('S') => {
            let _ = input.next();
            Some(Command::RewriteCurrentLine)
        }
        Some('~') => {
            let _ = input.next();
            Some(Command::Switchcase)
        }
        Some('.') => {
            let _ = input.next();
            Some(Command::RepeatLastAction)
        }
        Some('o') => {
            let _ = input.next();
            Some(Command::SwapCursorAndAnchor)
        }
        _ => None,
    }
}

#[derive(Debug, PartialEq, Eq)]
pub enum Command {
    Incomplete,
    Delete,
    DeleteChar,
    ReplaceChar(char),
    SubstituteCharWithInsert,
    PasteAfter,
    PasteBefore,
    EnterViAppend,
    EnterViInsert,
    Undo,
    ChangeToLineEnd,
    DeleteToEnd,
    AppendToEnd,
    PrependToStart,
    RewriteCurrentLine,
    Change,
    HistorySearch,
    Switchcase,
    RepeatLastAction,
    Yank,
    // These DoSthInsidePair commands are agnostic to whether user pressed the left char or right char
    ChangeInsidePair { left: char, right: char },
    DeleteInsidePair { left: char, right: char },
    YankInsidePair { left: char, right: char },
    DeleteAroundPair { left: char, right: char },
    YankAroundPair { left: char, right: char },
    ChangeTextObject { text_object: TextObject },
    YankTextObject { text_object: TextObject },
    DeleteTextObject { text_object: TextObject },
    SwapCursorAndAnchor,
}

impl Command {
    pub fn whole_line_char(&self) -> Option<char> {
        match self {
            Command::Delete => Some('d'),
            Command::Change => Some('c'),
            Command::Yank => Some('y'),
            _ => None,
        }
    }

    pub fn requires_motion(&self) -> bool {
        matches!(self, Command::Delete | Command::Change | Command::Yank)
    }

    pub fn to_reedline(&self, vi_state: &mut Vi) -> Vec<ReedlineOption> {
        match self {
            Self::EnterViInsert => vec![ReedlineOption::Event(ReedlineEvent::Repaint)],
            Self::EnterViAppend => vec![ReedlineOption::Edit(EditCommand::MoveRight {
                select: false,
            })],
            Self::PasteAfter => vec![ReedlineOption::Edit(EditCommand::PasteCutBufferAfter)],
            Self::PasteBefore => vec![ReedlineOption::Edit(EditCommand::PasteCutBufferBefore)],
            Self::Undo => vec![ReedlineOption::Edit(EditCommand::Undo)],
            Self::ChangeToLineEnd => vec![ReedlineOption::Edit(EditCommand::ClearToLineEnd)],
            Self::DeleteToEnd => vec![ReedlineOption::Edit(EditCommand::CutToLineEnd)],
            Self::AppendToEnd => vec![ReedlineOption::Edit(EditCommand::MoveToLineEnd {
                select: false,
            })],
            Self::PrependToStart => vec![ReedlineOption::Edit(EditCommand::MoveToLineStart {
                select: false,
            })],
            Self::RewriteCurrentLine => vec![ReedlineOption::Edit(EditCommand::CutCurrentLine)],
            Self::DeleteChar => {
                if vi_state.mode == ViMode::Visual {
                    vec![ReedlineOption::Edit(EditCommand::CutSelection)]
                } else {
                    vec![ReedlineOption::Edit(EditCommand::CutChar)]
                }
            }
            Self::ReplaceChar(c) => {
                vec![ReedlineOption::Edit(EditCommand::ReplaceChar(*c))]
            }
            Self::SubstituteCharWithInsert => {
                if vi_state.mode == ViMode::Visual {
                    vec![ReedlineOption::Edit(EditCommand::CutSelection)]
                } else {
                    vec![ReedlineOption::Edit(EditCommand::CutChar)]
                }
            }
            Self::HistorySearch => vec![ReedlineOption::Event(ReedlineEvent::SearchHistory)],
            Self::Switchcase => vec![ReedlineOption::Edit(EditCommand::SwitchcaseChar)],
            // Whenever a motion is required to finish the command we must be in visual mode
            Self::Delete | Self::Change => vec![ReedlineOption::Edit(EditCommand::CutSelection)],
            Self::Yank => vec![ReedlineOption::Edit(EditCommand::CopySelection)],
            Self::Incomplete => vec![ReedlineOption::Incomplete],
            Self::RepeatLastAction => match &vi_state.previous {
                Some(event) => vec![ReedlineOption::Event(event.clone())],
                None => vec![],
            },
            Self::ChangeInsidePair { left, right } => {
                vec![ReedlineOption::Edit(EditCommand::CutInsidePair {
                    left: *left,
                    right: *right,
                })]
            }
            Self::DeleteInsidePair { left, right } => {
                vec![ReedlineOption::Edit(EditCommand::CutInsidePair {
                    left: *left,
                    right: *right,
                })]
            }
            Self::YankInsidePair { left, right } => {
                vec![ReedlineOption::Edit(EditCommand::CopyInsidePair {
                    left: *left,
                    right: *right,
                })]
            }
            Self::DeleteAroundPair { left, right } => {
                vec![ReedlineOption::Edit(EditCommand::CutAroundPair {
                    left: *left,
                    right: *right,
                })]
            }
            Self::YankAroundPair { left, right } => {
                vec![ReedlineOption::Edit(EditCommand::CopyAroundPair {
                    left: *left,
                    right: *right,
                })]
            }
            Self::ChangeTextObject { text_object } => {
                vec![ReedlineOption::Edit(EditCommand::CutTextObject {
                    text_object: *text_object,
                })]
            }
            Self::YankTextObject { text_object } => {
                vec![ReedlineOption::Edit(EditCommand::CopyTextObject {
                    text_object: *text_object,
                })]
            }
            Self::DeleteTextObject { text_object } => {
                vec![ReedlineOption::Edit(EditCommand::CutTextObject {
                    text_object: *text_object,
                })]
            }
            Self::SwapCursorAndAnchor => {
                vec![ReedlineOption::Edit(EditCommand::SwapCursorAndAnchor)]
            }
        }
    }

    pub fn to_reedline_with_motion(
        &self,
        motion: &Motion,
        vi_state: &mut Vi,
    ) -> Option<Vec<ReedlineOption>> {
        match self {
            Self::Delete => match motion {
                Motion::End => Some(vec![ReedlineOption::Edit(EditCommand::CutToLineEnd)]),
                Motion::Line => Some(vec![ReedlineOption::Edit(EditCommand::CutCurrentLine)]),
                Motion::NextWord => {
                    Some(vec![ReedlineOption::Edit(EditCommand::CutWordRightToNext)])
                }
                Motion::NextBigWord => Some(vec![ReedlineOption::Edit(
                    EditCommand::CutBigWordRightToNext,
                )]),
                Motion::NextWordEnd => Some(vec![ReedlineOption::Edit(EditCommand::CutWordRight)]),
                Motion::NextBigWordEnd => {
                    Some(vec![ReedlineOption::Edit(EditCommand::CutBigWordRight)])
                }
                Motion::PreviousWord => Some(vec![ReedlineOption::Edit(EditCommand::CutWordLeft)]),
                Motion::PreviousBigWord => {
                    Some(vec![ReedlineOption::Edit(EditCommand::CutBigWordLeft)])
                }
                Motion::RightUntil(c) => {
                    vi_state.last_char_search = Some(ViCharSearch::ToRight(*c));
                    Some(vec![ReedlineOption::Edit(EditCommand::CutRightUntil(*c))])
                }
                Motion::RightBefore(c) => {
                    vi_state.last_char_search = Some(ViCharSearch::TillRight(*c));
                    Some(vec![ReedlineOption::Edit(EditCommand::CutRightBefore(*c))])
                }
                Motion::LeftUntil(c) => {
                    vi_state.last_char_search = Some(ViCharSearch::ToLeft(*c));
                    Some(vec![ReedlineOption::Edit(EditCommand::CutLeftUntil(*c))])
                }
                Motion::LeftBefore(c) => {
                    vi_state.last_char_search = Some(ViCharSearch::TillLeft(*c));
                    Some(vec![ReedlineOption::Edit(EditCommand::CutLeftBefore(*c))])
                }
                Motion::Start => Some(vec![ReedlineOption::Edit(EditCommand::CutFromLineStart)]),
                Motion::NonBlankStart => Some(vec![ReedlineOption::Edit(
                    EditCommand::CutFromLineNonBlankStart,
                )]),
                Motion::Left => Some(vec![ReedlineOption::Edit(EditCommand::Backspace)]),
                Motion::Right => Some(vec![ReedlineOption::Edit(EditCommand::Delete)]),
                Motion::Up => None,
                Motion::Down => None,
                Motion::FirstLine => Some(vec![ReedlineOption::Edit(
                    EditCommand::CutFromStartLinewise {
                        leave_blank_line: false,
                    },
                )]),
                Motion::LastLine => {
                    Some(vec![ReedlineOption::Edit(EditCommand::CutToEndLinewise {
                        leave_blank_line: false,
                    })])
                }
                Motion::ReplayCharSearch => vi_state
                    .last_char_search
                    .as_ref()
                    .map(|char_search| vec![ReedlineOption::Edit(char_search.to_cut())]),
                Motion::ReverseCharSearch => vi_state
                    .last_char_search
                    .as_ref()
                    .map(|char_search| vec![ReedlineOption::Edit(char_search.reverse().to_cut())]),
            },
            Self::Change => {
                let op = match motion {
                    Motion::End => Some(vec![ReedlineOption::Edit(EditCommand::CutToLineEnd)]),
                    Motion::Line => Some(vec![
                        ReedlineOption::Edit(EditCommand::MoveToLineStart { select: false }),
                        ReedlineOption::Edit(EditCommand::CutToLineEnd),
                    ]),
                    Motion::NextWord => Some(vec![ReedlineOption::Edit(EditCommand::CutWordRight)]),
                    Motion::NextBigWord => {
                        Some(vec![ReedlineOption::Edit(EditCommand::CutBigWordRight)])
                    }
                    Motion::NextWordEnd => {
                        Some(vec![ReedlineOption::Edit(EditCommand::CutWordRight)])
                    }
                    Motion::NextBigWordEnd => {
                        Some(vec![ReedlineOption::Edit(EditCommand::CutBigWordRight)])
                    }
                    Motion::PreviousWord => {
                        Some(vec![ReedlineOption::Edit(EditCommand::CutWordLeft)])
                    }
                    Motion::PreviousBigWord => {
                        Some(vec![ReedlineOption::Edit(EditCommand::CutBigWordLeft)])
                    }
                    Motion::RightUntil(c) => {
                        vi_state.last_char_search = Some(ViCharSearch::ToRight(*c));
                        Some(vec![ReedlineOption::Edit(EditCommand::CutRightUntil(*c))])
                    }
                    Motion::RightBefore(c) => {
                        vi_state.last_char_search = Some(ViCharSearch::TillRight(*c));
                        Some(vec![ReedlineOption::Edit(EditCommand::CutRightBefore(*c))])
                    }
                    Motion::LeftUntil(c) => {
                        vi_state.last_char_search = Some(ViCharSearch::ToLeft(*c));
                        Some(vec![ReedlineOption::Edit(EditCommand::CutLeftUntil(*c))])
                    }
                    Motion::LeftBefore(c) => {
                        vi_state.last_char_search = Some(ViCharSearch::TillLeft(*c));
                        Some(vec![ReedlineOption::Edit(EditCommand::CutLeftBefore(*c))])
                    }
                    Motion::Start => {
                        Some(vec![ReedlineOption::Edit(EditCommand::CutFromLineStart)])
                    }
                    Motion::NonBlankStart => Some(vec![ReedlineOption::Edit(
                        EditCommand::CutFromLineNonBlankStart,
                    )]),
                    Motion::Left => Some(vec![ReedlineOption::Edit(EditCommand::Backspace)]),
                    Motion::Right => Some(vec![ReedlineOption::Edit(EditCommand::Delete)]),
                    Motion::Up => None,
                    Motion::Down => None,
                    Motion::FirstLine => Some(vec![ReedlineOption::Edit(
                        EditCommand::CutFromStartLinewise {
                            leave_blank_line: true,
                        },
                    )]),
                    Motion::LastLine => {
                        Some(vec![ReedlineOption::Edit(EditCommand::CutToEndLinewise {
                            leave_blank_line: true,
                        })])
                    }
                    Motion::ReplayCharSearch => vi_state
                        .last_char_search
                        .as_ref()
                        .map(|char_search| vec![ReedlineOption::Edit(char_search.to_cut())]),
                    Motion::ReverseCharSearch => {
                        vi_state.last_char_search.as_ref().map(|char_search| {
                            vec![ReedlineOption::Edit(char_search.reverse().to_cut())]
                        })
                    }
                };
                // Semihack: Append `Repaint` to ensure the mode change gets displayed
                op.map(|mut vec| {
                    vec.push(ReedlineOption::Event(ReedlineEvent::Repaint));
                    vec
                })
            }
            Self::Yank => match motion {
                Motion::End => Some(vec![ReedlineOption::Edit(EditCommand::CopyToLineEnd)]),
                Motion::Line => Some(vec![ReedlineOption::Edit(EditCommand::CopyCurrentLine)]),
                Motion::NextWord => {
                    Some(vec![ReedlineOption::Edit(EditCommand::CopyWordRightToNext)])
                }
                Motion::NextBigWord => Some(vec![ReedlineOption::Edit(
                    EditCommand::CopyBigWordRightToNext,
                )]),
                Motion::NextWordEnd => Some(vec![ReedlineOption::Edit(EditCommand::CopyWordRight)]),
                Motion::NextBigWordEnd => {
                    Some(vec![ReedlineOption::Edit(EditCommand::CopyBigWordRight)])
                }
                Motion::PreviousWord => Some(vec![ReedlineOption::Edit(EditCommand::CopyWordLeft)]),
                Motion::PreviousBigWord => {
                    Some(vec![ReedlineOption::Edit(EditCommand::CopyBigWordLeft)])
                }
                Motion::RightUntil(c) => {
                    vi_state.last_char_search = Some(ViCharSearch::ToRight(*c));
                    Some(vec![ReedlineOption::Edit(EditCommand::CopyRightUntil(*c))])
                }
                Motion::RightBefore(c) => {
                    vi_state.last_char_search = Some(ViCharSearch::TillRight(*c));
                    Some(vec![ReedlineOption::Edit(EditCommand::CopyRightBefore(*c))])
                }
                Motion::LeftUntil(c) => {
                    vi_state.last_char_search = Some(ViCharSearch::ToLeft(*c));
                    Some(vec![ReedlineOption::Edit(EditCommand::CopyLeftUntil(*c))])
                }
                Motion::LeftBefore(c) => {
                    vi_state.last_char_search = Some(ViCharSearch::TillLeft(*c));
                    Some(vec![ReedlineOption::Edit(EditCommand::CopyLeftBefore(*c))])
                }
                Motion::Start => Some(vec![ReedlineOption::Edit(EditCommand::CopyFromLineStart)]),
                Motion::NonBlankStart => Some(vec![ReedlineOption::Edit(
                    EditCommand::CopyFromLineNonBlankStart,
                )]),
                Motion::Left => Some(vec![ReedlineOption::Edit(EditCommand::CopyLeft)]),
                Motion::Right => Some(vec![ReedlineOption::Edit(EditCommand::CopyRight)]),
                Motion::Up => None,
                Motion::Down => None,
                Motion::FirstLine => Some(vec![ReedlineOption::Edit(
                    EditCommand::CopyFromStartLinewise,
                )]),
                Motion::LastLine => {
                    Some(vec![ReedlineOption::Edit(EditCommand::CopyToEndLinewise)])
                }
                Motion::ReplayCharSearch => vi_state
                    .last_char_search
                    .as_ref()
                    .map(|char_search| vec![ReedlineOption::Edit(char_search.to_copy())]),
                Motion::ReverseCharSearch => vi_state
                    .last_char_search
                    .as_ref()
                    .map(|char_search| vec![ReedlineOption::Edit(char_search.reverse().to_copy())]),
            },
            _ => None,
        }
    }
}

fn char_to_text_object(c: char, scope: TextObjectScope) -> Option<TextObject> {
    match c {
        'w' => Some(TextObject {
            scope,
            object_type: TextObjectType::Word,
        }),
        'W' => Some(TextObject {
            scope,
            object_type: TextObjectType::BigWord,
        }),
        'b' => Some(TextObject {
            scope,
            object_type: TextObjectType::Brackets,
        }),
        'q' => Some(TextObject {
            scope,
            object_type: TextObjectType::Quote,
        }),
        _ => None,
    }
}

fn bracket_pair_for(c: char) -> Option<(char, char)> {
    match c {
        '(' | ')' => Some(('(', ')')),
        '[' | ']' => Some(('[', ']')),
        '{' | '}' => Some(('{', '}')),
        '<' | '>' => Some(('<', '>')),
        '"' => Some(('"', '"')),
        '$' => Some(('$', '$')),
        '\'' => Some(('\'', '\'')),
        '`' => Some(('`', '`')),
        _ => None,
    }
}