use std::collections::BTreeSet;
use crate::parser::CstDocument;
use crate::syntax::{SyntaxKind, SyntaxNode, SyntaxToken};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum PositionKind {
StructField,
ListElement,
MapKey,
MapValue,
Tuple,
Value,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum CompletionKind {
Field,
Variant,
MapKey,
Option,
Delimiter,
}
impl CompletionKind {
#[inline]
#[must_use]
fn order(self) -> u8 {
match self {
Self::Field => 0,
Self::Variant => 1,
Self::MapKey => 2,
Self::Option => 3,
Self::Delimiter => 4,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CompletionItem {
pub label: String,
pub insert_text: String,
pub kind: CompletionKind,
pub rank: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CompletionContext {
pub position: Option<PositionKind>,
pub prefix: String,
pub sibling_field_names: Vec<String>,
pub in_file_variant_names: Vec<String>,
pub in_file_map_keys: Vec<String>,
pub items: Vec<CompletionItem>,
}
impl CompletionContext {
#[must_use]
fn empty() -> Self {
Self {
position: None,
prefix: String::new(),
sibling_field_names: Vec::new(),
in_file_variant_names: Vec::new(),
in_file_map_keys: Vec::new(),
items: Vec::new(),
}
}
}
#[must_use]
pub fn completion_context(doc: &CstDocument, offset: usize) -> CompletionContext {
let offset = offset.min(doc.source_len());
let root = doc.root();
if !has_significant_token(&root) {
return CompletionContext::empty();
}
let prefix = prefix_before(&root, offset);
let Some(position) = resolve_position(&root, offset) else {
return CompletionContext::empty();
};
let sibling_field_names = enclosing_sibling_field_names(&root, offset);
let in_file_variant_names = collect_in_file(&root, SyntaxKind::EnumVariant);
let in_file_map_keys = collect_map_keys(&root);
let items = build_items(
position,
&prefix,
&sibling_field_names,
&in_file_variant_names,
&in_file_map_keys,
);
CompletionContext {
position: Some(position),
prefix,
sibling_field_names,
in_file_variant_names,
in_file_map_keys,
items,
}
}
#[must_use]
pub fn completions(ctx: &CompletionContext) -> Vec<CompletionItem> {
ctx.items.clone()
}
fn smallest_node_at(root: &SyntaxNode, offset: usize) -> SyntaxNode {
let mut node = root.clone();
loop {
let next = node.children().find(|child| {
let r = child.text_range();
r.start() <= offset && offset <= r.end()
});
match next {
Some(child) if child != node => node = child,
_ => break,
}
}
node
}
fn resolve_position(root: &SyntaxNode, offset: usize) -> Option<PositionKind> {
let node = smallest_node_at(root, offset);
let mut cur = Some(node.clone());
while let Some(n) = cur {
match n.kind() {
SyntaxKind::Struct => {
return struct_position(&n, offset);
}
SyntaxKind::List => {
return inside_brackets(&n, offset, SyntaxKind::LBracket, SyntaxKind::RBracket)
.then_some(PositionKind::ListElement);
}
SyntaxKind::Tuple => {
return inside_brackets(&n, offset, SyntaxKind::LParen, SyntaxKind::RParen)
.then_some(PositionKind::Tuple);
}
SyntaxKind::Map => {
return map_position(&n, offset);
}
SyntaxKind::MapEntry => {
return map_entry_position(&n, offset);
}
SyntaxKind::StructField => {
return struct_field_position(&n, offset);
}
SyntaxKind::Root => {
return Some(PositionKind::Value);
}
_ => cur = n.parent(),
}
}
None
}
fn struct_position(node: &SyntaxNode, offset: usize) -> Option<PositionKind> {
if !inside_brackets(node, offset, SyntaxKind::LParen, SyntaxKind::RParen) {
return None;
}
if let Some(field) = node
.children()
.find(|c| c.kind() == SyntaxKind::StructField && c.text_range().contains(offset))
{
return struct_field_position(&field, offset);
}
Some(PositionKind::StructField)
}
fn struct_field_position(field: &SyntaxNode, offset: usize) -> Option<PositionKind> {
match colon_offset(field) {
Some(colon) if offset > colon => Some(PositionKind::Value),
_ => Some(PositionKind::StructField),
}
}
fn map_position(node: &SyntaxNode, offset: usize) -> Option<PositionKind> {
if !inside_brackets(node, offset, SyntaxKind::LBrace, SyntaxKind::RBrace) {
return None;
}
if let Some(entry) = node
.children()
.find(|c| c.kind() == SyntaxKind::MapEntry && c.text_range().contains(offset))
{
return map_entry_position(&entry, offset);
}
Some(PositionKind::MapKey)
}
fn map_entry_position(entry: &SyntaxNode, offset: usize) -> Option<PositionKind> {
match colon_offset(entry) {
Some(colon) if offset > colon => Some(PositionKind::MapValue),
_ => Some(PositionKind::MapKey),
}
}
fn colon_offset(node: &SyntaxNode) -> Option<usize> {
node.children_with_tokens()
.filter_map(|el| el.as_token().cloned())
.find(|t| t.kind() == SyntaxKind::Colon)
.map(|t| t.text_range().start())
}
fn inside_brackets(node: &SyntaxNode, offset: usize, open: SyntaxKind, close: SyntaxKind) -> bool {
let open_end = node
.children_with_tokens()
.filter_map(|el| el.as_token().cloned())
.find(|t| t.kind() == open)
.map(|t| t.text_range().end());
let close_start = node
.children_with_tokens()
.filter_map(|el| el.as_token().cloned())
.find(|t| t.kind() == close)
.map(|t| t.text_range().start());
let after_open = open_end.map_or(true, |e| offset >= e);
let before_close = close_start.map_or(true, |s| offset <= s);
after_open && before_close
}
fn prefix_before(root: &SyntaxNode, offset: usize) -> String {
let mut best: Option<SyntaxToken> = None;
for tok in root.descendant_tokens() {
let r = tok.text_range();
if r.start() < offset && offset <= r.end() {
best = Some(tok);
}
if r.start() >= offset {
break;
}
}
let Some(tok) = best else {
return String::new();
};
if tok.kind() != SyntaxKind::Ident {
return String::new();
}
let r = tok.text_range();
let take = offset.saturating_sub(r.start());
let text = tok.text();
let take = take.min(text.len());
let mut end = take;
while end > 0 && !text.is_char_boundary(end) {
end -= 1;
}
text[..end].to_string()
}
fn has_significant_token(root: &SyntaxNode) -> bool {
root.descendant_tokens().any(|t| !t.is_trivia())
}
fn enclosing_sibling_field_names(root: &SyntaxNode, offset: usize) -> Vec<String> {
let node = smallest_node_at(root, offset);
let mut cur = Some(node);
while let Some(n) = cur {
if n.kind() == SyntaxKind::Struct {
let mut names: Vec<String> = n
.children()
.filter(|c| c.kind() == SyntaxKind::StructField)
.filter_map(|f| {
f.first_token_of(SyntaxKind::Ident)
.map(|t| t.text().to_string())
})
.collect();
dedup_sorted(&mut names);
return names;
}
cur = n.parent();
}
Vec::new()
}
fn collect_in_file(root: &SyntaxNode, kind: SyntaxKind) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
collect_in_file_rec(root, kind, &mut out);
dedup_sorted(&mut out);
out
}
fn collect_in_file_rec(node: &SyntaxNode, kind: SyntaxKind, out: &mut Vec<String>) {
if node.kind() == kind {
if let Some(t) = node.first_token_of(SyntaxKind::Ident) {
out.push(t.text().to_string());
}
}
for child in node.children() {
collect_in_file_rec(&child, kind, out);
}
}
fn collect_map_keys(root: &SyntaxNode) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
collect_map_keys_rec(root, &mut out);
dedup_sorted(&mut out);
out
}
fn collect_map_keys_rec(node: &SyntaxNode, out: &mut Vec<String>) {
if node.kind() == SyntaxKind::MapEntry {
let colon = colon_offset(node);
if let Some(key_node) = node.children().find(|c| {
let before_colon = colon.map_or(true, |co| c.text_range().start() < co);
before_colon && is_keyable_node(c.kind())
}) {
if let Some(t) = key_node.first_token_of(SyntaxKind::Ident) {
out.push(t.text().to_string());
}
}
}
for child in node.children() {
collect_map_keys_rec(&child, out);
}
}
fn is_keyable_node(kind: SyntaxKind) -> bool {
matches!(kind, SyntaxKind::EnumVariant | SyntaxKind::Literal)
}
fn dedup_sorted(v: &mut Vec<String>) {
let set: BTreeSet<String> = v.drain(..).collect();
v.extend(set);
}
fn build_items(
position: PositionKind,
prefix: &str,
sibling_field_names: &[String],
in_file_variant_names: &[String],
in_file_map_keys: &[String],
) -> Vec<CompletionItem> {
let mut raw: Vec<CompletionItem> = Vec::new();
match position {
PositionKind::StructField => {
for name in sibling_field_names {
raw.push(field_item(name));
}
}
PositionKind::MapKey => {
for key in in_file_map_keys {
raw.push(map_key_item(key));
}
}
PositionKind::ListElement
| PositionKind::Tuple
| PositionKind::MapValue
| PositionKind::Value => {
raw.push(option_item("None", "None"));
raw.push(option_item("Some", "Some()"));
for v in in_file_variant_names {
raw.push(variant_item(v));
}
for (label, insert) in [("(", "()"), ("[", "[]"), ("{", "{}")] {
raw.push(delimiter_item(label, insert));
}
}
}
finalize(raw, prefix)
}
fn finalize(mut items: Vec<CompletionItem>, prefix: &str) -> Vec<CompletionItem> {
items.retain(|it| it.label.starts_with(prefix));
if !prefix.is_empty() {
items.retain(|it| it.label != prefix);
}
items.sort_by(|a, b| {
a.kind
.order()
.cmp(&b.kind.order())
.then_with(|| a.label.cmp(&b.label))
.then_with(|| a.insert_text.cmp(&b.insert_text))
});
items.dedup_by(|a, b| a.label == b.label && a.kind == b.kind);
for (i, item) in items.iter_mut().enumerate() {
item.rank = (i as u32) + 1;
}
items
}
fn field_item(name: &str) -> CompletionItem {
CompletionItem {
label: name.to_string(),
insert_text: name.to_string(),
kind: CompletionKind::Field,
rank: 0,
}
}
fn map_key_item(key: &str) -> CompletionItem {
CompletionItem {
label: key.to_string(),
insert_text: key.to_string(),
kind: CompletionKind::MapKey,
rank: 0,
}
}
fn variant_item(name: &str) -> CompletionItem {
CompletionItem {
label: name.to_string(),
insert_text: name.to_string(),
kind: CompletionKind::Variant,
rank: 0,
}
}
fn option_item(label: &str, insert: &str) -> CompletionItem {
CompletionItem {
label: label.to_string(),
insert_text: insert.to_string(),
kind: CompletionKind::Option,
rank: 0,
}
}
fn delimiter_item(label: &str, insert: &str) -> CompletionItem {
CompletionItem {
label: label.to_string(),
insert_text: insert.to_string(),
kind: CompletionKind::Delimiter,
rank: 0,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::parse;
fn ctx_at(src: &str, byte_offset: usize) -> CompletionContext {
completion_context(&parse(src), byte_offset)
}
#[test]
fn empty_buffer_yields_no_items() {
let ctx = ctx_at("", 0);
assert_eq!(ctx.position, None);
assert!(ctx.items.is_empty());
}
#[test]
fn whitespace_only_buffer_yields_no_items() {
let ctx = ctx_at(" \n\t ", 2);
assert_eq!(ctx.position, None);
assert!(ctx.items.is_empty());
}
#[test]
fn top_level_value_offers_option_and_delimiters() {
let ctx = ctx_at("Foo", 3);
assert_eq!(ctx.position, Some(PositionKind::Value));
assert_eq!(ctx.prefix, "Foo");
}
#[test]
fn struct_field_position_is_classified() {
let src = "Point(x: 1, )";
let ctx = completion_context(&parse(src), src.len() - 1);
assert_eq!(ctx.position, Some(PositionKind::StructField));
assert!(ctx.sibling_field_names.contains(&"x".to_string()));
}
#[test]
fn struct_field_value_position_after_colon() {
let src = "Point(x: )";
let colon = src.find(':').unwrap();
let ctx = completion_context(&parse(src), colon + 2);
assert_eq!(ctx.position, Some(PositionKind::Value));
}
#[test]
fn list_element_position_is_classified() {
let src = "[1, ]";
let ctx = completion_context(&parse(src), 4); assert_eq!(ctx.position, Some(PositionKind::ListElement));
}
#[test]
fn tuple_position_is_classified() {
let src = "(1, 2)";
let ctx = completion_context(&parse(src), 4); assert_eq!(ctx.position, Some(PositionKind::Tuple));
}
#[test]
fn map_key_and_value_positions() {
let src = "{ name: 1 }";
let key_ctx = completion_context(&parse(src), 2);
assert_eq!(key_ctx.position, Some(PositionKind::MapKey));
let colon = src.find(':').unwrap();
let val_ctx = completion_context(&parse(src), colon + 2);
assert_eq!(val_ctx.position, Some(PositionKind::MapValue));
}
#[test]
fn collects_in_file_variant_names() {
let src = "[Alpha, Beta, ]";
let ctx = completion_context(&parse(src), src.len() - 1);
assert!(ctx.in_file_variant_names.contains(&"Alpha".to_string()));
assert!(ctx.in_file_variant_names.contains(&"Beta".to_string()));
}
#[test]
fn collects_in_file_map_keys() {
let src = "{ alpha: 1, beta: 2 }";
let ctx = completion_context(&parse(src), 2);
assert!(ctx.in_file_map_keys.contains(&"alpha".to_string()));
assert!(ctx.in_file_map_keys.contains(&"beta".to_string()));
}
#[test]
fn items_ordered_kind_then_alpha_below_literal() {
let src = "[]";
let ctx = completion_context(&parse(src), 1); assert_eq!(ctx.position, Some(PositionKind::ListElement));
let kinds: Vec<CompletionKind> = ctx.items.iter().map(|i| i.kind).collect();
let first_option = kinds.iter().position(|k| *k == CompletionKind::Option);
let first_delim = kinds.iter().position(|k| *k == CompletionKind::Delimiter);
assert!(first_option < first_delim);
for (i, item) in ctx.items.iter().enumerate() {
assert_eq!(item.rank, (i as u32) + 1);
assert!(item.rank >= 1, "no item may share the literal's rank 0");
}
let none_pos = ctx.items.iter().position(|i| i.label == "None");
let some_pos = ctx.items.iter().position(|i| i.label == "Some");
assert!(none_pos < some_pos);
}
#[test]
fn prefix_filters_candidates() {
let src = "So";
let ctx = completion_context(&parse(src), 2);
assert_eq!(ctx.prefix, "So");
assert!(ctx.items.iter().any(|i| i.label == "Some"));
assert!(ctx.items.iter().all(|i| i.label != "None"));
}
#[test]
fn exact_literal_is_not_reoffered() {
let src = "Some";
let ctx = completion_context(&parse(src), 4);
assert!(ctx.items.iter().all(|i| i.label != "Some"));
}
#[test]
fn some_insert_text_round_trips() {
let src = "[]";
let ctx = completion_context(&parse(src), 1);
let some = ctx
.items
.iter()
.find(|i| i.label == "Some")
.expect("Some offered at a value slot");
let parsed = parse(&some.insert_text);
assert!(
parsed.diagnostics().is_empty(),
"Some insert_text must parse cleanly: {:?}",
parsed.diagnostics()
);
}
#[test]
fn delimiter_insert_text_round_trips() {
let src = "[]";
let ctx = completion_context(&parse(src), 1);
for item in ctx
.items
.iter()
.filter(|i| i.kind == CompletionKind::Delimiter)
{
let parsed = parse(&item.insert_text);
assert!(
parsed.diagnostics().is_empty(),
"delimiter insert_text {:?} must parse cleanly",
item.insert_text
);
}
}
#[test]
fn out_of_range_offset_is_clamped_not_panicking() {
let src = "Foo(x: 1)";
let ctx = completion_context(&parse(src), 9_999);
assert!(ctx.position.is_some() || ctx.items.is_empty());
}
#[test]
fn completions_free_fn_matches_items() {
let src = "[]";
let ctx = completion_context(&parse(src), 1);
assert_eq!(completions(&ctx), ctx.items);
}
}