//! Document outline API — returns functions, types, enums from AST. use axum::{ extract::Query, http::StatusCode, Json, }; use serde::{Deserialize, Serialize}; type ApiResult = Result, (StatusCode, Json)>; #[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, ) -> ApiResult> { let items = extract_outline(&q.source); Ok(Json(items)) } fn extract_outline(source: &str) -> Vec { 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 { 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()) } }