Files
el/ide/vessels/el-ide-server/src/api/references.rs
T
Will Anderson be013d2b42 rename crates/ → vessels/ — El's word for buildable units
Per the consolidation onto El: 'crates' is the Rust word, 'vessel' is
El's (per spec/language.md §15). The directory rename is the structural
marker that this slot holds an El buildable unit, even if its current
contents are still Rust pending port.

Mechanical: git mv crates vessels, sed workspace members and any path
dependencies, update CI workflow paths, update README references.
Cross-repo path dependencies (`../foo/crates/bar`) updated workspace-
wide so cargo metadata still resolves where the Rust still builds.
2026-04-30 15:34:20 -05:00

120 lines
3.5 KiB
Rust

//! Definition and references API — find all uses of a symbol.
use axum::{
extract::Query,
http::StatusCode,
Json,
};
use serde::{Deserialize, Serialize};
type ApiResult<T> = Result<Json<T>, (StatusCode, Json<serde_json::Value>)>;
#[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<SymbolQuery>,
) -> ApiResult<Option<SymbolLocation>> {
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<SymbolQuery>,
) -> ApiResult<Vec<SymbolLocation>> {
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<SymbolLocation> {
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<SymbolLocation> {
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
}