//! el-lsp — Minimal Language Server for the Engram language. //! //! Provides completions, hover info, diagnostics, and a type graph. //! Driven by the el-types type checker and el-parser AST. mod completion; mod diagnostic; mod hover; mod type_graph; pub use completion::{Completion, CompletionKind}; pub use diagnostic::Diagnostic; pub use hover::HoverInfo; pub use type_graph::{TypeEdge, TypeGraph, TypeNode}; use el_types::{TypeChecker, TypeEnv}; // ── LanguageServer ──────────────────────────────────────────────────────────── pub struct LanguageServer; impl LanguageServer { pub fn new() -> Self { Self } /// Compute completions at the given cursor byte position. pub fn complete(&self, source: &str, cursor_pos: usize) -> Vec { let env = build_type_env(source); completion::completions_at(&env, source, cursor_pos) } /// Return hover information for the token at the given byte position. pub fn hover(&self, source: &str, cursor_pos: usize) -> Option { let env = build_type_env(source); hover::hover_at(&env, source, cursor_pos) } /// Run the type checker and return diagnostics. pub fn diagnostics(&self, source: &str) -> Vec { diagnostic::check(source) } /// Build a type graph from the source. pub fn type_graph(&self, source: &str) -> TypeGraph { let env = build_type_env(source); type_graph::build(&env) } } impl Default for LanguageServer { fn default() -> Self { Self::new() } } // ── Helpers ─────────────────────────────────────────────────────────────────── /// Parse source and run the type checker, returning the final TypeEnv. fn build_type_env(source: &str) -> TypeEnv { let tokens = match el_lexer::tokenize(source) { Ok(t) => t, Err(_) => return TypeEnv::with_builtins(), }; let program = match el_parser::parse(tokens, source.to_string()) { Ok(p) => p, Err(_) => return TypeEnv::with_builtins(), }; let mut checker = TypeChecker::with_builtins(); checker.check(&program); checker.env } #[cfg(test)] mod tests { use super::*; const SAMPLE: &str = r#" type Point { x: Float y: Float } type Circle { center: Point radius: Float } enum Color { Red Green Blue Custom(String) } fn distance(a: Point, b: Point) -> Float { let dx: Float = a.x - b.x let dy: Float = a.y - b.y return dx * dx + dy * dy } fn main() -> Void { let greeting: String = "Hello from Engram" let count: Int = 42 } "#; #[test] fn test_diagnostics_clean_source() { let lsp = LanguageServer::new(); let diags = lsp.diagnostics(SAMPLE); // Should have no errors for valid source let errors: Vec<_> = diags.iter().filter(|d| d.severity == "error").collect(); assert!(errors.is_empty(), "unexpected errors: {errors:?}"); } #[test] fn test_diagnostics_invalid_source() { let lsp = LanguageServer::new(); let diags = lsp.diagnostics("let x: UnknownType = 42"); // Should surface at least one diagnostic for unknown type assert!(!diags.is_empty()); } #[test] fn test_type_graph_has_nodes() { let lsp = LanguageServer::new(); let graph = lsp.type_graph(SAMPLE); // Should have at least the built-in types plus Point, Circle, Color assert!(!graph.nodes.is_empty()); let names: Vec<_> = graph.nodes.iter().map(|n| n.name.as_str()).collect(); assert!(names.contains(&"Point"), "expected Point in type graph"); assert!(names.contains(&"Circle"), "expected Circle in type graph"); } #[test] fn test_type_graph_has_edges() { let lsp = LanguageServer::new(); let graph = lsp.type_graph(SAMPLE); // Circle has a field `center: Point` → should produce an edge let has_circle_edge = graph .edges .iter() .any(|e| e.from == "Circle" && e.to == "Point"); assert!(has_circle_edge, "expected Circle->Point edge; edges: {:?}", graph.edges); } #[test] fn test_completions_return_keywords() { let lsp = LanguageServer::new(); let completions = lsp.complete("", 0); let labels: Vec<_> = completions.iter().map(|c| c.label.as_str()).collect(); assert!(labels.contains(&"let"), "expected 'let' keyword completion"); assert!(labels.contains(&"fn"), "expected 'fn' keyword completion"); } #[test] fn test_completions_include_types() { let lsp = LanguageServer::new(); let completions = lsp.complete(SAMPLE, 0); let labels: Vec<_> = completions.iter().map(|c| c.label.as_str()).collect(); // User-defined types should appear assert!(labels.contains(&"Point"), "expected Point in completions"); } #[test] fn test_hover_on_builtin_type() { let lsp = LanguageServer::new(); // Position at the "Float" token in the sample let source = "let x: Float = 3.14"; // Find the byte offset of "Float" let pos = source.find("Float").unwrap(); let info = lsp.hover(source, pos); assert!(info.is_some(), "expected hover info for Float"); let info = info.unwrap(); assert_eq!(info.type_name, "Float"); } #[test] fn test_completions_sorted_by_score() { let lsp = LanguageServer::new(); let completions = lsp.complete(SAMPLE, 0); // Completions should be sorted descending by score for window in completions.windows(2) { assert!( window[0].score >= window[1].score, "completions not sorted by score: {:?} before {:?}", window[0], window[1] ); } } }