use std::collections::BTreeSet;
use crate::completion::{
completion_target::CompletionTarget,
yaml_lines::{indent, is_blank_or_comment, is_list_item, line_map_key},
};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CursorContext {
pub path: Vec<String>,
pub target: CompletionTarget,
pub sibling_keys: BTreeSet<String>,
}
impl CursorContext {
pub fn at(text: &str, line: u32, character: u32) -> CursorContext {
let lines = text.split('\n').collect::<Vec<&str>>();
let line_idx = (line as usize).min(lines.len().saturating_sub(1));
let current_line = lines.get(line_idx).copied().unwrap_or("");
let current_indent = indent(current_line);
let prefix = {
let chars = current_line.chars().collect::<Vec<char>>();
let col = (character as usize).min(chars.len());
chars[..col].iter().collect::<String>()
};
let trimmed = prefix.trim_start();
let (is_list, list_needs_space, after_dash) = match trimmed.strip_prefix("- ") {
Some(rest) => (true, false, rest),
None if trimmed == "-" => (true, true, ""),
None => (false, false, trimmed),
};
if let Some(colon_idx) = after_dash.find(':') {
let key = after_dash[..colon_idx].trim().to_string();
let value_part = &after_dash[colon_idx + 1..];
let in_flow_list = value_part.matches('[').count() > value_part.matches(']').count();
let needs_space = value_part.is_empty();
let path = Self::ancestor_chain(&lines, line_idx, current_indent);
return CursorContext {
path,
target: CompletionTarget::Value {
key,
in_sequence: in_flow_list,
needs_space,
},
sibling_keys: BTreeSet::new(),
};
}
if is_list {
if let Some((path, key)) = Self::list_value_target(&lines, line_idx, current_indent) {
return CursorContext {
path,
target: CompletionTarget::Value {
key,
in_sequence: true,
needs_space: list_needs_space,
},
sibling_keys: BTreeSet::new(),
};
}
}
let path = Self::ancestor_chain(&lines, line_idx, current_indent);
let sibling_keys = Self::sibling_keys(&lines, line_idx, current_indent);
CursorContext {
path,
target: CompletionTarget::Key,
sibling_keys,
}
}
fn sibling_keys(lines: &[&str], from_line_idx: usize, base_indent: usize) -> BTreeSet<String> {
let mut sibling_keys = BTreeSet::new();
let mut collect = |line: &str| -> bool {
if is_blank_or_comment(line) {
return true;
}
let line_indent = indent(line);
if line_indent < base_indent {
return false;
}
if line_indent == base_indent
&& let Some(key) = line_map_key(line)
{
sibling_keys.insert(key);
}
true
};
for line in lines[..from_line_idx].iter().rev() {
if !collect(line) {
break;
}
}
for line in lines.iter().skip(from_line_idx + 1) {
if !collect(line) {
break;
}
}
sibling_keys
}
fn list_value_target(
lines: &[&str],
from_line_idx: usize,
base_indent: usize,
) -> Option<(Vec<String>, String)> {
let owner_idx =
lines[..from_line_idx]
.iter()
.enumerate()
.rev()
.find_map(|(idx, line)| {
if is_blank_or_comment(line) {
return None;
}
let line_indent = indent(line);
if line_indent > base_indent
|| (line_indent == base_indent && is_list_item(line))
{
return None;
}
Some(idx)
})?;
let owner_key = line_map_key(lines[owner_idx])?;
let owner_indent = indent(lines[owner_idx]);
let path = Self::ancestor_chain(lines, owner_idx, owner_indent);
Some((path, owner_key))
}
fn ancestor_chain(lines: &[&str], from_line_idx: usize, base_indent: usize) -> Vec<String> {
let mut path = Vec::new();
let mut needed_indent = base_indent;
for line in lines[..from_line_idx].iter().rev() {
if is_blank_or_comment(line) {
continue;
}
let line_indent = indent(line);
if line_indent >= needed_indent {
continue;
}
if let Some(key) = line_map_key(line) {
path.push(key);
}
needed_indent = line_indent;
if needed_indent == 0 {
break;
}
}
path.reverse();
path
}
}