use super::{SourceTree, Symbol, SymbolKind, shared};
use achitekfile::TextPosition;
use tree_sitter::Node;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CompletionKind {
Keyword,
Property,
Value,
Reference,
Function,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Completion {
label: String,
detail: Option<String>,
kind: CompletionKind,
}
impl Completion {
pub fn label(&self) -> &str {
&self.label
}
pub fn detail(&self) -> Option<&str> {
self.detail.as_deref()
}
pub fn kind(&self) -> CompletionKind {
self.kind
}
}
pub(super) fn completions_for_position(
syntax: &SourceTree,
symbols: &[Symbol],
position: TextPosition,
) -> Vec<Completion> {
let line = source_line(syntax.source(), position.line);
let prefix = prefix_before_column(line, position.byte);
let trimmed = prefix.trim_start();
if trimmed.starts_with("type") {
return prompt_type_completions();
}
if trimmed.starts_with("depends_on") {
return depends_on_completions(symbols);
}
if in_validate_block(syntax, position) {
return validate_attribute_completions(syntax, position);
}
if in_prompt_block(syntax, position) {
return prompt_attribute_completions(syntax, position);
}
if in_blueprint_block(syntax, position) {
return blueprint_attribute_completions();
}
top_level_completions()
}
fn source_line(source: &str, row: usize) -> &str {
source.lines().nth(row).unwrap_or("")
}
fn prefix_before_column(line: &str, column: usize) -> &str {
let end = column.min(line.len());
&line[..end]
}
fn in_prompt_block(syntax: &SourceTree, position: TextPosition) -> bool {
ancestor_kinds_at_position(syntax, position).contains(&"prompt_block")
}
fn in_validate_block(syntax: &SourceTree, position: TextPosition) -> bool {
ancestor_kinds_at_position(syntax, position).contains(&"validate_block")
}
fn in_blueprint_block(syntax: &SourceTree, position: TextPosition) -> bool {
ancestor_kinds_at_position(syntax, position).contains(&"blueprint_block")
}
fn ancestor_kinds_at_position(syntax: &SourceTree, position: TextPosition) -> Vec<&str> {
let point = tree_sitter::Point {
row: position.line,
column: position.byte,
};
let mut kinds = Vec::new();
if let Some(mut node) = syntax
.root_node()
.named_descendant_for_point_range(point, point)
{
loop {
kinds.push(node.kind());
let Some(parent) = node.parent() else {
break;
};
node = parent;
}
}
kinds
}
fn ancestor_node_at_position<'a>(
syntax: &'a SourceTree,
position: TextPosition,
kind: &str,
) -> Option<Node<'a>> {
let point = tree_sitter::Point {
row: position.line,
column: position.byte,
};
let mut node = syntax
.root_node()
.named_descendant_for_point_range(point, point)?;
loop {
if node.kind() == kind {
return Some(node);
}
node = node.parent()?;
}
}
fn find_named_descendant_by_kind<'a>(node: Node<'a>, kind: &str) -> Option<Node<'a>> {
if node.kind() == kind {
return Some(node);
}
for index in 0..node.child_count() {
let Some(child) =
node.child(u32::try_from(index).expect("child index should fit into u32"))
else {
continue;
};
if let Some(found) = find_named_descendant_by_kind(child, kind) {
return Some(found);
}
}
None
}
fn top_level_completions() -> Vec<Completion> {
vec![
completion(
"blueprint",
Some("Declare blueprint metadata"),
CompletionKind::Keyword,
),
completion(
"prompt",
Some("Declare an interactive prompt"),
CompletionKind::Keyword,
),
]
}
fn blueprint_attribute_completions() -> Vec<Completion> {
vec![
completion(
"version",
Some("Achitekfile schema version"),
CompletionKind::Property,
),
completion(
"name",
Some("Blueprint identifier"),
CompletionKind::Property,
),
completion(
"description",
Some("Blueprint description"),
CompletionKind::Property,
),
completion("author", Some("Blueprint author"), CompletionKind::Property),
completion(
"min_achitek_version",
Some("Minimum required Achitek version"),
CompletionKind::Property,
),
]
}
fn prompt_attribute_completions(syntax: &SourceTree, position: TextPosition) -> Vec<Completion> {
let prompt_block = ancestor_node_at_position(syntax, position, "prompt_block");
let prompt_type = prompt_block.and_then(|node| shared::prompt_type_for_block(syntax, node));
let mut items = vec![
completion("type", Some("Prompt type"), CompletionKind::Property),
completion("help", Some("Prompt help text"), CompletionKind::Property),
completion("default", Some("Default answer"), CompletionKind::Property),
completion(
"required",
Some("Whether the prompt is required"),
CompletionKind::Property,
),
completion(
"depends_on",
Some("Conditional visibility expression"),
CompletionKind::Property,
),
completion(
"validate",
Some("Validation block"),
CompletionKind::Keyword,
),
];
if matches!(prompt_type, None | Some("select" | "multiselect")) {
items.push(completion(
"choices",
Some("Selectable options"),
CompletionKind::Property,
));
}
if let Some(prompt_block) = prompt_block {
items.retain(|item| {
let kind = match item.label() {
"type" => "type_attribute",
"help" => "help_attribute",
"choices" => "choices_attribute",
"default" => "default_attribute",
"required" => "required_attribute",
"depends_on" => "depends_on_attribute",
"validate" => "validate_block",
_ => return true,
};
find_named_descendant_by_kind(prompt_block, kind).is_none()
});
}
items
}
fn validate_attribute_completions(syntax: &SourceTree, position: TextPosition) -> Vec<Completion> {
let prompt_block = ancestor_node_at_position(syntax, position, "prompt_block");
let validate_block = ancestor_node_at_position(syntax, position, "validate_block");
let prompt_type = prompt_block.and_then(|node| shared::prompt_type_for_block(syntax, node));
let mut items = Vec::new();
if matches!(prompt_type, None | Some("string" | "paragraph")) {
items.extend([
completion(
"regex",
Some("Regular expression validation"),
CompletionKind::Property,
),
completion(
"min_length",
Some("Minimum string length"),
CompletionKind::Property,
),
completion(
"max_length",
Some("Maximum string length"),
CompletionKind::Property,
),
]);
}
if matches!(prompt_type, None | Some("multiselect")) {
items.extend([
completion(
"min_selections",
Some("Minimum number of selected values"),
CompletionKind::Property,
),
completion(
"max_selections",
Some("Maximum number of selected values"),
CompletionKind::Property,
),
]);
}
if let Some(validate_block) = validate_block {
items.retain(|item| {
let kind = match item.label() {
"regex" => "regex_attribute",
"min_length" => "min_length_attribute",
"max_length" => "max_length_attribute",
"min_selections" => "min_selections_attribute",
"max_selections" => "max_selections_attribute",
_ => return true,
};
find_named_descendant_by_kind(validate_block, kind).is_none()
});
}
items
}
fn prompt_type_completions() -> Vec<Completion> {
vec![
completion(
"string",
Some("Single-line text prompt"),
CompletionKind::Value,
),
completion(
"paragraph",
Some("Multi-line text prompt"),
CompletionKind::Value,
),
completion("bool", Some("Boolean yes/no prompt"), CompletionKind::Value),
completion(
"select",
Some("Single-choice prompt"),
CompletionKind::Value,
),
completion(
"multiselect",
Some("Multi-choice prompt"),
CompletionKind::Value,
),
]
}
fn depends_on_completions(symbols: &[Symbol]) -> Vec<Completion> {
let mut completions = vec![
completion(
"all",
Some("Require all nested conditions"),
CompletionKind::Function,
),
completion(
"any",
Some("Require any nested condition"),
CompletionKind::Function,
),
];
completions.extend(symbols.iter().filter_map(|symbol| {
if symbol.kind() == SymbolKind::Prompt {
Some(completion(
symbol.name(),
Some("Prompt reference"),
CompletionKind::Reference,
))
} else {
None
}
}));
completions
}
fn completion(label: &str, detail: Option<&str>, kind: CompletionKind) -> Completion {
Completion {
label: label.to_owned(),
detail: detail.map(str::to_owned),
kind,
}
}