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
1467 lines
38 KiB
Rust
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, ¶ms);
|
|
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, ¶ms);
|
|
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, ¶ms);
|
|
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, ¶ms);
|
|
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, ¶ms);
|
|
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, ¶ms);
|
|
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, ¶ms);
|
|
|
|
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, ¶ms);
|
|
|
|
// 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, ¶ms);
|
|
|
|
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, ¶ms);
|
|
|
|
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, ¶ms);
|
|
|
|
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, ¶ms);
|
|
|
|
// 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, ¶ms);
|
|
|
|
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, ¶ms);
|
|
|
|
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, ¶ms);
|
|
|
|
// 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, ¶ms);
|
|
|
|
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, ¶ms);
|
|
|
|
// 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, ¶ms);
|
|
|
|
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, ¶ms);
|
|
|
|
// 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, ¶ms);
|
|
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, ¶ms);
|
|
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, ¶ms);
|
|
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, ¶ms);
|
|
|
|
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, ¶ms);
|
|
|
|
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")));
|
|
}
|
|
}
|