fix(lsp): context-aware completions now use correct token variants
The Logos lexer produces dedicated token variants for all keywords (Token::Behavior, Token::LifeArc, etc.) — Token::Ident is only for user-defined identifiers. determine_context() was matching against Token::Ident(keyword), so last_keyword was always None and every nested block returned CompletionContext::Unknown, causing all_keyword_completions() to dump top-level declaration keywords everywhere. Changes: - determine_context(): match actual token variants (Token::Behavior, Token::LifeArc, Token::Relationship, Token::Character, etc.) and track last_context directly instead of a String keyword - behavior_keyword_completions(): add all word-based BT keywords (choose, then, repeat, if, when, invert, retry, timeout, cooldown, succeed_always, fail_always) - InFieldBlock arm: always include field_keyword_completions() so generic fields (age, bond, etc.) appear even without a species - Tests: add negative assertions confirming top-level keywords are absent inside behavior/life_arc blocks; add new test test_behavior_no_toplevel_keywords for nested block isolation
This commit is contained in:
@@ -75,12 +75,11 @@ pub fn get_completions(doc: &Document, params: &CompletionParams) -> Option<Comp
|
||||
items.extend(entity_completions(doc));
|
||||
},
|
||||
| CompletionContext::InFieldBlock => {
|
||||
// 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<CompletionContext> = 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<CompletionItem> {
|
||||
/// Get behavior tree keywords
|
||||
fn behavior_keyword_completions() -> Vec<CompletionItem> {
|
||||
vec![
|
||||
// Symbolic syntax
|
||||
keyword_item(
|
||||
"?",
|
||||
"Selector node (try options in order)",
|
||||
@@ -715,6 +700,38 @@ fn behavior_keyword_completions() -> Vec<CompletionItem> {
|
||||
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}"),
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -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"),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user