ralph-coder 0.2.1

An agentic code generation CLI powered by multiple LLM backends
Documentation
use ralph::tools::lsp_tools::{
    find_references_grep, go_to_definition_grep, hover_grep, word_at_position,
};
use std::fs;
use tempfile::TempDir;

// ── word_at_position ──────────────────────────────────────────────────────────

#[test]
fn word_at_position_middle_of_word() {
    let src = "fn hello_world() {}";
    // "hello_world" starts at col 4; col 7 is inside the word
    assert_eq!(word_at_position(src, 1, 7), Some("hello_world".to_string()));
}

#[test]
fn word_at_position_start_of_word() {
    let src = "fn hello() {}";
    assert_eq!(word_at_position(src, 1, 4), Some("hello".to_string()));
}

#[test]
fn word_at_position_end_of_word() {
    let src = "fn hello() {}";
    // col 8 is the last char of "hello" (h=4 e=5 l=6 l=7 o=8)
    assert_eq!(word_at_position(src, 1, 8), Some("hello".to_string()));
}

#[test]
fn word_at_position_just_past_word_returns_preceding_word() {
    let src = "fn hello() {}";
    // col 3 is the space after "fn"; the function returns the word to the left
    assert_eq!(word_at_position(src, 1, 3), Some("fn".to_string()));
}

#[test]
fn word_at_position_out_of_range_col_returns_none() {
    let src = "fn hello() {}";
    assert_eq!(word_at_position(src, 1, 999), None);
}

#[test]
fn word_at_position_out_of_range_line_returns_none() {
    let src = "fn hello() {}";
    assert_eq!(word_at_position(src, 99, 1), None);
}

#[test]
fn word_at_position_second_line() {
    let src = "first line\nsecond word here";
    // Line 2, col 8 is inside "word"
    assert_eq!(word_at_position(src, 2, 8), Some("word".to_string()));
}

// ── go_to_definition_grep ─────────────────────────────────────────────────────

#[test]
fn go_to_definition_finds_fn() {
    let dir = TempDir::new().unwrap();
    let src = "fn my_function() {\n    println!(\"hello\");\n}\n";
    fs::write(dir.path().join("main.rs"), src).unwrap();

    // Position on "my_function" call site in another file
    let caller = "fn main() {\n    my_function();\n}\n";
    fs::write(dir.path().join("caller.rs"), caller).unwrap();

    let result = go_to_definition_grep(
        dir.path(),
        std::path::Path::new("caller.rs"),
        2, // line 2: "    my_function();"
        5, // col 5: inside "my_function"
    );
    assert!(
        result.contains("my_function"),
        "expected symbol name in result: {}",
        result
    );
}

#[test]
fn go_to_definition_nonexistent_file() {
    let dir = TempDir::new().unwrap();
    let result = go_to_definition_grep(dir.path(), std::path::Path::new("missing.rs"), 1, 1);
    assert!(
        result.contains("Cannot read"),
        "expected error message: {}",
        result
    );
}

#[test]
fn go_to_definition_no_match_returns_tip() {
    let dir = TempDir::new().unwrap();
    // A file with no definitions at all
    fs::write(dir.path().join("empty.rs"), "// just a comment\n").unwrap();
    let result = go_to_definition_grep(dir.path(), std::path::Path::new("empty.rs"), 1, 4);
    // "just" is a word but won't match definition patterns
    assert!(
        result.contains("No definition found") || result.contains("Definition candidates"),
        "unexpected result: {}",
        result
    );
}

// ── find_references_grep ──────────────────────────────────────────────────────

#[test]
fn find_references_finds_usages() {
    let dir = TempDir::new().unwrap();
    let src = "fn foo() {}\nfn bar() { foo(); foo(); }\n";
    fs::write(dir.path().join("lib.rs"), src).unwrap();

    let result = find_references_grep(
        dir.path(),
        std::path::Path::new("lib.rs"),
        1, // line 1: "fn foo()"
        4, // col 4: "f" of "foo"
    );
    assert!(
        result.contains("foo"),
        "expected 'foo' in references: {}",
        result
    );
    assert!(
        result.contains("grep"),
        "expected grep fallback note: {}",
        result
    );
}

#[test]
fn find_references_nonexistent_file() {
    let dir = TempDir::new().unwrap();
    let result = find_references_grep(dir.path(), std::path::Path::new("ghost.rs"), 1, 1);
    assert!(result.contains("Cannot read"), "expected error: {}", result);
}

// ── hover_grep ────────────────────────────────────────────────────────────────

#[test]
fn hover_returns_context_lines() {
    let dir = TempDir::new().unwrap();
    let src = "line one\nline two\nline three\nline four\nline five\n";
    fs::write(dir.path().join("sample.txt"), src).unwrap();

    let result = hover_grep(
        dir.path(),
        std::path::Path::new("sample.txt"),
        3, // target line 3
        1,
    );
    assert!(result.contains("three"), "target line missing: {}", result);
    // Should include surrounding lines (1 or 2 before/after)
    assert!(
        result.contains("two") || result.contains("four"),
        "context missing: {}",
        result
    );
}

#[test]
fn hover_nonexistent_file() {
    let dir = TempDir::new().unwrap();
    let result = hover_grep(dir.path(), std::path::Path::new("nope.rs"), 1, 1);
    assert!(result.contains("Cannot read"), "expected error: {}", result);
}