//! 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"), } } } }