Files
storybook/src/lsp/completion_tests.rs
Sienna Meridian Satterwhite b3110c8b0f feat(lsp): add completions for type system keywords
Added top-level keyword completions for:
- concept: with semicolon-terminated snippet
- sub_concept: with dot notation snippet
- concept_comparison: with brace-delimited snippet

Added test verifying type system keywords appear in
completions with correct snippet formatting.
2026-02-14 14:32:51 +00:00

334 lines
12 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"));
},
| _ => 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"));
},
| _ => 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 behavior tree keywords
assert!(items.iter().any(|item| item.label == "?"));
assert!(items.iter().any(|item| item.label == ">"));
assert!(items.iter().any(|item| item.label == "*"));
},
| _ => 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"));
},
| _ => 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"));
},
| _ => 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 == "concept_comparison"),
"Should have concept_comparison 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_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"),
}
}
}
}