1use anyhow::Result;
2
3use crate::data::chord_types::{
4 Action, Component, Positional, Scope, is_valid_combination, is_valid_jump_combination,
5};
6
7use super::errors::ChordError;
8use super::types::{ChordArgs, ChordQuery};
9
10pub fn parse(input: &str) -> Result<ChordQuery> {
11 let input = input.trim();
12 if input.is_empty() {
13 return Err(ChordError::parse(input, 0, "empty chord input").into());
14 }
15
16 let (chord_part, raw_args) = split_chord_and_args(input)?;
17
18 if let Some(query) = try_parse_short_form(chord_part, &raw_args, input)? {
19 return Ok(query);
20 }
21
22 if let Some(query) = try_parse_long_form(chord_part, &raw_args, input)? {
23 return Ok(query);
24 }
25
26 let suggestion = suggest_chord(chord_part);
27 match suggestion {
28 Some(sug) => Err(ChordError::parse_with_suggestion(
29 input,
30 0,
31 format!("unknown chord '{chord_part}'"),
32 sug,
33 )
34 .into()),
35 None => Err(ChordError::parse(input, 0, format!("unknown chord '{chord_part}'")).into()),
36 }
37}
38
39fn split_chord_and_args(input: &str) -> Result<(&str, Option<&str>)> {
40 let Some(paren_start) = input.find('(') else {
41 return Ok((input, None));
42 };
43 if !input.ends_with(')') {
44 return Err(ChordError::parse(
45 input,
46 paren_start,
47 "unterminated argument list (missing closing ')')",
48 )
49 .into());
50 }
51 let chord_part = &input[..paren_start];
52 let args_content = &input[paren_start + 1..input.len() - 1];
53 Ok((chord_part, Some(args_content)))
54}
55
56fn parse_args(raw_args: &Option<&str>) -> ChordArgs {
57 let mut args = ChordArgs::default();
58 let raw = match raw_args {
59 Some(s) if !s.is_empty() => *s,
60 _ => return args,
61 };
62
63 for pair in split_kv_pairs(raw) {
64 let pair = pair.trim();
65 if pair.is_empty() {
66 continue;
67 }
68 if let Some((key, val)) = pair.split_once(':') {
69 let key = key.trim();
70 let val = val.trim().trim_matches('"');
71 match key {
72 "target" if !val.is_empty() => {
73 args.target_name = Some(val.to_string());
74 args.target_line = val.parse().ok();
75 }
76 "parent" => {
77 args.parent_name = Some(val.to_string());
78 }
79 "cursor" => {
80 if let Some((l, c)) = val.split_once(',') {
81 if let (Ok(line), Ok(col)) = (l.trim().parse(), c.trim().parse()) {
82 args.cursor_pos = Some((line, col));
83 }
84 }
85 }
86 "value" => {
87 args.value = Some(val.to_string());
88 }
89 "find" => {
90 args.find = Some(val.to_string());
91 }
92 "replace" => {
93 args.replace = Some(val.to_string());
94 }
95 _ => {}
96 }
97 }
98 }
99
100 args
101}
102
103fn split_kv_pairs(input: &str) -> Vec<&str> {
104 let mut pairs = Vec::new();
105 let mut depth = 0;
106 let mut in_quotes = false;
107 let mut start = 0;
108
109 for (i, ch) in input.char_indices() {
110 match ch {
111 '"' => in_quotes = !in_quotes,
112 '(' if !in_quotes => {
113 depth += 1;
114 }
115 ')' if !in_quotes => {
116 depth -= 1;
117 }
118 ',' if !in_quotes && depth == 0 => {
119 pairs.push(&input[start..i]);
120 start = i + 1;
121 }
122 _ => {}
123 }
124 }
125 if start < input.len() {
126 pairs.push(&input[start..]);
127 }
128 pairs
129}
130
131fn try_parse_short_form(
132 chord_part: &str,
133 raw_args: &Option<&str>,
134 _original_input: &str,
135) -> Result<Option<ChordQuery>> {
136 if chord_part.len() != 4 {
137 return Ok(None);
138 }
139
140 let chars: Vec<&str> = chord_part
141 .char_indices()
142 .map(|(i, c)| &chord_part[i..i + c.len_utf8()])
143 .collect();
144
145 if chars.len() != 4 {
146 return Ok(None);
147 }
148
149 let action = match Action::from_short(chars[0]) {
150 Some(a) => a,
151 None => return Ok(None),
152 };
153 let positional = match Positional::from_short(chars[1]) {
154 Some(p) => p,
155 None => return Ok(None),
156 };
157 let scope = match Scope::from_short(chars[2]) {
158 Some(s) => s,
159 None => return Ok(None),
160 };
161 let component = match Component::from_short(chars[3]) {
162 Some(c) => c,
163 None => return Ok(None),
164 };
165
166 if !is_valid_combination(scope, component) {
167 return Err(ChordError::invalid_combination(scope, component).into());
168 }
169
170 if matches!(scope, Scope::Delimiter)
171 && matches!(positional, Positional::Next | Positional::Previous)
172 {
173 return Err(ChordError::parse(
174 _original_input,
175 0,
176 "Next/Previous positional is not valid for Delimiter scope",
177 )
178 .into());
179 }
180
181 if action == Action::Jump && !is_valid_jump_combination(positional, component) {
182 let msg = if positional == Positional::Outside {
183 "Jump with Outside positional requires Beginning or End component to specify direction"
184 } else {
185 "Jump does not operate on Value, Parameters, or Arguments components"
186 };
187 return Err(ChordError::parse(_original_input, 0, msg).into());
188 }
189
190 let args = parse_args(raw_args);
191
192 if action == Action::Jump && args.value.is_some() {
193 return Err(
194 ChordError::parse(_original_input, 0, "Jump does not accept a value argument").into(),
195 );
196 }
197
198 Ok(Some(ChordQuery {
199 action,
200 positional,
201 scope,
202 component,
203 args,
204 requires_lsp: scope.requires_lsp(),
205 }))
206}
207
208fn try_parse_long_form(
209 chord_part: &str,
210 raw_args: &Option<&str>,
211 _original_input: &str,
212) -> Result<Option<ChordQuery>> {
213 let (action, rest) = match parse_long_action(chord_part) {
214 Some(r) => r,
215 None => return Ok(None),
216 };
217 let (positional, rest) = match parse_long_positional(rest) {
218 Some(r) => r,
219 None => return Ok(None),
220 };
221 let (scope, rest) = match parse_long_scope(rest) {
222 Some(r) => r,
223 None => return Ok(None),
224 };
225 let component = match parse_long_component(rest) {
226 Some(c) => c,
227 None => return Ok(None),
228 };
229
230 if !is_valid_combination(scope, component) {
231 return Err(ChordError::invalid_combination(scope, component).into());
232 }
233
234 if matches!(scope, Scope::Delimiter)
235 && matches!(positional, Positional::Next | Positional::Previous)
236 {
237 return Err(ChordError::parse(
238 _original_input,
239 0,
240 "Next/Previous positional is not valid for Delimiter scope",
241 )
242 .into());
243 }
244
245 if action == Action::Jump && !is_valid_jump_combination(positional, component) {
246 let msg = if positional == Positional::Outside {
247 "Jump with Outside positional requires Beginning or End component to specify direction"
248 } else {
249 "Jump does not operate on Value, Parameters, or Arguments components"
250 };
251 return Err(ChordError::parse(_original_input, 0, msg).into());
252 }
253
254 let args = parse_args(raw_args);
255
256 if action == Action::Jump && args.value.is_some() {
257 return Err(
258 ChordError::parse(_original_input, 0, "Jump does not accept a value argument").into(),
259 );
260 }
261
262 Ok(Some(ChordQuery {
263 action,
264 positional,
265 scope,
266 component,
267 args,
268 requires_lsp: scope.requires_lsp(),
269 }))
270}
271
272fn parse_long_action(input: &str) -> Option<(Action, &str)> {
273 let pairs = [
274 ("Change", Action::Change),
275 ("Replace", Action::Replace),
276 ("Delete", Action::Delete),
277 ("Yank", Action::Yank),
278 ("Append", Action::Append),
279 ("Prepend", Action::Prepend),
280 ("Insert", Action::Insert),
281 ("Jump", Action::Jump),
282 ];
283 for (prefix, action) in pairs {
284 if let Some(rest) = input.strip_prefix(prefix) {
285 return Some((action, rest));
286 }
287 }
288 None
289}
290
291fn parse_long_positional(input: &str) -> Option<(Positional, &str)> {
292 let pairs = [
293 ("Inside", Positional::Inside),
294 ("Until", Positional::Until),
295 ("After", Positional::After),
296 ("Before", Positional::Before),
297 ("Next", Positional::Next),
298 ("Previous", Positional::Previous),
299 ("Entire", Positional::Entire),
300 ("Outside", Positional::Outside),
301 ("To", Positional::To),
302 ];
303 for (prefix, positional) in pairs {
304 if let Some(rest) = input.strip_prefix(prefix) {
305 return Some((positional, rest));
306 }
307 }
308 None
309}
310
311fn parse_long_scope(input: &str) -> Option<(Scope, &str)> {
312 let pairs = [
313 ("Function", Scope::Function),
314 ("Variable", Scope::Variable),
315 ("Delimiter", Scope::Delimiter),
316 ("Buffer", Scope::Buffer),
317 ("Struct", Scope::Struct),
318 ("Member", Scope::Member),
319 ("Line", Scope::Line),
320 ];
321 for (prefix, scope) in pairs {
322 if let Some(rest) = input.strip_prefix(prefix) {
323 return Some((scope, rest));
324 }
325 }
326 None
327}
328
329fn parse_long_component(input: &str) -> Option<Component> {
330 match input {
331 "Beginning" => Some(Component::Beginning),
332 "Contents" => Some(Component::Contents),
333 "End" => Some(Component::End),
334 "Value" => Some(Component::Value),
335 "Parameters" => Some(Component::Parameters),
336 "Arguments" => Some(Component::Arguments),
337 "Name" => Some(Component::Name),
338 "Self" => Some(Component::Self_),
339 _ => None,
340 }
341}
342
343fn suggest_chord(input: &str) -> Option<String> {
344 let input_chars: String = input.chars().take(4).collect();
345 if input_chars.chars().count() < 4 {
346 return None;
347 }
348
349 let actions = ['c', 'r', 'd', 'y', 'a', 'p', 'i', 'j'];
350 let positionals = ['i', 'u', 'a', 'b', 'n', 'p', 'e', 'o', 't'];
351 let scopes = ['l', 'b', 'f', 'v', 's', 'm', 'd'];
352 let components = ['b', 'c', 'e', 'v', 'p', 'a', 'n', 's'];
353
354 let mut best_dist = usize::MAX;
355 let mut best = None;
356
357 for &a in &actions {
358 for &p in &positionals {
359 for &s in &scopes {
360 for &c in &components {
361 let candidate = format!("{a}{p}{s}{c}");
362 let scope = Scope::from_short(&s.to_string()).unwrap();
363 let comp = Component::from_short(&c.to_string()).unwrap();
364 if !is_valid_combination(scope, comp) {
365 continue;
366 }
367 let dist = levenshtein(&input_chars, &candidate);
368 if dist < best_dist && dist <= 2 {
369 best_dist = dist;
370 best = Some(candidate);
371 }
372 }
373 }
374 }
375 }
376
377 best
378}
379
380fn levenshtein(a: &str, b: &str) -> usize {
381 let a: Vec<char> = a.chars().collect();
382 let b: Vec<char> = b.chars().collect();
383 let mut dp = vec![vec![0usize; b.len() + 1]; a.len() + 1];
384
385 for (i, row) in dp.iter_mut().enumerate().take(a.len() + 1) {
386 row[0] = i;
387 }
388 for (j, val) in dp[0].iter_mut().enumerate().take(b.len() + 1) {
389 *val = j;
390 }
391
392 for i in 1..=a.len() {
393 for j in 1..=b.len() {
394 let cost = if a[i - 1] == b[j - 1] { 0 } else { 1 };
395 dp[i][j] = (dp[i - 1][j] + 1)
396 .min(dp[i][j - 1] + 1)
397 .min(dp[i - 1][j - 1] + cost);
398 }
399 }
400
401 dp[a.len()][b.len()]
402}
403
404#[cfg(test)]
405mod tests {
406 use super::parse;
407 use crate::data::chord_types::{
408 Action, Component, Positional, Scope, is_valid_combination, is_valid_jump_combination,
409 };
410
411 const ALL_ACTIONS: &[Action] = &[
412 Action::Change,
413 Action::Replace,
414 Action::Delete,
415 Action::Yank,
416 Action::Append,
417 Action::Prepend,
418 Action::Insert,
419 Action::Jump,
420 ];
421
422 const ALL_POSITIONALS: &[Positional] = &[
423 Positional::Inside,
424 Positional::Until,
425 Positional::After,
426 Positional::Before,
427 Positional::Next,
428 Positional::Previous,
429 Positional::Entire,
430 Positional::Outside,
431 Positional::To,
432 ];
433
434 const ALL_SCOPES: &[Scope] = &[
435 Scope::Line,
436 Scope::Buffer,
437 Scope::Function,
438 Scope::Variable,
439 Scope::Struct,
440 Scope::Member,
441 Scope::Delimiter,
442 ];
443
444 const ALL_COMPONENTS: &[Component] = &[
445 Component::Beginning,
446 Component::Contents,
447 Component::End,
448 Component::Value,
449 Component::Parameters,
450 Component::Arguments,
451 Component::Name,
452 Component::Self_,
453 ];
454
455 #[test]
456 fn all_valid_short_forms_parse_and_invalid_fail() {
457 for &action in ALL_ACTIONS {
458 for &pos in ALL_POSITIONALS {
459 for &scope in ALL_SCOPES {
460 for &comp in ALL_COMPONENTS {
461 let short = format!(
462 "{}{}{}{}",
463 action.short(),
464 pos.short(),
465 scope.short(),
466 comp.short()
467 );
468 let result = parse(&short);
469 let scope_comp_valid = is_valid_combination(scope, comp);
470 let jump_valid =
471 action != Action::Jump || is_valid_jump_combination(pos, comp);
472 let delimiter_positional_valid = scope != Scope::Delimiter
473 || !matches!(pos, Positional::Next | Positional::Previous);
474 let should_parse =
475 scope_comp_valid && jump_valid && delimiter_positional_valid;
476 if should_parse {
477 let q = result.unwrap_or_else(|e| {
478 panic!("expected {short} to parse OK, got: {e}")
479 });
480 assert_eq!(q.action, action, "action mismatch for {short}");
481 assert_eq!(q.positional, pos, "positional mismatch for {short}");
482 assert_eq!(q.scope, scope, "scope mismatch for {short}");
483 assert_eq!(q.component, comp, "component mismatch for {short}");
484 assert_eq!(
485 q.requires_lsp,
486 scope.requires_lsp(),
487 "requires_lsp mismatch for {short}"
488 );
489 } else {
490 assert!(
491 result.is_err(),
492 "expected {short} to fail (invalid combo), but it parsed OK"
493 );
494 }
495 }
496 }
497 }
498 }
499 }
500
501 #[test]
502 fn all_valid_long_forms_parse() {
503 for &action in ALL_ACTIONS {
504 for &pos in ALL_POSITIONALS {
505 for &scope in ALL_SCOPES {
506 for &comp in ALL_COMPONENTS {
507 if !is_valid_combination(scope, comp) {
508 continue;
509 }
510 if action == Action::Jump && !is_valid_jump_combination(pos, comp) {
511 continue;
512 }
513 if scope == Scope::Delimiter
514 && matches!(pos, Positional::Next | Positional::Previous)
515 {
516 continue;
517 }
518 let long = format!("{action}{pos}{scope}{comp}");
519 let result = parse(&long);
520 let q = result
521 .unwrap_or_else(|e| panic!("expected {long} to parse OK, got: {e}"));
522 assert_eq!(q.action, action, "action mismatch for {long}");
523 assert_eq!(q.positional, pos, "positional mismatch for {long}");
524 assert_eq!(q.scope, scope, "scope mismatch for {long}");
525 assert_eq!(q.component, comp, "component mismatch for {long}");
526 assert_eq!(q.requires_lsp, scope.requires_lsp());
527 }
528 }
529 }
530 }
531 }
532
533 #[test]
534 fn spot_check_change_inside_function_contents() {
535 let q = parse("cifc").unwrap();
536 assert_eq!(q.action, Action::Change);
537 assert_eq!(q.positional, Positional::Inside);
538 assert_eq!(q.scope, Scope::Function);
539 assert_eq!(q.component, Component::Contents);
540 assert!(q.requires_lsp);
541 }
542
543 #[test]
544 fn spot_check_delete_entire_line_self() {
545 let q = parse("dels").unwrap();
546 assert_eq!(q.action, Action::Delete);
547 assert_eq!(q.positional, Positional::Entire);
548 assert_eq!(q.scope, Scope::Line);
549 assert_eq!(q.component, Component::Self_);
550 assert!(!q.requires_lsp);
551 }
552
553 #[test]
554 fn spot_check_yank_entire_struct_self() {
555 let q = parse("yess").unwrap();
556 assert_eq!(q.action, Action::Yank);
557 assert_eq!(q.positional, Positional::Entire);
558 assert_eq!(q.scope, Scope::Struct);
559 assert_eq!(q.component, Component::Self_);
560 assert!(q.requires_lsp);
561 }
562
563 #[test]
564 fn spot_check_append_after_line_end() {
565 let q = parse("aale").unwrap();
566 assert_eq!(q.action, Action::Append);
567 assert_eq!(q.positional, Positional::After);
568 assert_eq!(q.scope, Scope::Line);
569 assert_eq!(q.component, Component::End);
570 assert!(!q.requires_lsp);
571 }
572
573 #[test]
574 fn spot_check_buffer_contents_is_invalid() {
575 assert!(parse("pbbc").is_err());
576 }
577
578 #[test]
579 fn spot_check_change_inside_function_beginning_is_invalid() {
580 assert!(parse("cifb").is_err());
581 }
582
583 #[test]
584 fn spot_check_replace_entire_variable_name() {
585 let q = parse("revn").unwrap();
586 assert_eq!(q.action, Action::Replace);
587 assert_eq!(q.positional, Positional::Entire);
588 assert_eq!(q.scope, Scope::Variable);
589 assert_eq!(q.component, Component::Name);
590 }
591
592 #[test]
593 fn spot_check_insert_until_member_value() {
594 let q = parse("iumv").unwrap();
595 assert_eq!(q.action, Action::Insert);
596 assert_eq!(q.positional, Positional::Until);
597 assert_eq!(q.scope, Scope::Member);
598 assert_eq!(q.component, Component::Value);
599 }
600
601 #[test]
602 fn short_form_and_long_form_equivalent() {
603 let short = parse("cifc").unwrap();
604 let long = parse("ChangeInsideFunctionContents").unwrap();
605 assert_eq!(short.action, long.action);
606 assert_eq!(short.positional, long.positional);
607 assert_eq!(short.scope, long.scope);
608 assert_eq!(short.component, long.component);
609 }
610
611 #[test]
612 fn args_target_key() {
613 let q = parse("cifc(target:getData)").unwrap();
614 assert_eq!(q.args.target_name.as_deref(), Some("getData"));
615 assert!(q.args.target_line.is_none());
616 assert!(q.args.cursor_pos.is_none());
617 assert!(q.args.value.is_none());
618 }
619
620 #[test]
621 fn args_target_key_works_for_all_lsp_scopes() {
622 let q = parse("cevv(target:myVar)").unwrap();
623 assert_eq!(q.args.target_name.as_deref(), Some("myVar"));
624 let q = parse("cesn(target:MyStruct)").unwrap();
625 assert_eq!(q.args.target_name.as_deref(), Some("MyStruct"));
626 let q = parse("cemn(target:myField)").unwrap();
627 assert_eq!(q.args.target_name.as_deref(), Some("myField"));
628 }
629
630 #[test]
631 fn args_old_scope_specific_keys_are_ignored() {
632 let q = parse("cifc(function:getData)").unwrap();
633 assert!(q.args.target_name.is_none());
634 let q = parse("cevv(variable:myVar)").unwrap();
635 assert!(q.args.target_name.is_none());
636 let q = parse("cesn(struct:MyStruct)").unwrap();
637 assert!(q.args.target_name.is_none());
638 let q = parse("cemn(member:myField)").unwrap();
639 assert!(q.args.target_name.is_none());
640 let q = parse("cifc(name:myFunc)").unwrap();
641 assert!(q.args.target_name.is_none());
642 let q = parse("cels(line:42)").unwrap();
643 assert!(q.args.target_line.is_none());
644 }
645
646 #[test]
647 fn args_target_line_number() {
648 let q = parse("cels(target:42)").unwrap();
649 assert_eq!(q.args.target_line, Some(42));
650 assert_eq!(q.args.target_name.as_deref(), Some("42"));
651 }
652
653 #[test]
654 fn args_cursor_position() {
655 let q = parse(r#"cels(cursor:"3,7")"#).unwrap();
656 assert_eq!(q.args.cursor_pos, Some((3, 7)));
657 }
658
659 #[test]
660 fn args_cursor_position_with_spaces() {
661 let q = parse(r#"cels(cursor:"0,12")"#).unwrap();
662 assert_eq!(q.args.cursor_pos, Some((0, 12)));
663 }
664
665 #[test]
666 fn args_value_plain() {
667 let q = parse("cels(value:hello)").unwrap();
668 assert_eq!(q.args.value.as_deref(), Some("hello"));
669 }
670
671 #[test]
672 fn args_value_quoted_with_spaces() {
673 let q = parse(r#"cifc(target:getData, value:"new body goes here")"#).unwrap();
674 assert_eq!(q.args.target_name.as_deref(), Some("getData"));
675 assert_eq!(q.args.value.as_deref(), Some("new body goes here"));
676 }
677
678 #[test]
679 fn args_value_with_parens_quoted() {
680 let q = parse(r#"cifp(target:getData, value:"(x: i32)")"#).unwrap();
681 assert_eq!(q.args.value.as_deref(), Some("(x: i32)"));
682 }
683
684 #[test]
685 fn args_extra_commas_ignored() {
686 let q = parse("cels(,target:1,,)").unwrap();
687 assert_eq!(q.args.target_line, Some(1));
688 }
689
690 #[test]
691 fn args_missing_value_for_target_is_none() {
692 let q = parse("cels(target:)").unwrap();
693 assert!(q.args.target_line.is_none());
694 assert!(q.args.target_name.is_none());
695 }
696
697 #[test]
698 fn args_unknown_key_is_ignored() {
699 let q = parse("cels(bogus:foo, target:2)").unwrap();
700 assert_eq!(q.args.target_line, Some(2));
701 }
702
703 #[test]
704 fn args_multiple_keys() {
705 let q = parse(r#"cifc(target:getData, value:"body")"#).unwrap();
706 assert_eq!(q.args.target_name.as_deref(), Some("getData"));
707 assert_eq!(q.args.value.as_deref(), Some("body"));
708 }
709
710 #[test]
711 fn invalid_combination_line_parameters_short() {
712 let result = parse("cilp");
713 assert!(result.is_err());
714 let msg = format!("{}", result.unwrap_err());
715 assert!(
716 msg.to_lowercase().contains("invalid"),
717 "expected 'invalid' in error: {msg}"
718 );
719 }
720
721 #[test]
722 fn invalid_combination_buffer_value_short() {
723 let result = parse("cibv");
724 assert!(result.is_err());
725 }
726
727 #[test]
728 fn invalid_combination_variable_parameters() {
729 let result = parse("civp");
730 assert!(result.is_err());
731 }
732
733 #[test]
734 fn invalid_combination_struct_arguments() {
735 let result = parse("cisa");
736 assert!(result.is_err());
737 }
738
739 #[test]
740 fn invalid_combination_long_form() {
741 let result = parse("ChangeInsideLineParameters");
742 assert!(result.is_err());
743 }
744
745 #[test]
746 fn invalid_combination_long_form_buffer_value() {
747 let result = parse("ChangeInsideBufferValue");
748 assert!(result.is_err());
749 }
750
751 #[test]
752 fn empty_input_errors() {
753 let result = parse("");
754 assert!(result.is_err());
755 let msg = format!("{}", result.unwrap_err());
756 assert!(msg.contains("empty"));
757 }
758
759 #[test]
760 fn whitespace_only_errors() {
761 let result = parse(" ");
762 assert!(result.is_err());
763 }
764
765 #[test]
766 fn unknown_short_chord_errors() {
767 let result = parse("zzzz");
768 assert!(result.is_err());
769 }
770
771 #[test]
772 fn near_miss_suggests_correction() {
773 let result = parse("xifv");
774 assert!(result.is_err());
775 let msg = format!("{}", result.unwrap_err());
776 assert!(
777 msg.contains("did you mean"),
778 "expected suggestion in error message: {msg}"
779 );
780 }
781
782 #[test]
783 fn whitespace_trimmed_around_chord() {
784 let q = parse(" cifc ").unwrap();
785 assert_eq!(q.action, Action::Change);
786 assert_eq!(q.positional, Positional::Inside);
787 assert_eq!(q.scope, Scope::Function);
788 assert_eq!(q.component, Component::Contents);
789 }
790
791 #[test]
792 fn short_form_sets_requires_lsp_false_for_line_and_buffer() {
793 assert!(!parse("cels").unwrap().requires_lsp);
794 assert!(!parse("cebs").unwrap().requires_lsp);
795 }
796
797 #[test]
798 fn short_form_sets_requires_lsp_true_for_lsp_scopes() {
799 assert!(parse("cefs").unwrap().requires_lsp);
800 assert!(parse("cevs").unwrap().requires_lsp);
801 assert!(parse("cess").unwrap().requires_lsp);
802 assert!(parse("cems").unwrap().requires_lsp);
803 }
804
805 #[test]
806 fn long_form_self_component_accepted() {
807 let q = parse("ChangeEntireLineSelf").unwrap();
808 assert_eq!(q.component, Component::Self_);
809 }
810
811 #[test]
812 fn unterminated_paren_errors() {
813 let result = parse("cifv(target:1");
814 assert!(result.is_err());
815 assert!(format!("{}", result.unwrap_err()).contains("unterminated"));
816 }
817
818 #[test]
819 fn args_parent_key() {
820 let q = parse("cemv(target:x, parent:Foo)").unwrap();
821 assert_eq!(q.args.target_name.as_deref(), Some("x"));
822 assert_eq!(q.args.parent_name.as_deref(), Some("Foo"));
823 }
824
825 #[test]
826 fn args_find_replace_keys() {
827 let q = parse(r#"rels(target:0, find:"foo", replace:"bar")"#).unwrap();
828 assert_eq!(q.args.find.as_deref(), Some("foo"));
829 assert_eq!(q.args.replace.as_deref(), Some("bar"));
830 }
831
832 #[test]
833 fn unicode_input_does_not_panic_in_suggest() {
834 let result = parse("cłfv");
835 assert!(result.is_err());
836 }
837
838 #[test]
841 fn jump_outside_invalid_component_rejects_with_direction_hint() {
842 let result = parse("joln");
844 assert!(result.is_err());
845 let msg = format!("{}", result.unwrap_err());
846 assert!(
847 msg.contains("Beginning") || msg.contains("End") || msg.contains("direction"),
848 "expected direction hint in error: {msg}"
849 );
850 }
851
852 #[test]
853 fn jump_outside_beginning_and_end_are_valid() {
854 assert!(parse("jolb").is_ok(), "jolb should parse OK");
856 assert!(parse("jole").is_ok(), "jole should parse OK");
857 }
858
859 #[test]
860 fn jump_outside_other_components_fail() {
861 assert!(parse("joln").is_err(), "joln (Name) should fail");
863 assert!(parse("jols").is_err(), "jols (Self_) should fail");
864 assert!(parse("jofp").is_err(), "jofp (Parameters) should fail");
866 }
867
868 #[test]
869 fn jump_non_outside_valid_combinations() {
870 assert!(
871 parse("jtfc").is_ok(),
872 "jtfc (To Function Contents) should parse OK"
873 );
874 assert!(
875 parse("jnfn").is_ok(),
876 "jnfn (Next Function Name) should parse OK"
877 );
878 assert!(
879 parse("jifc").is_ok(),
880 "jifc (Inside Function Contents) should parse OK"
881 );
882 }
883
884 #[test]
885 fn jump_with_value_argument_rejects() {
886 let result = parse(r#"jtfc(value:"text")"#);
887 assert!(result.is_err());
888 let msg = format!("{}", result.unwrap_err());
889 assert!(
890 msg.contains("value") || msg.contains("Jump"),
891 "expected value/Jump in error: {msg}"
892 );
893 }
894
895 #[test]
896 fn jump_bare_short_form_no_args_required() {
897 assert!(parse("jtfc").is_ok());
898 assert!(parse("jolb").is_ok());
899 assert!(parse("jefc").is_ok());
900 }
901
902 #[test]
903 fn delimiter_scope_next_positional_rejects() {
904 let result = parse("cnds");
906 assert!(result.is_err());
907 let msg = format!("{}", result.unwrap_err());
908 assert!(
909 msg.contains("Delimiter") || msg.contains("Next") || msg.contains("Previous"),
910 "expected Delimiter/Next in error: {msg}"
911 );
912 }
913
914 #[test]
915 fn delimiter_scope_previous_positional_rejects() {
916 let result = parse("cpds");
918 assert!(result.is_err());
919 }
920}