pi_agent_rust 0.1.7

High-performance AI coding agent CLI - Rust port of Pi Agent
Documentation
use std::path::{Path, PathBuf};

use url::Url;

pub(super) fn next_non_whitespace_token(text: &str, start: usize) -> (&str, usize) {
    if start >= text.len() {
        return ("", text.len());
    }
    let mut end = text.len();
    for (offset, ch) in text[start..].char_indices() {
        if ch.is_whitespace() {
            end = start + offset;
            break;
        }
    }
    (&text[start..end], end)
}

pub(super) fn parse_quoted_file_ref(text: &str, start: usize) -> Option<(String, String, usize)> {
    let mut chars = text[start..].chars();
    let quote = chars.next()?;
    if quote != '"' && quote != '\'' {
        return None;
    }

    let mut path = String::new();
    let mut escaped = false;
    let mut end = None;
    let after_quote = start + quote.len_utf8();

    for (offset, ch) in text[after_quote..].char_indices() {
        if escaped {
            path.push(ch);
            escaped = false;
            continue;
        }
        if ch == '\\' {
            escaped = true;
            continue;
        }
        if ch == quote {
            end = Some(after_quote + offset);
            break;
        }
        path.push(ch);
    }

    let end = end?;
    let mut trailing = String::new();
    let mut token_end = end + quote.len_utf8();
    for ch in text[token_end..].chars() {
        if is_trailing_punct(ch) {
            trailing.push(ch);
            token_end += ch.len_utf8();
        } else {
            break;
        }
    }

    Some((path, trailing, token_end))
}

pub(super) fn strip_wrapping_quotes(input: &str) -> &str {
    let bytes = input.as_bytes();
    if bytes.len() >= 2 {
        let first = bytes[0];
        let last = bytes[bytes.len() - 1];
        if (first == b'"' && last == b'"') || (first == b'\'' && last == b'\'') {
            return &input[1..bytes.len() - 1];
        }
    }
    input
}

pub(super) fn looks_like_windows_path(input: &str) -> bool {
    let bytes = input.as_bytes();
    (bytes.len() >= 2 && bytes[1] == b':') || input.starts_with("\\\\")
}

pub(super) fn unescape_dragged_path(input: &str) -> String {
    if looks_like_windows_path(input) {
        return input.to_string();
    }

    let mut out = String::with_capacity(input.len());
    let mut chars = input.chars().peekable();
    while let Some(ch) = chars.next() {
        if ch == '\\' {
            if let Some(next) = chars.peek().copied() {
                if matches!(
                    next,
                    ' ' | '(' | ')' | '[' | ']' | '{' | '}' | '"' | '\'' | '\\'
                ) {
                    out.push(next);
                    let _ = chars.next();
                    continue;
                }
            }
        }
        out.push(ch);
    }
    out
}

pub(super) fn file_url_to_path(input: &str) -> Option<PathBuf> {
    if !input.starts_with("file://") {
        return None;
    }
    Url::parse(input).ok()?.to_file_path().ok()
}

pub(super) fn path_for_display(path: &Path, cwd: &Path) -> String {
    path.strip_prefix(cwd).map_or_else(
        |_| path.to_string_lossy().to_string(),
        |p| p.to_string_lossy().to_string(),
    )
}

pub(super) fn format_file_ref(path: &str) -> String {
    if path.chars().any(char::is_whitespace) {
        if !path.contains('"') {
            format!("@\"{path}\"")
        } else if !path.contains('\'') {
            format!("@'{path}'")
        } else {
            format!("@\"{}\"", path.replace('"', "\\\""))
        }
    } else {
        format!("@{path}")
    }
}

pub(super) fn split_trailing_punct(token: &str) -> (&str, &str) {
    let mut split = token.len();
    for (idx, ch) in token.char_indices().rev() {
        if is_trailing_punct(ch) {
            split = idx;
        } else {
            break;
        }
    }
    token.split_at(split)
}

const fn is_trailing_punct(ch: char) -> bool {
    matches!(
        ch,
        ',' | '.' | ';' | ':' | '!' | '?' | ')' | ']' | '}' | '"' | '\''
    )
}

pub(super) fn is_file_ref_boundary(text: &str, at: usize) -> bool {
    if at == 0 {
        return true;
    }
    let prev = text[..at].chars().last().unwrap_or(' ');
    prev.is_whitespace() || matches!(prev, '(' | '[' | '{' | '<' | '"' | '\'')
}

#[cfg(test)]
mod tests {
    use std::path::{Path, PathBuf};

    use super::*;

    #[test]
    fn strip_wrapping_quotes_double() {
        assert_eq!(strip_wrapping_quotes("\"hello\""), "hello");
    }

    #[test]
    fn strip_wrapping_quotes_single() {
        assert_eq!(strip_wrapping_quotes("'hello'"), "hello");
    }

    #[test]
    fn strip_wrapping_quotes_mismatched() {
        assert_eq!(strip_wrapping_quotes("\"hello'"), "\"hello'");
    }

    #[test]
    fn strip_wrapping_quotes_no_quotes() {
        assert_eq!(strip_wrapping_quotes("hello"), "hello");
    }

    #[test]
    fn strip_wrapping_quotes_empty() {
        assert_eq!(strip_wrapping_quotes(""), "");
    }

    #[test]
    fn strip_wrapping_quotes_single_char() {
        assert_eq!(strip_wrapping_quotes("\""), "\"");
    }

    #[test]
    fn windows_path_drive_letter() {
        assert!(looks_like_windows_path("C:\\Users\\foo"));
        assert!(looks_like_windows_path("D:file.txt"));
    }

    #[test]
    fn windows_path_unc() {
        assert!(looks_like_windows_path("\\\\server\\share"));
    }

    #[test]
    fn unix_path_not_windows() {
        assert!(!looks_like_windows_path("/home/user/file"));
        assert!(!looks_like_windows_path("relative/path"));
    }

    #[test]
    fn unescape_dragged_path_backslash_space() {
        assert_eq!(unescape_dragged_path("my\\ file.txt"), "my file.txt");
    }

    #[test]
    fn unescape_dragged_path_backslash_parens() {
        assert_eq!(unescape_dragged_path("file\\(1\\).txt"), "file(1).txt");
    }

    #[test]
    fn unescape_dragged_path_windows_preserved() {
        assert_eq!(unescape_dragged_path("C:\\Users\\foo"), "C:\\Users\\foo");
    }

    #[test]
    fn unescape_dragged_path_no_escapes() {
        assert_eq!(unescape_dragged_path("simple.txt"), "simple.txt");
    }

    #[test]
    #[cfg(unix)]
    fn file_url_to_path_valid() {
        let result = file_url_to_path("file:///tmp/test.txt");
        assert_eq!(result, Some(PathBuf::from("/tmp/test.txt")));
    }

    #[test]
    fn file_url_to_path_not_file_url() {
        assert!(file_url_to_path("https://example.com").is_none());
        assert!(file_url_to_path("/tmp/test.txt").is_none());
    }

    #[test]
    fn format_file_ref_simple() {
        assert_eq!(format_file_ref("src/main.rs"), "@src/main.rs");
    }

    #[test]
    fn format_file_ref_with_spaces() {
        assert_eq!(format_file_ref("my file.rs"), "@\"my file.rs\"");
    }

    #[test]
    fn format_file_ref_with_double_quotes_in_path() {
        assert_eq!(format_file_ref("my \"file\".rs"), "@'my \"file\".rs'");
    }

    #[test]
    fn format_file_ref_with_both_quotes() {
        assert_eq!(
            format_file_ref("it's a \"file\" name.rs"),
            "@\"it's a \\\"file\\\" name.rs\""
        );
    }

    #[test]
    fn split_trailing_punct_period() {
        assert_eq!(split_trailing_punct("file.rs."), ("file.rs", "."));
    }

    #[test]
    fn split_trailing_punct_comma() {
        assert_eq!(split_trailing_punct("word,"), ("word", ","));
    }

    #[test]
    fn split_trailing_punct_no_trailing() {
        assert_eq!(split_trailing_punct("word"), ("word", ""));
    }

    #[test]
    fn split_trailing_punct_all_punct() {
        assert_eq!(split_trailing_punct("!?"), ("", "!?"));
    }

    #[test]
    fn split_trailing_punct_empty() {
        assert_eq!(split_trailing_punct(""), ("", ""));
    }

    #[test]
    fn file_ref_boundary_at_start() {
        assert!(is_file_ref_boundary("@file", 0));
    }

    #[test]
    fn file_ref_boundary_after_space() {
        assert!(is_file_ref_boundary("see @file", 4));
    }

    #[test]
    fn file_ref_boundary_after_paren() {
        assert!(is_file_ref_boundary("(@file)", 1));
    }

    #[test]
    fn file_ref_boundary_mid_word() {
        assert!(!is_file_ref_boundary("foo@bar", 3));
    }

    #[test]
    fn path_for_display_within_cwd() {
        let cwd = Path::new("/home/user/project");
        let path = Path::new("/home/user/project/src/main.rs");
        assert_eq!(path_for_display(path, cwd), "src/main.rs");
    }

    #[test]
    fn path_for_display_outside_cwd() {
        let cwd = Path::new("/home/user/project");
        let path = Path::new("/tmp/file.txt");
        assert_eq!(path_for_display(path, cwd), "/tmp/file.txt");
    }

    #[test]
    fn path_for_display_same_as_cwd() {
        let cwd = Path::new("/home/user");
        let path = Path::new("/home/user");
        assert_eq!(path_for_display(path, cwd), "");
    }

    #[test]
    fn next_token_basic() {
        let (token, end) = next_non_whitespace_token("hello world", 0);
        assert_eq!(token, "hello");
        assert_eq!(end, 5);
    }

    #[test]
    fn next_token_past_end() {
        let (token, end) = next_non_whitespace_token("abc", 10);
        assert_eq!(token, "");
        assert_eq!(end, 3);
    }

    #[test]
    fn next_token_last_word() {
        let (token, end) = next_non_whitespace_token("a bc", 2);
        assert_eq!(token, "bc");
        assert_eq!(end, 4);
    }
}