diff --git a/src/lsp/completion.rs b/src/lsp/completion.rs index 75eaae4..22a2c5d 100644 --- a/src/lsp/completion.rs +++ b/src/lsp/completion.rs @@ -75,12 +75,11 @@ pub fn get_completions(doc: &Document, params: &CompletionParams) -> Option { - // Inside a field block, suggest fields from the species/templates + // Always include generic field keywords (age, bond, from, etc.) + items.extend(field_keyword_completions()); + // Also include contextual species/template fields if available 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 => { @@ -485,39 +484,35 @@ fn determine_context(text: &str, offset: usize) -> CompletionContext { // Track state by analyzing tokens let mut nesting_level: i32 = 0; - let mut last_keyword = None; + let mut last_context: Option = 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; - } + 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::Colon => seen_colon_without_brace = true, + | Token::Behavior => { + last_context = Some(CompletionContext::InBehavior); + 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()); + | Token::LifeArc => { + last_context = Some(CompletionContext::InLifeArc); + seen_colon_without_brace = false; + }, + | Token::Relationship => { + last_context = Some(CompletionContext::InRelationship); + seen_colon_without_brace = false; + }, + | Token::Character | + Token::Template | + Token::Species | + Token::Institution | + Token::Location => { + last_context = Some(CompletionContext::InFieldBlock); seen_colon_without_brace = false; }, | _ => {}, @@ -534,18 +529,7 @@ fn determine_context(text: &str, offset: usize) -> CompletionContext { 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, - } + last_context.unwrap_or(CompletionContext::Unknown) } /// Get entity completions (all symbols) @@ -707,6 +691,7 @@ fn field_keyword_completions() -> Vec { /// Get behavior tree keywords fn behavior_keyword_completions() -> Vec { vec![ + // Symbolic syntax keyword_item( "?", "Selector node (try options in order)", @@ -715,6 +700,38 @@ fn behavior_keyword_completions() -> Vec { 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}"), + // Word-based syntax + keyword_item( + "choose", + "Selector — try options in order", + "choose {\n $0\n}", + ), + keyword_item("then", "Sequence — execute in order", "then {\n $0\n}"), + keyword_item("repeat", "Repeat node", "repeat {\n $0\n}"), + keyword_item( + "if", + "Conditional decorator", + "if(${1:condition}) {\n $0\n}", + ), + keyword_item("when", "Condition node", "when(${1:condition})"), + keyword_item("invert", "Invert result", "invert {\n $0\n}"), + keyword_item("retry", "Retry decorator", "retry(${1:3}) {\n $0\n}"), + keyword_item( + "timeout", + "Timeout decorator", + "timeout(${1:5s}) {\n $0\n}", + ), + keyword_item( + "cooldown", + "Cooldown decorator", + "cooldown(${1:10s}) {\n $0\n}", + ), + keyword_item( + "succeed_always", + "Always succeed", + "succeed_always {\n $0\n}", + ), + keyword_item("fail_always", "Always fail", "fail_always {\n $0\n}"), ] } diff --git a/src/lsp/completion_tests.rs b/src/lsp/completion_tests.rs index fb46a45..c1b5d9f 100644 --- a/src/lsp/completion_tests.rs +++ b/src/lsp/completion_tests.rs @@ -110,10 +110,33 @@ mod tests { if let Some(response) = result { match response { | tower_lsp::lsp_types::CompletionResponse::Array(items) => { - // Should have behavior tree keywords + // Should have symbolic behavior tree keywords assert!(items.iter().any(|item| item.label == "?")); assert!(items.iter().any(|item| item.label == ">")); assert!(items.iter().any(|item| item.label == "*")); + // Should have word-based behavior tree keywords + assert!(items.iter().any(|item| item.label == "choose")); + assert!(items.iter().any(|item| item.label == "then")); + assert!(items.iter().any(|item| item.label == "repeat")); + assert!(items.iter().any(|item| item.label == "if")); + assert!(items.iter().any(|item| item.label == "when")); + // Should NOT have top-level declaration keywords + assert!( + !items.iter().any(|item| item.label == "institution"), + "institution should not appear inside behavior block" + ); + assert!( + !items.iter().any(|item| item.label == "species"), + "species should not appear inside behavior block" + ); + assert!( + !items.iter().any(|item| item.label == "character"), + "character should not appear inside behavior block" + ); + assert!( + !items.iter().any(|item| item.label == "location"), + "location should not appear inside behavior block" + ); }, | _ => panic!("Expected array response"), } @@ -136,6 +159,61 @@ mod tests { // Should have life arc keywords assert!(items.iter().any(|item| item.label == "state")); assert!(items.iter().any(|item| item.label == "on")); + // Should NOT have top-level declaration keywords + assert!( + !items.iter().any(|item| item.label == "institution"), + "institution should not appear inside life_arc block" + ); + assert!( + !items.iter().any(|item| item.label == "behavior"), + "behavior should not appear inside life_arc block" + ); + assert!( + !items.iter().any(|item| item.label == "character"), + "character should not appear inside life_arc block" + ); + }, + | _ => panic!("Expected array response"), + } + } + } + + #[test] + fn test_behavior_no_toplevel_keywords() { + // Cursor inside nested `then` block — context should remain InBehavior + let source = "behavior Test {\n then {\n \n }\n}"; + let doc = Document::new(source.to_string()); + // Position inside nested then block (line 2, after spaces) + let params = make_params(2, 8); + + let result = completion::get_completions(&doc, ¶ms); + assert!(result.is_some()); + + if let Some(response) = result { + match response { + | tower_lsp::lsp_types::CompletionResponse::Array(items) => { + // Behavior keywords should still be present at any nesting depth + assert!( + items.iter().any(|item| item.label == "?"), + "? should appear in nested behavior block" + ); + assert!( + items.iter().any(|item| item.label == "choose"), + "choose should appear in nested behavior block" + ); + // Top-level keywords must not appear + assert!( + !items.iter().any(|item| item.label == "institution"), + "institution must not appear in nested behavior block" + ); + assert!( + !items.iter().any(|item| item.label == "species"), + "species must not appear in nested behavior block" + ); + assert!( + !items.iter().any(|item| item.label == "character"), + "character must not appear in nested behavior block" + ); }, | _ => panic!("Expected array response"), }