//! Definition and references API — find all uses of a symbol. use axum::{ extract::Query, http::StatusCode, Json, }; use serde::{Deserialize, Serialize}; type ApiResult = Result, (StatusCode, Json)>; #[derive(Debug, Deserialize)] pub struct SymbolQuery { pub source: String, pub pos: usize, } #[derive(Debug, Serialize)] pub struct SymbolLocation { pub line: usize, pub col: usize, pub snippet: String, } /// GET /api/definition?source=...&pos=... — find definition of symbol at pos. pub async fn definition( Query(q): Query, ) -> ApiResult> { let loc = find_definition(&q.source, q.pos); Ok(Json(loc)) } /// GET /api/references?source=...&pos=... — find all references to symbol at pos. pub async fn references( Query(q): Query, ) -> ApiResult> { let refs = find_references(&q.source, q.pos); Ok(Json(refs)) } // ── Implementation ──────────────────────────────────────────────────────────── fn word_at(source: &str, pos: usize) -> Option<&str> { if pos > source.len() { return None; } let bytes = source.as_bytes(); let mut start = pos; let mut end = pos; while start > 0 && (bytes[start - 1].is_ascii_alphanumeric() || bytes[start - 1] == b'_') { start -= 1; } while end < bytes.len() && (bytes[end].is_ascii_alphanumeric() || bytes[end] == b'_') { end += 1; } if start == end { None } else { Some(&source[start..end]) } } fn find_definition(source: &str, pos: usize) -> Option { let word = word_at(source, pos)?; if word.is_empty() { return None; } // Look for `fn word`, `type word`, `enum word`, `let word`, `protocol word` let patterns = [ format!("fn {word}"), format!("type {word}"), format!("enum {word}"), format!("protocol {word}"), ]; for (i, line) in source.lines().enumerate() { for pat in &patterns { if line.contains(pat.as_str()) { let col = line.find(pat.as_str()).unwrap_or(0); return Some(SymbolLocation { line: i + 1, col: col + 1, snippet: line.trim().to_string(), }); } } } None } fn find_references(source: &str, pos: usize) -> Vec { let word = match word_at(source, pos) { Some(w) if !w.is_empty() => w, _ => return vec![], }; let mut refs = Vec::new(); for (i, line) in source.lines().enumerate() { let mut search = line; let mut offset = 0; while let Some(idx) = search.find(word) { // Check word boundaries let abs = offset + idx; let before_ok = abs == 0 || !line.as_bytes()[abs - 1].is_ascii_alphanumeric() && line.as_bytes()[abs - 1] != b'_'; let after_ok = abs + word.len() >= line.len() || !line.as_bytes()[abs + word.len()].is_ascii_alphanumeric() && line.as_bytes()[abs + word.len()] != b'_'; if before_ok && after_ok { refs.push(SymbolLocation { line: i + 1, col: abs + 1, snippet: line.trim().to_string(), }); } offset += idx + word.len(); search = &search[idx + word.len()..]; } } refs }