slash-lib 0.1.0

Executor types and high-level API for the slash-command language
Documentation
use std::path::PathBuf;

use slash_lang::parser::ast::Arg;

use crate::command::{MethodDef, SlashCommand};
use crate::executor::{CommandOutput, ExecutionError, PipeValue};

/// `/find(pattern)` — search for files matching a glob pattern.
///
/// `/find(src/**/*.rs)` — list matching file paths, one per line.
/// `/find(src).content(TODO)` — search file contents for a string.
///
/// Pure Rust directory walking + pattern matching. No subprocess.
pub struct Find;

impl SlashCommand for Find {
    fn name(&self) -> &str {
        "find"
    }

    fn methods(&self) -> &[MethodDef] {
        static METHODS: [MethodDef; 1] = [MethodDef::with_value("content")];
        &METHODS
    }

    fn execute(
        &self,
        primary: Option<&str>,
        args: &[Arg],
        _input: Option<&PipeValue>,
    ) -> Result<CommandOutput, ExecutionError> {
        let pattern = primary.ok_or_else(|| {
            ExecutionError::Runner("/find requires a pattern: /find(src/**/*.rs)".into())
        })?;

        let content_filter = args
            .iter()
            .find(|a| a.name == "content")
            .and_then(|a| a.value.as_deref());

        let paths = glob_walk(pattern)?;

        let result = if let Some(needle) = content_filter {
            // Search file contents for the needle.
            let mut matches = Vec::new();
            for path in &paths {
                if let Ok(content) = std::fs::read_to_string(path) {
                    for (line_num, line) in content.lines().enumerate() {
                        if line.contains(needle) {
                            matches.push(format!("{}:{}:{}", path.display(), line_num + 1, line));
                        }
                    }
                }
            }
            matches.join("\n")
        } else {
            // Just list matching paths.
            paths
                .iter()
                .map(|p| p.display().to_string())
                .collect::<Vec<_>>()
                .join("\n")
        };

        if result.is_empty() {
            return Ok(CommandOutput {
                stdout: None,
                stderr: None,
                success: true,
            });
        }

        let mut out = result;
        out.push('\n');
        Ok(CommandOutput {
            stdout: Some(out.into_bytes()),
            stderr: None,
            success: true,
        })
    }
}

/// Walk directories matching a glob-like pattern.
///
/// Supports `*` (any segment), `**` (recursive), and `?` (single char).
/// This is a minimal implementation — no external crate dependency.
fn glob_walk(pattern: &str) -> Result<Vec<PathBuf>, ExecutionError> {
    let mut results = Vec::new();
    let parts: Vec<&str> = pattern.split('/').collect();
    walk_recursive(&PathBuf::from("."), &parts, 0, &mut results);
    results.sort();
    Ok(results)
}

fn walk_recursive(dir: &PathBuf, parts: &[&str], depth: usize, results: &mut Vec<PathBuf>) {
    if depth >= parts.len() {
        return;
    }

    let part = parts[depth];
    let is_last = depth == parts.len() - 1;

    if part == "**" {
        // Match zero or more directories.
        // Try skipping ** (depth + 1 at current dir).
        walk_recursive(dir, parts, depth + 1, results);

        // Try descending into each subdirectory with ** still active.
        if let Ok(entries) = std::fs::read_dir(dir) {
            for entry in entries.flatten() {
                let path = entry.path();
                if path.is_dir() {
                    walk_recursive(&path, parts, depth, results);
                }
            }
        }
    } else {
        // Match against entries in this directory.
        if let Ok(entries) = std::fs::read_dir(dir) {
            for entry in entries.flatten() {
                let name = entry.file_name();
                let name_str = name.to_string_lossy();
                if glob_matches(part, &name_str) {
                    let path = entry.path();
                    if is_last {
                        if path.is_file() {
                            results.push(path);
                        }
                    } else if path.is_dir() {
                        walk_recursive(&path, parts, depth + 1, results);
                    }
                }
            }
        }
    }
}

/// Simple glob matching: `*` matches any sequence, `?` matches one char.
fn glob_matches(pattern: &str, name: &str) -> bool {
    let p: Vec<char> = pattern.chars().collect();
    let n: Vec<char> = name.chars().collect();
    glob_match_inner(&p, 0, &n, 0)
}

fn glob_match_inner(p: &[char], pi: usize, n: &[char], ni: usize) -> bool {
    if pi == p.len() {
        return ni == n.len();
    }
    if p[pi] == '*' {
        // Try matching * against 0..=remaining chars.
        for skip in 0..=(n.len() - ni) {
            if glob_match_inner(p, pi + 1, n, ni + skip) {
                return true;
            }
        }
        false
    } else if p[pi] == '?' {
        ni < n.len() && glob_match_inner(p, pi + 1, n, ni + 1)
    } else {
        ni < n.len() && p[pi] == n[ni] && glob_match_inner(p, pi + 1, n, ni + 1)
    }
}