diff --git a/editor/src/SyntaxThemeBridge.h b/editor/src/SyntaxThemeBridge.h new file mode 100644 index 0000000..f2c1b46 --- /dev/null +++ b/editor/src/SyntaxThemeBridge.h @@ -0,0 +1,126 @@ +#pragma once +// Step 348: Syntax Theme Bridge — maps TokenCategory to theme colors +// Connects the existing SyntaxHighlighter's TokenCategory enum to WhetstoneTheme colors +// Headless: no ImGui dependency. Provides Color4 values that renderers can apply. + +#include +#include +#include +#include "ThemeData.h" + +// TokenCategory duplicated here for headless testing (original is in SyntaxHighlighter.h +// which depends on tree-sitter). Enum values must match. +enum class TokenCategoryBridge { + Plain, Keyword, String, Comment, Number, Identifier, + Operator, Type, Punctuation, Function, Parameter, Builtin +}; + +struct ThemedSpan { + uint32_t start; + uint32_t end; + TokenCategoryBridge category; + Color4 color; +}; + +class SyntaxThemeBridge { +public: + // Map a token category to its theme color + static Color4 colorForCategory(TokenCategoryBridge cat, const WhetstoneTheme& theme) { + switch (cat) { + case TokenCategoryBridge::Keyword: return theme.syntaxKeyword; + case TokenCategoryBridge::String: return theme.syntaxString; + case TokenCategoryBridge::Comment: return theme.syntaxComment; + case TokenCategoryBridge::Number: return theme.syntaxNumber; + case TokenCategoryBridge::Function: return theme.syntaxFunction; + case TokenCategoryBridge::Type: return theme.syntaxType; + case TokenCategoryBridge::Operator: return theme.syntaxOperator; + case TokenCategoryBridge::Parameter: return theme.syntaxAnnotation; + case TokenCategoryBridge::Builtin: return theme.syntaxFunction; + case TokenCategoryBridge::Identifier: return theme.text; + case TokenCategoryBridge::Punctuation: return theme.textDim; + case TokenCategoryBridge::Plain: + default: return theme.text; + } + } + + // Get all category-color mappings for a theme (useful for settings UI) + static std::map getAllMappings(const WhetstoneTheme& theme) { + std::map m; + m["Keyword"] = theme.syntaxKeyword; + m["String"] = theme.syntaxString; + m["Comment"] = theme.syntaxComment; + m["Number"] = theme.syntaxNumber; + m["Function"] = theme.syntaxFunction; + m["Type"] = theme.syntaxType; + m["Operator"] = theme.syntaxOperator; + m["Parameter"] = theme.syntaxAnnotation; + m["Builtin"] = theme.syntaxFunction; + m["Identifier"] = theme.text; + m["Punctuation"] = theme.textDim; + m["Plain"] = theme.text; + return m; + } + + // Apply theme colors to a set of spans (returns ThemedSpans with colors filled in) + static std::vector applyTheme( + const std::vector>& ranges, + const std::vector& categories, + const WhetstoneTheme& theme) + { + std::vector result; + size_t n = std::min(ranges.size(), categories.size()); + result.reserve(n); + for (size_t i = 0; i < n; ++i) { + result.push_back({ranges[i].first, ranges[i].second, + categories[i], colorForCategory(categories[i], theme)}); + } + return result; + } + + // Get the line number color (always textDim) + static Color4 lineNumberColor(const WhetstoneTheme& theme) { return theme.textDim; } + + // Get the current-line highlight color (bgAlt) + static Color4 currentLineColor(const WhetstoneTheme& theme) { return theme.bgAlt; } + + // Get selection colors + static Color4 selectionBgColor(const WhetstoneTheme& theme) { return theme.selectionBg; } + static Color4 selectionTextColor(const WhetstoneTheme& theme) { return theme.selectionText; } + + // Semanno annotation color (distinct from regular comments) + static Color4 semannoAnnotationColor(const WhetstoneTheme& theme) { return theme.syntaxAnnotation; } + + // Check if all syntax colors are distinct from background (readability check) + static bool allColorsReadable(const WhetstoneTheme& theme) { + auto bg = theme.bg; + auto check = [&](const Color4& c) { + float dr = c.r - bg.r, dg = c.g - bg.g, db = c.b - bg.b; + float dist = dr*dr + dg*dg + db*db; + return dist > 0.01f; // minimum contrast threshold + }; + return check(theme.syntaxKeyword) && check(theme.syntaxString) && + check(theme.syntaxNumber) && check(theme.syntaxComment) && + check(theme.syntaxFunction) && check(theme.syntaxType) && + check(theme.syntaxOperator) && check(theme.syntaxAnnotation) && + check(theme.text) && check(theme.textDim); + } + + // Get category name as string + static std::string categoryName(TokenCategoryBridge cat) { + switch (cat) { + case TokenCategoryBridge::Keyword: return "Keyword"; + case TokenCategoryBridge::String: return "String"; + case TokenCategoryBridge::Comment: return "Comment"; + case TokenCategoryBridge::Number: return "Number"; + case TokenCategoryBridge::Function: return "Function"; + case TokenCategoryBridge::Type: return "Type"; + case TokenCategoryBridge::Operator: return "Operator"; + case TokenCategoryBridge::Parameter: return "Parameter"; + case TokenCategoryBridge::Builtin: return "Builtin"; + case TokenCategoryBridge::Identifier: return "Identifier"; + case TokenCategoryBridge::Punctuation: return "Punctuation"; + case TokenCategoryBridge::Plain: + default: return "Plain"; + } + } +}; diff --git a/editor/tests/step348_test.cpp b/editor/tests/step348_test.cpp new file mode 100644 index 0000000..789d3fe --- /dev/null +++ b/editor/tests/step348_test.cpp @@ -0,0 +1,150 @@ +// Step 348: Syntax Highlighting Theme Bridge (12 tests) +// Tests TokenCategory→Color4 mapping, themed spans, readability, semanno colors + +#include +#include +#include +#include +#include "SyntaxThemeBridge.h" + +int main() { + int passed = 0; + + // Test 1: Keyword maps to syntaxKeyword color + { + auto theme = getDefaultDarkTheme(); + auto c = SyntaxThemeBridge::colorForCategory(TokenCategoryBridge::Keyword, theme); + assert(c.toHex() == theme.syntaxKeyword.toHex()); + std::cout << "Test 1 PASSED: Keyword → syntaxKeyword\n"; + passed++; + } + + // Test 2: All categories produce non-black colors + { + auto theme = getDefaultDarkTheme(); + std::vector cats = { + TokenCategoryBridge::Plain, TokenCategoryBridge::Keyword, + TokenCategoryBridge::String, TokenCategoryBridge::Comment, + TokenCategoryBridge::Number, TokenCategoryBridge::Identifier, + TokenCategoryBridge::Operator, TokenCategoryBridge::Type, + TokenCategoryBridge::Punctuation, TokenCategoryBridge::Function, + TokenCategoryBridge::Parameter, TokenCategoryBridge::Builtin + }; + for (auto cat : cats) { + auto c = SyntaxThemeBridge::colorForCategory(cat, theme); + assert(!c.isPureBlack()); + } + std::cout << "Test 2 PASSED: All 12 categories produce non-black colors\n"; + passed++; + } + + // Test 3: String and Comment mapped to different colors + { + auto theme = getDefaultDarkTheme(); + auto str = SyntaxThemeBridge::colorForCategory(TokenCategoryBridge::String, theme); + auto cmt = SyntaxThemeBridge::colorForCategory(TokenCategoryBridge::Comment, theme); + assert(str.toHex() != cmt.toHex()); + std::cout << "Test 3 PASSED: String ≠ Comment color\n"; + passed++; + } + + // Test 4: getAllMappings returns 12 entries + { + auto theme = getDefaultDarkTheme(); + auto map = SyntaxThemeBridge::getAllMappings(theme); + assert(map.size() == 12); + assert(map.count("Keyword") == 1); + assert(map.count("String") == 1); + assert(map.count("Plain") == 1); + std::cout << "Test 4 PASSED: getAllMappings returns 12 entries\n"; + passed++; + } + + // Test 5: applyTheme produces ThemedSpans with correct colors + { + auto theme = getDefaultDarkTheme(); + std::vector> ranges = {{0, 3}, {4, 10}, {11, 15}}; + std::vector cats = { + TokenCategoryBridge::Keyword, TokenCategoryBridge::String, TokenCategoryBridge::Number + }; + auto spans = SyntaxThemeBridge::applyTheme(ranges, cats, theme); + assert(spans.size() == 3); + assert(spans[0].color.toHex() == theme.syntaxKeyword.toHex()); + assert(spans[1].color.toHex() == theme.syntaxString.toHex()); + assert(spans[2].color.toHex() == theme.syntaxNumber.toHex()); + assert(spans[0].start == 0 && spans[0].end == 3); + std::cout << "Test 5 PASSED: applyTheme produces correctly colored spans\n"; + passed++; + } + + // Test 6: Line number color is textDim + { + auto theme = getDefaultDarkTheme(); + auto c = SyntaxThemeBridge::lineNumberColor(theme); + assert(c.toHex() == theme.textDim.toHex()); + std::cout << "Test 6 PASSED: Line numbers use textDim\n"; + passed++; + } + + // Test 7: Current line highlight is bgAlt + { + auto theme = getDefaultDarkTheme(); + auto c = SyntaxThemeBridge::currentLineColor(theme); + assert(c.toHex() == theme.bgAlt.toHex()); + std::cout << "Test 7 PASSED: Current line uses bgAlt\n"; + passed++; + } + + // Test 8: Selection colors correct + { + auto theme = getDefaultDarkTheme(); + assert(SyntaxThemeBridge::selectionBgColor(theme).toHex() == theme.selectionBg.toHex()); + assert(SyntaxThemeBridge::selectionTextColor(theme).toHex() == theme.selectionText.toHex()); + std::cout << "Test 8 PASSED: Selection colors correct\n"; + passed++; + } + + // Test 9: Semanno annotation gets distinct color + { + auto theme = getDefaultDarkTheme(); + auto ann = SyntaxThemeBridge::semannoAnnotationColor(theme); + auto cmt = SyntaxThemeBridge::colorForCategory(TokenCategoryBridge::Comment, theme); + assert(ann.toHex() != cmt.toHex()); // annotation ≠ comment + assert(ann.toHex() == theme.syntaxAnnotation.toHex()); + std::cout << "Test 9 PASSED: Semanno annotation color distinct from comment\n"; + passed++; + } + + // Test 10: allColorsReadable — dark theme passes contrast check + { + auto theme = getDefaultDarkTheme(); + assert(SyntaxThemeBridge::allColorsReadable(theme)); + std::cout << "Test 10 PASSED: Dark theme passes readability check\n"; + passed++; + } + + // Test 11: allColorsReadable — light theme passes too + { + auto theme = getDefaultLightTheme(); + assert(SyntaxThemeBridge::allColorsReadable(theme)); + std::cout << "Test 11 PASSED: Light theme passes readability check\n"; + passed++; + } + + // Test 12: categoryName returns correct strings + { + assert(SyntaxThemeBridge::categoryName(TokenCategoryBridge::Keyword) == "Keyword"); + assert(SyntaxThemeBridge::categoryName(TokenCategoryBridge::String) == "String"); + assert(SyntaxThemeBridge::categoryName(TokenCategoryBridge::Comment) == "Comment"); + assert(SyntaxThemeBridge::categoryName(TokenCategoryBridge::Number) == "Number"); + assert(SyntaxThemeBridge::categoryName(TokenCategoryBridge::Function) == "Function"); + assert(SyntaxThemeBridge::categoryName(TokenCategoryBridge::Type) == "Type"); + assert(SyntaxThemeBridge::categoryName(TokenCategoryBridge::Plain) == "Plain"); + std::cout << "Test 12 PASSED: categoryName strings correct\n"; + passed++; + } + + std::cout << "\nResults: " << passed << "/12\n"; + assert(passed == 12); + return 0; +}