release: Storybook v0.2.0 - Major syntax and features update
BREAKING CHANGES: - Relationship syntax now requires blocks for all participants - Removed self/other perspective blocks from relationships - Replaced 'guard' keyword with 'if' for behavior tree decorators Language Features: - Add tree-sitter grammar with improved if/condition disambiguation - Add comprehensive tutorial and reference documentation - Add SBIR v0.2.0 binary format specification - Add resource linking system for behaviors and schedules - Add year-long schedule patterns (day, season, recurrence) - Add behavior tree enhancements (named nodes, decorators) Documentation: - Complete tutorial series (9 chapters) with baker family examples - Complete reference documentation for all language features - SBIR v0.2.0 specification with binary format details - Added locations and institutions documentation Examples: - Convert all examples to baker family scenario - Add comprehensive working examples Tooling: - Zed extension with LSP integration - Tree-sitter grammar for syntax highlighting - Build scripts and development tools Version Updates: - Main package: 0.1.0 → 0.2.0 - Tree-sitter grammar: 0.1.0 → 0.2.0 - Zed extension: 0.1.0 → 0.2.0 - Storybook editor: 0.1.0 → 0.2.0
This commit is contained in:
775
src/lsp/completion.rs
Normal file
775
src/lsp/completion.rs
Normal file
@@ -0,0 +1,775 @@
|
||||
//! 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::String(_) => "String".to_string(),
|
||||
| Value::Int(_) => "Int".to_string(),
|
||||
| Value::Float(_) => "Float".to_string(),
|
||||
| Value::Bool(_) => "Bool".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(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
let mut items = Vec::new();
|
||||
|
||||
// Add special keywords for templates
|
||||
items.push(simple_item(
|
||||
"include",
|
||||
"Include a template",
|
||||
"include ${1:TemplateName}",
|
||||
));
|
||||
|
||||
// Templates can suggest common field patterns
|
||||
return Some(items);
|
||||
}
|
||||
},
|
||||
| _ => {},
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
| Token::RBrace => nesting_level = nesting_level.saturating_sub(1),
|
||||
| Token::Colon => {
|
||||
// Mark that we've seen a colon
|
||||
seen_colon_without_brace = true;
|
||||
},
|
||||
| Token::LBrace if seen_colon_without_brace => {
|
||||
// Opening brace after colon - we've entered the block
|
||||
seen_colon_without_brace = false;
|
||||
},
|
||||
| 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,
|
||||
| DeclKind::Enum => CompletionItemKind::ENUM,
|
||||
};
|
||||
|
||||
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("enum", "Define an enumeration", "enum ${1:Name} {\n ${2:Value1}\n ${3:Value2}\n}"),
|
||||
keyword_item("use", "Import declarations", "use ${1:path::to::item};"),
|
||||
]
|
||||
}
|
||||
|
||||
/// 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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user