Skip to content

Commit 45b3117

Browse files
authored
Upgrade lexer to better handle LParen vs Arith and add test cases (#777)
Fixes #774 ### Problem Inside arithmetic blocks (e.g., `$((...))`), the `flash` lexer greedily tokenized inner double parentheses `((` and `))` as compound operators (`ArithCommand`/`DoubleRParen`) instead of individual math grouping tokens. This broke bracket balancing for nested arithmetic like `echo $(( ((2) + 2) ))`. ### Solution * **Stateful Lexer (`flash`)**: Added context tracking to the lexer so that inner `((` and `))` are split into separate `LParen`/`RParen` tokens when inside an active arithmetic block. * **Context-Aware Parser (`flyline`)**: Updated `dparser` to treat `LParen` as a nesting opener only when inside an active arithmetic block, preventing false-positive nesting on standard syntax like case patterns (`(pattern)`). * **Testing**: Added unit and acceptance tests validating bracket-matching annotations and parser acceptance.
1 parent ab7646d commit 45b3117

4 files changed

Lines changed: 79 additions & 2 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ unicode-width = { version = "0.2.0", default-features = false }
3636
unicode-segmentation = "1.13.2"
3737
itertools = "0.14.0"
3838
glob = "0.3.3"
39-
flash = { git = "https://github.com/HalFrgrd/flash.git", rev = "dfceaffd316fcc357ce4604fb9971112a7c1de62" }
39+
flash = { git = "https://github.com/HalFrgrd/flash.git", rev = "7f37b29befc07aba742d71e7d9164d4ed75eaf50" }
4040
skim = { git = "https://github.com/HalFrgrd/skim", rev = "7908ee85a5f6cd49575925fcc0bdef3999409d26", default-features = false, features = ["algos_only"]}
4141
lscolors = "0.21.0"
4242
serde_json = "1.0"
@@ -75,3 +75,4 @@ image = "ghcr.io/cross-rs/aarch64-unknown-linux-gnu:main-centos"
7575
# This prevents Cargo from compiling both the registry and git versions of crossterm, which would duplicate
7676
# the static event reader and cause stdin inputs typed during active commands (e.g. `sleep 1`) to be lost.
7777
crossterm = { git = "https://github.com/HalFrgrd/crossterm.git", rev = "78def95567c26a463d9f7554d48c3940d797a254" }
78+

src/command_acceptance.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ mod tests {
6464
assert_eq!(will_bash_accept_buffer("echo $((1 + 2"), false);
6565
assert_eq!(will_bash_accept_buffer("echo $((1 + 2)"), false);
6666
assert_eq!(will_bash_accept_buffer("echo $((1 + 2))"), true);
67+
assert_eq!(will_bash_accept_buffer("echo $(( ((2) + 2) ))"), true);
68+
assert_eq!(will_bash_accept_buffer("(( ((2) + 2) ))"), true);
69+
assert_eq!(will_bash_accept_buffer("case $x in (1) echo ;; esac"), true);
6770
assert_eq!(will_bash_accept_buffer("echo ${VAR}"), true);
6871
assert_eq!(will_bash_accept_buffer("echo ${VAR"), false);
6972
// test backticks

src/dparser.rs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,12 @@ impl DParser {
213213
true
214214
}
215215
}
216+
TokenKind::LParen => {
217+
matches!(
218+
current_nesting,
219+
Some(TokenKind::ArithSubst | TokenKind::ArithCommand | TokenKind::LParen)
220+
)
221+
}
216222
_ => true,
217223
}
218224
}
@@ -473,6 +479,7 @@ impl DParser {
473479
| TokenKind::CmdSubst
474480
| TokenKind::ArithSubst
475481
| TokenKind::ArithCommand
482+
| TokenKind::LParen
476483
| TokenKind::ParamExpansion
477484
| TokenKind::ProcessSubstIn
478485
| TokenKind::ProcessSubstOut
@@ -1466,6 +1473,72 @@ mod tests {
14661473
);
14671474
}
14681475

1476+
#[test]
1477+
fn test_arithmetic_nested_bracket_annotations() {
1478+
let input = "echo $(( ((2) + 2) ))";
1479+
let mut parser = DParser::from(input);
1480+
parser.walk_to_end();
1481+
1482+
let tokens = parser.tokens();
1483+
1484+
for t in tokens {
1485+
dbg!("{:?} - {:?}", &t.token, &t.annotations);
1486+
}
1487+
1488+
// Expected tokens:
1489+
// 0: "echo"
1490+
// 1: " "
1491+
// 2: "$((", kind: ArithSubst, opening matching with the final "))"
1492+
// 3: " "
1493+
// 4: "(", kind: LParen, matches with 12: ")"
1494+
// 5: "(", kind: LParen, matches with 7: ")"
1495+
// 6: "2"
1496+
// 7: ")", kind: RParen, matches with 5
1497+
// 8: " "
1498+
// 9: "+"
1499+
// 10: " "
1500+
// 11: "2"
1501+
// 12: ")", kind: RParen, matches with 4
1502+
// 13: " "
1503+
// 14: "))", kind: DoubleRParen, matches with 2
1504+
1505+
assert_eq!(tokens[2].token.kind, TokenKind::ArithSubst);
1506+
assert_eq!(tokens[2].annotations.opening, Some(OpeningState::Matched(14)));
1507+
1508+
assert_eq!(tokens[4].token.kind, TokenKind::LParen);
1509+
assert_eq!(tokens[4].annotations.opening, Some(OpeningState::Matched(12)));
1510+
1511+
assert_eq!(tokens[5].token.kind, TokenKind::LParen);
1512+
assert_eq!(tokens[5].annotations.opening, Some(OpeningState::Matched(7)));
1513+
1514+
assert_eq!(tokens[7].token.kind, TokenKind::RParen);
1515+
assert_eq!(
1516+
tokens[7].annotations.closing,
1517+
Some(ClosingAnnotation {
1518+
opening_idx: 5,
1519+
is_auto_inserted: false
1520+
})
1521+
);
1522+
1523+
assert_eq!(tokens[12].token.kind, TokenKind::RParen);
1524+
assert_eq!(
1525+
tokens[12].annotations.closing,
1526+
Some(ClosingAnnotation {
1527+
opening_idx: 4,
1528+
is_auto_inserted: false
1529+
})
1530+
);
1531+
1532+
assert_eq!(tokens[14].token.kind, TokenKind::DoubleRParen);
1533+
assert_eq!(
1534+
tokens[14].annotations.closing,
1535+
Some(ClosingAnnotation {
1536+
opening_idx: 2,
1537+
is_auto_inserted: false
1538+
})
1539+
);
1540+
}
1541+
14691542
#[test]
14701543
fn test_env_var_annotations() {
14711544
let input = r#"echo $HOME"#;

0 commit comments

Comments
 (0)