fix(lsp): fix remaining completion scoping issues
- is_typing_declaration_name(): had the same Token::Ident bug as determine_context() — matched string keywords against Token::Ident but the lexer produces dedicated variants. Extracted shared is_declaration_keyword() helper that matches actual token variants. - determine_context(): add Token::Schedule so schedule blocks get InFieldBlock context instead of falling through to Unknown. - Tests: add negative assertions to test_top_level_completions, test_field_block_completions, and test_relationship_completions. Add test_no_completions_after_declaration_keyword, test_no_completions_while_typing_name, and test_schedule_completions_no_toplevel.
This commit is contained in:
@@ -135,6 +135,24 @@ fn position_to_offset(doc: &Document, line: usize, character: usize) -> Option<u
|
||||
Some(line_start + character)
|
||||
}
|
||||
|
||||
/// Returns true if the token is a declaration keyword (character, behavior,
|
||||
/// etc.)
|
||||
fn is_declaration_keyword(token: &crate::syntax::lexer::Token) -> bool {
|
||||
use crate::syntax::lexer::Token;
|
||||
matches!(
|
||||
token,
|
||||
Token::Character |
|
||||
Token::Template |
|
||||
Token::Species |
|
||||
Token::Behavior |
|
||||
Token::LifeArc |
|
||||
Token::Relationship |
|
||||
Token::Institution |
|
||||
Token::Location |
|
||||
Token::Schedule
|
||||
)
|
||||
}
|
||||
|
||||
/// 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::{
|
||||
@@ -155,43 +173,16 @@ fn is_typing_declaration_name(text: &str, offset: usize) -> bool {
|
||||
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"
|
||||
) {
|
||||
// Check last token — cursor is right after a declaration keyword
|
||||
if is_declaration_keyword(&tokens[last_idx].1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check second-to-last token (in case we're in the middle of typing an
|
||||
// identifier)
|
||||
// identifier after the keyword)
|
||||
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"
|
||||
) {
|
||||
if is_declaration_keyword(&tokens[second_last_idx].1) {
|
||||
// Make sure the last token is an identifier (not a colon or brace)
|
||||
if let (_offset, Token::Ident(_), _end) = &tokens[last_idx] {
|
||||
return true;
|
||||
@@ -199,7 +190,6 @@ fn is_typing_declaration_name(text: &str, offset: usize) -> bool {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
@@ -511,7 +501,8 @@ fn determine_context(text: &str, offset: usize) -> CompletionContext {
|
||||
Token::Template |
|
||||
Token::Species |
|
||||
Token::Institution |
|
||||
Token::Location => {
|
||||
Token::Location |
|
||||
Token::Schedule => {
|
||||
last_context = Some(CompletionContext::InFieldBlock);
|
||||
seen_colon_without_brace = false;
|
||||
},
|
||||
|
||||
@@ -44,6 +44,28 @@ mod tests {
|
||||
assert!(items.iter().any(|item| item.label == "character"));
|
||||
assert!(items.iter().any(|item| item.label == "template"));
|
||||
assert!(items.iter().any(|item| item.label == "behavior"));
|
||||
// Should NOT have behavior-only keywords
|
||||
assert!(
|
||||
!items.iter().any(|item| item.label == "?"),
|
||||
"? should not appear at top level"
|
||||
);
|
||||
assert!(
|
||||
!items.iter().any(|item| item.label == "choose"),
|
||||
"choose should not appear at top level"
|
||||
);
|
||||
assert!(
|
||||
!items.iter().any(|item| item.label == "then"),
|
||||
"then should not appear at top level"
|
||||
);
|
||||
// Should NOT have field-only keywords
|
||||
assert!(
|
||||
!items.iter().any(|item| item.label == "age"),
|
||||
"age should not appear at top level"
|
||||
);
|
||||
assert!(
|
||||
!items.iter().any(|item| item.label == "bond"),
|
||||
"bond should not appear at top level"
|
||||
);
|
||||
},
|
||||
| _ => panic!("Expected array response"),
|
||||
}
|
||||
@@ -67,6 +89,24 @@ mod tests {
|
||||
assert!(items.iter().any(|item| item.label == "from"));
|
||||
assert!(items.iter().any(|item| item.label == "age"));
|
||||
assert!(items.iter().any(|item| item.label == "bond"));
|
||||
// Should NOT have top-level declaration keywords
|
||||
assert!(
|
||||
!items.iter().any(|item| item.label == "character"),
|
||||
"character should not appear inside character block"
|
||||
);
|
||||
assert!(
|
||||
!items.iter().any(|item| item.label == "behavior"),
|
||||
"behavior should not appear inside character block"
|
||||
);
|
||||
// Should NOT have behavior-only keywords
|
||||
assert!(
|
||||
!items.iter().any(|item| item.label == "?"),
|
||||
"? should not appear inside character block"
|
||||
);
|
||||
assert!(
|
||||
!items.iter().any(|item| item.label == "choose"),
|
||||
"choose should not appear inside character block"
|
||||
);
|
||||
},
|
||||
| _ => panic!("Expected array response"),
|
||||
}
|
||||
@@ -237,6 +277,24 @@ mod tests {
|
||||
assert!(items.iter().any(|item| item.label == "as"));
|
||||
assert!(items.iter().any(|item| item.label == "self"));
|
||||
assert!(items.iter().any(|item| item.label == "other"));
|
||||
// Should NOT have top-level declaration keywords
|
||||
assert!(
|
||||
!items.iter().any(|item| item.label == "institution"),
|
||||
"institution should not appear inside relationship block"
|
||||
);
|
||||
assert!(
|
||||
!items.iter().any(|item| item.label == "behavior"),
|
||||
"behavior should not appear inside relationship block"
|
||||
);
|
||||
assert!(
|
||||
!items.iter().any(|item| item.label == "character"),
|
||||
"character keyword should not appear inside relationship block"
|
||||
);
|
||||
// Should NOT have behavior-only keywords
|
||||
assert!(
|
||||
!items.iter().any(|item| item.label == "?"),
|
||||
"? should not appear inside relationship block"
|
||||
);
|
||||
},
|
||||
| _ => panic!("Expected array response"),
|
||||
}
|
||||
@@ -377,6 +435,66 @@ character Bob {}"#;
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_completions_after_declaration_keyword() {
|
||||
// After typing "behavior " the user is naming a new entity — no completions
|
||||
let source = "behavior ";
|
||||
let doc = Document::new(source.to_string());
|
||||
let params = make_params(0, 9); // right after "behavior "
|
||||
|
||||
let result = completion::get_completions(&doc, ¶ms);
|
||||
assert!(
|
||||
result.is_none(),
|
||||
"Should suppress completions when typing a name after a declaration keyword"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_completions_while_typing_name() {
|
||||
// User is typing "behavior Fo" — in the middle of naming
|
||||
let source = "behavior Fo";
|
||||
let doc = Document::new(source.to_string());
|
||||
let params = make_params(0, 11); // right after "Fo"
|
||||
|
||||
let result = completion::get_completions(&doc, ¶ms);
|
||||
assert!(
|
||||
result.is_none(),
|
||||
"Should suppress completions when in the middle of typing a declaration name"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_schedule_completions_no_toplevel() {
|
||||
let source = "schedule Daily {\n \n}";
|
||||
let doc = Document::new(source.to_string());
|
||||
let params = make_params(1, 4);
|
||||
|
||||
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) => {
|
||||
// Should NOT have top-level declaration keywords
|
||||
assert!(
|
||||
!items.iter().any(|item| item.label == "institution"),
|
||||
"institution should not appear inside schedule block"
|
||||
);
|
||||
assert!(
|
||||
!items.iter().any(|item| item.label == "behavior"),
|
||||
"behavior should not appear inside schedule block"
|
||||
);
|
||||
// Should NOT have behavior-only keywords
|
||||
assert!(
|
||||
!items.iter().any(|item| item.label == "?"),
|
||||
"? should not appear inside schedule block"
|
||||
);
|
||||
},
|
||||
| _ => panic!("Expected array response"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_duplicate_completions() {
|
||||
let source = "character Alice {}\ncharacter Alice {}"; // Duplicate name
|
||||
|
||||
Reference in New Issue
Block a user