From b9f208deed7b8e012fbcd84202e2fb1d5eb8eeb9 Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Fri, 17 Jul 2026 03:24:51 +0800 Subject: feat(picker2): complete Picker2 prototype MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Picker2 replaces the original Picker1 with a two-phase (tag → pick) + mask-bitmap architecture, decoupling argument matching from type conversion via a composable matcher pipeline. Architecture - FlagMatcher — boolean flags (--verbose) - ArgMatcher — single flag+value pairs (--name Alice) - MultiArgMatcher — multi-value flag groups (--files a.txt b.txt) - PositionalMatcher — positional arguments, respects `--` - SingleMatcher — composite for Single-type Pickables Types - Flag (Active/Inactive) — semantic bool flag value - String + 14 numeric types via SinglePickable trait - Vec — greedy multi-value (all SinglePickable types) - VecUntil — bounded multi-value via BoundaryCheck trait - VecUntil, pick_string, pick_numbers, pick_bool, pick_flag Style system - UNIX_STYLE (kebab), POWERSHELL_STYLE (Pascal), WINDOWS_STYLE (Pascal) - naming_case auto-conversion via just_fmt integration - Style-aware separator (= for Unix, : for PS/Windows) Infrastructure - internal_repeat! macro generates PickerPattern1..=32 - SinglePickable blanket impl → Pickable - MultiPickableWithBoundary trait with greedy/bounded variants - 151 integration tests - Docs updated for parser feature --- mingling_picker_macros/src/internal_repeat.rs | 315 ++++++++++++++++++++++++++ 1 file changed, 315 insertions(+) create mode 100644 mingling_picker_macros/src/internal_repeat.rs (limited to 'mingling_picker_macros/src/internal_repeat.rs') diff --git a/mingling_picker_macros/src/internal_repeat.rs b/mingling_picker_macros/src/internal_repeat.rs new file mode 100644 index 0000000..4b2242b --- /dev/null +++ b/mingling_picker_macros/src/internal_repeat.rs @@ -0,0 +1,315 @@ +use proc_macro::{Delimiter, Group, Ident, Literal, TokenStream, TokenTree}; + +pub(crate) fn internal_repeat(input: TokenStream) -> TokenStream { + let tokens: Vec = input.into_iter().collect(); + let (range_start, range_end, body_start) = parse_range(&tokens); + + let mut body: Vec = tokens[body_start..].to_vec(); + if body.len() == 1 + && let TokenTree::Group(g) = &body[0] + && g.delimiter() == Delimiter::Brace + { + body = g.stream().into_iter().collect(); + } + + let mut result = Vec::new(); + for i in range_start..=range_end { + result.extend(expand_body(&body, i, range_start, range_end)); + } + result.into_iter().collect() +} + +/// Parse `start .. end =>` or `start ..= end =>` or `count =>` (backward compat). +/// Returns `(start, end_inclusive, body_start_index)`. +fn parse_range(tokens: &[TokenTree]) -> (usize, usize, usize) { + // Find => separator + let arrow_pos = tokens.windows(2).position(|w| { + matches!(&w[0], TokenTree::Punct(p) if p.as_char() == '=') + && matches!(&w[1], TokenTree::Punct(p) if p.as_char() == '>') + }); + + let (arrow_pos, body_start) = match arrow_pos { + Some(p) => (p, p + 2), + None => return (1, 12, 0), // fallback + }; + + let before: Vec<&TokenTree> = tokens[..arrow_pos].iter().collect(); + + // Try to find `..` or `..=` pattern + // `..` is two Punct('.') tokens + let dotdot = before.windows(2).position(|w| { + matches!(w[0], TokenTree::Punct(p) if p.as_char() == '.') + && matches!(w[1], TokenTree::Punct(p) if p.as_char() == '.') + }); + + if let Some(dd) = dotdot { + // Start value: tokens before `..` + let start = parse_usize_tokens(&before[..dd]); + let after_dd = &before[dd + 2..]; + + // Check for `..=` (inclusive range) + let (inclusive, end_tokens) = if after_dd + .first() + .is_some_and(|t| matches!(t, TokenTree::Punct(p) if p.as_char() == '=')) + { + (true, &after_dd[1..]) + } else { + (false, after_dd) + }; + + let end = parse_usize_tokens(end_tokens); + + if inclusive { + (start, end, body_start) + } else { + // Exclusive end: if end >= start, iterate start..end, so end_inclusive = end - 1 + if end > start { + (start, end - 1, body_start) + } else { + (1, 12, body_start) // fallback + } + } + } else { + // No `..` found — fallback to simple count + let count = parse_usize_tokens(&before); + (1, count, body_start) + } +} + +/// Parse a sequence of tokens as a single usize value. +fn parse_usize_tokens(tokens: &[&TokenTree]) -> usize { + let s: String = tokens + .iter() + .map(|t| match t { + TokenTree::Literal(l) => l.to_string(), + TokenTree::Ident(id) => id.to_string(), + _ => String::new(), + }) + .collect::>() + .join("") + .replace(' ', ""); + + s.parse().unwrap_or(12) +} + +/// Walk tokens, replacing: +/// `$` → current +/// `^$` → max +/// `$^` → min +/// `$+` → current + 1 (clamped) +/// `$-` → current - 1 (clamped) +/// `ident$` → ident{current} +/// and expanding `( … )+` / `( … ,)+` / `( … ;)+` groups. +fn expand_body(tokens: &[TokenTree], current: usize, min: usize, max: usize) -> Vec { + let mut out = Vec::new(); + let mut i = 0; + while i < tokens.len() { + // Check for a parenthesized repetition group: ( ... ) sep? + + if let Some(exp) = try_expand_paren_group(tokens, i, current, min, max) { + let (items, consumed) = exp; + out.extend(items); + i += consumed; + continue; + } + + // `^$` — max value + if let TokenTree::Punct(p) = &tokens[i] + && p.as_char() == '^' + && i + 1 < tokens.len() + && let TokenTree::Punct(p2) = &tokens[i + 1] + && p2.as_char() == '$' + { + out.push(TokenTree::Literal(Literal::usize_suffixed(max))); + i += 2; + continue; + } + + // `ident$` / `ident$+` / `ident$-` / `ident$^` + // → {ident}{current} / {ident}{current+1} / {ident}{current-1} / {ident}{min} + if let TokenTree::Ident(id) = &tokens[i] { + if i + 1 < tokens.len() + && let TokenTree::Punct(p) = &tokens[i + 1] + && p.as_char() == '$' + { + // Check for ident$+ / ident$- / ident$^ + if i + 2 < tokens.len() { + match &tokens[i + 2] { + TokenTree::Punct(p2) if p2.as_char() == '+' => { + // ident$+ → {ident}{current+1} + let name = format!("{}{}", id, current + 1); + out.push(TokenTree::Ident(Ident::new(&name, id.span()))); + i += 3; + continue; + } + TokenTree::Punct(p2) if p2.as_char() == '-' => { + // ident$- → {ident}{current-1} + let val = current.saturating_sub(1); + let name = format!("{}{}", id, val); + out.push(TokenTree::Ident(Ident::new(&name, id.span()))); + i += 3; + continue; + } + TokenTree::Punct(p2) if p2.as_char() == '^' => { + let name = format!("{}{}", id, min); + out.push(TokenTree::Ident(Ident::new(&name, id.span()))); + i += 3; + continue; + } + _ => {} + } + } + // ident$ alone → {ident}{current} + let name = format!("{}{}", id, current); + out.push(TokenTree::Ident(Ident::new(&name, id.span()))); + i += 2; + continue; + } + out.push(tokens[i].clone()); + i += 1; + continue; + } + + match &tokens[i] { + TokenTree::Punct(p) if p.as_char() == '$' => { + // lookahead for $^, $+, $- + if i + 1 < tokens.len() { + match &tokens[i + 1] { + TokenTree::Punct(p2) if p2.as_char() == '^' => { + // $^ → min + out.push(TokenTree::Literal(Literal::usize_suffixed(min))); + i += 2; + continue; + } + TokenTree::Punct(p2) if p2.as_char() == '+' => { + // $+ → current + 1 + out.push(TokenTree::Literal(Literal::usize_suffixed(current + 1))); + i += 2; + continue; + } + TokenTree::Punct(p2) if p2.as_char() == '-' => { + // $- → current - 1 + let val = current.saturating_sub(1); + out.push(TokenTree::Literal(Literal::usize_suffixed(val))); + i += 2; + continue; + } + _ => {} + } + } + // `$` alone → current + out.push(TokenTree::Literal(Literal::usize_suffixed(current))); + i += 1; + continue; + } + TokenTree::Group(g) => { + let inner = expand_body_vec(&g.stream(), current, min, max); + out.push(TokenTree::Group(Group::new( + g.delimiter(), + inner.into_iter().collect(), + ))); + } + other => out.push(other.clone()), + } + i += 1; + } + out +} + +fn expand_body_vec(stream: &TokenStream, current: usize, min: usize, max: usize) -> Vec { + let v: Vec = stream.clone().into_iter().collect(); + expand_body(&v, current, min, max) +} + +/// Try to expand a repetition group. +/// +/// New syntax (repetition marker `+` is the LAST token INSIDE the parens): +/// - `(group,+)` — repeat `*` times (current), `,` is the separator +/// - `(group,+)` — repeat `*` times, `,` is the separator +/// - `(group,+)` — repeat `*` times, `;` is the separator +/// - `(group +)` — repeat `*` times, no separator +/// - `(group,+)+` — repeat `*+1` times (with separator) +/// - `(group,+)--` — repeat `*-1` times (with separator) [not yet used] +/// +/// The `*` is the current counter value. An optional `+` or `-` immediately +/// after the closing paren shifts the repeat count up or down by one. +fn try_expand_paren_group( + tokens: &[TokenTree], + i: usize, + current: usize, + _min: usize, + _max: usize, +) -> Option<(Vec, usize)> { + let group = match tokens.get(i)? { + TokenTree::Group(g) if g.delimiter() == Delimiter::Parenthesis => g, + _ => return None, + }; + + let stream: Vec = group.stream().into_iter().collect(); + + // Repetition syntax: the LAST token inside the parens MUST be `+`. + // `(content,+)` — repeat * times, `,` separator + // `(content;+)` — repeat * times, `;` separator + // `(content,+)` — repeat * times, no separator + // `(content,+)+` — repeat *+1 times + // `(content,+)-` — repeat *-1 times + // An optional `+` / `-` right after `)` shifts the count by ±1. + // + // Any `+` tokens inside `content` are treated as regular Rust syntax + // (trait bounds, etc.) — the repetition marker is ONLY the final `+`. + + let last_is_plus = stream + .last() + .is_some_and(|t| matches!(t, TokenTree::Punct(p) if p.as_char() == '+')); + if !last_is_plus { + return None; + } + + // Determine separator and inner content. + let (inner, sep_str): (Vec, &str) = { + if stream.len() >= 2 { + let sep_idx = stream.len() - 2; + match &stream[sep_idx] { + TokenTree::Punct(p) if p.as_char() == ',' => { + // (...,content,+) — inner is everything before the last `,` + let content: Vec = stream[..sep_idx].into(); + (content, ",") + } + TokenTree::Punct(p) if p.as_char() == ';' => { + let content: Vec = stream[..sep_idx].into(); + (content, ";") + } + _ => { + // (content+) — no separator, the `+` is the only special token + let content: Vec = stream[..stream.len() - 1].into(); + (content, "") + } + } + } else { + // (+) — bare repetition marker, empty content + (vec![], "") + } + }; + + // Handle modifier after `)` + let rest = &tokens[i + 1..]; + let modifier: isize = match rest.first() { + Some(TokenTree::Punct(p)) if p.as_char() == '+' => 1, + Some(TokenTree::Punct(p)) if p.as_char() == '-' => -1, + _ => 0, + }; + let consumed = if modifier != 0 { 2 } else { 1 }; + let repeat_count = (current as isize + modifier) as usize; + + let mut out = Vec::new(); + for n in 1..=repeat_count { + if n > 1 && !sep_str.is_empty() { + out.push(TokenTree::Punct(proc_macro::Punct::new( + sep_str.chars().next().unwrap(), + proc_macro::Spacing::Alone, + ))); + } + out.extend(expand_body(&inner, n, 1, repeat_count)); + } + + Some((out, consumed)) +} -- cgit