ralph-coder 0.2.1

An agentic code generation CLI powered by multiple LLM backends
Documentation
/// Grep-based fallbacks for LSP navigation tools.
///
/// Used when no `--lsp` flag is given, or when the queried file's extension
/// does not match any active LSP client.  Results are less precise than a
/// real LSP server but require no external tooling.
use crate::tools::search_tools;
use std::path::Path;

// ── Identifier extraction ─────────────────────────────────────────────────────

/// Extract the identifier (word) that covers `col` on `line` (both 1-based).
/// Returns `None` when the position is out of range or lands on whitespace/punctuation.
pub fn word_at_position(content: &str, line: u32, col: u32) -> Option<String> {
    let lines: Vec<&str> = content.lines().collect();
    let row = lines.get(line.saturating_sub(1) as usize)?;
    let col0 = col.saturating_sub(1) as usize;
    if col0 >= row.len() {
        return None;
    }

    let is_ident = |c: char| c.is_alphanumeric() || c == '_';

    // Walk left to find start of word
    let start = row[..col0]
        .rfind(|c: char| !is_ident(c))
        .map(|i| i + 1)
        .unwrap_or(0);

    // Walk right to find end of word
    let end = row[col0..]
        .find(|c: char| !is_ident(c))
        .map(|i| col0 + i)
        .unwrap_or(row.len());

    let word = &row[start..end];
    if word.is_empty() {
        None
    } else {
        Some(word.to_string())
    }
}

// ── Grep fallbacks ────────────────────────────────────────────────────────────

/// Find where the symbol at `file:line:col` is defined (grep-based).
pub fn go_to_definition_grep(workspace: &Path, file: &Path, line: u32, col: u32) -> String {
    let content = match std::fs::read_to_string(workspace.join(file)) {
        Ok(c) => c,
        Err(e) => return format!("Cannot read '{}': {}", file.display(), e),
    };
    let word = match word_at_position(&content, line, col) {
        Some(w) => w,
        None => return format!("No identifier at {}:{}", line, col),
    };

    // Build a pattern that matches common definition keywords
    let escaped = regex::escape(&word);
    let pattern = format!(
        r"(?:fn|struct|enum|trait|impl|class|def|interface|type|const|let|var|function)\s+{}\b",
        escaped
    );

    match search_tools::search_codebase(&pattern, workspace) {
        Ok(results) if !results.starts_with("No matches") => {
            format!("Definition candidates for '{}' (grep):\n{}", word, results)
        }
        _ => format!(
            "No definition found for '{}'. \
             Tip: use --lsp for accurate jump-to-definition.",
            word
        ),
    }
}

/// Find all references to the symbol at `file:line:col` (grep-based).
pub fn find_references_grep(workspace: &Path, file: &Path, line: u32, col: u32) -> String {
    let content = match std::fs::read_to_string(workspace.join(file)) {
        Ok(c) => c,
        Err(e) => return format!("Cannot read '{}': {}", file.display(), e),
    };
    let word = match word_at_position(&content, line, col) {
        Some(w) => w,
        None => return format!("No identifier at {}:{}", line, col),
    };

    let pattern = format!(r"\b{}\b", regex::escape(&word));
    match search_tools::search_codebase(&pattern, workspace) {
        Ok(results) => format!(
            "References to '{}' (grep — use --lsp for accurate results):\n{}",
            word, results
        ),
        Err(e) => format!("Search error: {}", e),
    }
}

/// Show type / doc context for the symbol at `file:line:col` (grep-based).
///
/// Returns the target line plus two lines of surrounding context, and a
/// grep search for the nearest definition of the same identifier.
pub fn hover_grep(workspace: &Path, file: &Path, line: u32, col: u32) -> String {
    let content = match std::fs::read_to_string(workspace.join(file)) {
        Ok(c) => c,
        Err(e) => return format!("Cannot read '{}': {}", file.display(), e),
    };

    let word = word_at_position(&content, line, col)
        .unwrap_or_else(|| format!("position {}:{}", line, col));

    let lines: Vec<&str> = content.lines().collect();
    let target = line.saturating_sub(1) as usize;
    let start = target.saturating_sub(2);
    let end = (target + 3).min(lines.len());

    let mut out = format!(
        "Context for '{}' at {}:{} (grep fallback):\n\n",
        word,
        file.display(),
        line
    );
    for (i, text) in lines[start..end].iter().enumerate() {
        let lnum = start + i + 1;
        let arrow = if lnum == line as usize { "" } else { " " };
        out.push_str(&format!("{} {:4}{}\n", arrow, lnum, text));
    }

    // Append a quick definition search
    let escaped = regex::escape(&word);
    let def_pattern = format!(
        r"(?:fn|struct|enum|trait|impl|class|def|interface|type|const)\s+{}\b",
        escaped
    );
    if let Ok(defs) = search_tools::search_codebase(&def_pattern, workspace) {
        if !defs.starts_with("No matches") {
            out.push_str(&format!("\nDefinition candidates:\n{}\n", defs));
        }
    }

    out
}