use std::collections::BTreeSet;
use crate::completion::{
id_category::IdCategory,
key_category::KeyCategory,
yaml_lines::{indent, is_blank_or_comment, is_list_item, line_map_key, split_flow_items},
};
#[derive(Clone, Debug, Default)]
pub struct DynamicCompletions {
thing_ids: BTreeSet<String>,
tag_ids: BTreeSet<String>,
process_ids: BTreeSet<String>,
step_ids: BTreeSet<String>,
edge_group_ids: BTreeSet<String>,
entity_type_ids: BTreeSet<String>,
}
impl DynamicCompletions {
pub fn from_text(text: &str) -> DynamicCompletions {
let lines = text.split('\n').collect::<Vec<&str>>();
let mut thing_ids = collect_block_keys(&lines, "things", true, false);
thing_ids.extend(collect_block_keys(&lines, "thing_names", true, true));
thing_ids.extend(collect_block_keys(&lines, "thing_descs", true, true));
let tag_ids = collect_block_keys(&lines, "tags", true, true);
let process_ids = collect_block_keys(&lines, "processes", true, true);
let step_ids = collect_block_keys(&lines, "steps", false, true);
let mut edge_group_ids = collect_block_keys(&lines, "thing_dependencies", true, true);
edge_group_ids.extend(collect_block_keys(&lines, "thing_interactions", true, true));
let entity_type_ids = collect_entity_types_values(&lines);
DynamicCompletions {
thing_ids,
tag_ids,
process_ids,
step_ids,
edge_group_ids,
entity_type_ids,
}
}
pub fn ids_for(&self, category: IdCategory) -> Vec<&str> {
match category {
IdCategory::Thing => self.thing_ids.iter().map(String::as_str).collect(),
IdCategory::Tag => self.tag_ids.iter().map(String::as_str).collect(),
IdCategory::ProcessStep => self.step_ids.iter().map(String::as_str).collect(),
IdCategory::EdgeGroup => self.edge_group_ids.iter().map(String::as_str).collect(),
IdCategory::EntityType => self.entity_type_ids.iter().map(String::as_str).collect(),
IdCategory::Any => self
.thing_ids
.iter()
.chain(&self.tag_ids)
.chain(&self.step_ids)
.chain(&self.edge_group_ids)
.map(String::as_str)
.collect(),
}
}
pub fn key_suggestions(&self, key_category: KeyCategory) -> Vec<String> {
let owned = |ids: Vec<&str>| ids.into_iter().map(str::to_string).collect::<Vec<String>>();
match key_category {
KeyCategory::ThingId => owned(self.ids_for(IdCategory::Thing)),
KeyCategory::EdgeGroupDep => vec![self.edge_group_suggestion("edge_dep")],
KeyCategory::EdgeGroupInteraction => vec![self.edge_group_suggestion("edge_ix")],
KeyCategory::TagName => vec![String::from("tag_example")],
KeyCategory::TagId => owned(self.ids_for(IdCategory::Tag)),
KeyCategory::EdgeId => self.edge_ids(),
KeyCategory::Entity => {
let mut suggestions = owned(self.ids_for(IdCategory::Thing));
suggestions.extend(self.process_ids.iter().cloned());
suggestions.extend(owned(self.ids_for(IdCategory::ProcessStep)));
suggestions.extend(self.edge_ids());
suggestions
}
KeyCategory::StyleAlias => vec![String::from("style_alias_custom")],
KeyCategory::ThemeStyles => {
let mut suggestions =
vec![String::from("node_defaults"), String::from("edge_defaults")];
suggestions.extend(owned(self.ids_for(IdCategory::Thing)));
suggestions.extend(owned(self.ids_for(IdCategory::EdgeGroup)));
suggestions.extend(self.edge_ids());
suggestions
}
KeyCategory::TagFocus => {
let mut suggestions = vec![String::from("tag_defaults")];
suggestions.extend(owned(self.ids_for(IdCategory::Tag)));
suggestions
}
KeyCategory::EntityType => owned(self.ids_for(IdCategory::EntityType)),
}
}
fn edge_group_suggestion(&self, prefix: &str) -> String {
let mut thing_ids = self.thing_ids.iter();
match (thing_ids.next(), thing_ids.next()) {
(Some(thing_id_0), Some(thing_id_1)) => {
format!("{prefix}__{thing_id_0}_{thing_id_1}")
}
(Some(thing_id_0), None) => format!("{prefix}__{thing_id_0}"),
_ => format!("{prefix}_"),
}
}
fn edge_ids(&self) -> Vec<String> {
self.edge_group_ids
.iter()
.map(|edge_group_id| format!("{edge_group_id}__0"))
.collect()
}
}
fn collect_block_keys(
lines: &[&str],
name: &str,
top_level_only: bool,
first_level_only: bool,
) -> BTreeSet<String> {
let mut keys = BTreeSet::new();
let mut i = 0;
while i < lines.len() {
let line = lines[i];
let is_block_start = !is_blank_or_comment(line)
&& (!top_level_only || indent(line) == 0)
&& line_map_key(line).as_deref() == Some(name);
if !is_block_start {
i += 1;
continue;
}
let block_indent = indent(line);
let body_start = i + 1;
let mut body_end = body_start;
let mut min_child_indent: Option<usize> = None;
while body_end < lines.len() {
let body_line = lines[body_end];
if is_blank_or_comment(body_line) {
body_end += 1;
continue;
}
let body_indent = indent(body_line);
if body_indent <= block_indent {
break;
}
min_child_indent = Some(min_child_indent.map_or(body_indent, |m| m.min(body_indent)));
body_end += 1;
}
for body_line in &lines[body_start..body_end] {
if is_blank_or_comment(body_line) {
continue;
}
if first_level_only && Some(indent(body_line)) != min_child_indent {
continue;
}
if let Some(key) = line_map_key(body_line) {
keys.insert(key);
}
}
i = body_end;
}
keys
}
fn collect_entity_types_values(lines: &[&str]) -> BTreeSet<String> {
let mut values = BTreeSet::new();
let Some(block_start) = lines.iter().position(|line| {
!is_blank_or_comment(line)
&& indent(line) == 0
&& line_map_key(line).as_deref() == Some("entity_types")
}) else {
return values;
};
for line in &lines[block_start + 1..] {
if is_blank_or_comment(line) {
continue;
}
if indent(line) == 0 {
break;
}
if is_list_item(line) {
let value = line.trim_start().trim_start_matches('-').trim();
if !value.is_empty() {
values.insert(value.to_string());
}
} else if line_map_key(line).is_some()
&& let Some(colon_idx) = line.find(':')
{
let value = line[colon_idx + 1..].trim();
if let Some(inner) = value.strip_prefix('[').and_then(|v| v.strip_suffix(']')) {
values.extend(split_flow_items(inner).into_iter().map(str::to_string));
}
}
}
values
}