Archived
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.
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
//! 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<Completion> {
|
||||
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<HoverInfo> {
|
||||
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> {
|
||||
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]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user