Files
storybook/src/lsp/completion_tests.rs
Sienna Meridian Satterwhite 1fa90aff0e 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.
2026-02-23 21:09:36 +00:00

530 lines
21 KiB
Rust

//! Tests for context-aware completion
#[cfg(test)]
mod tests {
use tower_lsp::lsp_types::{
CompletionParams,
Position,
TextDocumentIdentifier,
TextDocumentPositionParams,
Url,
};
use crate::lsp::{
completion,
document::Document,
};
fn make_params(line: u32, character: u32) -> CompletionParams {
CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier {
uri: Url::parse("file:///test.sb").unwrap(),
},
position: Position { line, character },
},
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
context: None,
}
}
#[test]
fn test_top_level_completions() {
let doc = Document::new("".to_string());
let params = make_params(0, 0);
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 have top-level keywords
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"),
}
}
}
#[test]
fn test_field_block_completions() {
let source = "character Alice {\n \n}";
let doc = Document::new(source.to_string());
// Position inside the character block (line 1, after spaces)
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 have field-related keywords
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"),
}
}
}
#[test]
fn test_completions_include_templates() {
// Test that templates show up in completions
let source = "template Child { age: number }\n";
let doc = Document::new(source.to_string());
let params = make_params(1, 0);
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 include Child template in completions
assert!(
items.iter().any(|item| item.label == "Child"),
"Should have Child template in completions"
);
},
| _ => panic!("Expected array response"),
}
}
}
#[test]
fn test_behavior_completions() {
let source = "behavior Test {\n \n}";
let doc = Document::new(source.to_string());
// Position inside behavior block
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 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"),
}
}
}
#[test]
fn test_life_arc_completions() {
let source = "life_arc Growing {\n \n}";
let doc = Document::new(source.to_string());
// Position inside life arc block
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 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, &params);
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"),
}
}
}
#[test]
fn test_relationship_completions() {
let source = "relationship Friends {\n \n}";
let doc = Document::new(source.to_string());
// Position inside relationship block
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 have relationship keywords
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"),
}
}
}
#[test]
fn test_type_suggestions_in_completions() {
// More complete example with proper syntax
let source = r#"template Child { age: number }
species Human {}
character Alice: Child {}
character Bob {}"#;
let doc = Document::new(source.to_string());
// Just check that templates and species are in completions
let params = make_params(0, 0);
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 have Child and Human in completions
assert!(items.iter().any(|item| item.label == "Child"));
assert!(items.iter().any(|item| item.label == "Human"));
},
| _ => panic!("Expected array response"),
}
}
}
#[test]
fn test_behavior_reference_in_symbols() {
// Check that behaviors are in symbol table and show up in completions
let source = "behavior WalkAround { patrol }\nbehavior Main { idle }";
let doc = Document::new(source.to_string());
let params = make_params(0, 0);
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) => {
// Behaviors should be in completions
assert!(
items.iter().any(|item| item.label.contains("WalkAround")),
"Should have WalkAround in completions"
);
assert!(
items.iter().any(|item| item.label.contains("Main")),
"Should have Main in completions"
);
},
| _ => panic!("Expected array response"),
}
}
}
#[test]
fn test_snippet_format_in_completions() {
let doc = Document::new("".to_string());
let params = make_params(0, 0);
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) => {
// Check that snippets have proper format
let char_item = items.iter().find(|item| item.label == "character");
assert!(char_item.is_some());
if let Some(item) = char_item {
assert!(item.insert_text.is_some());
assert!(item.insert_text_format.is_some());
// Should contain snippet placeholders
assert!(item.insert_text.as_ref().unwrap().contains("${"));
}
},
| _ => panic!("Expected array response"),
}
}
}
#[test]
fn test_type_system_completions() {
let doc = Document::new("".to_string());
let params = make_params(0, 0);
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 have type system keywords
assert!(
items.iter().any(|item| item.label == "concept"),
"Should have concept keyword"
);
assert!(
items.iter().any(|item| item.label == "sub_concept"),
"Should have sub_concept keyword"
);
assert!(
items.iter().any(|item| item.label == "definition"),
"Should have definition keyword"
);
// Check snippets have correct format
let concept_item = items.iter().find(|item| item.label == "concept");
assert!(concept_item.is_some());
if let Some(item) = concept_item {
let snippet = item.insert_text.as_ref().unwrap();
assert!(
snippet.contains(";"),
"concept snippet should end with semicolon"
);
}
let sub_item = items.iter().find(|item| item.label == "sub_concept");
assert!(sub_item.is_some());
if let Some(item) = sub_item {
let snippet = item.insert_text.as_ref().unwrap();
assert!(
snippet.contains("."),
"sub_concept snippet should contain dot notation"
);
}
},
| _ => panic!("Expected array response"),
}
}
}
#[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
let doc = Document::new(source.to_string());
let params = make_params(0, 0);
// Duplicate definitions cause NameTable::from_file() to fail,
// resulting in an empty name table and no completions.
// This is correct - duplicates should be caught as validation errors.
assert!(
!doc.resolve_errors.is_empty(),
"Should have validation error for duplicate"
);
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) => {
// Count how many times "Alice" appears
let alice_count = items.iter().filter(|item| item.label == "Alice").count();
assert_eq!(
alice_count, 0,
"Should have no completions when there are duplicate definitions"
);
},
| _ => panic!("Expected array response"),
}
}
}
}