use crate::color::parse_color;
use crate::motion::{parse_timing_function, Interpolation, StepPosition};
use crate::tokenizer::tokenize;
use crate::validate::{Severity, ValidationIssue, Validator};
use crate::variables::VariableRegistry;
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VerificationResult {
pub valid: bool,
pub error_count: usize,
pub warning_count: usize,
pub errors: Vec<Diagnostic>,
pub warnings: Vec<Diagnostic>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Diagnostic {
pub line: usize,
pub issue_type: String,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub context: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub suggestion: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompletionResult {
pub items: Vec<CompletionItem>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompletionItem {
pub label: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub insert_text: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub detail: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub kind: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GridPosition {
pub x: usize,
pub y: usize,
pub token: String,
pub row_width: usize,
pub expected_width: usize,
pub sprite_name: String,
pub aligned: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ColorResolutionResult {
pub colors: Vec<ResolvedColor>,
pub error_count: usize,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub errors: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResolvedColor {
pub token: String,
pub original: String,
pub resolved: String,
pub palette: String,
pub is_variable: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimingAnalysisResult {
pub animations: Vec<TimingAnalysis>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimingAnalysis {
pub animation: String,
pub timing_function: String,
pub description: String,
pub curve_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub ascii_curve: Option<String>,
}
#[derive(Debug, Default)]
pub struct LspAgentClient {
strict: bool,
}
impl LspAgentClient {
pub fn new() -> Self {
Self { strict: false }
}
pub fn strict() -> Self {
Self { strict: true }
}
pub fn with_strict(mut self, strict: bool) -> Self {
self.strict = strict;
self
}
pub fn verify_content(&self, content: &str) -> VerificationResult {
let mut validator = Validator::new();
for (line_idx, line) in content.lines().enumerate() {
let line_number = line_idx + 1;
validator.validate_line(line_number, line);
}
let issues = validator.into_issues();
let errors: Vec<Diagnostic> = issues
.iter()
.filter(|i| matches!(i.severity, Severity::Error))
.map(Self::issue_to_diagnostic)
.collect();
let warnings: Vec<Diagnostic> = issues
.iter()
.filter(|i| matches!(i.severity, Severity::Warning))
.map(Self::issue_to_diagnostic)
.collect();
let error_count = errors.len();
let warning_count = warnings.len();
let valid =
if self.strict { error_count == 0 && warning_count == 0 } else { error_count == 0 };
VerificationResult { valid, error_count, warning_count, errors, warnings }
}
pub fn get_completions(
&self,
content: &str,
line: usize,
_character: usize,
) -> CompletionResult {
let defined_tokens = Self::collect_defined_tokens(content);
let mut items: Vec<CompletionItem> = Vec::new();
items.push(CompletionItem {
label: "{_}".to_string(),
insert_text: Some("{_}".to_string()),
detail: Some("Transparent (built-in)".to_string()),
kind: Some("color".to_string()),
});
items.push(CompletionItem {
label: ".".to_string(),
insert_text: Some(".".to_string()),
detail: Some("Transparent (shorthand)".to_string()),
kind: Some("color".to_string()),
});
let is_in_grid = if line > 0 {
content.lines().nth(line - 1).map(Self::is_grid_context).unwrap_or(false)
} else {
false
};
for (token, color) in defined_tokens {
items.push(CompletionItem {
label: token.clone(),
insert_text: Some(token),
detail: Some(color),
kind: Some("color".to_string()),
});
}
if is_in_grid {
items.sort_by(|a, b| {
let a_is_token = a.label.starts_with('{');
let b_is_token = b.label.starts_with('{');
b_is_token.cmp(&a_is_token)
});
}
CompletionResult { items }
}
pub fn get_grid_position(
&self,
content: &str,
line: usize,
character: usize,
) -> Option<GridPosition> {
if line == 0 {
return None;
}
let line_content = content.lines().nth(line - 1)?;
Self::parse_grid_context(line_content, character as u32)
}
pub fn verify_content_json(&self, content: &str) -> String {
let result = self.verify_content(content);
serde_json::to_string_pretty(&result)
.unwrap_or_else(|e| format!(r#"{{"error": "Failed to serialize result: {}"}}"#, e))
}
pub fn get_completions_json(&self, content: &str, line: usize, character: usize) -> String {
let result = self.get_completions(content, line, character);
serde_json::to_string_pretty(&result)
.unwrap_or_else(|e| format!(r#"{{"error": "Failed to serialize result: {}"}}"#, e))
}
pub fn resolve_colors(&self, content: &str) -> ColorResolutionResult {
let mut colors = Vec::new();
let mut errors = Vec::new();
let registry = Self::build_variable_registry(content);
for line in content.lines() {
let obj: Value = match serde_json::from_str(line) {
Ok(v) => v,
Err(_) => continue,
};
let obj = match obj.as_object() {
Some(o) => o,
None => continue,
};
let obj_type = match obj.get("type").and_then(|t| t.as_str()) {
Some(t) if t == "palette" => t,
_ => continue,
};
let _ = obj_type;
let palette_name =
obj.get("name").and_then(|n| n.as_str()).unwrap_or("unknown").to_string();
let palette_colors = match obj.get("colors").and_then(|c| c.as_object()) {
Some(c) => c,
None => continue,
};
for (key, value) in palette_colors {
let original_value = match value.as_str() {
Some(s) => s.to_string(),
None => continue,
};
let is_variable = key.starts_with("--");
let is_token = key.starts_with('{') && key.ends_with('}');
if !is_variable && !is_token {
continue;
}
let resolved_value = match registry.resolve(&original_value) {
Ok(v) => v,
Err(e) => {
errors.push(format!("{}: {}", key, e));
continue;
}
};
let hex_value = match parse_color(&resolved_value) {
Ok(rgba) => {
if rgba.0[3] == 255 {
format!("#{:02X}{:02X}{:02X}", rgba.0[0], rgba.0[1], rgba.0[2])
} else {
format!(
"#{:02X}{:02X}{:02X}{:02X}",
rgba.0[0], rgba.0[1], rgba.0[2], rgba.0[3]
)
}
}
Err(e) => {
errors.push(format!("{}: {}", key, e));
continue;
}
};
colors.push(ResolvedColor {
token: key.clone(),
original: original_value,
resolved: hex_value,
palette: palette_name.clone(),
is_variable,
});
}
}
ColorResolutionResult { error_count: errors.len(), colors, errors }
}
pub fn resolve_colors_json(&self, content: &str) -> String {
let result = self.resolve_colors(content);
serde_json::to_string_pretty(&result)
.unwrap_or_else(|e| format!(r#"{{"error": "Failed to serialize result: {}"}}"#, e))
}
pub fn analyze_timing(&self, content: &str) -> TimingAnalysisResult {
let mut animations = Vec::new();
for line in content.lines() {
let obj: Value = match serde_json::from_str(line) {
Ok(v) => v,
Err(_) => continue,
};
let obj = match obj.as_object() {
Some(o) => o,
None => continue,
};
let obj_type = match obj.get("type").and_then(|t| t.as_str()) {
Some(t) if t == "animation" => t,
_ => continue,
};
let _ = obj_type;
let anim_name =
obj.get("name").and_then(|n| n.as_str()).unwrap_or("unknown").to_string();
let timing_str =
obj.get("timing_function").and_then(|t| t.as_str()).unwrap_or("linear").to_string();
let (description, curve_type, ascii_curve) = match parse_timing_function(&timing_str) {
Ok(interpolation) => {
let desc = Self::describe_interpolation(&interpolation);
let curve_type = Self::classify_curve_type(&interpolation);
let ascii = Self::render_ascii_curve(&interpolation, 20, 8);
(desc, curve_type, Some(ascii))
}
Err(_) => (
format!("Unknown timing function: {}", timing_str),
"unknown".to_string(),
None,
),
};
animations.push(TimingAnalysis {
animation: anim_name,
timing_function: timing_str,
description,
curve_type,
ascii_curve,
});
}
TimingAnalysisResult { animations }
}
pub fn analyze_timing_json(&self, content: &str) -> String {
let result = self.analyze_timing(content);
serde_json::to_string_pretty(&result)
.unwrap_or_else(|e| format!(r#"{{"error": "Failed to serialize result: {}"}}"#, e))
}
fn build_variable_registry(content: &str) -> VariableRegistry {
let mut registry = VariableRegistry::new();
for line in content.lines() {
let obj: Value = match serde_json::from_str(line) {
Ok(v) => v,
Err(_) => continue,
};
let obj = match obj.as_object() {
Some(o) => o,
None => continue,
};
if obj.get("type").and_then(|t| t.as_str()) != Some("palette") {
continue;
}
let colors = match obj.get("colors").and_then(|c| c.as_object()) {
Some(c) => c,
None => continue,
};
for (key, value) in colors {
if key.starts_with("--") {
if let Some(value_str) = value.as_str() {
registry.define(key, value_str);
}
}
}
}
registry
}
fn describe_interpolation(interpolation: &Interpolation) -> String {
match interpolation {
Interpolation::Linear => "Constant speed from start to end.".to_string(),
Interpolation::EaseIn => "Starts slow, accelerates toward end.".to_string(),
Interpolation::EaseOut => "Starts fast, decelerates toward end.".to_string(),
Interpolation::EaseInOut => "Slow start and end, fast middle.".to_string(),
Interpolation::Bounce => "Bouncy effect at the end, like a ball.".to_string(),
Interpolation::Elastic => "Elastic overshoot effect, springs past target.".to_string(),
Interpolation::Bezier { p1, p2 } => {
format!(
"Custom cubic bezier curve ({:.2}, {:.2}, {:.2}, {:.2}).",
p1.0, p1.1, p2.0, p2.1
)
}
Interpolation::Steps { count, position } => {
let pos_desc = match position {
StepPosition::JumpStart => "starts immediately",
StepPosition::JumpEnd => "ends on final value",
StepPosition::JumpNone => "never sits on endpoints",
StepPosition::JumpBoth => "sits on both endpoints",
};
format!("Jumps in {} discrete step(s), {}.", count, pos_desc)
}
}
}
fn classify_curve_type(interpolation: &Interpolation) -> String {
match interpolation {
Interpolation::Linear => "linear".to_string(),
Interpolation::EaseIn | Interpolation::EaseOut | Interpolation::EaseInOut => {
"smooth".to_string()
}
Interpolation::Bounce => "bouncy".to_string(),
Interpolation::Elastic => "elastic".to_string(),
Interpolation::Bezier { .. } => "smooth".to_string(),
Interpolation::Steps { .. } => "stepped".to_string(),
}
}
fn render_ascii_curve(interpolation: &Interpolation, width: usize, height: usize) -> String {
use crate::motion::ease;
let mut grid = vec![vec![' '; width]; height];
for x in 0..width {
let t = x as f64 / (width - 1) as f64;
let y = ease(t, interpolation);
let y_idx = ((1.0 - y) * (height - 1) as f64).round() as usize;
let y_idx = y_idx.min(height - 1);
grid[y_idx][x] = '█';
}
let mut result = String::new();
result.push_str(&format!("┌{}┐\n", "─".repeat(width)));
for row in &grid {
result.push('│');
result.extend(row.iter());
result.push_str("│\n");
}
result.push_str(&format!("└{}┘", "─".repeat(width)));
result
}
fn issue_to_diagnostic(issue: &ValidationIssue) -> Diagnostic {
Diagnostic {
line: issue.line,
issue_type: issue.issue_type.to_string(),
message: issue.message.clone(),
context: issue.context.clone(),
suggestion: issue.suggestion.clone(),
}
}
fn is_grid_context(line: &str) -> bool {
if let Ok(obj) = serde_json::from_str::<Value>(line) {
if let Some(obj) = obj.as_object() {
return obj.get("type").and_then(|t| t.as_str()) == Some("sprite")
&& obj.contains_key("grid");
}
}
false
}
fn collect_defined_tokens(content: &str) -> Vec<(String, String)> {
let mut tokens = Vec::new();
for line in content.lines() {
let obj: Value = match serde_json::from_str(line) {
Ok(v) => v,
Err(_) => continue,
};
let obj = match obj.as_object() {
Some(o) => o,
None => continue,
};
let obj_type = match obj.get("type").and_then(|t| t.as_str()) {
Some(t) => t,
None => continue,
};
if obj_type != "palette" {
continue;
}
let colors = match obj.get("colors").and_then(|c| c.as_object()) {
Some(c) => c,
None => continue,
};
for (key, value) in colors {
if key.starts_with('{') && key.ends_with('}') {
let color_str = match value.as_str() {
Some(s) => s.to_string(),
None => continue,
};
tokens.push((key.clone(), color_str));
}
}
}
tokens
}
fn parse_grid_context(line: &str, char_pos: u32) -> Option<GridPosition> {
let obj: Value = serde_json::from_str(line).ok()?;
let obj = obj.as_object()?;
if obj.get("type")?.as_str()? != "sprite" {
return None;
}
let sprite_name = obj.get("name")?.as_str()?.to_string();
let grid = obj.get("grid")?.as_array()?;
if grid.is_empty() {
return None;
}
let expected_width = if let Some(size) = obj.get("size").and_then(|s| s.as_array()) {
size.first().and_then(|v| v.as_u64()).unwrap_or(0) as usize
} else {
let first_row = grid.first()?.as_str()?;
let (tokens, _) = tokenize(first_row);
tokens.len()
};
let grid_key_pos = line.find("\"grid\"")?;
let after_key = &line[grid_key_pos..];
let bracket_offset = after_key.find('[')?;
let grid_array_start = grid_key_pos + bracket_offset;
if (char_pos as usize) <= grid_array_start {
return None;
}
let grid_portion = &line[grid_array_start..];
let char_in_grid = (char_pos as usize) - grid_array_start;
let mut pos = 0;
let chars: Vec<char> = grid_portion.chars().collect();
for (row_idx, grid_row) in grid.iter().enumerate() {
let row_str = grid_row.as_str()?;
while pos < chars.len() && chars[pos] != '"' {
pos += 1;
}
if pos >= chars.len() {
return None;
}
let string_start = pos + 1;
pos += 1;
while pos < chars.len() && chars[pos] != '"' {
if chars[pos] == '\\' && pos + 1 < chars.len() {
pos += 2;
continue;
}
pos += 1;
}
let string_end = pos;
if char_in_grid >= string_start && char_in_grid < string_end {
let char_in_string = char_in_grid - string_start;
let (tokens, _) = tokenize(row_str);
let row_width = tokens.len();
let mut string_pos = 0;
for (token_idx, token) in tokens.iter().enumerate() {
let token_start = string_pos;
let token_end = string_pos + token.len();
if char_in_string >= token_start && char_in_string < token_end {
return Some(GridPosition {
x: token_idx,
y: row_idx,
token: token.clone(),
row_width,
expected_width,
sprite_name,
aligned: row_width == expected_width,
});
}
string_pos = token_end;
}
}
pos += 1;
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_verify_valid_content() {
let client = LspAgentClient::new();
let content = r##"{"type": "palette", "name": "test", "colors": {"{a}": "#FF0000"}}"##;
let result = client.verify_content(content);
assert!(result.valid);
assert_eq!(result.error_count, 0);
assert_eq!(result.warning_count, 0);
}
#[test]
fn test_verify_invalid_json() {
let client = LspAgentClient::new();
let content = "{not valid json}";
let result = client.verify_content(content);
assert!(!result.valid);
assert_eq!(result.error_count, 1);
assert_eq!(result.errors[0].issue_type, "json_syntax");
}
#[test]
fn test_verify_missing_type() {
let client = LspAgentClient::new();
let content = r#"{"name": "test"}"#;
let result = client.verify_content(content);
assert!(!result.valid);
assert_eq!(result.error_count, 1);
assert_eq!(result.errors[0].issue_type, "missing_type");
}
#[test]
fn test_verify_warning_not_error() {
let client = LspAgentClient::new();
let content = r#"{"type": "unknown", "name": "test"}"#;
let result = client.verify_content(content);
assert!(result.valid);
assert_eq!(result.error_count, 0);
assert_eq!(result.warning_count, 1);
}
#[test]
fn test_verify_strict_mode() {
let client = LspAgentClient::strict();
let content = r#"{"type": "unknown", "name": "test"}"#;
let result = client.verify_content(content);
assert!(!result.valid);
assert_eq!(result.warning_count, 1);
}
#[test]
fn test_verify_undefined_token() {
let client = LspAgentClient::new();
let content = r##"{"type": "palette", "name": "test", "colors": {"{a}": "#FF0000"}}
{"type": "sprite", "name": "s", "palette": "test", "grid": ["{a}{b}"]}"##;
let result = client.verify_content(content);
assert!(result.valid); assert_eq!(result.warning_count, 1);
assert!(result.warnings[0].message.contains("{b}"));
}
#[test]
fn test_get_completions_basic() {
let client = LspAgentClient::new();
let content = r##"{"type": "palette", "name": "test", "colors": {"{red}": "#FF0000", "{blue}": "#0000FF"}}"##;
let result = client.get_completions(content, 1, 0);
assert!(result.items.len() >= 4);
let labels: Vec<&str> = result.items.iter().map(|i| i.label.as_str()).collect();
assert!(labels.contains(&"{_}"));
assert!(labels.contains(&"."));
assert!(labels.contains(&"{red}"));
assert!(labels.contains(&"{blue}"));
}
#[test]
fn test_get_completions_with_palette() {
let client = LspAgentClient::new();
let content = r##"{"type": "palette", "name": "p", "colors": {"{skin}": "#FFE0BD", "{hair}": "#4A3C31"}}
{"type": "sprite", "name": "s", "palette": "p", "grid": ["{"]}"##;
let result = client.get_completions(content, 2, 50);
let labels: Vec<&str> = result.items.iter().map(|i| i.label.as_str()).collect();
assert!(labels.contains(&"{skin}"));
assert!(labels.contains(&"{hair}"));
}
#[test]
fn test_get_grid_position() {
let client = LspAgentClient::new();
let content = r#"{"type": "sprite", "name": "test", "grid": ["{a}{b}{c}"]}"#;
let grid_start = content.find("[\"").unwrap() + 2 + 3; let pos = client.get_grid_position(content, 1, grid_start);
assert!(pos.is_some());
let pos = pos.unwrap();
assert_eq!(pos.x, 1);
assert_eq!(pos.y, 0);
assert_eq!(pos.token, "{b}");
assert_eq!(pos.sprite_name, "test");
assert!(pos.aligned);
}
#[test]
fn test_verify_content_json() {
let client = LspAgentClient::new();
let content = r##"{"type": "palette", "name": "test", "colors": {"{a}": "#FF0000"}}"##;
let json = client.verify_content_json(content);
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["valid"], true);
assert_eq!(parsed["error_count"], 0);
}
#[test]
fn test_get_completions_json() {
let client = LspAgentClient::new();
let content = r##"{"type": "palette", "name": "test", "colors": {"{a}": "#FF0000"}}"##;
let json = client.get_completions_json(content, 1, 0);
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert!(parsed["items"].as_array().unwrap().len() >= 3);
}
#[test]
fn test_multiline_verification() {
let client = LspAgentClient::new();
let content = r##"{"type": "palette", "name": "p1", "colors": {"{a}": "#FF0000"}}
{"type": "palette", "name": "p2", "colors": {"{b}": "#00FF00"}}
{"type": "sprite", "name": "s1", "palette": "p1", "grid": ["{a}"]}
{"type": "sprite", "name": "s2", "palette": "p2", "grid": ["{b}"]}"##;
let result = client.verify_content(content);
assert!(result.valid);
assert_eq!(result.error_count, 0);
assert_eq!(result.warning_count, 0);
}
#[test]
fn test_suggestion_in_diagnostic() {
let client = LspAgentClient::new();
let content = r##"{"type": "palette", "name": "p", "colors": {"{skin}": "#FFE0BD"}}
{"type": "sprite", "name": "s", "palette": "p", "grid": ["{skni}"]}"##;
let result = client.verify_content(content);
assert_eq!(result.warning_count, 1);
assert!(result.warnings[0].suggestion.is_some());
assert!(result.warnings[0].suggestion.as_ref().unwrap().contains("{skin}"));
}
#[test]
fn test_empty_content() {
let client = LspAgentClient::new();
let result = client.verify_content("");
assert!(result.valid);
assert_eq!(result.error_count, 0);
assert_eq!(result.warning_count, 0);
}
#[test]
fn test_whitespace_only_content() {
let client = LspAgentClient::new();
let result = client.verify_content(" \n\n \n");
assert!(result.valid);
assert_eq!(result.error_count, 0);
}
#[test]
fn test_resolve_colors_simple_hex() {
let client = LspAgentClient::new();
let content = r##"{"type": "palette", "name": "test", "colors": {"{red}": "#FF0000", "{blue}": "#0000FF"}}"##;
let result = client.resolve_colors(content);
assert_eq!(result.error_count, 0);
assert_eq!(result.colors.len(), 2);
let red = result.colors.iter().find(|c| c.token == "{red}").unwrap();
assert_eq!(red.resolved, "#FF0000");
assert_eq!(red.palette, "test");
assert!(!red.is_variable);
}
#[test]
fn test_resolve_colors_css_variable() {
let client = LspAgentClient::new();
let content = r##"{"type": "palette", "name": "hero", "colors": {"--base": "#FF6347", "{skin}": "var(--base)"}}"##;
let result = client.resolve_colors(content);
assert_eq!(result.error_count, 0);
let base = result.colors.iter().find(|c| c.token == "--base").unwrap();
assert_eq!(base.resolved, "#FF6347");
assert!(base.is_variable);
let skin = result.colors.iter().find(|c| c.token == "{skin}").unwrap();
assert_eq!(skin.original, "var(--base)");
assert_eq!(skin.resolved, "#FF6347");
assert!(!skin.is_variable);
}
#[test]
fn test_resolve_colors_color_mix() {
let client = LspAgentClient::new();
let content = r##"{"type": "palette", "name": "hero", "colors": {"{shadow}": "color-mix(in srgb, red 50%, black)"}}"##;
let result = client.resolve_colors(content);
assert_eq!(result.error_count, 0);
assert_eq!(result.colors.len(), 1);
let shadow = result.colors.iter().find(|c| c.token == "{shadow}").unwrap();
assert!(shadow.original.contains("color-mix"));
assert!(shadow.resolved.starts_with('#'));
}
#[test]
fn test_resolve_colors_chained_variables() {
let client = LspAgentClient::new();
let content = r##"{"type": "palette", "name": "hero", "colors": {"--primary": "#FF0000", "--accent": "var(--primary)", "{highlight}": "var(--accent)"}}"##;
let result = client.resolve_colors(content);
assert_eq!(result.error_count, 0);
let highlight = result.colors.iter().find(|c| c.token == "{highlight}").unwrap();
assert_eq!(highlight.resolved, "#FF0000");
}
#[test]
fn test_resolve_colors_multiple_palettes() {
let client = LspAgentClient::new();
let content = r##"{"type": "palette", "name": "p1", "colors": {"--base": "#AA0000", "{a}": "var(--base)"}}
{"type": "palette", "name": "p2", "colors": {"{b}": "#00BB00"}}"##;
let result = client.resolve_colors(content);
assert_eq!(result.error_count, 0);
assert_eq!(result.colors.len(), 3);
let a = result.colors.iter().find(|c| c.token == "{a}").unwrap();
assert_eq!(a.palette, "p1");
let b = result.colors.iter().find(|c| c.token == "{b}").unwrap();
assert_eq!(b.palette, "p2");
}
#[test]
fn test_resolve_colors_json() {
let client = LspAgentClient::new();
let content = r##"{"type": "palette", "name": "test", "colors": {"{a}": "#FF0000"}}"##;
let json = client.resolve_colors_json(content);
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["error_count"], 0);
assert!(!parsed["colors"].as_array().unwrap().is_empty());
}
#[test]
fn test_analyze_timing_linear() {
let client = LspAgentClient::new();
let content = r#"{"type": "animation", "name": "slide", "timing_function": "linear"}"#;
let result = client.analyze_timing(content);
assert_eq!(result.animations.len(), 1);
let anim = &result.animations[0];
assert_eq!(anim.animation, "slide");
assert_eq!(anim.timing_function, "linear");
assert_eq!(anim.curve_type, "linear");
assert!(anim.description.contains("Constant speed"));
assert!(anim.ascii_curve.is_some());
}
#[test]
fn test_analyze_timing_ease_in_out() {
let client = LspAgentClient::new();
let content = r#"{"type": "animation", "name": "fade", "timing_function": "ease-in-out"}"#;
let result = client.analyze_timing(content);
assert_eq!(result.animations.len(), 1);
let anim = &result.animations[0];
assert_eq!(anim.curve_type, "smooth");
assert!(anim.description.contains("Slow start and end"));
}
#[test]
fn test_analyze_timing_cubic_bezier() {
let client = LspAgentClient::new();
let content = r#"{"type": "animation", "name": "custom", "timing_function": "cubic-bezier(0.25, 0.1, 0.25, 1.0)"}"#;
let result = client.analyze_timing(content);
assert_eq!(result.animations.len(), 1);
let anim = &result.animations[0];
assert_eq!(anim.curve_type, "smooth");
assert!(anim.description.contains("cubic bezier"));
}
#[test]
fn test_analyze_timing_steps() {
let client = LspAgentClient::new();
let content =
r#"{"type": "animation", "name": "walk", "timing_function": "steps(4, jump-end)"}"#;
let result = client.analyze_timing(content);
assert_eq!(result.animations.len(), 1);
let anim = &result.animations[0];
assert_eq!(anim.curve_type, "stepped");
assert!(anim.description.contains("4 discrete step"));
}
#[test]
fn test_analyze_timing_bounce() {
let client = LspAgentClient::new();
let content = r#"{"type": "animation", "name": "drop", "timing_function": "bounce"}"#;
let result = client.analyze_timing(content);
assert_eq!(result.animations.len(), 1);
let anim = &result.animations[0];
assert_eq!(anim.curve_type, "bouncy");
assert!(anim.description.contains("Bouncy effect"));
}
#[test]
fn test_analyze_timing_default_linear() {
let client = LspAgentClient::new();
let content = r#"{"type": "animation", "name": "idle", "frames": ["f1", "f2"]}"#;
let result = client.analyze_timing(content);
assert_eq!(result.animations.len(), 1);
let anim = &result.animations[0];
assert_eq!(anim.timing_function, "linear");
}
#[test]
fn test_analyze_timing_multiple_animations() {
let client = LspAgentClient::new();
let content = r#"{"type": "animation", "name": "walk", "timing_function": "linear"}
{"type": "animation", "name": "run", "timing_function": "ease-in"}
{"type": "animation", "name": "jump", "timing_function": "bounce"}"#;
let result = client.analyze_timing(content);
assert_eq!(result.animations.len(), 3);
let walk = result.animations.iter().find(|a| a.animation == "walk").unwrap();
assert_eq!(walk.curve_type, "linear");
let run = result.animations.iter().find(|a| a.animation == "run").unwrap();
assert_eq!(run.curve_type, "smooth");
let jump = result.animations.iter().find(|a| a.animation == "jump").unwrap();
assert_eq!(jump.curve_type, "bouncy");
}
#[test]
fn test_analyze_timing_json() {
let client = LspAgentClient::new();
let content = r#"{"type": "animation", "name": "test", "timing_function": "ease"}"#;
let json = client.analyze_timing_json(content);
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert!(!parsed["animations"].as_array().unwrap().is_empty());
}
#[test]
fn test_ascii_curve_rendering() {
let client = LspAgentClient::new();
let content = r#"{"type": "animation", "name": "test", "timing_function": "ease-in-out"}"#;
let result = client.analyze_timing(content);
let anim = &result.animations[0];
let curve = anim.ascii_curve.as_ref().unwrap();
assert!(curve.contains('┌'));
assert!(curve.contains('┐'));
assert!(curve.contains('└'));
assert!(curve.contains('┘'));
assert!(curve.contains('│'));
assert!(curve.contains('█'));
}
}