- 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.
530 lines
21 KiB
Rust
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, ¶ms);
|
|
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, ¶ms);
|
|
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, ¶ms);
|
|
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, ¶ms);
|
|
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, ¶ms);
|
|
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, ¶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"),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[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, ¶ms);
|
|
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, ¶ms);
|
|
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, ¶ms);
|
|
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, ¶ms);
|
|
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, ¶ms);
|
|
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, ¶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
|
|
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, ¶ms);
|
|
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"),
|
|
}
|
|
}
|
|
}
|
|
}
|