use std::collections::HashMap;
use oxilean_elab::info_tree::{InfoData, InfoTree, InfoTreeBuilder};
use oxilean_elab::{elaborate_decl as elab_decl_impl, PendingDecl};
use oxilean_kernel::{check_declaration, Declaration, Environment, Name, ReducibilityHint};
use oxilean_parse::incremental::{parse_incremental_change, TextChange};
use oxilean_parse::{Lexer, Parser, TokenKind};
use super::document::Document;
use super::lsp_types::{
Diagnostic, DiagnosticSeverity, DocumentSymbol, Location, Range, SymbolKind, TextEdit,
};
#[derive(Clone, Debug)]
pub struct DefinitionInfo {
pub name: String,
pub kind: SymbolKind,
pub range: Range,
pub ty: Option<String>,
pub doc: Option<String>,
}
#[derive(Clone, Debug, Default)]
pub struct AnalysisResult {
pub diagnostics: Vec<Diagnostic>,
pub symbols: Vec<DocumentSymbol>,
pub definitions: Vec<DefinitionInfo>,
}
pub fn analyze_document(uri: &str, content: &str, env: &Environment) -> AnalysisResult {
let mut lexer = Lexer::new(content);
let tokens = lexer.tokenize();
analyze_from_tokens(uri, tokens, env)
}
pub fn analyze_document_and_cache(doc: &mut Document, env: &Environment) -> AnalysisResult {
let mut lexer = Lexer::new(&doc.content);
let tokens = lexer.tokenize();
let result = analyze_from_tokens(&doc.uri, tokens.clone(), env);
doc.cached_tokens = tokens;
doc.cached_decls = parse_decls_from_source(&doc.content);
result
}
pub fn analyze_document_incremental(
doc: &mut Document,
change: &TextChange,
env: &Environment,
) -> AnalysisResult {
if doc.cached_tokens.is_empty() {
return analyze_document_and_cache(doc, env);
}
let result = parse_incremental_change(&doc.cached_tokens, &doc.content, change);
let tokens = result.tokens;
let analysis = analyze_from_tokens(&doc.uri, tokens.clone(), env);
doc.cached_tokens = tokens;
analysis
}
pub fn parse_decls_from_source(source: &str) -> Vec<oxilean_parse::Located<oxilean_parse::Decl>> {
use oxilean_parse::{Lexer, Parser};
let tokens = Lexer::new(source).tokenize();
let mut parser = Parser::new(tokens);
let mut decls = Vec::new();
loop {
if parser.is_eof() {
break;
}
match parser.parse_decl() {
Ok(d) => decls.push(d),
Err(e) => {
let is_eof = e.is_eof() || parser.is_eof();
if is_eof {
break;
}
parser.advance();
}
}
}
decls
}
pub fn analyze_document_incremental_decls(doc: &mut Document, env: &Environment) -> AnalysisResult {
let new_decls = parse_decls_from_source(&doc.content);
let edits = oxilean_parse::incremental::diff_modules(&doc.cached_decls, &new_decls);
let first_change_new_idx: usize = edits
.iter()
.filter_map(|e| {
use oxilean_parse::incremental::EditKind;
match e.kind {
EditKind::Inserted | EditKind::Modified => e.new_idx,
EditKind::Deleted => {
Some(e.old_idx.unwrap_or(0))
}
EditKind::Unchanged => None,
}
})
.min()
.unwrap_or(new_decls.len());
let tail_result = if first_change_new_idx >= new_decls.len() {
AnalysisResult::default()
} else {
analyze_document(&doc.uri, &doc.content, env)
};
doc.cached_decls = new_decls;
tail_result
}
fn analyze_from_tokens(
uri: &str,
tokens: Vec<oxilean_parse::tokens::Token>,
env: &Environment,
) -> AnalysisResult {
let mut result = AnalysisResult::default();
for token in &tokens {
if let TokenKind::Error(msg) = &token.kind {
let line = if token.span.line > 0 {
token.span.line as u32 - 1
} else {
0
};
let col = if token.span.column > 0 {
token.span.column as u32 - 1
} else {
0
};
result.diagnostics.push(Diagnostic::error(
Range::single_line(line, col, col + 1),
format!("lexer error: {}", msg),
));
}
}
extract_symbols_from_tokens(uri, &tokens, &mut result);
for def in &result.definitions {
let name = Name::str(&def.name);
if env.contains(&name) {
result.diagnostics.push(Diagnostic::warning(
def.range.clone(),
format!("'{}' shadows existing declaration in environment", def.name),
));
}
}
result
}
fn extract_symbols_from_tokens(
_uri: &str,
tokens: &[oxilean_parse::tokens::Token],
result: &mut AnalysisResult,
) {
let mut i = 0;
while i < tokens.len() {
let token = &tokens[i];
let (decl_keyword, sym_kind) = match &token.kind {
TokenKind::Definition => ("def", SymbolKind::Function),
TokenKind::Theorem => ("theorem", SymbolKind::Method),
TokenKind::Lemma => ("theorem", SymbolKind::Method),
TokenKind::Axiom => ("axiom", SymbolKind::Constant),
TokenKind::Inductive => ("inductive", SymbolKind::Enum),
TokenKind::Structure => ("structure", SymbolKind::Struct),
TokenKind::Class => ("class", SymbolKind::Class),
TokenKind::Instance => ("instance", SymbolKind::Property),
TokenKind::Namespace => ("namespace", SymbolKind::Namespace),
_ => {
i += 1;
continue;
}
};
if i + 1 < tokens.len() {
if let TokenKind::Ident(name) = &tokens[i + 1].kind {
let line = if token.span.line > 0 {
token.span.line as u32 - 1
} else {
0
};
let col = if token.span.column > 0 {
token.span.column as u32 - 1
} else {
0
};
let name_token = &tokens[i + 1];
let name_col = if name_token.span.column > 0 {
name_token.span.column as u32 - 1
} else {
0
};
let name_end = name_col + (name_token.span.end - name_token.span.start) as u32;
let range = Range::single_line(line, col, name_end);
let selection_range = Range::single_line(line, name_col, name_end);
result.symbols.push(DocumentSymbol {
name: name.clone(),
detail: Some(format!("{} {}", decl_keyword, name)),
kind: sym_kind,
range: range.clone(),
selection_range,
children: Vec::new(),
});
result.definitions.push(DefinitionInfo {
name: name.clone(),
kind: sym_kind,
range,
ty: None,
doc: None,
});
}
}
i += 1;
}
}
pub fn find_references_in_document(uri: &str, content: &str, name: &str) -> Vec<Location> {
let mut locations = Vec::new();
let mut lexer = Lexer::new(content);
let tokens = lexer.tokenize();
for token in &tokens {
if let TokenKind::Ident(ident) = &token.kind {
if ident == name {
let line = if token.span.line > 0 {
token.span.line as u32 - 1
} else {
0
};
let col = if token.span.column > 0 {
token.span.column as u32 - 1
} else {
0
};
let end_col = col + (token.span.end - token.span.start) as u32;
locations.push(Location::new(uri, Range::single_line(line, col, end_col)));
}
}
}
locations
}
pub fn elaborate_document(
content: &str,
) -> (Vec<(String, String)>, Vec<InfoTree>, Vec<Diagnostic>) {
let trimmed = content.trim();
if trimmed.is_empty() {
return (Vec::new(), Vec::new(), Vec::new());
}
let mut decls: Vec<(String, String)> = Vec::new();
let mut info_trees: Vec<InfoTree> = Vec::new();
let mut diagnostics: Vec<Diagnostic> = Vec::new();
let mut env = Environment::new();
let mut lexer = Lexer::new(content);
let tokens = lexer.tokenize();
let mut parser = Parser::new(tokens);
loop {
if parser.is_eof() {
break;
}
let surface_decl = match parser.parse_decl() {
Ok(located) => located,
Err(e) => {
let msg = e.to_string();
if msg.contains("end of file") || msg.contains("EOF") {
break;
}
diagnostics.push(Diagnostic::new(
Range::single_line(0, 0, 0),
DiagnosticSeverity::Error,
format!("parse error: {}", msg),
));
parser.advance();
continue;
}
};
let inner_decl = &surface_decl.value;
let pending = match elab_decl_impl(&env, inner_decl) {
Ok(p) => p,
Err(e) => {
diagnostics.push(Diagnostic::new(
Range::single_line(0, 0, 0),
DiagnosticSeverity::Error,
format!("elaboration error: {}", e),
));
continue;
}
};
let (decl_name_str, decl_ty_str) = match &pending {
PendingDecl::Definition { name, ty, .. } => (format!("{}", name), format!("{}", ty)),
PendingDecl::Theorem { name, ty, .. } => (format!("{}", name), format!("{}", ty)),
PendingDecl::Axiom { name, ty, .. } => (format!("{}", name), format!("{}", ty)),
PendingDecl::Inductive { name, ty, .. } => (format!("{}", name), format!("{}", ty)),
PendingDecl::Opaque { name, ty, .. } => (format!("{}", name), format!("{}", ty)),
};
let kernel_decl = pending_to_declaration(pending);
match check_declaration(&mut env, kernel_decl) {
Ok(()) => {
decls.push((decl_name_str.clone(), decl_ty_str.clone()));
let mut builder = InfoTreeBuilder::new();
let name_kernel = Name::str(&decl_name_str);
builder.push_command_info(0, content.len(), decl_name_str, Some(name_kernel));
let trees = builder.finish();
info_trees.extend(trees);
}
Err(e) => {
diagnostics.push(Diagnostic::new(
Range::single_line(0, 0, 0),
DiagnosticSeverity::Error,
format!("type error in '{}': {}", decl_name_str, e),
));
}
}
}
(decls, info_trees, diagnostics)
}
fn pending_to_declaration(pending: PendingDecl) -> Declaration {
match pending {
PendingDecl::Definition { name, ty, val, .. } => Declaration::Definition {
name,
univ_params: vec![],
ty,
val,
hint: ReducibilityHint::Regular(0),
},
PendingDecl::Theorem {
name, ty, proof, ..
} => Declaration::Theorem {
name,
univ_params: vec![],
ty,
val: proof,
},
PendingDecl::Axiom { name, ty, .. } => Declaration::Axiom {
name,
univ_params: vec![],
ty,
},
PendingDecl::Inductive { name, ty, .. } => Declaration::Axiom {
name,
univ_params: vec![],
ty,
},
PendingDecl::Opaque { name, ty, val } => Declaration::Opaque {
name,
univ_params: vec![],
ty,
val,
},
}
}
#[derive(Debug, Default)]
pub struct AnalysisCache {
results: HashMap<String, (i64, AnalysisResult)>,
}
impl AnalysisCache {
pub fn new() -> Self {
Self {
results: HashMap::new(),
}
}
pub fn get(&self, uri: &str, version: i64) -> Option<&AnalysisResult> {
self.results
.get(uri)
.filter(|(v, _)| *v == version)
.map(|(_, r)| r)
}
pub fn store(&mut self, uri: impl Into<String>, version: i64, result: AnalysisResult) {
self.results.insert(uri.into(), (version, result));
}
pub fn invalidate(&mut self, uri: &str) {
self.results.remove(uri);
}
pub fn clear(&mut self) {
self.results.clear();
}
}
pub fn get_keyword_hover(word: &str) -> Option<String> {
let info = match word {
"def" | "definition" => {
"**def** -- Define a new function or value.\n\n```lean\ndef name : Type := value\n```"
}
"theorem" | "lemma" => {
"**theorem** -- State and prove a proposition.\n\n```lean\ntheorem name : Prop := proof\n```"
}
"axiom" => {
"**axiom** -- Postulate a type without proof.\n\n```lean\naxiom name : Type\n```"
}
"inductive" => {
"**inductive** -- Define an inductive type.\n\n```lean\ninductive Name where\n | ctor : Name\n```"
}
"structure" => "**structure** -- Define a record type with named fields.",
"class" => "**class** -- Define a type class for ad-hoc polymorphism.",
"instance" => "**instance** -- Provide a type class instance.",
"fun" => "**fun** -- Lambda abstraction.\n\n```lean\nfun x => x + 1\n```",
"forall" => "**forall** -- Universal quantification / dependent function type.",
"match" => "**match** -- Pattern matching.\n\n```lean\nmatch x with\n | pattern => result\n```",
"let" => "**let** -- Local binding.\n\n```lean\nlet x := value\nin body\n```",
"if" => "**if** -- Conditional expression.\n\n```lean\nif cond then a else b\n```",
"do" => "**do** -- Do-notation for monadic code.",
"by" => "**by** -- Enter tactic mode to construct a proof.",
"sorry" => "**sorry** -- Placeholder for incomplete proofs (axiom).",
"Prop" => "**Prop** -- The type of propositions (`Sort 0`).",
"Type" => "**Type** -- The type of types (`Sort 1`).",
"Sort" => "**Sort** -- The type of a universe level.",
"where" => "**where** -- Introduce local definitions after a declaration.",
"have" => "**have** -- Introduce a local hypothesis.",
"show" => "**show** -- Annotate the expected type of an expression.",
"namespace" => "**namespace** -- Open a namespace for declarations.",
"section" => "**section** -- Begin a section for local variables.",
"open" => "**open** -- Open a namespace to use its names unqualified.",
"import" => "**import** -- Import definitions from another module.",
_ => return None,
};
Some(info.to_string())
}
pub fn format_document(content: &str) -> Vec<TextEdit> {
let mut edits = Vec::new();
let lines: Vec<&str> = content.lines().collect();
for (i, line) in lines.iter().enumerate() {
let trimmed_end = line.trim_end();
if trimmed_end.len() != line.len() {
edits.push(TextEdit::new(
Range::single_line(i as u32, trimmed_end.len() as u32, line.len() as u32),
"",
));
}
}
if !content.is_empty() && !content.ends_with('\n') {
let last_line = lines.len().saturating_sub(1);
let last_col = lines.last().map_or(0, |l| l.len());
edits.push(TextEdit::new(
Range::single_line(last_line as u32, last_col as u32, last_col as u32),
"\n",
));
}
edits
}
pub fn make_code_action(
title: &str,
_uri: &str,
edits: Vec<TextEdit>,
) -> super::json_rpc::JsonValue {
use super::json_rpc::JsonValue;
let mut entries = vec![
("title".to_string(), JsonValue::String(title.to_string())),
(
"kind".to_string(),
JsonValue::String("quickfix".to_string()),
),
];
if !edits.is_empty() {
entries.push((
"edit".to_string(),
JsonValue::Object(vec![(
"changes".to_string(),
JsonValue::Array(edits.iter().map(|e| e.to_json()).collect()),
)]),
));
}
JsonValue::Object(entries)
}
#[cfg(test)]
mod incremental_relex_tests {
use super::*;
use oxilean_parse::Lexer;
fn make_doc(uri: &str, content: &str) -> Document {
let mut doc = Document::new(uri, 1, content);
let tokens = Lexer::new(content).tokenize();
doc.cached_tokens = tokens;
doc
}
fn kind_names(tokens: &[oxilean_parse::tokens::Token]) -> Vec<String> {
tokens
.iter()
.map(|t| match &t.kind {
TokenKind::Ident(_) => "Ident".into(),
TokenKind::Nat(_) => "Nat".into(),
TokenKind::String(_) => "String".into(),
TokenKind::DocComment(_) => "DocComment".into(),
TokenKind::Error(_) => "Error".into(),
TokenKind::Eof => "Eof".into(),
other => format!("{:?}", other),
})
.collect()
}
#[test]
fn test_incremental_relex_single_token() {
let source = "def foo := 42";
let mut doc = make_doc("file:///t.lean", source);
let change = TextChange::new(4, 7, "bar");
doc.content = change.apply(&doc.content);
doc.line_offsets = super::super::document::compute_line_offsets(&doc.content);
let env = Environment::new();
let _ = analyze_document_incremental(&mut doc, &change, &env);
assert!(
doc.content.contains("bar"),
"document content should contain 'bar', got: {:?}",
doc.content
);
let ident_names: Vec<String> = doc
.cached_tokens
.iter()
.filter_map(|t| {
if let TokenKind::Ident(n) = &t.kind {
Some(n.clone())
} else {
None
}
})
.collect();
assert!(
ident_names.contains(&"bar".to_string()),
"cached_tokens should contain identifier 'bar', got: {:?}",
ident_names
);
assert!(
!ident_names.contains(&"foo".to_string()),
"cached_tokens should NOT contain old identifier 'foo' after edit"
);
}
#[test]
fn test_incremental_relex_equals_full() {
let source = "theorem myThm : True := trivial";
let mut doc = make_doc("file:///eq.lean", source);
let change = TextChange::new(8, 13, "renamed");
doc.content = change.apply(&doc.content);
doc.line_offsets = super::super::document::compute_line_offsets(&doc.content);
let env = Environment::new();
let _ = analyze_document_incremental(&mut doc, &change, &env);
let full_tokens = Lexer::new(&doc.content).tokenize();
let incr_kinds = kind_names(&doc.cached_tokens);
let full_kinds = kind_names(&full_tokens);
assert_eq!(
incr_kinds, full_kinds,
"incremental re-lex kind sequence must match full re-lex\n incremental: {:?}\n full: {:?}",
incr_kinds, full_kinds
);
}
#[test]
fn test_incremental_relex_fallback_when_cache_empty() {
let source = "def x := 1";
let mut doc = Document::new("file:///empty.lean", 1, source);
assert!(doc.cached_tokens.is_empty());
let change = TextChange::new(9, 10, "2");
doc.content = change.apply(&doc.content);
doc.line_offsets = super::super::document::compute_line_offsets(&doc.content);
let env = Environment::new();
let _ = analyze_document_incremental(&mut doc, &change, &env);
assert!(
!doc.cached_tokens.is_empty(),
"cached_tokens should be populated after fallback full re-lex"
);
}
#[test]
fn test_analyze_and_cache_updates_tokens() {
let source = "namespace Foo\ndef bar := 0\nend Foo";
let mut doc = Document::new("file:///ns.lean", 1, source);
assert!(doc.cached_tokens.is_empty());
let env = Environment::new();
let _ = analyze_document_and_cache(&mut doc, &env);
assert!(
!doc.cached_tokens.is_empty(),
"analyze_document_and_cache must populate cached_tokens"
);
let has_namespace = doc
.cached_tokens
.iter()
.any(|t| t.kind == TokenKind::Namespace);
assert!(
has_namespace,
"cached tokens should include a Namespace token"
);
}
}
#[cfg(test)]
mod incremental_decls_tests {
use super::*;
const SRC_3DECLS: &str =
"theorem a : True := True.intro\ntheorem b : True := True.intro\ntheorem c : True := True.intro";
const SRC_B_MODIFIED: &str =
"theorem a : True := True.intro\ntheorem b : Nat := True.intro\ntheorem c : True := True.intro";
const SRC_APPENDED: &str =
"theorem a : True := True.intro\ntheorem b : True := True.intro\ntheorem c : True := True.intro\ntheorem d : True := True.intro";
fn make_doc(content: &str) -> Document {
Document::new("test://test", 1, content)
}
#[test]
fn test_parse_decls_count() {
let decls = parse_decls_from_source(SRC_3DECLS);
assert_eq!(
decls.len(),
3,
"expected 3 declarations, got {}",
decls.len()
);
}
#[test]
fn test_parse_decls_empty_source() {
let decls = parse_decls_from_source("");
assert!(
decls.is_empty(),
"empty source should yield zero declarations"
);
}
#[test]
fn test_parse_decls_appended() {
let decls = parse_decls_from_source(SRC_APPENDED);
assert_eq!(
decls.len(),
4,
"expected 4 declarations after append, got {}",
decls.len()
);
}
#[test]
fn test_cached_decls_empty_on_new_doc() {
let doc = make_doc(SRC_3DECLS);
assert!(
doc.cached_decls.is_empty(),
"fresh document should have empty cached_decls"
);
}
#[test]
fn test_cached_decls_populated_after_analysis() {
let mut doc = make_doc(SRC_3DECLS);
let env = Environment::new();
let _ = analyze_document_incremental_decls(&mut doc, &env);
assert_eq!(
doc.cached_decls.len(),
3,
"cached_decls should have 3 entries after incremental analysis"
);
}
#[test]
fn test_cached_decls_populated_by_and_cache() {
let mut doc = make_doc(SRC_3DECLS);
let env = Environment::new();
let _ = analyze_document_and_cache(&mut doc, &env);
assert_eq!(
doc.cached_decls.len(),
3,
"analyze_document_and_cache must populate cached_decls"
);
}
#[test]
fn test_cached_decls_cleared_on_full_replace() {
let mut doc = make_doc(SRC_3DECLS);
doc.cached_decls = parse_decls_from_source(SRC_3DECLS);
assert_eq!(doc.cached_decls.len(), 3);
doc.update(2, SRC_APPENDED);
assert!(
doc.cached_decls.is_empty(),
"cached_decls must be empty after a full-replace update()"
);
}
#[test]
fn test_first_change_idx_on_modify() {
let old_decls = parse_decls_from_source(SRC_3DECLS);
let new_decls = parse_decls_from_source(SRC_B_MODIFIED);
let edits = oxilean_parse::incremental::diff_modules(&old_decls, &new_decls);
let first_modified_new_idx = edits
.iter()
.filter_map(|e| {
use oxilean_parse::incremental::EditKind;
match e.kind {
EditKind::Modified | EditKind::Inserted | EditKind::Deleted => e.new_idx,
EditKind::Unchanged => None,
}
})
.min();
assert!(
first_modified_new_idx.is_some(),
"expected at least one non-Unchanged edit"
);
assert!(
first_modified_new_idx.unwrap() >= 1,
"first change should be at decl index >= 1 (decl 'a' is unchanged)"
);
}
#[test]
fn test_appended_decl_incremental_diff() {
let old_decls = parse_decls_from_source(SRC_3DECLS);
let new_decls = parse_decls_from_source(SRC_APPENDED);
let edits = oxilean_parse::incremental::diff_modules(&old_decls, &new_decls);
use oxilean_parse::incremental::EditKind;
let unchanged_count = edits
.iter()
.filter(|e| e.kind == EditKind::Unchanged)
.count();
let inserted_count = edits
.iter()
.filter(|e| e.kind == EditKind::Inserted)
.count();
assert_eq!(unchanged_count, 3, "expected 3 Unchanged edits (a, b, c)");
assert_eq!(inserted_count, 1, "expected 1 Inserted edit (d)");
}
#[test]
fn test_identical_source_no_change() {
let old_decls = parse_decls_from_source(SRC_3DECLS);
let new_decls = parse_decls_from_source(SRC_3DECLS);
let edits = oxilean_parse::incremental::diff_modules(&old_decls, &new_decls);
use oxilean_parse::incremental::EditKind;
assert!(
edits.iter().all(|e| e.kind == EditKind::Unchanged),
"identical sources must produce only Unchanged edits; got: {edits:?}"
);
}
#[test]
fn test_incremental_decls_second_pass_updates_cache() {
let mut doc = make_doc(SRC_3DECLS);
let env = Environment::new();
let _ = analyze_document_incremental_decls(&mut doc, &env);
assert_eq!(doc.cached_decls.len(), 3);
let _ = analyze_document_incremental_decls(&mut doc, &env);
assert_eq!(
doc.cached_decls.len(),
3,
"cached_decls should remain 3 on a no-op second pass"
);
}
#[test]
fn test_incremental_decls_append_updates_cache() {
let mut doc = make_doc(SRC_3DECLS);
let env = Environment::new();
let _ = analyze_document_incremental_decls(&mut doc, &env);
assert_eq!(doc.cached_decls.len(), 3);
doc.content = SRC_APPENDED.to_string();
doc.line_offsets = super::super::document::compute_line_offsets(&doc.content);
let _ = analyze_document_incremental_decls(&mut doc, &env);
assert_eq!(
doc.cached_decls.len(),
4,
"cached_decls should be 4 after appending a declaration"
);
}
}