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));
|
items.extend(entity_completions(doc));
|
||||||
},
|
},
|
||||||
| CompletionContext::InFieldBlock => {
|
| 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) {
|
if let Some(species_fields) = get_contextual_field_completions(doc, offset) {
|
||||||
items.extend(species_fields);
|
items.extend(species_fields);
|
||||||
} else {
|
|
||||||
// Fallback to generic field keywords if we can't determine context
|
|
||||||
items.extend(field_keyword_completions());
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
| CompletionContext::AfterColon => {
|
| CompletionContext::AfterColon => {
|
||||||
@@ -485,39 +484,35 @@ fn determine_context(text: &str, offset: usize) -> CompletionContext {
|
|||||||
|
|
||||||
// Track state by analyzing tokens
|
// Track state by analyzing tokens
|
||||||
let mut nesting_level: i32 = 0;
|
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;
|
let mut seen_colon_without_brace = false;
|
||||||
|
|
||||||
for (_offset, token, _end) in &tokens {
|
for (_offset, token, _end) in &tokens {
|
||||||
match token {
|
match token {
|
||||||
| Token::LBrace => {
|
| Token::LBrace => {
|
||||||
nesting_level += 1;
|
nesting_level += 1;
|
||||||
if seen_colon_without_brace {
|
seen_colon_without_brace = false;
|
||||||
// Opening brace after colon - we've entered the block
|
|
||||||
seen_colon_without_brace = false;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
| Token::RBrace => nesting_level = nesting_level.saturating_sub(1),
|
| Token::RBrace => nesting_level = nesting_level.saturating_sub(1),
|
||||||
| Token::Colon => {
|
| Token::Colon => seen_colon_without_brace = true,
|
||||||
// Mark that we've seen a colon
|
| Token::Behavior => {
|
||||||
seen_colon_without_brace = true;
|
last_context = Some(CompletionContext::InBehavior);
|
||||||
|
seen_colon_without_brace = false;
|
||||||
},
|
},
|
||||||
| Token::Ident(keyword)
|
| Token::LifeArc => {
|
||||||
if matches!(
|
last_context = Some(CompletionContext::InLifeArc);
|
||||||
keyword.as_str(),
|
seen_colon_without_brace = false;
|
||||||
"character" |
|
},
|
||||||
"template" |
|
| Token::Relationship => {
|
||||||
"species" |
|
last_context = Some(CompletionContext::InRelationship);
|
||||||
"behavior" |
|
seen_colon_without_brace = false;
|
||||||
"life_arc" |
|
},
|
||||||
"relationship" |
|
| Token::Character |
|
||||||
"institution" |
|
Token::Template |
|
||||||
"location" |
|
Token::Species |
|
||||||
"enum" |
|
Token::Institution |
|
||||||
"schedule"
|
Token::Location => {
|
||||||
) =>
|
last_context = Some(CompletionContext::InFieldBlock);
|
||||||
{
|
|
||||||
last_keyword = Some(keyword.clone());
|
|
||||||
seen_colon_without_brace = false;
|
seen_colon_without_brace = false;
|
||||||
},
|
},
|
||||||
| _ => {},
|
| _ => {},
|
||||||
@@ -534,18 +529,7 @@ fn determine_context(text: &str, offset: usize) -> CompletionContext {
|
|||||||
return CompletionContext::TopLevel;
|
return CompletionContext::TopLevel;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine context based on last keyword and nesting
|
last_context.unwrap_or(CompletionContext::Unknown)
|
||||||
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)
|
/// Get entity completions (all symbols)
|
||||||
@@ -707,6 +691,7 @@ fn field_keyword_completions() -> Vec<CompletionItem> {
|
|||||||
/// Get behavior tree keywords
|
/// Get behavior tree keywords
|
||||||
fn behavior_keyword_completions() -> Vec<CompletionItem> {
|
fn behavior_keyword_completions() -> Vec<CompletionItem> {
|
||||||
vec![
|
vec![
|
||||||
|
// Symbolic syntax
|
||||||
keyword_item(
|
keyword_item(
|
||||||
"?",
|
"?",
|
||||||
"Selector node (try options in order)",
|
"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(">", "Sequence node (execute in order)", "> {\n $0\n}"),
|
||||||
keyword_item("*", "Repeat node (loop forever)", "* {\n $0\n}"),
|
keyword_item("*", "Repeat node (loop forever)", "* {\n $0\n}"),
|
||||||
simple_item("@", "Subtree reference", "@${1:behavior::name}"),
|
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 {
|
if let Some(response) = result {
|
||||||
match response {
|
match response {
|
||||||
| tower_lsp::lsp_types::CompletionResponse::Array(items) => {
|
| 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 == ">"));
|
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"),
|
| _ => panic!("Expected array response"),
|
||||||
}
|
}
|
||||||
@@ -136,6 +159,61 @@ mod tests {
|
|||||||
// Should have life arc keywords
|
// Should have life arc keywords
|
||||||
assert!(items.iter().any(|item| item.label == "state"));
|
assert!(items.iter().any(|item| item.label == "state"));
|
||||||
assert!(items.iter().any(|item| item.label == "on"));
|
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"),
|
| _ => panic!("Expected array response"),
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user