//! 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 { 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 { 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> { 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> { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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() } }