use crate::lsp::{
analyze_document, Document, DocumentStore, Hover, Location, MarkupContent, Position, Range,
SymbolKind,
};
use oxilean_kernel::{Environment, Name};
use oxilean_parse::{Lexer, TokenKind};
use super::functions::*;
use std::collections::{HashMap, VecDeque};
#[allow(dead_code)]
#[derive(Clone, Debug)]
pub struct HoverEvent {
pub uri: String,
pub line: u32,
pub character: u32,
pub word: Option<String>,
pub hit: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum CompletionKind {
Keyword,
Function,
Type,
Tactic,
Snippet,
}
pub struct OutlineProvider<'a> {
env: &'a Environment,
}
impl<'a> OutlineProvider<'a> {
pub fn new(env: &'a Environment) -> Self {
Self { env }
}
pub fn get_outline(&self, doc: &Document) -> Vec<OutlineSymbol> {
let analysis = analyze_document(&doc.uri, &doc.content, self.env);
analysis
.definitions
.iter()
.map(|def| OutlineSymbol {
name: def.name.clone(),
kind: def.kind,
range: def.range.clone(),
ty: def.ty.clone(),
children: Vec::new(),
})
.collect()
}
pub fn find_symbol(&self, doc: &Document, name: &str) -> Option<OutlineSymbol> {
self.get_outline(doc)
.into_iter()
.find(|sym| sym.name == name)
}
pub fn count_of_kind(&self, doc: &Document, kind: &SymbolKind) -> usize {
self.get_outline(doc)
.iter()
.filter(|sym| &sym.kind == kind)
.count()
}
}
#[allow(dead_code)]
pub struct HoverRequestHandler<'a> {
hover: HoverProvider<'a>,
signature: SignatureHelpProvider<'a>,
outline: OutlineProvider<'a>,
env: &'a oxilean_kernel::Environment,
cache: HoverCache,
}
#[allow(dead_code)]
impl<'a> HoverRequestHandler<'a> {
pub fn new(env: &'a oxilean_kernel::Environment) -> Self {
Self {
hover: HoverProvider::new(env),
signature: SignatureHelpProvider::new(env),
outline: OutlineProvider::new(env),
env,
cache: HoverCache::new(256),
}
}
pub fn handle(&mut self, doc: &Document, pos: &Position) -> Option<Hover> {
if let Some(cached) = self.cache.get(&doc.uri, pos.line, pos.character) {
return cached.as_ref().map(|r| self.hover.to_lsp_hover(r));
}
let result = self.hover.hover_at_position(doc, pos);
let lsp_hover = result.as_ref().map(|r| self.hover.to_lsp_hover(r));
self.cache.insert(&doc.uri, pos.line, pos.character, result);
lsp_hover
}
pub fn on_document_change(&mut self, uri: &str) {
self.cache.invalidate(uri);
}
pub fn clear_cache(&mut self) {
self.cache.clear();
}
pub fn cache_stats(&self) -> (usize, usize) {
(self.cache.len(), 256)
}
}
#[allow(dead_code)]
#[derive(Clone, Debug)]
pub struct ProofGoal {
pub hypotheses: Vec<(String, String)>,
pub goal: String,
pub label: Option<String>,
}
#[allow(dead_code)]
impl ProofGoal {
pub fn new(goal: impl Into<String>) -> Self {
Self {
hypotheses: Vec::new(),
goal: goal.into(),
label: None,
}
}
pub fn add_hyp(&mut self, name: impl Into<String>, ty: impl Into<String>) {
self.hypotheses.push((name.into(), ty.into()));
}
pub fn format_hover(&self) -> String {
let mut result = String::from("**Proof State**\n\n```lean\n");
for (name, ty) in &self.hypotheses {
result.push_str(&format!("{} : {}\n", name, ty));
}
result.push_str("⊢ ");
result.push_str(&self.goal);
result.push_str("\n```");
if let Some(label) = &self.label {
result.push_str(&format!("\n\nGoal: *{}*", label));
}
result
}
pub fn is_closed(&self) -> bool {
self.goal == "True" || self.goal.is_empty()
}
pub fn hyp_count(&self) -> usize {
self.hypotheses.len()
}
}
#[allow(dead_code)]
pub struct DocumentationGenerator;
#[allow(dead_code)]
impl DocumentationGenerator {
pub fn new() -> Self {
Self
}
pub fn generate_decl_doc(
&self,
kind: &str,
name: &str,
ty: &str,
doc_comment: Option<&str>,
) -> String {
let mut result = format!("```lean\n{} {} : {}\n```\n", kind, name, ty);
if let Some(doc) = doc_comment {
result.push('\n');
result.push_str(doc);
}
result
}
pub fn generate_keyword_brief(&self, kw: &str) -> Option<String> {
let brief =
KEYWORD_BRIEFS.iter().find_map(
|&(k, b)| {
if k == kw {
Some(b.to_string())
} else {
None
}
},
);
brief
}
pub fn generate_tactic_doc(
&self,
name: &str,
description: &str,
example: Option<&str>,
) -> String {
let mut doc = format!("**Tactic** `{}`\n\n{}", name, description);
if let Some(ex) = example {
doc.push_str(&format!("\n\n**Example:**\n```lean\n{}\n```", ex));
}
doc
}
pub fn format_type_alternatives(&self, alternatives: &[&str]) -> String {
if alternatives.is_empty() {
return String::new();
}
format!(
"One of:\n{}",
alternatives
.iter()
.map(|a| format!("- `{}`", a))
.collect::<Vec<_>>()
.join("\n")
)
}
}
#[allow(dead_code)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum HoverMergeStrategy {
FirstWins,
ConcatenateAll,
LongestWins,
}
#[allow(dead_code)]
pub struct HoverHistory {
events: std::collections::VecDeque<HoverEvent>,
max_size: usize,
}
#[allow(dead_code)]
impl HoverHistory {
pub fn new(max_size: usize) -> Self {
Self {
events: std::collections::VecDeque::new(),
max_size,
}
}
pub fn record(&mut self, event: HoverEvent) {
if self.events.len() >= self.max_size {
self.events.pop_front();
}
self.events.push_back(event);
}
pub fn hit_rate(&self) -> f64 {
if self.events.is_empty() {
return 0.0;
}
let hits = self.events.iter().filter(|e| e.hit).count();
hits as f64 / self.events.len() as f64
}
pub fn event_count(&self) -> usize {
self.events.len()
}
pub fn recent_events(&self) -> &std::collections::VecDeque<HoverEvent> {
&self.events
}
pub fn clear(&mut self) {
self.events.clear();
}
}
#[allow(dead_code)]
#[derive(Clone, Debug, Default)]
pub struct ProofState {
pub goals: Vec<ProofGoal>,
}
#[allow(dead_code)]
impl ProofState {
pub fn new() -> Self {
Self::default()
}
pub fn add_goal(&mut self, goal: ProofGoal) {
self.goals.push(goal);
}
pub fn format_hover(&self) -> String {
if self.goals.is_empty() {
return "**No goals remaining** ✓".to_string();
}
if self.goals.len() == 1 {
return self.goals[0].format_hover();
}
let mut result = format!("**{} Goals**\n\n", self.goals.len());
for (i, goal) in self.goals.iter().enumerate() {
result.push_str(&format!("**Goal {}:**\n", i + 1));
result.push_str(&goal.format_hover());
result.push('\n');
}
result
}
pub fn goal_count(&self) -> usize {
self.goals.len()
}
pub fn all_closed(&self) -> bool {
self.goals.iter().all(|g| g.is_closed())
}
}
#[allow(dead_code)]
pub struct NamespaceHoverProvider<'a> {
env: &'a oxilean_kernel::Environment,
}
#[allow(dead_code)]
impl<'a> NamespaceHoverProvider<'a> {
pub fn new(env: &'a oxilean_kernel::Environment) -> Self {
Self { env }
}
pub fn hover_qualified_name(&self, qualified: &str) -> Option<String> {
let name = oxilean_kernel::Name::str(qualified);
if let Some(ci) = self.env.find(&name) {
let ty_str = format!("{:?}", ci.ty());
let kind = if ci.is_inductive() {
"inductive"
} else if ci.is_constructor() {
"constructor"
} else if ci.is_axiom() {
"axiom"
} else if ci.is_theorem() {
"theorem"
} else {
"definition"
};
return Some(format_declaration_hover(kind, qualified, &ty_str));
}
let namespace_prefix = format!("{}.", qualified);
let mut items: Vec<String> = self
.env
.constant_names()
.filter(|n| n.to_string().starts_with(&namespace_prefix))
.map(|n| n.to_string())
.take(10)
.collect();
items.sort();
if !items.is_empty() {
let list = items
.iter()
.map(|i| format!("- `{}`", i))
.collect::<Vec<_>>()
.join("\n");
return Some(format!(
"**Namespace** `{}`\n\nContains:\n{}",
qualified, list
));
}
None
}
pub fn count_in_namespace(&self, prefix: &str) -> usize {
let ns_prefix = format!("{}.", prefix);
self.env
.constant_names()
.filter(|n| n.to_string().starts_with(&ns_prefix))
.count()
}
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct HoverSection {
pub kind: HoverSectionKind,
pub content: String,
}
#[allow(dead_code)]
impl HoverSection {
pub fn new(kind: HoverSectionKind, content: &str) -> Self {
Self {
kind,
content: content.to_string(),
}
}
pub fn to_markdown(&self) -> String {
match &self.kind {
HoverSectionKind::TypeSignature => format!("```lean\n{}\n```", self.content),
HoverSectionKind::Documentation => self.content.clone(),
HoverSectionKind::Examples => {
format!("**Examples:**\n```lean\n{}\n```", self.content)
}
HoverSectionKind::SeeAlso => format!("**See also:** {}", self.content),
HoverSectionKind::Source => format!("*Source: {}*", self.content),
}
}
}
#[allow(dead_code)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HoverSectionKind {
TypeSignature,
Documentation,
Examples,
SeeAlso,
Source,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct RichHoverContent {
pub sections: Vec<HoverSection>,
}
#[allow(dead_code)]
impl RichHoverContent {
pub fn new() -> Self {
Self {
sections: Vec::new(),
}
}
pub fn add_section(&mut self, section: HoverSection) {
self.sections.push(section);
}
pub fn add_type_sig(&mut self, sig: &str) {
self.sections
.push(HoverSection::new(HoverSectionKind::TypeSignature, sig));
}
pub fn add_doc(&mut self, doc: &str) {
self.sections
.push(HoverSection::new(HoverSectionKind::Documentation, doc));
}
pub fn to_markdown(&self) -> String {
self.sections
.iter()
.map(|s| s.to_markdown())
.collect::<Vec<_>>()
.join("\n\n---\n\n")
}
pub fn is_empty(&self) -> bool {
self.sections.is_empty()
}
pub fn section_count(&self) -> usize {
self.sections.len()
}
}
#[derive(Clone, Debug)]
pub struct SignatureInfo {
pub label: String,
pub documentation: Option<String>,
pub parameters: Vec<ParameterInfo>,
pub active_parameter: Option<usize>,
}
#[derive(Clone, Debug)]
pub struct ReferenceContext {
pub include_declaration: bool,
}
#[derive(Clone, Debug)]
pub struct CompletionItem {
pub label: String,
pub detail: Option<String>,
pub documentation: Option<String>,
pub kind: CompletionKind,
pub insert_text: Option<String>,
}
pub struct SignatureHelpProvider<'a> {
env: &'a Environment,
}
impl<'a> SignatureHelpProvider<'a> {
pub fn new(env: &'a Environment) -> Self {
Self { env }
}
pub fn signature_help(&self, doc: &Document, pos: &Position) -> Option<SignatureInfo> {
let (word, _) = doc.word_at_position(pos)?;
let name = Name::str(&word);
if let Some(ci) = self.env.find(&name) {
let ty_str = format!("{:?}", ci.ty());
Some(SignatureInfo {
label: format!("{} : {}", word, ty_str),
documentation: None,
parameters: self.extract_parameters_from_str(&ty_str),
active_parameter: None,
})
} else {
None
}
}
fn extract_parameters_from_str(&self, ty_str: &str) -> Vec<ParameterInfo> {
let parts = split_arrow_top(ty_str);
if parts.len() <= 1 {
return Vec::new();
}
parts[..parts.len() - 1]
.iter()
.enumerate()
.map(|(i, ty_segment)| {
let label = extract_param_label(i, ty_segment.trim());
ParameterInfo {
label,
documentation: None,
}
})
.collect()
}
pub fn param_count(&self, name: &str) -> Option<usize> {
let n = Name::str(name);
let ci = self.env.find(&n)?;
let ty_str = format!("{:?}", ci.ty());
Some(ty_str.matches("->").count())
}
}
pub struct HoverProvider<'a> {
env: &'a Environment,
}
impl<'a> HoverProvider<'a> {
pub fn new(env: &'a Environment) -> Self {
Self { env }
}
pub fn hover_at_position(&self, doc: &Document, pos: &Position) -> Option<HoverResult> {
let (word, range) = doc.word_at_position(pos)?;
if let Some(info) = self.hover_keyword(&word) {
return Some(HoverResult {
content: info,
range,
});
}
if let Some(info) = self.hover_declaration(&word) {
return Some(HoverResult {
content: info,
range,
});
}
if let Some(info) = self.hover_literal(&word) {
return Some(HoverResult {
content: info,
range,
});
}
if let Some(info) = self.hover_tactic(&word) {
return Some(HoverResult {
content: info,
range,
});
}
if let Some(info) = self.hover_local_definition(doc, &word) {
return Some(HoverResult {
content: info,
range,
});
}
None
}
pub fn to_lsp_hover(&self, result: &HoverResult) -> Hover {
Hover::new(
MarkupContent::markdown(&result.content),
Some(result.range.clone()),
)
}
pub fn hover_keyword(&self, word: &str) -> Option<String> {
KEYWORD_DOCS.iter().find_map(|&(kw, doc)| {
if kw == word {
Some(doc.to_string())
} else {
None
}
})
}
pub fn hover_declaration(&self, word: &str) -> Option<String> {
let name = Name::str(word);
let ci = self.env.find(&name)?;
let ty_str = format!("{:?}", ci.ty());
let kind = if ci.is_inductive() {
"inductive"
} else if ci.is_constructor() {
"constructor"
} else if ci.is_axiom() {
"axiom"
} else if ci.is_theorem() {
"theorem"
} else {
"definition"
};
Some(format_declaration_hover(kind, word, &ty_str))
}
pub fn hover_literal(&self, word: &str) -> Option<String> {
if word.chars().all(|c| c.is_ascii_digit()) {
let val: u64 = word.parse().ok()?;
return Some(format!(
"```lean\n{} : Nat\n```\nNatural number literal (= {})",
word, val
));
}
if word.starts_with('-') && word[1..].chars().all(|c| c.is_ascii_digit()) {
let val: i64 = word.parse().ok()?;
return Some(format!(
"```lean\n{} : Int\n```\nInteger literal (= {})",
word, val
));
}
None
}
pub fn hover_tactic(&self, word: &str) -> Option<String> {
TACTIC_DOCS.iter().find_map(|&(name, doc)| {
if name == word {
Some(format!("**Tactic** `{}`\n\n{}", name, doc))
} else {
None
}
})
}
fn hover_local_definition(&self, doc: &Document, word: &str) -> Option<String> {
let analysis = analyze_document(&doc.uri, &doc.content, self.env);
for def in &analysis.definitions {
if def.name == word {
let kind_str = match def.kind {
SymbolKind::Function => "def",
SymbolKind::Method => "theorem",
SymbolKind::Constant => "axiom",
SymbolKind::Enum => "inductive",
SymbolKind::Struct => "structure",
SymbolKind::Class => "class",
_ => "declaration",
};
let ty_info = def
.ty
.as_deref()
.map(|t| format!(" : {}", t))
.unwrap_or_default();
return Some(format!("```lean\n{} {}{}\n```", kind_str, word, ty_info));
}
}
None
}
}
#[derive(Clone, Debug)]
pub struct ParameterInfo {
pub label: String,
pub documentation: Option<String>,
}
#[derive(Clone, Debug)]
pub struct OutlineSymbol {
pub name: String,
pub kind: SymbolKind,
pub range: Range,
pub ty: Option<String>,
pub children: Vec<OutlineSymbol>,
}
pub struct DefinitionProvider<'a> {
env: &'a Environment,
}
impl<'a> DefinitionProvider<'a> {
pub fn new(env: &'a Environment) -> Self {
Self { env }
}
pub fn goto_definition(&self, doc: &Document, pos: &Position) -> Option<Location> {
let (word, _) = doc.word_at_position(pos)?;
if let Some(loc) = self.find_definition_in_source(doc, &word) {
return Some(loc);
}
if let Some(loc) = self.find_definition_in_env(&word, &doc.uri) {
return Some(loc);
}
None
}
pub fn find_definition_in_env(&self, word: &str, uri: &str) -> Option<Location> {
let name = Name::str(word);
if self.env.find(&name).is_some() {
Some(Location::new(
uri,
Range::single_line(0, 0, word.len() as u32),
))
} else {
None
}
}
pub fn find_definition_in_source(&self, doc: &Document, word: &str) -> Option<Location> {
let analysis = analyze_document(&doc.uri, &doc.content, self.env);
for def in &analysis.definitions {
if def.name == word {
return Some(Location::new(&doc.uri, def.range.clone()));
}
}
None
}
pub fn find_definition_in_imports(
&self,
store: &DocumentStore,
word: &str,
_current_uri: &str,
) -> Option<Location> {
for uri in store.uris() {
if let Some(doc) = store.get_document(uri) {
let analysis = analyze_document(uri, &doc.content, self.env);
for def in &analysis.definitions {
if def.name == word {
return Some(Location::new(uri.as_str(), def.range.clone()));
}
}
}
}
None
}
}
pub struct ReferenceProvider<'a> {
env: &'a Environment,
}
impl<'a> ReferenceProvider<'a> {
pub fn new(env: &'a Environment) -> Self {
Self { env }
}
pub fn find_references(
&self,
doc: &Document,
pos: &Position,
context: &ReferenceContext,
) -> Vec<Location> {
let (word, _) = match doc.word_at_position(pos) {
Some(w) => w,
None => return Vec::new(),
};
let mut locations = self.find_references_in_document(doc, &word);
if !context.include_declaration {
let analysis = analyze_document(&doc.uri, &doc.content, self.env);
for def in &analysis.definitions {
if def.name == word {
locations.retain(|loc| loc.range != def.range);
}
}
}
locations
}
pub fn find_references_in_document(&self, doc: &Document, name: &str) -> Vec<Location> {
let mut locations = Vec::new();
let mut lexer = Lexer::new(&doc.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(
&doc.uri,
Range::single_line(line, col, end_col),
));
}
}
}
locations
}
pub fn find_references_in_workspace(&self, store: &DocumentStore, name: &str) -> Vec<Location> {
let mut all_locations = Vec::new();
for uri in store.uris() {
if let Some(doc) = store.get_document(uri) {
all_locations.extend(self.find_references_in_document(doc, name));
}
}
all_locations
}
}
#[derive(Clone, Debug)]
pub struct HoverResult {
pub content: String,
pub range: Range,
}
pub struct CompletionProvider<'a> {
env: &'a Environment,
}
impl<'a> CompletionProvider<'a> {
pub fn new(env: &'a Environment) -> Self {
Self { env }
}
pub fn completions(&self, doc: &Document, pos: &Position) -> Vec<CompletionItem> {
let prefix = self.get_prefix_at(doc, pos);
let mut items = Vec::new();
for &(kw, _kw_doc) in KEYWORD_DOCS {
if kw.starts_with(&prefix) {
items.push(CompletionItem {
label: kw.to_string(),
detail: Some("keyword".to_string()),
documentation: None,
kind: CompletionKind::Keyword,
insert_text: None,
});
}
}
for &(name, _tac_doc) in TACTIC_DOCS {
if name.starts_with(&prefix) {
items.push(CompletionItem {
label: name.to_string(),
detail: Some("tactic".to_string()),
documentation: None,
kind: CompletionKind::Tactic,
insert_text: None,
});
}
}
for env_name in self.env.constant_names() {
let name_str = format!("{}", env_name);
if name_str.starts_with(&prefix) {
items.push(CompletionItem {
label: name_str,
detail: Some("definition".to_string()),
documentation: None,
kind: CompletionKind::Function,
insert_text: None,
});
}
}
items
}
pub fn completions_of_kind(
&self,
doc: &Document,
pos: &Position,
kind: CompletionKind,
) -> Vec<CompletionItem> {
self.completions(doc, pos)
.into_iter()
.filter(|item| item.kind == kind)
.collect()
}
fn get_prefix_at(&self, doc: &Document, pos: &Position) -> String {
doc.word_at_position(pos)
.map(|(w, _)| w)
.unwrap_or_default()
}
}
#[allow(dead_code)]
pub struct HoverCache {
entries: std::collections::HashMap<(String, u32, u32), Option<HoverResult>>,
max_entries: usize,
size: usize,
}
#[allow(dead_code)]
impl HoverCache {
pub fn new(max_entries: usize) -> Self {
Self {
entries: std::collections::HashMap::new(),
max_entries,
size: 0,
}
}
pub fn get(&self, uri: &str, line: u32, character: u32) -> Option<&Option<HoverResult>> {
self.entries.get(&(uri.to_string(), line, character))
}
pub fn insert(&mut self, uri: &str, line: u32, character: u32, result: Option<HoverResult>) {
if self.size >= self.max_entries {
self.entries.clear();
self.size = 0;
}
let key = (uri.to_string(), line, character);
if self.entries.insert(key, result).is_none() {
self.size += 1;
}
}
pub fn invalidate(&mut self, uri: &str) {
self.entries.retain(|(u, _, _), _| u != uri);
self.size = self.entries.len();
}
pub fn clear(&mut self) {
self.entries.clear();
self.size = 0;
}
pub fn len(&self) -> usize {
self.size
}
pub fn is_empty(&self) -> bool {
self.size == 0
}
}