Added top-level keyword completions for: - concept: with semicolon-terminated snippet - sub_concept: with dot notation snippet - concept_comparison: with brace-delimited snippet Added test verifying type system keywords appear in completions with correct snippet formatting.
775 lines
30 KiB
Rust
775 lines
30 KiB
Rust
//! Autocomplete/completion provider
|
|
//!
|
|
//! Provides context-aware completion suggestions for:
|
|
//! - Keywords (filtered by context)
|
|
//! - Entity names (characters, templates, etc.)
|
|
//! - Field names (from templates/species when in character block)
|
|
//! - Type names (templates/species when after ':')
|
|
//! - Enum values
|
|
//! - Action names (in behavior trees)
|
|
|
|
use tower_lsp::lsp_types::{
|
|
CompletionItem,
|
|
CompletionItemKind,
|
|
CompletionList,
|
|
CompletionParams,
|
|
CompletionResponse,
|
|
Documentation,
|
|
MarkupContent,
|
|
MarkupKind,
|
|
};
|
|
|
|
use super::document::Document;
|
|
use crate::syntax::ast::Value;
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
enum CompletionContext {
|
|
/// Top-level of the document
|
|
TopLevel,
|
|
/// Inside a character/template field block
|
|
InFieldBlock,
|
|
/// After a colon (expecting a type or value)
|
|
AfterColon,
|
|
/// Inside a behavior tree
|
|
InBehavior,
|
|
/// Inside a life arc
|
|
InLifeArc,
|
|
/// Inside a relationship
|
|
InRelationship,
|
|
/// Unknown context
|
|
Unknown,
|
|
}
|
|
|
|
/// Get completion items at a position
|
|
pub fn get_completions(doc: &Document, params: &CompletionParams) -> Option<CompletionResponse> {
|
|
let position = params.text_document_position.position;
|
|
|
|
// Check for field accessor using the specialized function
|
|
// It will return Some only if there's an identifier followed by a dot
|
|
if let Some(field_items) = get_field_accessor_completions(doc, position) {
|
|
return Some(CompletionResponse::List(CompletionList {
|
|
is_incomplete: false,
|
|
items: field_items,
|
|
}));
|
|
}
|
|
|
|
// Convert position to byte offset for context-based completions
|
|
let offset = position_to_offset(doc, position.line as usize, position.character as usize)?;
|
|
|
|
// Check if we're typing a new identifier name after a declaration keyword
|
|
if is_typing_declaration_name(&doc.text, offset) {
|
|
// Don't show completions when typing a new identifier
|
|
return None;
|
|
}
|
|
|
|
// Determine context by analyzing text around cursor
|
|
let context = determine_context(&doc.text, offset);
|
|
|
|
let mut items = Vec::new();
|
|
|
|
match context {
|
|
| CompletionContext::TopLevel => {
|
|
// At top level, suggest declaration keywords
|
|
items.extend(top_level_keyword_completions());
|
|
// Also suggest existing entity names for reference
|
|
items.extend(entity_completions(doc));
|
|
},
|
|
| CompletionContext::InFieldBlock => {
|
|
// Inside a field block, suggest fields from the species/templates
|
|
if let Some(species_fields) = get_contextual_field_completions(doc, offset) {
|
|
items.extend(species_fields);
|
|
} else {
|
|
// Fallback to generic field keywords if we can't determine context
|
|
items.extend(field_keyword_completions());
|
|
}
|
|
},
|
|
| CompletionContext::AfterColon => {
|
|
// After colon, suggest types (templates, species)
|
|
items.extend(type_completions(doc));
|
|
},
|
|
| CompletionContext::InBehavior => {
|
|
// In behavior tree, suggest behavior-specific keywords
|
|
items.extend(behavior_keyword_completions());
|
|
items.extend(behavior_completions(doc)); // Reference to other
|
|
// behaviors
|
|
},
|
|
| CompletionContext::InLifeArc => {
|
|
// In life arc, suggest state-related keywords
|
|
items.extend(life_arc_keyword_completions());
|
|
},
|
|
| CompletionContext::InRelationship => {
|
|
// In relationship, suggest relationship-specific keywords
|
|
items.extend(relationship_keyword_completions());
|
|
items.extend(character_completions(doc)); // For participants
|
|
},
|
|
| CompletionContext::Unknown => {
|
|
// When context is unclear, provide all completions
|
|
items.extend(all_keyword_completions());
|
|
items.extend(entity_completions(doc));
|
|
},
|
|
}
|
|
|
|
// Set sort_text for proper ordering: field accessors first (0xxx), then others
|
|
// (1xxx)
|
|
for item in &mut items {
|
|
let detail = item.detail.as_deref().unwrap_or("");
|
|
let is_field = detail.contains("field") || detail.contains("trait");
|
|
|
|
// Field accessors get "0" prefix, others get "1" prefix
|
|
let prefix = if is_field { "0" } else { "1" };
|
|
item.sort_text = Some(format!("{}{}", prefix, item.label));
|
|
}
|
|
|
|
// Sort by sort_text for consistent ordering
|
|
items.sort_by(|a, b| {
|
|
let sort_a = a.sort_text.as_deref().unwrap_or(&a.label);
|
|
let sort_b = b.sort_text.as_deref().unwrap_or(&b.label);
|
|
sort_a.cmp(sort_b)
|
|
});
|
|
|
|
Some(CompletionResponse::Array(items))
|
|
}
|
|
|
|
/// Convert LSP position to byte offset
|
|
fn position_to_offset(doc: &Document, line: usize, character: usize) -> Option<usize> {
|
|
let line_start = doc.positions.line_offset(line)?;
|
|
Some(line_start + character)
|
|
}
|
|
|
|
/// Check if we're typing a new identifier name after a declaration keyword
|
|
fn is_typing_declaration_name(text: &str, offset: usize) -> bool {
|
|
use crate::syntax::lexer::{
|
|
Lexer,
|
|
Token,
|
|
};
|
|
|
|
// Get text before cursor (up to 200 chars)
|
|
let start = offset.saturating_sub(200);
|
|
let before = &text[start..offset.min(text.len())];
|
|
|
|
// Tokenize using lexer
|
|
let lexer = Lexer::new(before);
|
|
let tokens: Vec<_> = lexer.collect();
|
|
|
|
// Check if the last token (or second-to-last if we just typed an identifier)
|
|
// is a declaration keyword
|
|
if !tokens.is_empty() {
|
|
let last_idx = tokens.len() - 1;
|
|
|
|
// Check last token
|
|
if let (_offset, Token::Ident(keyword), _end) = &tokens[last_idx] {
|
|
if matches!(
|
|
keyword.as_str(),
|
|
"character" |
|
|
"template" |
|
|
"species" |
|
|
"behavior" |
|
|
"life_arc" |
|
|
"relationship" |
|
|
"institution" |
|
|
"location" |
|
|
"enum" |
|
|
"schedule"
|
|
) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Check second-to-last token (in case we're in the middle of typing an
|
|
// identifier)
|
|
if tokens.len() >= 2 {
|
|
let second_last_idx = tokens.len() - 2;
|
|
if let (_offset, Token::Ident(keyword), _end) = &tokens[second_last_idx] {
|
|
if matches!(
|
|
keyword.as_str(),
|
|
"character" |
|
|
"template" |
|
|
"species" |
|
|
"behavior" |
|
|
"life_arc" |
|
|
"relationship" |
|
|
"institution" |
|
|
"location" |
|
|
"enum" |
|
|
"schedule"
|
|
) {
|
|
// Make sure the last token is an identifier (not a colon or brace)
|
|
if let (_offset, Token::Ident(_), _end) = &tokens[last_idx] {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
false
|
|
}
|
|
|
|
/// Format a value as its type string for documentation
|
|
fn format_value_type(value: &Value) -> String {
|
|
match value {
|
|
| Value::Identifier(path) => path.join("."),
|
|
| Value::Text(_) => "Text".to_string(),
|
|
| Value::Number(_) => "Number".to_string(),
|
|
| Value::Decimal(_) => "Decimal".to_string(),
|
|
| Value::Boolean(_) => "Boolean".to_string(),
|
|
| Value::List(items) => {
|
|
if items.is_empty() {
|
|
"List".to_string()
|
|
} else {
|
|
format!("[{}]", format_value_type(&items[0]))
|
|
}
|
|
},
|
|
| Value::Object(_) => "Object".to_string(),
|
|
| Value::Range(start, end) => {
|
|
format!("{}..{}", format_value_type(start), format_value_type(end))
|
|
},
|
|
| Value::Time(_) => "Time".to_string(),
|
|
| Value::Duration(_) => "Duration".to_string(),
|
|
| Value::ProseBlock(_) => "ProseBlock".to_string(),
|
|
| Value::Override(_) => "Override".to_string(),
|
|
| Value::Any => "Any".to_string(),
|
|
}
|
|
}
|
|
|
|
/// Get field completions based on the current character/template context
|
|
fn get_contextual_field_completions(doc: &Document, offset: usize) -> Option<Vec<CompletionItem>> {
|
|
use crate::{
|
|
resolve::names::DeclKind,
|
|
syntax::ast::Declaration,
|
|
};
|
|
|
|
let ast = doc.ast.as_ref()?;
|
|
|
|
// Find which declaration contains the cursor offset
|
|
for decl in &ast.declarations {
|
|
match decl {
|
|
| Declaration::Character(character) => {
|
|
// Check if cursor is inside this character block
|
|
if offset >= character.span.start && offset <= character.span.end {
|
|
let mut items = Vec::new();
|
|
|
|
// Add special keywords
|
|
items.push(simple_item(
|
|
"from",
|
|
"Apply a template",
|
|
"from ${1:TemplateName}",
|
|
));
|
|
items.push(simple_item(
|
|
"include",
|
|
"Include a template",
|
|
"include ${1:TemplateName}",
|
|
));
|
|
|
|
// Add fields from species
|
|
if let Some(ref species_name) = character.species {
|
|
if let Some(species_entry) = doc.name_table.resolve_name(species_name) {
|
|
if species_entry.kind == DeclKind::Species {
|
|
for species_decl in &ast.declarations {
|
|
if let Declaration::Species(species) = species_decl {
|
|
if &species.name == species_name {
|
|
for field in &species.fields {
|
|
items.push(CompletionItem {
|
|
label: format!("{}:", field.name),
|
|
kind: Some(CompletionItemKind::FIELD),
|
|
detail: Some(format!("({})", species_name)),
|
|
insert_text: Some(format!("{}: $0", field.name)),
|
|
insert_text_format: Some(tower_lsp::lsp_types::InsertTextFormat::SNIPPET),
|
|
..Default::default()
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return Some(items);
|
|
}
|
|
},
|
|
| Declaration::Template(template) => {
|
|
// Check if cursor is inside this template block
|
|
if offset >= template.span.start && offset <= template.span.end {
|
|
// Templates can suggest common field patterns
|
|
return Some(vec![simple_item(
|
|
"include",
|
|
"Include a template",
|
|
"include ${1:TemplateName}",
|
|
)]);
|
|
}
|
|
},
|
|
| _ => {},
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
/// Get field completions when triggered by `.` using lexer
|
|
fn get_field_accessor_completions(
|
|
doc: &Document,
|
|
position: tower_lsp::lsp_types::Position,
|
|
) -> Option<Vec<CompletionItem>> {
|
|
use crate::{
|
|
resolve::names::DeclKind,
|
|
syntax::{
|
|
ast::Declaration,
|
|
lexer::{
|
|
Lexer,
|
|
Token,
|
|
},
|
|
},
|
|
};
|
|
|
|
// Lex the line up to the cursor to find the identifier before the dot
|
|
let line_offset = doc.positions.line_offset(position.line as usize)?;
|
|
let line_end = (line_offset + position.character as usize).min(doc.text.len());
|
|
let line_text = &doc.text[line_offset..line_end];
|
|
|
|
// Lex tokens on this line
|
|
let lexer = Lexer::new(line_text);
|
|
let tokens: Vec<_> = lexer.collect();
|
|
|
|
// Check if there's a dot token - if not, this isn't a field accessor
|
|
let has_dot = tokens
|
|
.iter()
|
|
.any(|(_, token, _)| matches!(token, Token::Dot));
|
|
if !has_dot {
|
|
return None;
|
|
}
|
|
|
|
// Find the last identifier before the last dot
|
|
let mut last_ident = None;
|
|
for (_start, token, _end) in &tokens {
|
|
match token {
|
|
| Token::Ident(name) => last_ident = Some(name.clone()),
|
|
| Token::Dot => {
|
|
// We found a dot - if we have an identifier, that's our target
|
|
if last_ident.is_some() {
|
|
break;
|
|
}
|
|
},
|
|
| _ => {},
|
|
}
|
|
}
|
|
|
|
// If there's a dot but no identifier, return empty list to block keywords
|
|
let identifier = match last_ident {
|
|
| Some(id) => id,
|
|
| None => return Some(Vec::new()),
|
|
};
|
|
|
|
// Look up the identifier - if it fails, still return empty to block keywords
|
|
let entry = match doc.name_table.resolve_name(&identifier) {
|
|
| Some(e) => e,
|
|
| None => return Some(Vec::new()),
|
|
};
|
|
let ast = match doc.ast.as_ref() {
|
|
| Some(a) => a,
|
|
| None => return Some(Vec::new()),
|
|
};
|
|
|
|
let mut items = Vec::new();
|
|
|
|
match entry.kind {
|
|
| DeclKind::Character => {
|
|
for decl in &ast.declarations {
|
|
if let Declaration::Character(character) = decl {
|
|
if character.name == identifier {
|
|
// Add character's own fields
|
|
for field in &character.fields {
|
|
let value_type = format_value_type(&field.value);
|
|
items.push(CompletionItem {
|
|
label: field.name.clone(),
|
|
kind: Some(CompletionItemKind::FIELD),
|
|
detail: None, // Keep inline display clean
|
|
documentation: Some(Documentation::MarkupContent(MarkupContent {
|
|
kind: MarkupKind::Markdown,
|
|
value: format!(
|
|
"**Field** of `{}`\n\nType: `{}`",
|
|
identifier, value_type
|
|
),
|
|
})),
|
|
..Default::default()
|
|
});
|
|
}
|
|
|
|
// Add species fields
|
|
if let Some(ref species_name) = character.species {
|
|
if let Some(species_entry) = doc.name_table.resolve_name(species_name) {
|
|
if species_entry.kind == DeclKind::Species {
|
|
for decl in &ast.declarations {
|
|
if let Declaration::Species(species) = decl {
|
|
if &species.name == species_name {
|
|
for field in &species.fields {
|
|
let value_type =
|
|
format_value_type(&field.value);
|
|
items.push(CompletionItem {
|
|
label: field.name.clone(),
|
|
kind: Some(CompletionItemKind::FIELD),
|
|
detail: Some(format!("({})", species_name)),
|
|
documentation: Some(Documentation::MarkupContent(MarkupContent {
|
|
kind: MarkupKind::Markdown,
|
|
value: format!("**Trait** from `{}`\n\nType: `{}`", species_name, value_type),
|
|
})),
|
|
..Default::default()
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add template fields
|
|
if let Some(ref template_names) = character.template {
|
|
for template_name in template_names {
|
|
if let Some(template_entry) =
|
|
doc.name_table.resolve_name(template_name)
|
|
{
|
|
if template_entry.kind == DeclKind::Template {
|
|
for decl in &ast.declarations {
|
|
if let Declaration::Template(template) = decl {
|
|
if &template.name == template_name {
|
|
for field in &template.fields {
|
|
let value_type =
|
|
format_value_type(&field.value);
|
|
items.push(CompletionItem {
|
|
label: field.name.clone(),
|
|
kind: Some(CompletionItemKind::FIELD),
|
|
detail: Some(format!("({})", template_name)),
|
|
documentation: Some(Documentation::MarkupContent(MarkupContent {
|
|
kind: MarkupKind::Markdown,
|
|
value: format!("**Template field** from `{}`\n\nType: `{}`", template_name, value_type),
|
|
})),
|
|
..Default::default()
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
// For non-character declarations, still return empty list to block keywords
|
|
| _ => return Some(Vec::new()),
|
|
}
|
|
|
|
// Always return Some to block keyword completions, even if no fields found
|
|
items.sort_by(|a, b| a.label.cmp(&b.label));
|
|
Some(items)
|
|
}
|
|
|
|
/// Determine completion context by analyzing tokens around cursor using lexer
|
|
fn determine_context(text: &str, offset: usize) -> CompletionContext {
|
|
use crate::syntax::lexer::{
|
|
Lexer,
|
|
Token,
|
|
};
|
|
|
|
// Get text before cursor (up to 500 chars for context)
|
|
let start = offset.saturating_sub(500);
|
|
let before = &text[start..offset.min(text.len())];
|
|
|
|
// Tokenize using lexer
|
|
let lexer = Lexer::new(before);
|
|
let tokens: Vec<_> = lexer.collect();
|
|
|
|
// Track state by analyzing tokens
|
|
let mut nesting_level: i32 = 0;
|
|
let mut last_keyword = None;
|
|
let mut seen_colon_without_brace = false;
|
|
|
|
for (_offset, token, _end) in &tokens {
|
|
match token {
|
|
| Token::LBrace => {
|
|
nesting_level += 1;
|
|
if seen_colon_without_brace {
|
|
// Opening brace after colon - we've entered the block
|
|
seen_colon_without_brace = false;
|
|
}
|
|
},
|
|
| Token::RBrace => nesting_level = nesting_level.saturating_sub(1),
|
|
| Token::Colon => {
|
|
// Mark that we've seen a colon
|
|
seen_colon_without_brace = true;
|
|
},
|
|
| Token::Ident(keyword)
|
|
if matches!(
|
|
keyword.as_str(),
|
|
"character" |
|
|
"template" |
|
|
"species" |
|
|
"behavior" |
|
|
"life_arc" |
|
|
"relationship" |
|
|
"institution" |
|
|
"location" |
|
|
"enum" |
|
|
"schedule"
|
|
) =>
|
|
{
|
|
last_keyword = Some(keyword.clone());
|
|
seen_colon_without_brace = false;
|
|
},
|
|
| _ => {},
|
|
}
|
|
}
|
|
|
|
// If we saw a colon without a brace after it, we're in type position
|
|
if seen_colon_without_brace {
|
|
return CompletionContext::AfterColon;
|
|
}
|
|
|
|
// At top level if no nesting
|
|
if nesting_level == 0 {
|
|
return CompletionContext::TopLevel;
|
|
}
|
|
|
|
// Determine context based on last keyword and nesting
|
|
match last_keyword.as_deref() {
|
|
| Some("behavior") if nesting_level > 0 => CompletionContext::InBehavior,
|
|
| Some("life_arc") if nesting_level > 0 => CompletionContext::InLifeArc,
|
|
| Some("relationship") if nesting_level > 0 => CompletionContext::InRelationship,
|
|
| Some("character" | "template" | "species" | "institution" | "location")
|
|
if nesting_level > 0 =>
|
|
{
|
|
CompletionContext::InFieldBlock
|
|
},
|
|
| _ => CompletionContext::Unknown,
|
|
}
|
|
}
|
|
|
|
/// Get entity completions (all symbols)
|
|
fn entity_completions(doc: &Document) -> Vec<CompletionItem> {
|
|
use crate::resolve::names::DeclKind;
|
|
|
|
let mut items = Vec::new();
|
|
|
|
for entry in doc.name_table.all_entries() {
|
|
let kind = match entry.kind {
|
|
| DeclKind::Character => CompletionItemKind::CLASS,
|
|
| DeclKind::Template => CompletionItemKind::INTERFACE,
|
|
| DeclKind::LifeArc => CompletionItemKind::FUNCTION,
|
|
| DeclKind::Schedule => CompletionItemKind::EVENT,
|
|
| DeclKind::Behavior => CompletionItemKind::MODULE,
|
|
| DeclKind::Institution => CompletionItemKind::MODULE,
|
|
| DeclKind::Relationship => CompletionItemKind::STRUCT,
|
|
| DeclKind::Location => CompletionItemKind::CONSTANT,
|
|
| DeclKind::Species => CompletionItemKind::CLASS,
|
|
};
|
|
|
|
let name = entry
|
|
.qualified_path
|
|
.last()
|
|
.unwrap_or(&String::new())
|
|
.clone();
|
|
|
|
items.push(CompletionItem {
|
|
label: name,
|
|
kind: Some(kind),
|
|
detail: Some(format!("{:?}", entry.kind)),
|
|
..Default::default()
|
|
});
|
|
}
|
|
|
|
items
|
|
}
|
|
|
|
/// Get type completions (templates and species)
|
|
fn type_completions(doc: &Document) -> Vec<CompletionItem> {
|
|
use crate::resolve::names::DeclKind;
|
|
|
|
let mut items = Vec::new();
|
|
|
|
for entry in doc.name_table.all_entries() {
|
|
match entry.kind {
|
|
| DeclKind::Template | DeclKind::Species => {
|
|
let name = entry
|
|
.qualified_path
|
|
.last()
|
|
.unwrap_or(&String::new())
|
|
.clone();
|
|
items.push(CompletionItem {
|
|
label: name,
|
|
kind: Some(CompletionItemKind::INTERFACE),
|
|
detail: Some(format!("{:?}", entry.kind)),
|
|
documentation: Some(Documentation::String("Type annotation".to_string())),
|
|
..Default::default()
|
|
});
|
|
},
|
|
| _ => {},
|
|
}
|
|
}
|
|
|
|
items
|
|
}
|
|
|
|
/// Get behavior completions
|
|
fn behavior_completions(doc: &Document) -> Vec<CompletionItem> {
|
|
use crate::resolve::names::DeclKind;
|
|
|
|
let mut items = Vec::new();
|
|
|
|
for entry in doc.name_table.entries_of_kind(DeclKind::Behavior) {
|
|
let name = entry
|
|
.qualified_path
|
|
.last()
|
|
.unwrap_or(&String::new())
|
|
.clone();
|
|
items.push(CompletionItem {
|
|
label: format!("@{}", name),
|
|
kind: Some(CompletionItemKind::REFERENCE),
|
|
detail: Some("Behavior tree reference".to_string()),
|
|
insert_text: Some(format!("@{}", name)),
|
|
..Default::default()
|
|
});
|
|
}
|
|
|
|
items
|
|
}
|
|
|
|
/// Get character completions
|
|
fn character_completions(doc: &Document) -> Vec<CompletionItem> {
|
|
use crate::resolve::names::DeclKind;
|
|
|
|
let mut items = Vec::new();
|
|
|
|
for entry in doc.name_table.entries_of_kind(DeclKind::Character) {
|
|
let name = entry
|
|
.qualified_path
|
|
.last()
|
|
.unwrap_or(&String::new())
|
|
.clone();
|
|
items.push(CompletionItem {
|
|
label: name,
|
|
kind: Some(CompletionItemKind::CLASS),
|
|
detail: Some("Character".to_string()),
|
|
..Default::default()
|
|
});
|
|
}
|
|
|
|
items
|
|
}
|
|
|
|
/// Get all keyword completions (fallback)
|
|
fn all_keyword_completions() -> Vec<CompletionItem> {
|
|
let mut items = top_level_keyword_completions();
|
|
items.extend(field_keyword_completions());
|
|
items.extend(behavior_keyword_completions());
|
|
items.extend(life_arc_keyword_completions());
|
|
items.extend(relationship_keyword_completions());
|
|
items
|
|
}
|
|
|
|
/// Get top-level declaration keywords
|
|
fn top_level_keyword_completions() -> Vec<CompletionItem> {
|
|
vec![
|
|
keyword_item("character", "Define a character entity", "character ${1:Name}: ${2:Species} {\n $0\n}"),
|
|
keyword_item("template", "Define a reusable field template", "template ${1:Name} {\n $0\n}"),
|
|
keyword_item("life_arc", "Define a state machine", "life_arc ${1:Name} {\n state ${2:initial} {\n $0\n }\n}"),
|
|
keyword_item("schedule", "Define a daily schedule", "schedule ${1:Name} {\n ${2:08:00} -> ${3:09:00}: ${4:block_name} {\n $0\n }\n}"),
|
|
keyword_item("behavior", "Define a behavior tree", "behavior ${1:Name} {\n $0\n}"),
|
|
keyword_item("institution", "Define an organization", "institution ${1:Name} {\n $0\n}"),
|
|
keyword_item("relationship", "Define a relationship", "relationship ${1:Name} {\n $0\n}"),
|
|
keyword_item("location", "Define a location", "location ${1:Name} {\n $0\n}"),
|
|
keyword_item("species", "Define a species", "species ${1:Name} {\n $0\n}"),
|
|
keyword_item("use", "Import declarations", "use ${1:path::to::item};"),
|
|
keyword_item("concept", "Define an algebraic data type", "concept ${1:Name};"),
|
|
keyword_item("sub_concept", "Define a sub-type", "sub_concept ${1:Parent}.${2:Name} {\n $0\n}"),
|
|
keyword_item("concept_comparison", "Pattern match on concepts", "concept_comparison ${1:Name} {\n $0\n}"),
|
|
]
|
|
}
|
|
|
|
/// Get field-level keywords
|
|
fn field_keyword_completions() -> Vec<CompletionItem> {
|
|
vec![
|
|
keyword_item("from", "Apply a template", "from ${1:TemplateName}"),
|
|
keyword_item("include", "Include a template", "include ${1:TemplateName}"),
|
|
keyword_item("strict", "Enforce strict template fields", "strict"),
|
|
// Common field names
|
|
simple_item("age", "Age field", "age: ${1:0}"),
|
|
simple_item("name", "Name field", "name: \"${1:Name}\""),
|
|
simple_item("bond", "Bond trait (0.0-1.0)", "bond: ${1:0.5}"),
|
|
simple_item("trust", "Trust trait (0.0-1.0)", "trust: ${1:0.5}"),
|
|
simple_item("love", "Love trait (0.0-1.0)", "love: ${1:0.5}"),
|
|
]
|
|
}
|
|
|
|
/// Get behavior tree keywords
|
|
fn behavior_keyword_completions() -> Vec<CompletionItem> {
|
|
vec![
|
|
keyword_item(
|
|
"?",
|
|
"Selector node (try options in order)",
|
|
"? {\n $0\n}",
|
|
),
|
|
keyword_item(">", "Sequence node (execute in order)", "> {\n $0\n}"),
|
|
keyword_item("*", "Repeat node (loop forever)", "* {\n $0\n}"),
|
|
simple_item("@", "Subtree reference", "@${1:behavior::name}"),
|
|
]
|
|
}
|
|
|
|
/// Get life arc keywords
|
|
fn life_arc_keyword_completions() -> Vec<CompletionItem> {
|
|
vec![
|
|
keyword_item(
|
|
"state",
|
|
"Define a life arc state",
|
|
"state ${1:name} {\n $0\n}",
|
|
),
|
|
keyword_item(
|
|
"on",
|
|
"Define a transition",
|
|
"on ${1:condition} -> ${2:target_state}",
|
|
),
|
|
]
|
|
}
|
|
|
|
/// Get relationship keywords
|
|
fn relationship_keyword_completions() -> Vec<CompletionItem> {
|
|
vec![
|
|
keyword_item(
|
|
"as",
|
|
"Define participant role",
|
|
"${1:CharacterName} as ${2:role} {\n $0\n}",
|
|
),
|
|
keyword_item("self", "Reference self in relationships", "self.${1:field}"),
|
|
keyword_item("other", "Reference other participant", "other.${1:field}"),
|
|
]
|
|
}
|
|
|
|
fn keyword_item(label: &str, detail: &str, snippet: &str) -> CompletionItem {
|
|
CompletionItem {
|
|
label: label.to_string(),
|
|
kind: Some(CompletionItemKind::KEYWORD),
|
|
detail: Some(detail.to_string()),
|
|
documentation: Some(Documentation::MarkupContent(MarkupContent {
|
|
kind: MarkupKind::Markdown,
|
|
value: format!("**{}**\n\n{}", label, detail),
|
|
})),
|
|
insert_text: Some(snippet.to_string()),
|
|
insert_text_format: Some(tower_lsp::lsp_types::InsertTextFormat::SNIPPET),
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
fn simple_item(label: &str, detail: &str, snippet: &str) -> CompletionItem {
|
|
CompletionItem {
|
|
label: label.to_string(),
|
|
kind: Some(CompletionItemKind::PROPERTY),
|
|
detail: Some(detail.to_string()),
|
|
insert_text: Some(snippet.to_string()),
|
|
insert_text_format: Some(tower_lsp::lsp_types::InsertTextFormat::SNIPPET),
|
|
..Default::default()
|
|
}
|
|
}
|