be013d2b42
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.
120 lines
3.5 KiB
Rust
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
|
|
}
|