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:
2026-02-23 21:09:36 +00:00
parent dcc27a6988
commit 1fa90aff0e
2 changed files with 146 additions and 37 deletions

View File

@@ -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, &params);
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, &params);
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, &params);
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