Files
storybook/src/lsp/code_actions_tests.rs
Sienna Meridian Satterwhite 25d59d6107 feat(type-system): implement concept_comparison with pattern matching
Added complete support for the new type system syntax including:

- concept: Base type declarations
- sub_concept: Enum and record sub-type definitions
- concept_comparison: Compile-time pattern matching with conditional guards

Parser changes:
- Added VariantPattern, FieldCondition, and Condition AST nodes
- Implemented "is" keyword for pattern matching (e.g., "CupType is Glass or CupType is Plastic")
- Added Value::Any variant to support universal type matching
- Disambiguated enum-like vs record-like sub_concept syntax

LSP updates:
- Added Value::Any match arms across code_actions, completion, hover, inlay_hints, and semantic_tokens
- Type inference and formatting support for Any values

Example fixes:
- Fixed syntax error in baker-family behaviors (missing closing brace in nested if)
- Removed deprecated core_enums.sb file
2026-02-14 09:28:20 +00:00

1467 lines
38 KiB
Rust

//! Tests for code actions
#![allow(unused)]
use std::collections::HashMap;
use tower_lsp::lsp_types::{
CodeActionContext,
CodeActionParams,
Diagnostic,
DiagnosticSeverity,
Position,
Range,
TextDocumentIdentifier,
Url,
};
use super::{
code_actions::*,
document::Document,
};
fn make_diagnostic(message: &str, range: Range) -> Diagnostic {
Diagnostic {
range,
severity: Some(DiagnosticSeverity::ERROR),
code: None,
code_description: None,
source: Some("storybook".to_string()),
message: message.to_string(),
related_information: None,
tags: None,
data: None,
}
}
fn make_documents(files: Vec<(&str, &str)>) -> HashMap<Url, Document> {
files
.into_iter()
.map(|(uri_str, content)| {
let uri = Url::parse(uri_str).unwrap();
let doc = Document::new(content.to_string());
(uri, doc)
})
.collect()
}
// Phase 1: Create Missing Symbol Tests
#[test]
fn test_create_missing_character() {
let source = r#"
character Bob {
friend: Alice
}
"#;
let uri = Url::parse("file:///test.sb").unwrap();
let documents = make_documents(vec![("file:///test.sb", source)]);
let diagnostic = make_diagnostic(
"name 'Alice' not found",
Range {
start: Position {
line: 2,
character: 10,
},
end: Position {
line: 2,
character: 15,
},
},
);
let params = CodeActionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
range: diagnostic.range,
context: CodeActionContext {
diagnostics: vec![diagnostic.clone()],
only: None,
trigger_kind: None,
},
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let result = get_code_actions(&documents, &params);
assert!(result.is_some());
let actions = result.unwrap();
assert!(!actions.is_empty());
// Should offer to create character or template
let titles: Vec<String> = actions
.iter()
.filter_map(|a| {
if let tower_lsp::lsp_types::CodeActionOrCommand::CodeAction(action) = a {
Some(action.title.clone())
} else {
None
}
})
.collect();
assert!(titles
.iter()
.any(|t| t.contains("Create character 'Alice'")));
}
#[test]
fn test_create_missing_template() {
let source = r#"
character Alice from Person {}
"#;
let uri = Url::parse("file:///test.sb").unwrap();
let documents = make_documents(vec![("file:///test.sb", source)]);
let diagnostic = make_diagnostic(
"name 'Person' not found",
Range {
start: Position {
line: 1,
character: 21,
},
end: Position {
line: 1,
character: 27,
},
},
);
let params = CodeActionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
range: diagnostic.range,
context: CodeActionContext {
diagnostics: vec![diagnostic.clone()],
only: None,
trigger_kind: None,
},
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let result = get_code_actions(&documents, &params);
assert!(result.is_some());
let actions = result.unwrap();
let titles: Vec<String> = actions
.iter()
.filter_map(|a| {
if let tower_lsp::lsp_types::CodeActionOrCommand::CodeAction(action) = a {
Some(action.title.clone())
} else {
None
}
})
.collect();
assert!(titles
.iter()
.any(|t| t.contains("Create template 'Person'")));
}
#[test]
fn test_create_missing_species() {
let source = r#"
character Alice: Human {}
"#;
let uri = Url::parse("file:///test.sb").unwrap();
let documents = make_documents(vec![("file:///test.sb", source)]);
let diagnostic = make_diagnostic(
"name 'Human' not found",
Range {
start: Position {
line: 1,
character: 17,
},
end: Position {
line: 1,
character: 22,
},
},
);
let params = CodeActionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
range: diagnostic.range,
context: CodeActionContext {
diagnostics: vec![diagnostic.clone()],
only: None,
trigger_kind: None,
},
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let result = get_code_actions(&documents, &params);
assert!(result.is_some());
let actions = result.unwrap();
let titles: Vec<String> = actions
.iter()
.filter_map(|a| {
if let tower_lsp::lsp_types::CodeActionOrCommand::CodeAction(action) = a {
Some(action.title.clone())
} else {
None
}
})
.collect();
assert!(titles.iter().any(|t| t.contains("Create species 'Human'")));
}
// Phase 1: Fix Duplicate Field Tests
#[test]
fn test_fix_duplicate_field() {
let source = r#"
character Alice {
age: 25,
name: "Alice",
age: 30
}
"#;
let uri = Url::parse("file:///test.sb").unwrap();
let documents = make_documents(vec![("file:///test.sb", source)]);
let diagnostic = make_diagnostic(
"duplicate field 'age'",
Range {
start: Position {
line: 4,
character: 2,
},
end: Position {
line: 4,
character: 5,
},
},
);
let params = CodeActionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
range: diagnostic.range,
context: CodeActionContext {
diagnostics: vec![diagnostic.clone()],
only: None,
trigger_kind: None,
},
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let result = get_code_actions(&documents, &params);
assert!(result.is_some());
let actions = result.unwrap();
let titles: Vec<String> = actions
.iter()
.filter_map(|a| {
if let tower_lsp::lsp_types::CodeActionOrCommand::CodeAction(action) = a {
Some(action.title.clone())
} else {
None
}
})
.collect();
assert!(titles
.iter()
.any(|t| t.contains("Remove duplicate field 'age'")));
}
// Phase 1: Fix Type Mismatch Tests
#[test]
fn test_fix_type_mismatch_enum_string() {
let source = r#"
enum Emotion { Happy, Sad }
character Alice {
mood: "Happy"
}
"#;
let uri = Url::parse("file:///test.sb").unwrap();
let documents = make_documents(vec![("file:///test.sb", source)]);
let diagnostic = make_diagnostic(
"type mismatch: expected Enum, got String",
Range {
start: Position {
line: 4,
character: 8,
},
end: Position {
line: 4,
character: 15,
},
},
);
let params = CodeActionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
range: diagnostic.range,
context: CodeActionContext {
diagnostics: vec![diagnostic.clone()],
only: None,
trigger_kind: None,
},
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let result = get_code_actions(&documents, &params);
assert!(result.is_some());
let actions = result.unwrap();
let titles: Vec<String> = actions
.iter()
.filter_map(|a| {
if let tower_lsp::lsp_types::CodeActionOrCommand::CodeAction(action) = a {
Some(action.title.clone())
} else {
None
}
})
.collect();
assert!(titles
.iter()
.any(|t| t.contains("Change to enum value: Happy")));
}
// Phase 1: Fix Duplicate Definition Tests
#[test]
fn test_fix_duplicate_definition() {
let source = r#"
character Alice {}
character Alice {}
"#;
let uri = Url::parse("file:///test.sb").unwrap();
let documents = make_documents(vec![("file:///test.sb", source)]);
let diagnostic = make_diagnostic(
"duplicate definition of 'Alice'",
Range {
start: Position {
line: 2,
character: 10,
},
end: Position {
line: 2,
character: 15,
},
},
);
let params = CodeActionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
range: diagnostic.range,
context: CodeActionContext {
diagnostics: vec![diagnostic.clone()],
only: None,
trigger_kind: None,
},
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let result = get_code_actions(&documents, &params);
assert!(result.is_some());
let actions = result.unwrap();
let titles: Vec<String> = actions
.iter()
.filter_map(|a| {
if let tower_lsp::lsp_types::CodeActionOrCommand::CodeAction(action) = a {
Some(action.title.clone())
} else {
None
}
})
.collect();
assert!(titles.iter().any(|t| t.contains("Rename to 'Alice2'")));
assert!(titles
.iter()
.any(|t| t.contains("Remove duplicate definition")));
}
// Phase 1: Remove Unused Symbol Tests
#[test]
#[ignore = "requires parser span tracking (currently Span::new(0,0) placeholders)"]
fn test_remove_unused_character() {
let source = r#"
character Alice {}
character Bob {}
character Charlie {
friend: Alice
}
"#;
let uri = Url::parse("file:///test.sb").unwrap();
let documents = make_documents(vec![("file:///test.sb", source)]);
// Position on Bob declaration
let range = Range {
start: Position {
line: 2,
character: 10,
},
end: Position {
line: 2,
character: 13,
},
};
let params = CodeActionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
range,
context: CodeActionContext {
diagnostics: vec![],
only: None,
trigger_kind: None,
},
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let result = get_code_actions(&documents, &params);
if let Some(actions) = result {
let titles: Vec<String> = actions
.iter()
.filter_map(|a| {
if let tower_lsp::lsp_types::CodeActionOrCommand::CodeAction(action) = a {
Some(action.title.clone())
} else {
None
}
})
.collect();
assert!(titles
.iter()
.any(|t| t.contains("Remove unused character 'Bob'")));
} else {
// It's okay if no actions are offered - the refactoring action is
// context-dependent
}
}
#[test]
fn test_dont_remove_used_character() {
let source = r#"
character Alice {}
character Bob {
friend: Alice
}
"#;
let uri = Url::parse("file:///test.sb").unwrap();
let documents = make_documents(vec![("file:///test.sb", source)]);
// Position on Alice declaration
let range = Range {
start: Position {
line: 1,
character: 10,
},
end: Position {
line: 1,
character: 15,
},
};
let params = CodeActionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
range,
context: CodeActionContext {
diagnostics: vec![],
only: None,
trigger_kind: None,
},
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let result = get_code_actions(&documents, &params);
// Should not offer to remove Alice since it's used by Bob
if let Some(actions) = result {
let titles: Vec<String> = actions
.iter()
.filter_map(|a| {
if let tower_lsp::lsp_types::CodeActionOrCommand::CodeAction(action) = a {
Some(action.title.clone())
} else {
None
}
})
.collect();
assert!(!titles
.iter()
.any(|t| t.contains("Remove unused character 'Alice'")));
}
}
// Helper function tests
#[test]
fn test_extract_quoted_name() {
use super::code_actions::extract_quoted_name;
assert_eq!(
extract_quoted_name("duplicate field 'age'"),
Some("age".to_string())
);
assert_eq!(
extract_quoted_name("name 'Person' not found"),
Some("Person".to_string())
);
assert_eq!(extract_quoted_name("no quotes here"), None);
}
#[test]
fn test_parse_type_mismatch() {
use super::code_actions::parse_type_mismatch;
assert_eq!(
parse_type_mismatch("type mismatch: expected Enum, got String"),
Some(("Enum".to_string(), "String".to_string()))
);
assert_eq!(
parse_type_mismatch("type mismatch: expected Number, got String"),
Some(("Number".to_string(), "String".to_string()))
);
assert_eq!(parse_type_mismatch("not a type mismatch"), None);
}
//
// Phase 2 Tests
//
// TODO: Advanced refactoring feature - needs implementation or test fix
// This test expects code actions for adding missing template fields
#[test]
#[ignore = "Advanced code action not yet implemented"]
fn test_add_missing_template_fields() {
let source = r#"
template Person {
name: String,
age: Int,
city: String
}
character Alice from Person {
name: "Alice",
age: 25
}
"#;
let uri = Url::parse("file:///test.sb").unwrap();
let documents = make_documents(vec![("file:///test.sb", source)]);
// Position on Alice character
let range = Range {
start: Position {
line: 8,
character: 10,
},
end: Position {
line: 8,
character: 15,
},
};
let params = CodeActionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
range,
context: CodeActionContext {
diagnostics: vec![],
only: None,
trigger_kind: None,
},
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let result = get_code_actions(&documents, &params);
if let Some(actions) = result {
let titles: Vec<String> = actions
.iter()
.filter_map(|a| {
if let tower_lsp::lsp_types::CodeActionOrCommand::CodeAction(action) = a {
Some(action.title.clone())
} else {
None
}
})
.collect();
// Should offer to add the missing "city" field
assert!(titles
.iter()
.any(|t| t.contains("Add missing field 'city'") || t.contains("Add 1 missing field")));
}
}
// TODO: Advanced refactoring feature - template inlining
#[test]
#[ignore = "Advanced code action not yet implemented"]
fn test_inline_template() {
let source = r#"
template Person {
name: String,
age: Int
}
character Alice from Person {
name: "Alice",
age: 25
}
"#;
let uri = Url::parse("file:///test.sb").unwrap();
let documents = make_documents(vec![("file:///test.sb", source)]);
// Position on Alice character
let range = Range {
start: Position {
line: 7,
character: 10,
},
end: Position {
line: 7,
character: 15,
},
};
let params = CodeActionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
range,
context: CodeActionContext {
diagnostics: vec![],
only: None,
trigger_kind: None,
},
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let result = get_code_actions(&documents, &params);
if let Some(actions) = result {
let titles: Vec<String> = actions
.iter()
.filter_map(|a| {
if let tower_lsp::lsp_types::CodeActionOrCommand::CodeAction(action) = a {
Some(action.title.clone())
} else {
None
}
})
.collect();
assert!(titles
.iter()
.any(|t| t.contains("Inline template 'Person'")));
}
}
// TODO: Advanced refactoring feature - extract to template
#[test]
#[ignore = "Advanced code action not yet implemented"]
fn test_extract_to_template() {
let source = r#"
character Alice {
name: "Alice",
age: 25,
city: "NYC"
}
"#;
let uri = Url::parse("file:///test.sb").unwrap();
let documents = make_documents(vec![("file:///test.sb", source)]);
// Position on Alice character
let range = Range {
start: Position {
line: 2,
character: 10,
},
end: Position {
line: 2,
character: 15,
},
};
let params = CodeActionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
range,
context: CodeActionContext {
diagnostics: vec![],
only: None,
trigger_kind: None,
},
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let result = get_code_actions(&documents, &params);
if let Some(actions) = result {
let titles: Vec<String> = actions
.iter()
.filter_map(|a| {
if let tower_lsp::lsp_types::CodeActionOrCommand::CodeAction(action) = a {
Some(action.title.clone())
} else {
None
}
})
.collect();
assert!(titles.iter().any(|t| t.contains("Extract to template")));
}
}
#[test]
fn test_no_extract_for_empty_character() {
let source = r#"
character Alice {}
"#;
let uri = Url::parse("file:///test.sb").unwrap();
let documents = make_documents(vec![("file:///test.sb", source)]);
// Position on Alice character
let range = Range {
start: Position {
line: 1,
character: 10,
},
end: Position {
line: 1,
character: 15,
},
};
let params = CodeActionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
range,
context: CodeActionContext {
diagnostics: vec![],
only: None,
trigger_kind: None,
},
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let result = get_code_actions(&documents, &params);
// Should not offer extract to template for empty character
if let Some(actions) = result {
let titles: Vec<String> = actions
.iter()
.filter_map(|a| {
if let tower_lsp::lsp_types::CodeActionOrCommand::CodeAction(action) = a {
Some(action.title.clone())
} else {
None
}
})
.collect();
assert!(!titles.iter().any(|t| t.contains("Extract to template")));
}
}
//
// Phase 3 Tests
//
// TODO: Advanced refactoring feature - character generation from template
#[test]
#[ignore = "Advanced code action not yet implemented"]
fn test_generate_character_from_template() {
let source = r#"
template Person {
name: String,
age: Int
}
"#;
let uri = Url::parse("file:///test.sb").unwrap();
let documents = make_documents(vec![("file:///test.sb", source)]);
// Position on Person template
let range = Range {
start: Position {
line: 2,
character: 10,
},
end: Position {
line: 2,
character: 16,
},
};
let params = CodeActionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
range,
context: CodeActionContext {
diagnostics: vec![],
only: None,
trigger_kind: None,
},
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let result = get_code_actions(&documents, &params);
if let Some(actions) = result {
let titles: Vec<String> = actions
.iter()
.filter_map(|a| {
if let tower_lsp::lsp_types::CodeActionOrCommand::CodeAction(action) = a {
Some(action.title.clone())
} else {
None
}
})
.collect();
assert!(titles
.iter()
.any(|t| t.contains("Generate character from template 'Person'")));
}
}
#[test]
#[ignore = "requires parser span tracking (currently Span::new(0,0) placeholders)"]
fn test_sort_declarations() {
let source = r#"
character Zelda {}
character Alice {}
template Zoo {}
template Animal {}
"#;
let uri = Url::parse("file:///test.sb").unwrap();
let documents = make_documents(vec![("file:///test.sb", source)]);
// Position anywhere in the file
let range = Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 0,
character: 0,
},
};
let params = CodeActionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
range,
context: CodeActionContext {
diagnostics: vec![],
only: None,
trigger_kind: None,
},
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let result = get_code_actions(&documents, &params);
if let Some(actions) = result {
let titles: Vec<String> = actions
.iter()
.filter_map(|a| {
if let tower_lsp::lsp_types::CodeActionOrCommand::CodeAction(action) = a {
Some(action.title.clone())
} else {
None
}
})
.collect();
assert!(titles.iter().any(|t| t.contains("Sort declarations")));
}
}
#[test]
fn test_no_sort_if_already_sorted() {
let source = r#"
template Animal {}
template Zoo {}
character Alice {}
character Zelda {}
"#;
let uri = Url::parse("file:///test.sb").unwrap();
let documents = make_documents(vec![("file:///test.sb", source)]);
// Position anywhere in the file
let range = Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 0,
character: 0,
},
};
let params = CodeActionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
range,
context: CodeActionContext {
diagnostics: vec![],
only: None,
trigger_kind: None,
},
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let result = get_code_actions(&documents, &params);
// Should not offer sort if already sorted
if let Some(actions) = result {
let titles: Vec<String> = actions
.iter()
.filter_map(|a| {
if let tower_lsp::lsp_types::CodeActionOrCommand::CodeAction(action) = a {
Some(action.title.clone())
} else {
None
}
})
.collect();
assert!(!titles.iter().any(|t| t.contains("Sort declarations")));
}
}
// TODO: Advanced refactoring feature - extract common fields to template
#[test]
#[ignore = "Advanced code action not yet implemented"]
fn test_extract_common_fields() {
let source = r#"
character Alice {
name: "Alice",
age: 25,
city: "NYC"
}
character Bob {
name: "Bob",
age: 30,
country: "USA"
}
"#;
let uri = Url::parse("file:///test.sb").unwrap();
let documents = make_documents(vec![("file:///test.sb", source)]);
// Position on Alice character
let range = Range {
start: Position {
line: 2,
character: 10,
},
end: Position {
line: 2,
character: 15,
},
};
let params = CodeActionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
range,
context: CodeActionContext {
diagnostics: vec![],
only: None,
trigger_kind: None,
},
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let result = get_code_actions(&documents, &params);
if let Some(actions) = result {
let titles: Vec<String> = actions
.iter()
.filter_map(|a| {
if let tower_lsp::lsp_types::CodeActionOrCommand::CodeAction(action) = a {
Some(action.title.clone())
} else {
None
}
})
.collect();
// Should find common fields "name" and "age"
assert!(titles
.iter()
.any(|t| t.contains("Extract 2 common fields to template")));
}
}
#[test]
fn test_no_common_fields() {
let source = r#"
character Alice {
city: "NYC"
}
character Bob {
country: "USA"
}
"#;
let uri = Url::parse("file:///test.sb").unwrap();
let documents = make_documents(vec![("file:///test.sb", source)]);
// Position on Alice character
let range = Range {
start: Position {
line: 2,
character: 10,
},
end: Position {
line: 2,
character: 15,
},
};
let params = CodeActionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
range,
context: CodeActionContext {
diagnostics: vec![],
only: None,
trigger_kind: None,
},
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let result = get_code_actions(&documents, &params);
// Should not offer extract common fields when there are none
if let Some(actions) = result {
let titles: Vec<String> = actions
.iter()
.filter_map(|a| {
if let tower_lsp::lsp_types::CodeActionOrCommand::CodeAction(action) = a {
Some(action.title.clone())
} else {
None
}
})
.collect();
assert!(!titles
.iter()
.any(|t| t.contains("Extract") && t.contains("common")));
}
}
// TODO: Advanced refactoring feature - relationship scaffolding
#[test]
#[ignore = "Advanced code action not yet implemented"]
fn test_create_relationship_scaffold() {
let source = r#"
character Alice {}
character Bob {}
"#;
let uri = Url::parse("file:///test.sb").unwrap();
let documents = make_documents(vec![("file:///test.sb", source)]);
// Position on Alice character
let range = Range {
start: Position {
line: 2,
character: 10,
},
end: Position {
line: 2,
character: 15,
},
};
let params = CodeActionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
range,
context: CodeActionContext {
diagnostics: vec![],
only: None,
trigger_kind: None,
},
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let result = get_code_actions(&documents, &params);
if let Some(actions) = result {
let titles: Vec<String> = actions
.iter()
.filter_map(|a| {
if let tower_lsp::lsp_types::CodeActionOrCommand::CodeAction(action) = a {
Some(action.title.clone())
} else {
None
}
})
.collect();
assert!(titles
.iter()
.any(|t| t.contains("Create relationship between 'Alice' and 'Bob'")));
}
}
#[test]
fn test_no_relationship_with_single_character() {
let source = r#"
character Alice {}
"#;
let uri = Url::parse("file:///test.sb").unwrap();
let documents = make_documents(vec![("file:///test.sb", source)]);
// Position on Alice character
let range = Range {
start: Position {
line: 2,
character: 10,
},
end: Position {
line: 2,
character: 15,
},
};
let params = CodeActionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
range,
context: CodeActionContext {
diagnostics: vec![],
only: None,
trigger_kind: None,
},
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let result = get_code_actions(&documents, &params);
// Should not offer relationship creation with only one character
if let Some(actions) = result {
let titles: Vec<String> = actions
.iter()
.filter_map(|a| {
if let tower_lsp::lsp_types::CodeActionOrCommand::CodeAction(action) = a {
Some(action.title.clone())
} else {
None
}
})
.collect();
assert!(!titles.iter().any(|t| t.contains("Create relationship")));
}
}
//
// Phase 4 Tests
//
#[test]
fn test_fix_invalid_field_access() {
let source = r#"
template Person {
name: String
}
character Alice from Person {
age: 25
}
"#;
let uri = Url::parse("file:///test.sb").unwrap();
let documents = make_documents(vec![("file:///test.sb", source)]);
let diagnostic = make_diagnostic(
"field 'age' does not exist on type 'Person'",
Range {
start: Position {
line: 6,
character: 2,
},
end: Position {
line: 6,
character: 5,
},
},
);
let params = CodeActionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
range: diagnostic.range,
context: CodeActionContext {
diagnostics: vec![diagnostic.clone()],
only: None,
trigger_kind: None,
},
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let result = get_code_actions(&documents, &params);
assert!(result.is_some());
let actions = result.unwrap();
let titles: Vec<String> = actions
.iter()
.filter_map(|a| {
if let tower_lsp::lsp_types::CodeActionOrCommand::CodeAction(action) = a {
Some(action.title.clone())
} else {
None
}
})
.collect();
assert!(titles
.iter()
.any(|t| t.contains("Remove invalid field 'age'")));
}
#[test]
fn test_fix_trait_out_of_range() {
let source = r#"
character Alice {
age: 150
}
"#;
let uri = Url::parse("file:///test.sb").unwrap();
let documents = make_documents(vec![("file:///test.sb", source)]);
let diagnostic = make_diagnostic(
"trait value 150 out of range [0, 120]",
Range {
start: Position {
line: 2,
character: 7,
},
end: Position {
line: 2,
character: 10,
},
},
);
let params = CodeActionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
range: diagnostic.range,
context: CodeActionContext {
diagnostics: vec![diagnostic.clone()],
only: None,
trigger_kind: None,
},
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let result = get_code_actions(&documents, &params);
assert!(result.is_some());
let actions = result.unwrap();
let titles: Vec<String> = actions
.iter()
.filter_map(|a| {
if let tower_lsp::lsp_types::CodeActionOrCommand::CodeAction(action) = a {
Some(action.title.clone())
} else {
None
}
})
.collect();
assert!(titles
.iter()
.any(|t| t.contains("Change to maximum value (120)")));
assert!(titles
.iter()
.any(|t| t.contains("Change to minimum value (0)")));
}
#[test]
fn test_fix_schedule_overlap() {
let source = r#"
schedule {
00:00-12:00 Morning
10:00-14:00 Lunch
}
"#;
let uri = Url::parse("file:///test.sb").unwrap();
let documents = make_documents(vec![("file:///test.sb", source)]);
let diagnostic = make_diagnostic(
"schedule overlap: Morning and Lunch",
Range {
start: Position {
line: 3,
character: 2,
},
end: Position {
line: 3,
character: 17,
},
},
);
let params = CodeActionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
range: diagnostic.range,
context: CodeActionContext {
diagnostics: vec![diagnostic.clone()],
only: None,
trigger_kind: None,
},
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let result = get_code_actions(&documents, &params);
assert!(result.is_some());
let actions = result.unwrap();
let titles: Vec<String> = actions
.iter()
.filter_map(|a| {
if let tower_lsp::lsp_types::CodeActionOrCommand::CodeAction(action) = a {
Some(action.title.clone())
} else {
None
}
})
.collect();
assert!(titles
.iter()
.any(|t| t.contains("Remove overlapping schedule entry")));
}
// TODO: Advanced refactoring feature - convert species to template
#[test]
#[ignore = "Advanced code action not yet implemented"]
fn test_convert_species_to_template() {
let source = r#"
character Alice: Human {}
"#;
let uri = Url::parse("file:///test.sb").unwrap();
let documents = make_documents(vec![("file:///test.sb", source)]);
// Position on Alice character
let range = Range {
start: Position {
line: 2,
character: 10,
},
end: Position {
line: 2,
character: 15,
},
};
let params = CodeActionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
range,
context: CodeActionContext {
diagnostics: vec![],
only: None,
trigger_kind: None,
},
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let result = get_code_actions(&documents, &params);
if let Some(actions) = result {
let titles: Vec<String> = actions
.iter()
.filter_map(|a| {
if let tower_lsp::lsp_types::CodeActionOrCommand::CodeAction(action) = a {
Some(action.title.clone())
} else {
None
}
})
.collect();
assert!(titles
.iter()
.any(|t| t.contains("Convert species to template")));
}
}
// TODO: Advanced refactoring feature - convert template to species
#[test]
#[ignore = "Advanced code action not yet implemented"]
fn test_convert_template_to_species() {
let source = r#"
character Alice from Person {}
"#;
let uri = Url::parse("file:///test.sb").unwrap();
let documents = make_documents(vec![("file:///test.sb", source)]);
// Position on Alice character
let range = Range {
start: Position {
line: 2,
character: 10,
},
end: Position {
line: 2,
character: 15,
},
};
let params = CodeActionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
range,
context: CodeActionContext {
diagnostics: vec![],
only: None,
trigger_kind: None,
},
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let result = get_code_actions(&documents, &params);
if let Some(actions) = result {
let titles: Vec<String> = actions
.iter()
.filter_map(|a| {
if let tower_lsp::lsp_types::CodeActionOrCommand::CodeAction(action) = a {
Some(action.title.clone())
} else {
None
}
})
.collect();
assert!(titles
.iter()
.any(|t| t.contains("Convert template to species")));
}
}