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 #[test]
1059 fn jump_outside_invalid_component_rejects_with_direction_hint() {
1060 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 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 assert!(parse("joln").is_err(), "joln (Name) should fail");
1081 assert!(parse("jols").is_err(), "jols (Self_) should fail");
1082 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 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 let result = parse("cpds");
1136 assert!(result.is_err());
1137 }
1138
1139 #[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 #[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}