use crate::lsp::{
analyze_document, CompletionItem, CompletionItemKind, Document, MarkupContent, Position, Range,
};
use oxilean_kernel::Environment;
use std::collections::HashMap;
use super::functions::*;
#[derive(Clone, Debug)]
pub struct CompletionList {
pub items: Vec<CompletionItem>,
pub is_incomplete: bool,
}
impl CompletionList {
pub fn new(items: Vec<CompletionItem>, is_incomplete: bool) -> Self {
Self {
items,
is_incomplete,
}
}
pub fn empty() -> Self {
Self {
items: Vec::new(),
is_incomplete: false,
}
}
}
#[derive(Clone, Debug)]
pub struct ExpectedTypeInfo {
pub ty: String,
pub confidence: f32,
pub source: TypeExpectationSource,
}
pub struct TacticEntry {
pub name: &'static str,
pub doc: &'static str,
}
#[derive(Clone, Debug)]
pub struct SimpleTextEdit {
pub range: Range,
pub new_text: String,
}
#[derive(Clone, Debug)]
pub struct CompletionPreview {
pub kind_label: String,
pub type_signature: Option<String>,
pub documentation: Option<String>,
pub deprecated: bool,
}
impl CompletionPreview {
pub fn keyword(_name: &str, description: &str) -> Self {
Self {
kind_label: "keyword".to_string(),
type_signature: None,
documentation: Some(description.to_string()),
deprecated: false,
}
}
pub fn tactic(name: &str, description: &str) -> Self {
Self {
kind_label: "tactic".to_string(),
type_signature: None,
documentation: Some(format!("**Tactic** `{}`\n\n{}", name, description)),
deprecated: false,
}
}
pub fn from_env(name: &str, ty: &str) -> Self {
Self {
kind_label: "declaration".to_string(),
type_signature: Some(format!("{} : {}", name, ty)),
documentation: None,
deprecated: false,
}
}
pub fn to_markdown(&self) -> String {
let mut md = format!("*{}*\n\n", self.kind_label);
if let Some(sig) = &self.type_signature {
md.push_str(&format!("```lean\n{}\n```\n\n", sig));
}
if let Some(doc) = &self.documentation {
md.push_str(doc);
}
if self.deprecated {
md.push_str("\n\n*Deprecated*");
}
md
}
}
#[derive(Clone, Debug, Default)]
pub struct CompletionHistory {
entries: Vec<String>,
max_size: usize,
}
impl CompletionHistory {
pub fn new(max_size: usize) -> Self {
Self {
entries: Vec::new(),
max_size,
}
}
pub fn record(&mut self, label: &str) {
self.entries.retain(|e| e != label);
self.entries.insert(0, label.to_string());
if self.entries.len() > self.max_size {
self.entries.truncate(self.max_size);
}
}
pub fn rank(&self, label: &str) -> Option<usize> {
self.entries.iter().position(|e| e == label)
}
pub fn boost_items(&self, items: &mut Vec<CompletionItem>) {
for item in items.iter_mut() {
if let Some(rank) = self.rank(&item.label) {
let boost = format!("{:03}", rank);
item.sort_text = Some(format!("0{}{}", boost, item.label));
}
}
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn clear(&mut self) {
self.entries.clear();
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum CompletionPriority {
Low = 0,
Medium = 50,
High = 100,
Exact = 200,
}
impl CompletionPriority {
pub fn sort_key(self) -> String {
let inverted = 999 - (self as i32);
format!("{:04}", inverted)
}
}
pub struct SmartCompletionProvider<'a> {
env: &'a Environment,
}
impl<'a> SmartCompletionProvider<'a> {
pub fn new(env: &'a Environment) -> Self {
Self { env }
}
pub fn completions_for_type(&self, expected_type: &str, prefix: &str) -> Vec<CompletionItem> {
let mut items = Vec::new();
match expected_type {
"Nat" | "nat" => items.extend(self.static_candidates(
&[
("0", "zero"),
("1", "one"),
("Nat.zero", "zero ctor"),
("Nat.succ", "succ ctor"),
("Nat.add", "addition"),
("Nat.mul", "multiplication"),
],
prefix,
)),
"Bool" | "bool" => items.extend(self.static_candidates(
&[
("true", "Boolean true"),
("false", "Boolean false"),
("Bool.and", "and"),
("Bool.or", "or"),
("Bool.not", "not"),
],
prefix,
)),
"Prop" => items.extend(self.static_candidates(
&[
("True", "trivially true"),
("False", "trivially false"),
("And", "conjunction"),
("Or", "disjunction"),
("Not", "negation"),
],
prefix,
)),
"String" => items.extend(self.static_candidates(
&[
("\"\"", "empty string"),
("String.append", "concatenate"),
("String.length", "length"),
],
prefix,
)),
_ => {
let pattern = format!("{}.", expected_type);
for name in self.env.constant_names() {
let name_str = name.to_string();
if name_str.starts_with(&pattern) && self.env.is_constructor(name) {
let field = &name_str[pattern.len()..];
if prefix.is_empty() || field.starts_with(prefix) {
items.push(CompletionItem {
label: name_str,
kind: CompletionItemKind::Constructor,
detail: Some(format!("constructor of {}", expected_type)),
documentation: None,
insert_text: None,
sort_text: None,
});
}
}
}
}
}
items
}
fn static_candidates(&self, candidates: &[(&str, &str)], prefix: &str) -> Vec<CompletionItem> {
candidates
.iter()
.filter(|&&(name, _)| prefix.is_empty() || name.starts_with(prefix))
.map(|&(name, doc)| CompletionItem {
label: name.to_string(),
kind: CompletionItemKind::Variable,
detail: Some(doc.to_string()),
documentation: Some(MarkupContent::plain(doc)),
insert_text: None,
sort_text: None,
})
.collect()
}
}
pub struct KeywordEntry {
pub keyword: &'static str,
pub doc: &'static str,
pub is_command: bool,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct CompletionTriggerChar {
pub character: char,
pub description: String,
}
#[allow(dead_code)]
impl CompletionTriggerChar {
pub fn new(character: char, description: &str) -> Self {
Self {
character,
description: description.to_string(),
}
}
}
pub struct CompletionEngine<'a> {
env: &'a Environment,
cache: HashMap<String, Vec<CompletionItem>>,
}
impl<'a> CompletionEngine<'a> {
pub fn new(env: &'a Environment) -> Self {
Self {
env,
cache: HashMap::new(),
}
}
pub fn complete_at_position(&mut self, doc: &Document, pos: &Position) -> CompletionList {
let prefix = self.extract_prefix(doc, pos);
let context = self.determine_context(doc, pos);
let mut items = Vec::new();
match context {
CompletionContext::InTactic => {
items.extend(tactic_completions(&prefix));
items.extend(keyword_completions(&prefix));
}
CompletionContext::InImport => {
items.extend(self.module_completions(&prefix));
}
CompletionContext::TriggerCharacter('.') => {
items.extend(self.field_completions(doc, pos));
}
CompletionContext::InType => {
items.extend(self.type_aware_completions(&prefix, None));
items.extend(keyword_completions(&prefix));
}
CompletionContext::InCommand => {
items.extend(keyword_completions(&prefix));
items.extend(snippet_completions(&prefix));
items.extend(self.env_completions(&prefix));
}
_ => {
items.extend(keyword_completions(&prefix));
items.extend(self.env_completions(&prefix));
items.extend(snippet_completions(&prefix));
}
}
items.sort_by(|a, b| {
let sa = self.score_completion(a, &prefix, &context);
let sb = self.score_completion(b, &prefix, &context);
sb.cmp(&sa)
});
let mut seen = std::collections::HashSet::new();
items.retain(|item| seen.insert(item.label.clone()));
let is_incomplete = items.len() > 100;
if items.len() > 100 {
items.truncate(100);
}
CompletionList::new(items, is_incomplete)
}
pub(crate) fn extract_prefix(&self, doc: &Document, pos: &Position) -> String {
if let Some(line) = doc.get_line(pos.line) {
let col = (pos.character as usize).min(line.len());
let bytes = line.as_bytes();
let mut start = col;
while start > 0 && is_ident_char(bytes[start - 1]) {
start -= 1;
}
line[start..col].to_string()
} else {
String::new()
}
}
pub fn determine_context(&self, doc: &Document, pos: &Position) -> CompletionContext {
if let Some(line) = doc.get_line(pos.line) {
let col = (pos.character as usize).min(line.len());
let before = &line[..col];
if before.ends_with('.') {
return CompletionContext::TriggerCharacter('.');
}
if before.ends_with(':') {
return CompletionContext::TriggerCharacter(':');
}
let trimmed = before.trim_start();
if trimmed.starts_with("import") || trimmed.starts_with("open") {
return CompletionContext::InImport;
}
if self.is_in_tactic_block(doc, pos) {
return CompletionContext::InTactic;
}
if before.contains(':') && !before.contains(":=") {
return CompletionContext::InType;
}
if trimmed.is_empty() || COMMAND_KEYWORDS.iter().any(|kw| trimmed.starts_with(kw)) {
return CompletionContext::InCommand;
}
CompletionContext::InExpr
} else {
CompletionContext::Invoked
}
}
fn is_in_tactic_block(&self, doc: &Document, pos: &Position) -> bool {
let mut line_idx = pos.line as usize;
let mut indent_at_pos = None;
loop {
if let Some(line) = doc.get_line(line_idx as u32) {
let trimmed = line.trim_start();
let indent = line.len() - trimmed.len();
if indent_at_pos.is_none() {
indent_at_pos = Some(indent);
}
if trimmed == "by" || trimmed.starts_with("by ") || trimmed.starts_with("by\n") {
if let Some(our_indent) = indent_at_pos {
return our_indent > indent;
}
}
if indent == 0 && COMMAND_KEYWORDS.iter().any(|kw| trimmed.starts_with(kw)) {
return false;
}
}
if line_idx == 0 {
break;
}
line_idx -= 1;
}
false
}
pub fn score_completion(
&self,
item: &CompletionItem,
prefix: &str,
context: &CompletionContext,
) -> i32 {
let mut score = 0i32;
if item.label.starts_with(prefix) {
score += 100;
}
if item
.label
.to_lowercase()
.starts_with(&prefix.to_lowercase())
{
score += 50;
}
match context {
CompletionContext::InTactic if item.kind == CompletionItemKind::Method => {
score += 80;
}
CompletionContext::InCommand => {
if item.kind == CompletionItemKind::Keyword {
score += 80;
}
if item.kind == CompletionItemKind::Snippet {
score += 60;
}
}
CompletionContext::InType
if (item.kind == CompletionItemKind::Class
|| item.kind == CompletionItemKind::Interface
|| item.kind == CompletionItemKind::Constructor) =>
{
score += 80;
}
_ => {}
}
score -= (item.label.len() as i32) / 4;
score
}
fn env_completions(&mut self, prefix: &str) -> Vec<CompletionItem> {
if let Some(cached) = self.cache.get(prefix) {
return cached.clone();
}
let mut items = Vec::new();
for name in self.env.constant_names() {
let name_str = name.to_string();
if prefix.is_empty() || name_str.starts_with(prefix) {
let detail = if self.env.is_inductive(name) {
"inductive type"
} else if self.env.is_constructor(name) {
"constructor"
} else {
"declaration"
};
items.push(CompletionItem::function(&name_str, detail));
}
}
self.cache.insert(prefix.to_string(), items.clone());
items
}
pub(crate) fn module_completions(&self, prefix: &str) -> Vec<CompletionItem> {
let mut items = Vec::new();
let modules = [
"Init",
"Init.Prelude",
"Init.Core",
"Std",
"Std.Data",
"Std.Logic",
"Mathlib",
];
for m in &modules {
if prefix.is_empty() || m.starts_with(prefix) {
items.push(CompletionItem {
label: m.to_string(),
kind: CompletionItemKind::Module,
detail: Some("module".to_string()),
documentation: Some(MarkupContent::plain(format!("Import module {}", m))),
insert_text: None,
sort_text: None,
});
}
}
items
}
pub fn type_aware_completions(
&self,
prefix: &str,
_expected_type: Option<&str>,
) -> Vec<CompletionItem> {
let mut items = Vec::new();
items.extend(self.compatible_constants(prefix));
items
}
pub fn compatible_constants(&self, prefix: &str) -> Vec<CompletionItem> {
let mut items = Vec::new();
for name in self.env.constant_names() {
let name_str = name.to_string();
if prefix.is_empty() || name_str.starts_with(prefix) {
if let Some(ci) = self.env.find(name) {
let ty_str = format!("{:?}", ci.ty());
items.push(CompletionItem {
label: name_str,
kind: CompletionItemKind::Function,
detail: Some(ty_str),
documentation: None,
insert_text: None,
sort_text: None,
});
}
}
}
items
}
pub fn field_completions(&self, doc: &Document, pos: &Position) -> Vec<CompletionItem> {
let mut items = Vec::new();
if let Some(line) = doc.get_line(pos.line) {
let col = (pos.character as usize).min(line.len());
if col > 0 && line.as_bytes().get(col.wrapping_sub(1)) == Some(&b'.') {
let bytes = line.as_bytes();
let end = col - 1;
let mut start = end;
while start > 0 && is_ident_char(bytes[start - 1]) {
start -= 1;
}
if start < end {
let ident_before_dot = &line[start..end];
let found = self.field_completions_for_type(ident_before_dot);
if !found.is_empty() {
items.extend(found);
} else {
let fallback = ["mk", "rec", "casesOn", "recOn", "noConfusion"];
for f in &fallback {
items.push(CompletionItem {
label: f.to_string(),
kind: CompletionItemKind::Method,
detail: Some("recursor/constructor".to_string()),
documentation: None,
insert_text: None,
sort_text: None,
});
}
}
}
}
}
items
}
fn field_completions_for_type(&self, type_name: &str) -> Vec<CompletionItem> {
let mut items = Vec::new();
let prefix = format!("{}.", type_name);
for name in self.env.constant_names() {
let name_str = name.to_string();
if name_str.starts_with(&prefix) {
let field = &name_str[prefix.len()..];
if field.starts_with('_') || field.contains('.') {
continue;
}
let (kind, detail) = if self.env.is_constructor(name) {
(CompletionItemKind::Constructor, "constructor")
} else if self.env.is_recursor(name) {
(CompletionItemKind::Method, "recursor")
} else {
(CompletionItemKind::Field, "projection")
};
items.push(CompletionItem {
label: field.to_string(),
kind,
detail: Some(detail.to_string()),
documentation: None,
insert_text: None,
sort_text: None,
});
}
}
items
}
pub fn constructor_completions(&self, prefix: &str) -> Vec<CompletionItem> {
let mut items = Vec::new();
for name in self.env.constant_names() {
if self.env.is_constructor(name) {
let name_str = name.to_string();
if prefix.is_empty() || name_str.starts_with(prefix) {
items.push(CompletionItem {
label: name_str,
kind: CompletionItemKind::Constructor,
detail: Some("constructor".to_string()),
documentation: None,
insert_text: None,
sort_text: None,
});
}
}
}
items
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum TypeExpectationSource {
Annotation,
Application,
MatchArm,
Unknown,
}
#[derive(Clone, Debug)]
pub struct MultiEditCompletion {
pub label: String,
pub kind: CompletionItemKind,
pub edits: Vec<SimpleTextEdit>,
pub detail: Option<String>,
}
impl MultiEditCompletion {
pub fn new(label: &str, kind: CompletionItemKind, edits: Vec<SimpleTextEdit>) -> Self {
Self {
label: label.to_string(),
kind,
edits,
detail: None,
}
}
pub fn with_import(label: &str, module: &str, insert_range: Range) -> Self {
Self {
label: label.to_string(),
kind: CompletionItemKind::Function,
edits: vec![
SimpleTextEdit {
range: Range::single_line(0, 0, 0),
new_text: format!("import {}\n", module),
},
SimpleTextEdit {
range: insert_range,
new_text: label.to_string(),
},
],
detail: Some(format!("from {}", module)),
}
}
pub fn to_completion_item(&self) -> CompletionItem {
CompletionItem {
label: self.label.clone(),
kind: self.kind,
detail: self.detail.clone(),
documentation: None,
insert_text: Some(self.label.clone()),
sort_text: None,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum CompletionContext {
TriggerCharacter(char),
Invoked,
InExpr,
InType,
InTactic,
InCommand,
InImport,
}
#[derive(Clone, Debug)]
pub struct PostfixTemplate {
pub trigger: String,
pub template: String,
pub description: String,
}
impl PostfixTemplate {
pub fn expand(&self, expr: &str) -> String {
self.template.replace("$EXPR", expr)
}
}
#[derive(Clone, Debug)]
pub struct ImportedCompletion {
pub label: String,
pub import_module: String,
pub already_imported: bool,
pub kind: CompletionItemKind,
pub detail: Option<String>,
}
impl ImportedCompletion {
pub fn new(label: &str, import_module: &str, kind: CompletionItemKind) -> Self {
Self {
label: label.to_string(),
import_module: import_module.to_string(),
already_imported: false,
kind,
detail: None,
}
}
pub fn to_completion_item(&self) -> CompletionItem {
let detail = if self.already_imported {
self.detail.clone()
} else {
Some(format!(
"{} (adds import {})",
self.detail.as_deref().unwrap_or(""),
self.import_module
))
};
CompletionItem {
label: self.label.clone(),
kind: self.kind,
detail,
documentation: None,
insert_text: None,
sort_text: None,
}
}
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct CompletionScore {
pub label: String,
pub score: f64,
}
#[allow(dead_code)]
impl CompletionScore {
pub fn new(label: &str, score: f64) -> Self {
Self {
label: label.to_string(),
score: score.clamp(0.0, 1.0),
}
}
}