Archived
b2c22e6616
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.
92 lines
2.7 KiB
Rust
92 lines
2.7 KiB
Rust
//! Document outline API — returns functions, types, enums from AST.
|
|
|
|
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 SourceQuery {
|
|
pub source: String,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct OutlineItem {
|
|
pub kind: String, // "fn" | "type" | "enum" | "protocol" | "impl"
|
|
pub name: String,
|
|
pub line: usize,
|
|
}
|
|
|
|
/// GET /api/outline?source=... — extract document symbols.
|
|
pub async fn outline(
|
|
Query(q): Query<SourceQuery>,
|
|
) -> ApiResult<Vec<OutlineItem>> {
|
|
let items = extract_outline(&q.source);
|
|
Ok(Json(items))
|
|
}
|
|
|
|
fn extract_outline(source: &str) -> Vec<OutlineItem> {
|
|
let mut items = Vec::new();
|
|
|
|
// Simple regex-like line scan for top-level declarations.
|
|
// Pattern: keyword followed by an identifier.
|
|
for (i, line) in source.lines().enumerate() {
|
|
let trimmed = line.trim();
|
|
let line_num = i + 1;
|
|
|
|
// fn name(
|
|
if let Some(rest) = trimmed.strip_prefix("fn ") {
|
|
if let Some(name) = ident_from(rest) {
|
|
items.push(OutlineItem { kind: "fn".into(), name, line: line_num });
|
|
}
|
|
continue;
|
|
}
|
|
// type Name {
|
|
if let Some(rest) = trimmed.strip_prefix("type ") {
|
|
if let Some(name) = ident_from(rest) {
|
|
items.push(OutlineItem { kind: "type".into(), name, line: line_num });
|
|
}
|
|
continue;
|
|
}
|
|
// enum Name {
|
|
if let Some(rest) = trimmed.strip_prefix("enum ") {
|
|
if let Some(name) = ident_from(rest) {
|
|
items.push(OutlineItem { kind: "enum".into(), name, line: line_num });
|
|
}
|
|
continue;
|
|
}
|
|
// protocol Name {
|
|
if let Some(rest) = trimmed.strip_prefix("protocol ") {
|
|
if let Some(name) = ident_from(rest) {
|
|
items.push(OutlineItem { kind: "protocol".into(), name, line: line_num });
|
|
}
|
|
continue;
|
|
}
|
|
// impl Protocol for Type {
|
|
if let Some(rest) = trimmed.strip_prefix("impl ") {
|
|
// name is "Protocol for Type" → take until {
|
|
let name = rest.trim_end_matches(|c: char| c == '{').trim();
|
|
if !name.is_empty() {
|
|
items.push(OutlineItem { kind: "impl".into(), name: name.to_string(), line: line_num });
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
|
|
items
|
|
}
|
|
|
|
fn ident_from(s: &str) -> Option<String> {
|
|
let s = s.trim_start();
|
|
let end = s.find(|c: char| !c.is_alphanumeric() && c != '_').unwrap_or(s.len());
|
|
if end == 0 {
|
|
None
|
|
} else {
|
|
Some(s[..end].to_string())
|
|
}
|
|
}
|