Skip to main content

ane/commands/chord_engine/
parser.rs

1use anyhow::Result;
2
3use crate::data::chord_types::{
4    Action, Component, Positional, Scope, is_valid_combination, is_valid_count_scope,
5    is_valid_jump_combination, is_valid_list_positional,
6};
7
8use super::errors::ChordError;
9use super::types::{ChordArgs, ChordQuery};
10
11pub fn parse(input: &str) -> Result<ChordQuery> {
12    let input = input.trim();
13    if input.is_empty() {
14        return Err(ChordError::parse(input, 0, "empty chord input").into());
15    }
16
17    let (chord_part, raw_args) = split_chord_and_args(input)?;
18
19    check_multi_digit_count(chord_part, input)?;
20
21    if let Some(query) = try_parse_short_form(chord_part, &raw_args, input)? {
22        return Ok(query);
23    }
24
25    if let Some(query) = try_parse_long_form(chord_part, &raw_args, input)? {
26        return Ok(query);
27    }
28
29    let suggestion = suggest_chord(chord_part);
30    match suggestion {
31        Some(sug) => Err(ChordError::parse_with_suggestion(
32            input,
33            0,
34            format!("unknown chord '{chord_part}'"),
35            sug,
36        )
37        .into()),
38        None => Err(ChordError::parse(input, 0, format!("unknown chord '{chord_part}'")).into()),
39    }
40}
41
42fn split_chord_and_args(input: &str) -> Result<(&str, Option<&str>)> {
43    let Some(paren_start) = input.find('(') else {
44        return Ok((input, None));
45    };
46    if !input.ends_with(')') {
47        return Err(ChordError::parse(
48            input,
49            paren_start,
50            "unterminated argument list (missing closing ')')",
51        )
52        .into());
53    }
54    let chord_part = &input[..paren_start];
55    let args_content = &input[paren_start + 1..input.len() - 1];
56    Ok((chord_part, Some(args_content)))
57}
58
59fn parse_args(raw_args: &Option<&str>) -> ChordArgs {
60    let mut args = ChordArgs::default();
61    let raw = match raw_args {
62        Some(s) if !s.is_empty() => *s,
63        _ => return args,
64    };
65
66    for pair in split_kv_pairs(raw) {
67        let pair = pair.trim();
68        if pair.is_empty() {
69            continue;
70        }
71        if let Some((key, val)) = pair.split_once(':') {
72            let key = key.trim();
73            let val = val.trim().trim_matches('"');
74            match key {
75                "target" if !val.is_empty() => {
76                    args.target_name = Some(val.to_string());
77                    args.target_line = val.parse().ok();
78                }
79                "parent" => {
80                    args.parent_name = Some(val.to_string());
81                }
82                "cursor" => {
83                    if let Some((l, c)) = val.split_once(',')
84                        && let (Ok(line), Ok(col)) = (l.trim().parse(), c.trim().parse())
85                    {
86                        args.cursor_pos = Some((line, col));
87                    }
88                }
89                "value" => {
90                    args.value = Some(val.to_string());
91                }
92                "find" => {
93                    args.find = Some(val.to_string());
94                }
95                "replace" => {
96                    args.replace = Some(val.to_string());
97                }
98                _ => {}
99            }
100        }
101    }
102
103    args
104}
105
106fn split_kv_pairs(input: &str) -> Vec<&str> {
107    let mut pairs = Vec::new();
108    let mut depth = 0;
109    let mut in_quotes = false;
110    let mut start = 0;
111
112    for (i, ch) in input.char_indices() {
113        match ch {
114            '"' => in_quotes = !in_quotes,
115            '(' if !in_quotes => {
116                depth += 1;
117            }
118            ')' if !in_quotes => {
119                depth -= 1;
120            }
121            ',' if !in_quotes && depth == 0 => {
122                pairs.push(&input[start..i]);
123                start = i + 1;
124            }
125            _ => {}
126        }
127    }
128    if start < input.len() {
129        pairs.push(&input[start..]);
130    }
131    pairs
132}
133
134fn try_parse_short_form(
135    chord_part: &str,
136    raw_args: &Option<&str>,
137    _original_input: &str,
138) -> Result<Option<ChordQuery>> {
139    if chord_part.len() != 4 {
140        return Ok(None);
141    }
142
143    let chars: Vec<&str> = chord_part
144        .char_indices()
145        .map(|(i, c)| &chord_part[i..i + c.len_utf8()])
146        .collect();
147
148    if chars.len() != 4 {
149        return Ok(None);
150    }
151
152    let action = match Action::from_short(chars[0]) {
153        Some(a) => a,
154        None => return Ok(None),
155    };
156    if chars[1] == "0" {
157        return Err(ChordError::parse(
158            _original_input,
159            1,
160            "count must be 1\u{2013}9; 0 is not a valid positional",
161        )
162        .into());
163    }
164    let positional = match Positional::from_short(chars[1]) {
165        Some(p) => p,
166        None => return Ok(None),
167    };
168    let scope = match Scope::from_short(chars[2]) {
169        Some(s) => s,
170        None => return Ok(None),
171    };
172    let component = match Component::from_short(chars[3]) {
173        Some(c) => c,
174        None => return Ok(None),
175    };
176
177    if !is_valid_combination(scope, component) {
178        return Err(ChordError::invalid_combination(scope, component).into());
179    }
180
181    if let Positional::Count(_) = positional {
182        if !is_valid_count_scope(scope) {
183            let reason = if scope == Scope::Buffer {
184                "numeric positional is not valid for Buffer scope: there is only one buffer"
185            } else {
186                "numeric positional is not valid for Delimiter scope"
187            };
188            return Err(ChordError::parse(_original_input, 0, reason).into());
189        }
190        if action == Action::Replace {
191            return Err(ChordError::parse(
192                _original_input,
193                0,
194                "Replace action with numeric positional is not supported",
195            )
196            .into());
197        }
198    }
199
200    if matches!(scope, Scope::Delimiter)
201        && matches!(positional, Positional::Next | Positional::Previous)
202    {
203        return Err(ChordError::parse(
204            _original_input,
205            0,
206            "Next/Previous positional is not valid for Delimiter scope",
207        )
208        .into());
209    }
210
211    if action == Action::Jump && !is_valid_jump_combination(positional, component) {
212        let msg = if positional == Positional::Outside {
213            "Jump with Outside positional requires Beginning or End component to specify direction"
214        } else {
215            "Jump does not operate on Value, Parameters, or Arguments components"
216        };
217        return Err(ChordError::parse(_original_input, 0, msg).into());
218    }
219
220    if action == Action::List && !is_valid_list_positional(positional) {
221        return Err(ChordError::parse(
222            _original_input,
223            0,
224            "List action does not support the Outside positional",
225        )
226        .into());
227    }
228
229    let args = parse_args(raw_args);
230
231    if action == Action::Jump && args.value.is_some() {
232        return Err(
233            ChordError::parse(_original_input, 0, "Jump does not accept a value argument").into(),
234        );
235    }
236
237    if action == Action::List {
238        if args.value.is_some() {
239            return Err(ChordError::parse(
240                _original_input,
241                0,
242                "List action does not accept a value argument",
243            )
244            .into());
245        }
246        if args.find.is_some() || args.replace.is_some() {
247            return Err(ChordError::parse(
248                _original_input,
249                0,
250                "List action does not accept find/replace arguments",
251            )
252            .into());
253        }
254        if scope.requires_lsp()
255            && !matches!(
256                component,
257                Component::Name | Component::Definition | Component::End | Component::Self_
258            )
259        {
260            return Err(ChordError::parse(
261                _original_input,
262                0,
263                "List action only supports Name, Definition, End, and Self components for LSP scopes",
264            )
265            .into());
266        }
267    }
268
269    Ok(Some(ChordQuery {
270        action,
271        positional,
272        scope,
273        component,
274        args,
275        requires_lsp: scope.requires_lsp(),
276    }))
277}
278
279fn try_parse_long_form(
280    chord_part: &str,
281    raw_args: &Option<&str>,
282    _original_input: &str,
283) -> Result<Option<ChordQuery>> {
284    let (action, rest) = match parse_long_action(chord_part) {
285        Some(r) => r,
286        None => return Ok(None),
287    };
288    if rest.starts_with('0') {
289        return Err(ChordError::parse(
290            _original_input,
291            0,
292            "count must be 1\u{2013}9; 0 is not a valid positional",
293        )
294        .into());
295    }
296    let (positional, rest) = match parse_long_positional(rest) {
297        Some(r) => r,
298        None => return Ok(None),
299    };
300    let (scope, rest) = match parse_long_scope(rest) {
301        Some(r) => r,
302        None => return Ok(None),
303    };
304    let component = match parse_long_component(rest) {
305        Some(c) => c,
306        None => return Ok(None),
307    };
308
309    if !is_valid_combination(scope, component) {
310        return Err(ChordError::invalid_combination(scope, component).into());
311    }
312
313    if let Positional::Count(_) = positional {
314        if !is_valid_count_scope(scope) {
315            let reason = if scope == Scope::Buffer {
316                "numeric positional is not valid for Buffer scope: there is only one buffer"
317            } else {
318                "numeric positional is not valid for Delimiter scope"
319            };
320            return Err(ChordError::parse(_original_input, 0, reason).into());
321        }
322        if action == Action::Replace {
323            return Err(ChordError::parse(
324                _original_input,
325                0,
326                "Replace action with numeric positional is not supported",
327            )
328            .into());
329        }
330    }
331
332    if matches!(scope, Scope::Delimiter)
333        && matches!(positional, Positional::Next | Positional::Previous)
334    {
335        return Err(ChordError::parse(
336            _original_input,
337            0,
338            "Next/Previous positional is not valid for Delimiter scope",
339        )
340        .into());
341    }
342
343    if action == Action::Jump && !is_valid_jump_combination(positional, component) {
344        let msg = if positional == Positional::Outside {
345            "Jump with Outside positional requires Beginning or End component to specify direction"
346        } else {
347            "Jump does not operate on Value, Parameters, or Arguments components"
348        };
349        return Err(ChordError::parse(_original_input, 0, msg).into());
350    }
351
352    if action == Action::List && !is_valid_list_positional(positional) {
353        return Err(ChordError::parse(
354            _original_input,
355            0,
356            "List action does not support the Outside positional",
357        )
358        .into());
359    }
360
361    let args = parse_args(raw_args);
362
363    if action == Action::Jump && args.value.is_some() {
364        return Err(
365            ChordError::parse(_original_input, 0, "Jump does not accept a value argument").into(),
366        );
367    }
368
369    if action == Action::List {
370        if args.value.is_some() {
371            return Err(ChordError::parse(
372                _original_input,
373                0,
374                "List action does not accept a value argument",
375            )
376            .into());
377        }
378        if args.find.is_some() || args.replace.is_some() {
379            return Err(ChordError::parse(
380                _original_input,
381                0,
382                "List action does not accept find/replace arguments",
383            )
384            .into());
385        }
386        if scope.requires_lsp()
387            && !matches!(
388                component,
389                Component::Name | Component::Definition | Component::End | Component::Self_
390            )
391        {
392            return Err(ChordError::parse(
393                _original_input,
394                0,
395                "List action only supports Name, Definition, End, and Self components for LSP scopes",
396            )
397            .into());
398        }
399    }
400
401    Ok(Some(ChordQuery {
402        action,
403        positional,
404        scope,
405        component,
406        args,
407        requires_lsp: scope.requires_lsp(),
408    }))
409}
410
411fn check_multi_digit_count(chord_part: &str, original: &str) -> Result<()> {
412    let chars: Vec<char> = chord_part.chars().collect();
413    if chars.len() >= 3
414        && Action::from_short(&chars[0].to_string()).is_some()
415        && chars[1].is_ascii_digit()
416        && chars[2].is_ascii_digit()
417    {
418        return Err(ChordError::parse(
419            original,
420            1,
421            "only single-digit counts (1\u{2013}9) are supported",
422        )
423        .into());
424    }
425
426    if let Some((_, rest)) = parse_long_action(chord_part) {
427        let rest_chars: Vec<char> = rest.chars().collect();
428        if rest_chars.len() >= 2 && rest_chars[0].is_ascii_digit() && rest_chars[1].is_ascii_digit()
429        {
430            return Err(ChordError::parse(
431                original,
432                0,
433                "only single-digit counts (1\u{2013}9) are supported",
434            )
435            .into());
436        }
437    }
438
439    Ok(())
440}
441
442fn parse_long_action(input: &str) -> Option<(Action, &str)> {
443    let pairs = [
444        ("Change", Action::Change),
445        ("Replace", Action::Replace),
446        ("Delete", Action::Delete),
447        ("Yank", Action::Yank),
448        ("Append", Action::Append),
449        ("Prepend", Action::Prepend),
450        ("Insert", Action::Insert),
451        ("Jump", Action::Jump),
452        ("List", Action::List),
453    ];
454    for (prefix, action) in pairs {
455        if let Some(rest) = input.strip_prefix(prefix) {
456            return Some((action, rest));
457        }
458    }
459    None
460}
461
462fn parse_long_positional(input: &str) -> Option<(Positional, &str)> {
463    if let Some(ch) = input.chars().next()
464        && ch.is_ascii_digit()
465        && ch != '0'
466    {
467        let n = ch as u8 - b'0';
468        return Some((Positional::Count(n), &input[1..]));
469    }
470
471    let pairs = [
472        ("Inside", Positional::Inside),
473        ("Until", Positional::Until),
474        ("After", Positional::After),
475        ("Before", Positional::Before),
476        ("Next", Positional::Next),
477        ("Previous", Positional::Previous),
478        ("Entire", Positional::Entire),
479        ("Outside", Positional::Outside),
480        ("First", Positional::First),
481        ("Last", Positional::Last),
482        ("To", Positional::To),
483    ];
484    for (prefix, positional) in pairs {
485        if let Some(rest) = input.strip_prefix(prefix) {
486            return Some((positional, rest));
487        }
488    }
489    None
490}
491
492fn parse_long_scope(input: &str) -> Option<(Scope, &str)> {
493    let pairs = [
494        ("Function", Scope::Function),
495        ("Variable", Scope::Variable),
496        ("Delimiter", Scope::Delimiter),
497        ("Buffer", Scope::Buffer),
498        ("Struct", Scope::Struct),
499        ("Member", Scope::Member),
500        ("Line", Scope::Line),
501    ];
502    for (prefix, scope) in pairs {
503        if let Some(rest) = input.strip_prefix(prefix) {
504            return Some((scope, rest));
505        }
506    }
507    None
508}
509
510fn parse_long_component(input: &str) -> Option<Component> {
511    match input {
512        "Beginning" => Some(Component::Beginning),
513        "Contents" => Some(Component::Contents),
514        "End" => Some(Component::End),
515        "Value" => Some(Component::Value),
516        "Parameters" => Some(Component::Parameters),
517        "Arguments" => Some(Component::Arguments),
518        "Name" => Some(Component::Name),
519        "Self" => Some(Component::Self_),
520        "Word" => Some(Component::Word),
521        "Definition" => Some(Component::Definition),
522        _ => None,
523    }
524}
525
526fn suggest_chord(input: &str) -> Option<String> {
527    let input_chars: String = input.chars().take(4).collect();
528    if input_chars.chars().count() < 4 {
529        return None;
530    }
531
532    let actions = ['c', 'r', 'd', 'y', 'a', 'p', 'i', 'j', 'l'];
533    let positionals = ['i', 'u', 'a', 'b', 'n', 'p', 'e', 'o', 't', 'l', 'f'];
534    let scopes = ['l', 'b', 'f', 'v', 's', 'm', 'd'];
535    let components = ['b', 'c', 'e', 'v', 'p', 'a', 'n', 's', 'w', 'd'];
536
537    let mut best_dist = usize::MAX;
538    let mut best = None;
539
540    for &a in &actions {
541        for &p in &positionals {
542            for &s in &scopes {
543                for &c in &components {
544                    let candidate = format!("{a}{p}{s}{c}");
545                    let scope = Scope::from_short(&s.to_string()).unwrap();
546                    let comp = Component::from_short(&c.to_string()).unwrap();
547                    if !is_valid_combination(scope, comp) {
548                        continue;
549                    }
550                    let dist = levenshtein(&input_chars, &candidate);
551                    if dist < best_dist && dist <= 2 {
552                        best_dist = dist;
553                        best = Some(candidate);
554                    }
555                }
556            }
557        }
558    }
559
560    best
561}
562
563fn levenshtein(a: &str, b: &str) -> usize {
564    let a: Vec<char> = a.chars().collect();
565    let b: Vec<char> = b.chars().collect();
566    let mut dp = vec![vec![0usize; b.len() + 1]; a.len() + 1];
567
568    for (i, row) in dp.iter_mut().enumerate().take(a.len() + 1) {
569        row[0] = i;
570    }
571    for (j, val) in dp[0].iter_mut().enumerate().take(b.len() + 1) {
572        *val = j;
573    }
574
575    for i in 1..=a.len() {
576        for j in 1..=b.len() {
577            let cost = if a[i - 1] == b[j - 1] { 0 } else { 1 };
578            dp[i][j] = (dp[i - 1][j] + 1)
579                .min(dp[i][j - 1] + 1)
580                .min(dp[i - 1][j - 1] + cost);
581        }
582    }
583
584    dp[a.len()][b.len()]
585}
586
587#[cfg(test)]
588mod tests {
589    use super::parse;
590    use crate::data::chord_types::{
591        Action, Component, Positional, Scope, is_valid_combination, is_valid_jump_combination,
592        is_valid_list_positional,
593    };
594
595    const ALL_ACTIONS: &[Action] = &[
596        Action::Change,
597        Action::Replace,
598        Action::Delete,
599        Action::Yank,
600        Action::Append,
601        Action::Prepend,
602        Action::Insert,
603        Action::Jump,
604        Action::List,
605    ];
606
607    const ALL_POSITIONALS: &[Positional] = &[
608        Positional::Inside,
609        Positional::Until,
610        Positional::After,
611        Positional::Before,
612        Positional::Next,
613        Positional::Previous,
614        Positional::Entire,
615        Positional::Outside,
616        Positional::To,
617        Positional::First,
618        Positional::Last,
619    ];
620
621    const ALL_SCOPES: &[Scope] = &[
622        Scope::Line,
623        Scope::Buffer,
624        Scope::Function,
625        Scope::Variable,
626        Scope::Struct,
627        Scope::Member,
628        Scope::Delimiter,
629    ];
630
631    const ALL_COMPONENTS: &[Component] = &[
632        Component::Beginning,
633        Component::Contents,
634        Component::End,
635        Component::Value,
636        Component::Parameters,
637        Component::Arguments,
638        Component::Name,
639        Component::Self_,
640        Component::Word,
641        Component::Definition,
642    ];
643
644    #[test]
645    fn all_valid_short_forms_parse_and_invalid_fail() {
646        for &action in ALL_ACTIONS {
647            for &pos in ALL_POSITIONALS {
648                for &scope in ALL_SCOPES {
649                    for &comp in ALL_COMPONENTS {
650                        let short = format!(
651                            "{}{}{}{}",
652                            action.short(),
653                            pos.short(),
654                            scope.short(),
655                            comp.short()
656                        );
657                        let result = parse(&short);
658                        let scope_comp_valid = is_valid_combination(scope, comp);
659                        let jump_valid =
660                            action != Action::Jump || is_valid_jump_combination(pos, comp);
661                        let delimiter_positional_valid = scope != Scope::Delimiter
662                            || !matches!(pos, Positional::Next | Positional::Previous);
663                        let list_positional_valid =
664                            action != Action::List || is_valid_list_positional(pos);
665                        let list_component_valid = action != Action::List
666                            || !scope.requires_lsp()
667                            || matches!(
668                                comp,
669                                Component::Name
670                                    | Component::Definition
671                                    | Component::End
672                                    | Component::Self_
673                            );
674                        let should_parse = scope_comp_valid
675                            && jump_valid
676                            && delimiter_positional_valid
677                            && list_positional_valid
678                            && list_component_valid;
679                        if should_parse {
680                            let q = result.unwrap_or_else(|e| {
681                                panic!("expected {short} to parse OK, got: {e}")
682                            });
683                            assert_eq!(q.action, action, "action mismatch for {short}");
684                            assert_eq!(q.positional, pos, "positional mismatch for {short}");
685                            assert_eq!(q.scope, scope, "scope mismatch for {short}");
686                            assert_eq!(q.component, comp, "component mismatch for {short}");
687                            assert_eq!(
688                                q.requires_lsp,
689                                scope.requires_lsp(),
690                                "requires_lsp mismatch for {short}"
691                            );
692                        } else {
693                            assert!(
694                                result.is_err(),
695                                "expected {short} to fail (invalid combo), but it parsed OK"
696                            );
697                        }
698                    }
699                }
700            }
701        }
702    }
703
704    #[test]
705    fn all_valid_long_forms_parse() {
706        for &action in ALL_ACTIONS {
707            for &pos in ALL_POSITIONALS {
708                for &scope in ALL_SCOPES {
709                    for &comp in ALL_COMPONENTS {
710                        if !is_valid_combination(scope, comp) {
711                            continue;
712                        }
713                        if action == Action::Jump && !is_valid_jump_combination(pos, comp) {
714                            continue;
715                        }
716                        if action == Action::List && !is_valid_list_positional(pos) {
717                            continue;
718                        }
719                        if action == Action::List
720                            && scope.requires_lsp()
721                            && !matches!(
722                                comp,
723                                Component::Name
724                                    | Component::Definition
725                                    | Component::End
726                                    | Component::Self_
727                            )
728                        {
729                            continue;
730                        }
731                        if scope == Scope::Delimiter
732                            && matches!(pos, Positional::Next | Positional::Previous)
733                        {
734                            continue;
735                        }
736                        let long = format!("{action}{pos}{scope}{comp}");
737                        let result = parse(&long);
738                        let q = result
739                            .unwrap_or_else(|e| panic!("expected {long} to parse OK, got: {e}"));
740                        assert_eq!(q.action, action, "action mismatch for {long}");
741                        assert_eq!(q.positional, pos, "positional mismatch for {long}");
742                        assert_eq!(q.scope, scope, "scope mismatch for {long}");
743                        assert_eq!(q.component, comp, "component mismatch for {long}");
744                        assert_eq!(q.requires_lsp, scope.requires_lsp());
745                    }
746                }
747            }
748        }
749    }
750
751    #[test]
752    fn spot_check_change_inside_function_contents() {
753        let q = parse("cifc").unwrap();
754        assert_eq!(q.action, Action::Change);
755        assert_eq!(q.positional, Positional::Inside);
756        assert_eq!(q.scope, Scope::Function);
757        assert_eq!(q.component, Component::Contents);
758        assert!(q.requires_lsp);
759    }
760
761    #[test]
762    fn spot_check_delete_entire_line_self() {
763        let q = parse("dels").unwrap();
764        assert_eq!(q.action, Action::Delete);
765        assert_eq!(q.positional, Positional::Entire);
766        assert_eq!(q.scope, Scope::Line);
767        assert_eq!(q.component, Component::Self_);
768        assert!(!q.requires_lsp);
769    }
770
771    #[test]
772    fn spot_check_yank_entire_struct_self() {
773        let q = parse("yess").unwrap();
774        assert_eq!(q.action, Action::Yank);
775        assert_eq!(q.positional, Positional::Entire);
776        assert_eq!(q.scope, Scope::Struct);
777        assert_eq!(q.component, Component::Self_);
778        assert!(q.requires_lsp);
779    }
780
781    #[test]
782    fn spot_check_append_after_line_end() {
783        let q = parse("aale").unwrap();
784        assert_eq!(q.action, Action::Append);
785        assert_eq!(q.positional, Positional::After);
786        assert_eq!(q.scope, Scope::Line);
787        assert_eq!(q.component, Component::End);
788        assert!(!q.requires_lsp);
789    }
790
791    #[test]
792    fn spot_check_buffer_contents_is_invalid() {
793        assert!(parse("pbbc").is_err());
794    }
795
796    #[test]
797    fn spot_check_change_inside_function_beginning_is_invalid() {
798        assert!(parse("cifb").is_err());
799    }
800
801    #[test]
802    fn spot_check_replace_entire_variable_name() {
803        let q = parse("revn").unwrap();
804        assert_eq!(q.action, Action::Replace);
805        assert_eq!(q.positional, Positional::Entire);
806        assert_eq!(q.scope, Scope::Variable);
807        assert_eq!(q.component, Component::Name);
808    }
809
810    #[test]
811    fn spot_check_insert_until_member_value() {
812        let q = parse("iumv").unwrap();
813        assert_eq!(q.action, Action::Insert);
814        assert_eq!(q.positional, Positional::Until);
815        assert_eq!(q.scope, Scope::Member);
816        assert_eq!(q.component, Component::Value);
817    }
818
819    #[test]
820    fn short_form_and_long_form_equivalent() {
821        let short = parse("cifc").unwrap();
822        let long = parse("ChangeInsideFunctionContents").unwrap();
823        assert_eq!(short.action, long.action);
824        assert_eq!(short.positional, long.positional);
825        assert_eq!(short.scope, long.scope);
826        assert_eq!(short.component, long.component);
827    }
828
829    #[test]
830    fn args_target_key() {
831        let q = parse("cifc(target:getData)").unwrap();
832        assert_eq!(q.args.target_name.as_deref(), Some("getData"));
833        assert!(q.args.target_line.is_none());
834        assert!(q.args.cursor_pos.is_none());
835        assert!(q.args.value.is_none());
836    }
837
838    #[test]
839    fn args_target_key_works_for_all_lsp_scopes() {
840        let q = parse("cevv(target:myVar)").unwrap();
841        assert_eq!(q.args.target_name.as_deref(), Some("myVar"));
842        let q = parse("cesn(target:MyStruct)").unwrap();
843        assert_eq!(q.args.target_name.as_deref(), Some("MyStruct"));
844        let q = parse("cemn(target:myField)").unwrap();
845        assert_eq!(q.args.target_name.as_deref(), Some("myField"));
846    }
847
848    #[test]
849    fn args_old_scope_specific_keys_are_ignored() {
850        let q = parse("cifc(function:getData)").unwrap();
851        assert!(q.args.target_name.is_none());
852        let q = parse("cevv(variable:myVar)").unwrap();
853        assert!(q.args.target_name.is_none());
854        let q = parse("cesn(struct:MyStruct)").unwrap();
855        assert!(q.args.target_name.is_none());
856        let q = parse("cemn(member:myField)").unwrap();
857        assert!(q.args.target_name.is_none());
858        let q = parse("cifc(name:myFunc)").unwrap();
859        assert!(q.args.target_name.is_none());
860        let q = parse("cels(line:42)").unwrap();
861        assert!(q.args.target_line.is_none());
862    }
863
864    #[test]
865    fn args_target_line_number() {
866        let q = parse("cels(target:42)").unwrap();
867        assert_eq!(q.args.target_line, Some(42));
868        assert_eq!(q.args.target_name.as_deref(), Some("42"));
869    }
870
871    #[test]
872    fn args_cursor_position() {
873        let q = parse(r#"cels(cursor:"3,7")"#).unwrap();
874        assert_eq!(q.args.cursor_pos, Some((3, 7)));
875    }
876
877    #[test]
878    fn args_cursor_position_with_spaces() {
879        let q = parse(r#"cels(cursor:"0,12")"#).unwrap();
880        assert_eq!(q.args.cursor_pos, Some((0, 12)));
881    }
882
883    #[test]
884    fn args_value_plain() {
885        let q = parse("cels(value:hello)").unwrap();
886        assert_eq!(q.args.value.as_deref(), Some("hello"));
887    }
888
889    #[test]
890    fn args_value_quoted_with_spaces() {
891        let q = parse(r#"cifc(target:getData, value:"new body goes here")"#).unwrap();
892        assert_eq!(q.args.target_name.as_deref(), Some("getData"));
893        assert_eq!(q.args.value.as_deref(), Some("new body goes here"));
894    }
895
896    #[test]
897    fn args_value_with_parens_quoted() {
898        let q = parse(r#"cifp(target:getData, value:"(x: i32)")"#).unwrap();
899        assert_eq!(q.args.value.as_deref(), Some("(x: i32)"));
900    }
901
902    #[test]
903    fn args_extra_commas_ignored() {
904        let q = parse("cels(,target:1,,)").unwrap();
905        assert_eq!(q.args.target_line, Some(1));
906    }
907
908    #[test]
909    fn args_missing_value_for_target_is_none() {
910        let q = parse("cels(target:)").unwrap();
911        assert!(q.args.target_line.is_none());
912        assert!(q.args.target_name.is_none());
913    }
914
915    #[test]
916    fn args_unknown_key_is_ignored() {
917        let q = parse("cels(bogus:foo, target:2)").unwrap();
918        assert_eq!(q.args.target_line, Some(2));
919    }
920
921    #[test]
922    fn args_multiple_keys() {
923        let q = parse(r#"cifc(target:getData, value:"body")"#).unwrap();
924        assert_eq!(q.args.target_name.as_deref(), Some("getData"));
925        assert_eq!(q.args.value.as_deref(), Some("body"));
926    }
927
928    #[test]
929    fn invalid_combination_line_parameters_short() {
930        let result = parse("cilp");
931        assert!(result.is_err());
932        let msg = format!("{}", result.unwrap_err());
933        assert!(
934            msg.to_lowercase().contains("invalid"),
935            "expected 'invalid' in error: {msg}"
936        );
937    }
938
939    #[test]
940    fn invalid_combination_buffer_value_short() {
941        let result = parse("cibv");
942        assert!(result.is_err());
943    }
944
945    #[test]
946    fn invalid_combination_variable_parameters() {
947        let result = parse("civp");
948        assert!(result.is_err());
949    }
950
951    #[test]
952    fn invalid_combination_struct_arguments() {
953        let result = parse("cisa");
954        assert!(result.is_err());
955    }
956
957    #[test]
958    fn invalid_combination_long_form() {
959        let result = parse("ChangeInsideLineParameters");
960        assert!(result.is_err());
961    }
962
963    #[test]
964    fn invalid_combination_long_form_buffer_value() {
965        let result = parse("ChangeInsideBufferValue");
966        assert!(result.is_err());
967    }
968
969    #[test]
970    fn empty_input_errors() {
971        let result = parse("");
972        assert!(result.is_err());
973        let msg = format!("{}", result.unwrap_err());
974        assert!(msg.contains("empty"));
975    }
976
977    #[test]
978    fn whitespace_only_errors() {
979        let result = parse("   ");
980        assert!(result.is_err());
981    }
982
983    #[test]
984    fn unknown_short_chord_errors() {
985        let result = parse("zzzz");
986        assert!(result.is_err());
987    }
988
989    #[test]
990    fn near_miss_suggests_correction() {
991        let result = parse("xifv");
992        assert!(result.is_err());
993        let msg = format!("{}", result.unwrap_err());
994        assert!(
995            msg.contains("did you mean"),
996            "expected suggestion in error message: {msg}"
997        );
998    }
999
1000    #[test]
1001    fn whitespace_trimmed_around_chord() {
1002        let q = parse("  cifc  ").unwrap();
1003        assert_eq!(q.action, Action::Change);
1004        assert_eq!(q.positional, Positional::Inside);
1005        assert_eq!(q.scope, Scope::Function);
1006        assert_eq!(q.component, Component::Contents);
1007    }
1008
1009    #[test]
1010    fn short_form_sets_requires_lsp_false_for_line_and_buffer() {
1011        assert!(!parse("cels").unwrap().requires_lsp);
1012        assert!(!parse("cebs").unwrap().requires_lsp);
1013    }
1014
1015    #[test]
1016    fn short_form_sets_requires_lsp_true_for_lsp_scopes() {
1017        assert!(parse("cefs").unwrap().requires_lsp);
1018        assert!(parse("cevs").unwrap().requires_lsp);
1019        assert!(parse("cess").unwrap().requires_lsp);
1020        assert!(parse("cems").unwrap().requires_lsp);
1021    }
1022
1023    #[test]
1024    fn long_form_self_component_accepted() {
1025        let q = parse("ChangeEntireLineSelf").unwrap();
1026        assert_eq!(q.component, Component::Self_);
1027    }
1028
1029    #[test]
1030    fn unterminated_paren_errors() {
1031        let result = parse("cifv(target:1");
1032        assert!(result.is_err());
1033        assert!(format!("{}", result.unwrap_err()).contains("unterminated"));
1034    }
1035
1036    #[test]
1037    fn args_parent_key() {
1038        let q = parse("cemv(target:x, parent:Foo)").unwrap();
1039        assert_eq!(q.args.target_name.as_deref(), Some("x"));
1040        assert_eq!(q.args.parent_name.as_deref(), Some("Foo"));
1041    }
1042
1043    #[test]
1044    fn args_find_replace_keys() {
1045        let q = parse(r#"rels(target:0, find:"foo", replace:"bar")"#).unwrap();
1046        assert_eq!(q.args.find.as_deref(), Some("foo"));
1047        assert_eq!(q.args.replace.as_deref(), Some("bar"));
1048    }
1049
1050    #[test]
1051    fn unicode_input_does_not_panic_in_suggest() {
1052        let result = parse("cłfv");
1053        assert!(result.is_err());
1054    }
1055
1056    // --- work item 0005: Jump / To / Delimiter ---
1057
1058    #[test]
1059    fn jump_outside_invalid_component_rejects_with_direction_hint() {
1060        // joln = Jump Outside Line Name — valid scope/component, invalid jump+outside
1061        let result = parse("joln");
1062        assert!(result.is_err());
1063        let msg = format!("{}", result.unwrap_err());
1064        assert!(
1065            msg.contains("Beginning") || msg.contains("End") || msg.contains("direction"),
1066            "expected direction hint in error: {msg}"
1067        );
1068    }
1069
1070    #[test]
1071    fn jump_outside_beginning_and_end_are_valid() {
1072        // jolb = Jump Outside Line Beginning; jole = Jump Outside Line End
1073        assert!(parse("jolb").is_ok(), "jolb should parse OK");
1074        assert!(parse("jole").is_ok(), "jole should parse OK");
1075    }
1076
1077    #[test]
1078    fn jump_outside_other_components_fail() {
1079        // Name and Self_ are valid scope/component combos but invalid for Jump+Outside
1080        assert!(parse("joln").is_err(), "joln (Name) should fail");
1081        assert!(parse("jols").is_err(), "jols (Self_) should fail");
1082        // Parameters is a valid Function component but invalid for Jump+Outside
1083        assert!(parse("jofp").is_err(), "jofp (Parameters) should fail");
1084    }
1085
1086    #[test]
1087    fn jump_non_outside_valid_combinations() {
1088        assert!(
1089            parse("jtfc").is_ok(),
1090            "jtfc (To Function Contents) should parse OK"
1091        );
1092        assert!(
1093            parse("jnfn").is_ok(),
1094            "jnfn (Next Function Name) should parse OK"
1095        );
1096        assert!(
1097            parse("jifc").is_ok(),
1098            "jifc (Inside Function Contents) should parse OK"
1099        );
1100    }
1101
1102    #[test]
1103    fn jump_with_value_argument_rejects() {
1104        let result = parse(r#"jtfc(value:"text")"#);
1105        assert!(result.is_err());
1106        let msg = format!("{}", result.unwrap_err());
1107        assert!(
1108            msg.contains("value") || msg.contains("Jump"),
1109            "expected value/Jump in error: {msg}"
1110        );
1111    }
1112
1113    #[test]
1114    fn jump_bare_short_form_no_args_required() {
1115        assert!(parse("jtfc").is_ok());
1116        assert!(parse("jolb").is_ok());
1117        assert!(parse("jefc").is_ok());
1118    }
1119
1120    #[test]
1121    fn delimiter_scope_next_positional_rejects() {
1122        // cnds = Change Next Delimiter Self_ — Delimiter does not support Next
1123        let result = parse("cnds");
1124        assert!(result.is_err());
1125        let msg = format!("{}", result.unwrap_err());
1126        assert!(
1127            msg.contains("Delimiter") || msg.contains("Next") || msg.contains("Previous"),
1128            "expected Delimiter/Next in error: {msg}"
1129        );
1130    }
1131
1132    #[test]
1133    fn delimiter_scope_previous_positional_rejects() {
1134        // cpds = Change Previous Delimiter Self_
1135        let result = parse("cpds");
1136        assert!(result.is_err());
1137    }
1138
1139    // --- work item 0011: Word / Definition / List ---
1140
1141    #[test]
1142    fn parse_lefn_list_entire_function_name() {
1143        let q = parse("lefn").unwrap();
1144        assert_eq!(q.action, Action::List);
1145        assert_eq!(q.positional, Positional::Entire);
1146        assert_eq!(q.scope, Scope::Function);
1147        assert_eq!(q.component, Component::Name);
1148    }
1149
1150    #[test]
1151    fn parse_lisn_list_inside_struct_name() {
1152        let q = parse("lisn").unwrap();
1153        assert_eq!(q.action, Action::List);
1154        assert_eq!(q.positional, Positional::Inside);
1155        assert_eq!(q.scope, Scope::Struct);
1156        assert_eq!(q.component, Component::Name);
1157    }
1158
1159    #[test]
1160    fn parse_lafn_list_after_function_name() {
1161        let q = parse("lafn").unwrap();
1162        assert_eq!(q.action, Action::List);
1163        assert_eq!(q.positional, Positional::After);
1164        assert_eq!(q.scope, Scope::Function);
1165        assert_eq!(q.component, Component::Name);
1166    }
1167
1168    #[test]
1169    fn parse_celw_change_entire_line_word() {
1170        let q = parse("celw").unwrap();
1171        assert_eq!(q.action, Action::Change);
1172        assert_eq!(q.positional, Positional::Entire);
1173        assert_eq!(q.scope, Scope::Line);
1174        assert_eq!(q.component, Component::Word);
1175    }
1176
1177    #[test]
1178    fn parse_jnlw_jump_next_line_word() {
1179        let q = parse("jnlw").unwrap();
1180        assert_eq!(q.action, Action::Jump);
1181        assert_eq!(q.positional, Positional::Next);
1182        assert_eq!(q.scope, Scope::Line);
1183        assert_eq!(q.component, Component::Word);
1184    }
1185
1186    #[test]
1187    fn parse_jllw_jump_last_line_word() {
1188        let q = parse("jllw").unwrap();
1189        assert_eq!(q.action, Action::Jump);
1190        assert_eq!(q.positional, Positional::Last);
1191        assert_eq!(q.scope, Scope::Line);
1192        assert_eq!(q.component, Component::Word);
1193    }
1194
1195    #[test]
1196    fn parse_jflw_jump_first_line_word() {
1197        let q = parse("jflw").unwrap();
1198        assert_eq!(q.action, Action::Jump);
1199        assert_eq!(q.positional, Positional::First);
1200        assert_eq!(q.scope, Scope::Line);
1201        assert_eq!(q.component, Component::Word);
1202    }
1203
1204    #[test]
1205    fn parse_jlfn_jump_last_function_name() {
1206        let q = parse("jlfn").unwrap();
1207        assert_eq!(q.action, Action::Jump);
1208        assert_eq!(q.positional, Positional::Last);
1209        assert_eq!(q.scope, Scope::Function);
1210        assert_eq!(q.component, Component::Name);
1211    }
1212
1213    #[test]
1214    fn parse_lefd_list_entire_function_definition() {
1215        let q = parse("lefd").unwrap();
1216        assert_eq!(q.action, Action::List);
1217        assert_eq!(q.positional, Positional::Entire);
1218        assert_eq!(q.scope, Scope::Function);
1219        assert_eq!(q.component, Component::Definition);
1220    }
1221
1222    #[test]
1223    fn parse_cefd_change_entire_function_definition() {
1224        let q = parse("cefd").unwrap();
1225        assert_eq!(q.action, Action::Change);
1226        assert_eq!(q.positional, Positional::Entire);
1227        assert_eq!(q.scope, Scope::Function);
1228        assert_eq!(q.component, Component::Definition);
1229    }
1230
1231    #[test]
1232    fn parse_yevd_yank_entire_variable_definition() {
1233        let q = parse("yevd").unwrap();
1234        assert_eq!(q.action, Action::Yank);
1235        assert_eq!(q.positional, Positional::Entire);
1236        assert_eq!(q.scope, Scope::Variable);
1237        assert_eq!(q.component, Component::Definition);
1238    }
1239
1240    #[test]
1241    fn parse_celd_invalid_line_definition_combo() {
1242        let result = parse("celd");
1243        assert!(
1244            result.is_err(),
1245            "celd should fail: Line+Definition is invalid"
1246        );
1247    }
1248
1249    #[test]
1250    fn parse_list_with_value_arg_errors() {
1251        let result = parse(r#"lefn(value:"x")"#);
1252        assert!(result.is_err());
1253        let msg = format!("{}", result.unwrap_err());
1254        assert!(
1255            msg.contains("List action does not accept a value argument"),
1256            "expected value-arg error: {msg}"
1257        );
1258    }
1259
1260    #[test]
1261    fn parse_list_outside_positional_errors() {
1262        let result = parse("lofn");
1263        assert!(result.is_err());
1264        let msg = format!("{}", result.unwrap_err());
1265        assert!(
1266            msg.contains("List action does not support the Outside positional"),
1267            "expected outside-positional error: {msg}"
1268        );
1269    }
1270
1271    #[test]
1272    fn long_form_list_entire_function_name_matches_short() {
1273        let short = parse("lefn").unwrap();
1274        let long = parse("ListEntireFunctionName").unwrap();
1275        assert_eq!(short.action, long.action);
1276        assert_eq!(short.positional, long.positional);
1277        assert_eq!(short.scope, long.scope);
1278        assert_eq!(short.component, long.component);
1279    }
1280
1281    #[test]
1282    fn long_form_list_entire_function_definition_matches_short() {
1283        let short = parse("lefd").unwrap();
1284        let long = parse("ListEntireFunctionDefinition").unwrap();
1285        assert_eq!(short.action, long.action);
1286        assert_eq!(short.positional, long.positional);
1287        assert_eq!(short.scope, long.scope);
1288        assert_eq!(short.component, long.component);
1289    }
1290
1291    #[test]
1292    fn long_form_jump_last_line_word_matches_short() {
1293        let short = parse("jllw").unwrap();
1294        let long = parse("JumpLastLineWord").unwrap();
1295        assert_eq!(short.action, long.action);
1296        assert_eq!(short.positional, long.positional);
1297        assert_eq!(short.scope, long.scope);
1298        assert_eq!(short.component, long.component);
1299    }
1300
1301    // --- work item 0012: numeric positional ---
1302
1303    #[test]
1304    fn count_j5lw_parses_to_jump_count5_line_word() {
1305        let q = parse("j5lw").unwrap();
1306        assert_eq!(q.action, Action::Jump);
1307        assert_eq!(q.positional, Positional::Count(5));
1308        assert_eq!(q.scope, Scope::Line);
1309        assert_eq!(q.component, Component::Word);
1310        assert!(!q.requires_lsp);
1311    }
1312
1313    #[test]
1314    fn count_j1ls_parses_to_jump_count1_line_self() {
1315        let q = parse("j1ls").unwrap();
1316        assert_eq!(q.action, Action::Jump);
1317        assert_eq!(q.positional, Positional::Count(1));
1318        assert_eq!(q.scope, Scope::Line);
1319        assert_eq!(q.component, Component::Self_);
1320    }
1321
1322    #[test]
1323    fn count_l9fd_parses_to_list_count9_function_definition() {
1324        let q = parse("l9fd").unwrap();
1325        assert_eq!(q.action, Action::List);
1326        assert_eq!(q.positional, Positional::Count(9));
1327        assert_eq!(q.scope, Scope::Function);
1328        assert_eq!(q.component, Component::Definition);
1329        assert!(q.requires_lsp);
1330    }
1331
1332    #[test]
1333    fn count_c3ls_parses_to_change_count3_line_self() {
1334        let q = parse("c3ls").unwrap();
1335        assert_eq!(q.action, Action::Change);
1336        assert_eq!(q.positional, Positional::Count(3));
1337        assert_eq!(q.scope, Scope::Line);
1338        assert_eq!(q.component, Component::Self_);
1339        assert!(!q.requires_lsp);
1340    }
1341
1342    #[test]
1343    fn count_zero_positional_errors_with_range_message() {
1344        let result = parse("j0lw");
1345        assert!(result.is_err());
1346        let msg = format!("{}", result.unwrap_err());
1347        assert!(
1348            msg.contains("0") || msg.contains("count"),
1349            "expected '0' or 'count' in error: {msg}"
1350        );
1351    }
1352
1353    #[test]
1354    fn count_buffer_scope_rejected() {
1355        let result = parse("j5bs");
1356        assert!(result.is_err());
1357        let msg = format!("{}", result.unwrap_err());
1358        assert!(
1359            msg.contains("Buffer") || msg.contains("numeric"),
1360            "expected Buffer/numeric in error: {msg}"
1361        );
1362    }
1363
1364    #[test]
1365    fn count_delimiter_scope_rejected() {
1366        let result = parse("j5ds");
1367        assert!(result.is_err());
1368        let msg = format!("{}", result.unwrap_err());
1369        assert!(
1370            msg.contains("Delimiter") || msg.contains("numeric"),
1371            "expected Delimiter/numeric in error: {msg}"
1372        );
1373    }
1374
1375    #[test]
1376    fn long_form_jump5lineword_matches_j5lw() {
1377        let short = parse("j5lw").unwrap();
1378        let long = parse("Jump5LineWord").unwrap();
1379        assert_eq!(short.action, long.action);
1380        assert_eq!(short.positional, long.positional);
1381        assert_eq!(short.scope, long.scope);
1382        assert_eq!(short.component, long.component);
1383    }
1384
1385    #[test]
1386    fn long_form_list9functiondefinition_matches_l9fd() {
1387        let short = parse("l9fd").unwrap();
1388        let long = parse("List9FunctionDefinition").unwrap();
1389        assert_eq!(short.action, long.action);
1390        assert_eq!(short.positional, long.positional);
1391        assert_eq!(short.scope, long.scope);
1392        assert_eq!(short.component, long.component);
1393    }
1394
1395    #[test]
1396    fn count_short_form_round_trip_emits_digit() {
1397        let q = parse("j5lw").unwrap();
1398        assert_eq!(q.short_form(), "j5lw");
1399    }
1400
1401    #[test]
1402    fn count_long_form_round_trip_emits_digit_inline() {
1403        let q = parse("j5lw").unwrap();
1404        assert_eq!(q.long_form(), "Jump5LineWord");
1405    }
1406
1407    #[test]
1408    fn count_replace_action_rejected() {
1409        let result = parse("r5ls");
1410        assert!(result.is_err());
1411        let msg = format!("{}", result.unwrap_err());
1412        assert!(
1413            msg.contains("Replace") || msg.contains("numeric"),
1414            "expected Replace/numeric in error: {msg}"
1415        );
1416    }
1417
1418    #[test]
1419    fn multi_digit_short_form_errors_with_single_digit_message() {
1420        let result = parse("j15lw");
1421        assert!(result.is_err());
1422        let msg = format!("{}", result.unwrap_err());
1423        assert!(
1424            msg.contains("single-digit"),
1425            "expected 'single-digit' in error: {msg}"
1426        );
1427    }
1428
1429    #[test]
1430    fn multi_digit_long_form_errors_with_single_digit_message() {
1431        let result = parse("Jump15LineWord");
1432        assert!(result.is_err());
1433        let msg = format!("{}", result.unwrap_err());
1434        assert!(
1435            msg.contains("single-digit"),
1436            "expected 'single-digit' in error: {msg}"
1437        );
1438    }
1439
1440    #[test]
1441    fn multi_digit_with_zero_short_form_errors_with_single_digit_message() {
1442        let result = parse("j10lw");
1443        assert!(result.is_err());
1444        let msg = format!("{}", result.unwrap_err());
1445        assert!(
1446            msg.contains("single-digit"),
1447            "expected 'single-digit' in error: {msg}"
1448        );
1449    }
1450}