slash-lang 0.1.0

Parser and AST for the slash-command language
Documentation
use crate::parser::ast::Arg;

/// Result of parsing a builder chain: command name, optional primary arg, and method args.
pub struct ChainParts {
    pub name: String,
    pub primary: Option<String>,
    pub args: Vec<Arg>,
}

/// Splits a bare command token (urgency/optional already stripped) into the command
/// name, optional primary argument, and builder-chain arguments.
///
/// `/read(src/main.rs)` → name=`read`, primary=`Some("src/main.rs")`, args=`[]`
/// `/edit(path).find(old).replace(new)` → name=`edit`, primary=`Some("path")`, args=`[find(old), replace(new)]`
/// `/echo.text(hello)` → name=`echo`, primary=`None`, args=`[text(hello)]`
/// `/build` → name=`build`, primary=`None`, args=`[]`
#[allow(clippy::result_unit_err)]
pub fn parse_builder_chain(bare: &str) -> Result<ChainParts, ()> {
    let s = bare.trim_start_matches('/');

    // Check for primary arg: `/cmd(value)` or `/cmd(value).method(...)`.
    // Find `(` before any `.` at depth 0 to detect primary arg.
    let first_open = s.find('(');
    let first_dot = find_dot_at_depth_zero(s);

    if let Some(paren_pos) = first_open {
        if first_dot.is_none() || paren_pos < first_dot.unwrap() {
            // Primary arg present: `/cmd(value)...`
            let cmd_name = &s[..paren_pos];
            let rest = &s[paren_pos..];

            // Extract the balanced primary arg value.
            let (primary_val, remainder) = extract_balanced_parens(rest)?;

            let primary = if primary_val.is_empty() {
                None
            } else {
                Some(primary_val.to_string())
            };

            let args = if let Some(after_dot) = remainder.strip_prefix('.') {
                parse_chain(after_dot)?
            } else if remainder.is_empty() {
                vec![]
            } else {
                return Err(());
            };

            return Ok(ChainParts {
                name: cmd_name.to_string(),
                primary,
                args,
            });
        }
    }

    // No primary arg — original logic.
    match s.split_once('.') {
        Some((cmd_raw, chain)) => Ok(ChainParts {
            name: cmd_raw.to_string(),
            primary: None,
            args: parse_chain(chain)?,
        }),
        None => Ok(ChainParts {
            name: s.to_string(),
            primary: None,
            args: vec![],
        }),
    }
}

/// Find the position of the first `.` that is not inside parentheses.
fn find_dot_at_depth_zero(s: &str) -> Option<usize> {
    let mut depth: usize = 0;
    for (i, ch) in s.char_indices() {
        match ch {
            '(' => depth += 1,
            ')' => {
                if depth == 0 {
                    return None;
                }
                depth -= 1;
            }
            '.' if depth == 0 => return Some(i),
            _ => {}
        }
    }
    None
}

/// Given a string starting with `(`, extract the balanced content and return
/// (inner_value, remainder_after_close_paren).
fn extract_balanced_parens(s: &str) -> Result<(&str, &str), ()> {
    debug_assert!(s.starts_with('('));
    let mut depth: usize = 0;
    for (i, ch) in s.char_indices() {
        match ch {
            '(' => depth += 1,
            ')' => {
                depth -= 1;
                if depth == 0 {
                    let inner = &s[1..i];
                    let remainder = &s[i + 1..];
                    return Ok((inner, remainder));
                }
            }
            _ => {}
        }
    }
    Err(())
}

fn parse_chain(chain: &str) -> Result<Vec<Arg>, ()> {
    split_segments(chain)?
        .into_iter()
        .filter(|s| !s.is_empty())
        .map(parse_arg)
        .collect()
}

/// Splits a chain string on `.` only when not inside parentheses.
/// Handles values containing dots: `flag(1.0)` is one segment.
/// Returns `Err(())` for unmatched parentheses.
fn split_segments(chain: &str) -> Result<Vec<&str>, ()> {
    let mut segments = Vec::new();
    let mut depth: usize = 0;
    let mut start = 0;
    for (i, ch) in chain.char_indices() {
        match ch {
            '(' => depth += 1,
            ')' => {
                if depth == 0 {
                    return Err(());
                }
                depth -= 1;
            }
            '.' if depth == 0 => {
                segments.push(&chain[start..i]);
                start = i + 1;
            }
            _ => {}
        }
    }
    if depth != 0 {
        return Err(());
    }
    segments.push(&chain[start..]);
    Ok(segments)
}

/// Parses a single chain segment.
/// `flag(val)` → `Arg { name: "flag", value: Some("val") }`
/// `flag()`   → `Arg { name: "flag", value: None }`
/// `flag`     → `Arg { name: "flag", value: None }`
/// Returns `Err(())` if the segment contains `(` without a matching `)`.
fn parse_arg(segment: &str) -> Result<Arg, ()> {
    if let Some((name, rest)) = segment.split_once('(') {
        let value = rest.strip_suffix(')').ok_or(())?;
        Ok(Arg {
            name: name.to_string(),
            value: if value.is_empty() {
                None
            } else {
                Some(value.to_string())
            },
        })
    } else {
        Ok(Arg {
            name: segment.to_string(),
            value: None,
        })
    }
}