Step 348: Syntax Highlighting Theme Bridge (12/12 tests)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
126
editor/src/SyntaxThemeBridge.h
Normal file
126
editor/src/SyntaxThemeBridge.h
Normal file
@@ -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 <string>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#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<std::string, Color4> getAllMappings(const WhetstoneTheme& theme) {
|
||||
std::map<std::string, Color4> 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<ThemedSpan> applyTheme(
|
||||
const std::vector<std::pair<uint32_t, uint32_t>>& ranges,
|
||||
const std::vector<TokenCategoryBridge>& categories,
|
||||
const WhetstoneTheme& theme)
|
||||
{
|
||||
std::vector<ThemedSpan> 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";
|
||||
}
|
||||
}
|
||||
};
|
||||
150
editor/tests/step348_test.cpp
Normal file
150
editor/tests/step348_test.cpp
Normal file
@@ -0,0 +1,150 @@
|
||||
// Step 348: Syntax Highlighting Theme Bridge (12 tests)
|
||||
// Tests TokenCategory→Color4 mapping, themed spans, readability, semanno colors
|
||||
|
||||
#include <cassert>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <set>
|
||||
#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<TokenCategoryBridge> 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<std::pair<uint32_t, uint32_t>> ranges = {{0, 3}, {4, 10}, {11, 15}};
|
||||
std::vector<TokenCategoryBridge> 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;
|
||||
}
|
||||
Reference in New Issue
Block a user