Skip to main content

ralph/tools/
lsp_tools.rs

1/// Grep-based fallbacks for LSP navigation tools.
2///
3/// Used when no `--lsp` flag is given, or when the queried file's extension
4/// does not match any active LSP client.  Results are less precise than a
5/// real LSP server but require no external tooling.
6use crate::tools::search_tools;
7use std::path::Path;
8
9// ── Identifier extraction ─────────────────────────────────────────────────────
10
11/// Extract the identifier (word) that covers `col` on `line` (both 1-based).
12/// Returns `None` when the position is out of range or lands on whitespace/punctuation.
13pub fn word_at_position(content: &str, line: u32, col: u32) -> Option<String> {
14    let lines: Vec<&str> = content.lines().collect();
15    let row = lines.get(line.saturating_sub(1) as usize)?;
16    let col0 = col.saturating_sub(1) as usize;
17    if col0 >= row.len() {
18        return None;
19    }
20
21    let is_ident = |c: char| c.is_alphanumeric() || c == '_';
22
23    // Walk left to find start of word
24    let start = row[..col0]
25        .rfind(|c: char| !is_ident(c))
26        .map(|i| i + 1)
27        .unwrap_or(0);
28
29    // Walk right to find end of word
30    let end = row[col0..]
31        .find(|c: char| !is_ident(c))
32        .map(|i| col0 + i)
33        .unwrap_or(row.len());
34
35    let word = &row[start..end];
36    if word.is_empty() {
37        None
38    } else {
39        Some(word.to_string())
40    }
41}
42
43// ── Grep fallbacks ────────────────────────────────────────────────────────────
44
45/// Find where the symbol at `file:line:col` is defined (grep-based).
46pub fn go_to_definition_grep(workspace: &Path, file: &Path, line: u32, col: u32) -> String {
47    let content = match std::fs::read_to_string(workspace.join(file)) {
48        Ok(c) => c,
49        Err(e) => return format!("Cannot read '{}': {}", file.display(), e),
50    };
51    let word = match word_at_position(&content, line, col) {
52        Some(w) => w,
53        None => return format!("No identifier at {}:{}", line, col),
54    };
55
56    // Build a pattern that matches common definition keywords
57    let escaped = regex::escape(&word);
58    let pattern = format!(
59        r"(?:fn|struct|enum|trait|impl|class|def|interface|type|const|let|var|function)\s+{}\b",
60        escaped
61    );
62
63    match search_tools::search_codebase(&pattern, workspace) {
64        Ok(results) if !results.starts_with("No matches") => {
65            format!("Definition candidates for '{}' (grep):\n{}", word, results)
66        }
67        _ => format!(
68            "No definition found for '{}'. \
69             Tip: use --lsp for accurate jump-to-definition.",
70            word
71        ),
72    }
73}
74
75/// Find all references to the symbol at `file:line:col` (grep-based).
76pub fn find_references_grep(workspace: &Path, file: &Path, line: u32, col: u32) -> String {
77    let content = match std::fs::read_to_string(workspace.join(file)) {
78        Ok(c) => c,
79        Err(e) => return format!("Cannot read '{}': {}", file.display(), e),
80    };
81    let word = match word_at_position(&content, line, col) {
82        Some(w) => w,
83        None => return format!("No identifier at {}:{}", line, col),
84    };
85
86    let pattern = format!(r"\b{}\b", regex::escape(&word));
87    match search_tools::search_codebase(&pattern, workspace) {
88        Ok(results) => format!(
89            "References to '{}' (grep — use --lsp for accurate results):\n{}",
90            word, results
91        ),
92        Err(e) => format!("Search error: {}", e),
93    }
94}
95
96/// Show type / doc context for the symbol at `file:line:col` (grep-based).
97///
98/// Returns the target line plus two lines of surrounding context, and a
99/// grep search for the nearest definition of the same identifier.
100pub fn hover_grep(workspace: &Path, file: &Path, line: u32, col: u32) -> String {
101    let content = match std::fs::read_to_string(workspace.join(file)) {
102        Ok(c) => c,
103        Err(e) => return format!("Cannot read '{}': {}", file.display(), e),
104    };
105
106    let word = word_at_position(&content, line, col)
107        .unwrap_or_else(|| format!("position {}:{}", line, col));
108
109    let lines: Vec<&str> = content.lines().collect();
110    let target = line.saturating_sub(1) as usize;
111    let start = target.saturating_sub(2);
112    let end = (target + 3).min(lines.len());
113
114    let mut out = format!(
115        "Context for '{}' at {}:{} (grep fallback):\n\n",
116        word,
117        file.display(),
118        line
119    );
120    for (i, text) in lines[start..end].iter().enumerate() {
121        let lnum = start + i + 1;
122        let arrow = if lnum == line as usize { "▶" } else { " " };
123        out.push_str(&format!("{} {:4} │ {}\n", arrow, lnum, text));
124    }
125
126    // Append a quick definition search
127    let escaped = regex::escape(&word);
128    let def_pattern = format!(
129        r"(?:fn|struct|enum|trait|impl|class|def|interface|type|const)\s+{}\b",
130        escaped
131    );
132    if let Ok(defs) = search_tools::search_codebase(&def_pattern, workspace) {
133        if !defs.starts_with("No matches") {
134            out.push_str(&format!("\nDefinition candidates:\n{}\n", defs));
135        }
136    }
137
138    out
139}