//! 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 { 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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"))); } }