use std::collections::BTreeSet;
use async_lsp::lsp_types::{CompletionItem, CompletionItemKind};
use disposition_input_model::theme::ThemeAttr;
use serde_json::Value;
use crate::completion::{
completion_target::CompletionTarget, cursor_context::CursorContext,
diagram_schema::DiagramSchema, dynamic_completions::DynamicCompletions,
id_category::IdCategory, key_category::KeyCategory,
};
pub struct CompletionEngine;
impl CompletionEngine {
pub fn completions(text: &str, line: u32, character: u32) -> Vec<CompletionItem> {
let schema = DiagramSchema::get();
let cursor_context = CursorContext::at(text, line, character);
let Some(container) = schema.schema_at(&cursor_context.path) else {
return Vec::new();
};
let container_ref_name = DiagramSchema::ref_name(container);
let container = schema.deref(container);
match &cursor_context.target {
CompletionTarget::Key => Self::key_completions(
schema,
container,
container_ref_name,
&cursor_context.sibling_keys,
text,
),
CompletionTarget::Value {
key,
in_sequence,
needs_space,
} => Self::value_completions(
schema,
container,
container_ref_name,
key,
*in_sequence,
*needs_space,
text,
),
}
}
fn key_completions(
schema: &DiagramSchema,
container: &Value,
container_ref_name: Option<&str>,
sibling_keys: &BTreeSet<String>,
text: &str,
) -> Vec<CompletionItem> {
let mut items = schema
.property_entries(container)
.into_iter()
.map(|property| CompletionItem {
label: property.name.to_string(),
kind: Some(CompletionItemKind::FIELD),
detail: property.description.map(first_line),
..CompletionItem::default()
})
.collect::<Vec<CompletionItem>>();
if let Some(property_names) = container.get("propertyNames") {
items.extend(
schema
.enum_entries(property_names)
.into_iter()
.map(|entry| CompletionItem {
label: entry.value.to_string(),
kind: Some(CompletionItemKind::FIELD),
detail: entry.description.map(first_line),
..CompletionItem::default()
}),
);
}
if let Some(key_category) = container_ref_name.and_then(KeyCategory::from_ref_name) {
items.extend(Self::dynamic_key_completions(schema, key_category, text));
}
items.retain(|item| !sibling_keys.contains(&item.label));
items
}
fn dynamic_key_completions(
schema: &DiagramSchema,
key_category: KeyCategory,
text: &str,
) -> Vec<CompletionItem> {
let dynamic_completions = DynamicCompletions::from_text(text);
let mut items = dynamic_completions
.key_suggestions(key_category)
.into_iter()
.map(|label| CompletionItem {
label,
kind: Some(CompletionItemKind::VALUE),
..CompletionItem::default()
})
.collect::<Vec<CompletionItem>>();
let builtin_def_name = match key_category {
KeyCategory::StyleAlias => Some("StyleAlias"),
KeyCategory::EntityType => Some("EntityType"),
_ => None,
};
if let Some(def) = builtin_def_name.and_then(|name| schema.def(name)) {
items.extend(
schema
.enum_entries(def)
.into_iter()
.map(|entry| CompletionItem {
label: entry.value.to_string(),
kind: Some(CompletionItemKind::ENUM_MEMBER),
detail: entry.description.map(first_line),
..CompletionItem::default()
}),
);
}
completion_items_dedup(&mut items);
items
}
#[allow(clippy::fn_params_excessive_bools)]
fn value_completions(
schema: &DiagramSchema,
container: &Value,
container_ref_name: Option<&str>,
key: &str,
in_sequence: bool,
needs_space: bool,
text: &str,
) -> Vec<CompletionItem> {
if container_ref_name == Some("CssClassPartials")
&& let Some(items) = Self::theme_attr_value_completions(key, needs_space)
{
return items;
}
let Some(value_schema) = schema.field_schema(container, key) else {
return Vec::new();
};
let array_items = schema.array_items(value_schema);
let element_schema = array_items.unwrap_or(value_schema);
let wrap_in_list = array_items.is_some() && !in_sequence;
let insert_text = |label: &str| value_insert_text(label, wrap_in_list, needs_space);
let mut items = Vec::new();
items.extend(
schema
.enum_entries(element_schema)
.into_iter()
.map(|entry| CompletionItem {
label: entry.value.to_string(),
kind: Some(CompletionItemKind::ENUM_MEMBER),
detail: entry.description.map(first_line),
insert_text: insert_text(entry.value),
..CompletionItem::default()
}),
);
if let Some(category) =
DiagramSchema::ref_name(element_schema).and_then(IdCategory::from_ref_name)
{
let dynamic_completions = DynamicCompletions::from_text(text);
items.extend(dynamic_completions.ids_for(category).into_iter().map(|id| {
CompletionItem {
label: id.to_string(),
kind: Some(CompletionItemKind::VALUE),
insert_text: insert_text(id),
..CompletionItem::default()
}
}));
}
completion_items_dedup(&mut items);
items
}
fn theme_attr_value_completions(key: &str, needs_space: bool) -> Option<Vec<CompletionItem>> {
let theme_attr =
serde_json::from_value::<ThemeAttr>(Value::String(key.to_string())).ok()?;
let items = theme_attr
.value_suggestions()
.iter()
.map(|value| CompletionItem {
label: (*value).to_string(),
kind: Some(CompletionItemKind::VALUE),
insert_text: value_insert_text(value, false, needs_space),
..CompletionItem::default()
})
.collect();
Some(items)
}
}
fn completion_items_dedup(items: &mut Vec<CompletionItem>) {
let mut seen = BTreeSet::new();
items.retain(|item| seen.insert(item.label.clone()));
}
fn value_insert_text(label: &str, wrap_in_list: bool, needs_space: bool) -> Option<String> {
if !wrap_in_list && !needs_space {
return None;
}
let space = if needs_space { " " } else { "" };
if wrap_in_list {
Some(format!("{space}[{label}]"))
} else {
Some(format!("{space}{label}"))
}
}
fn first_line(description: &str) -> String {
description
.lines()
.map(str::trim)
.find(|line| !line.is_empty())
.unwrap_or_default()
.to_string()
}