sdl_runtime_compiler git · main
SDL3 game for running and compiling code at runtime
C++ 72.3% C 26.2%git clone https://git.christianimmanuel.de/sdl-graphics/sdl_runtime_compiler.gitwget https://git.christianimmanuel.de/sdl-graphics/sdl_runtime_compiler/archive/sdl_runtime_compiler.tar.gzAdd tree-sitter to create propper cpp-token tree
.gitignore | 1 +
CMakeLists.txt | 54 ++++++
README.md | 11 ++
src/c_token/token_color.hpp | 39 ----
src/c_token/token_db.cpp | 362 -----------------------------------
src/c_token/token_db.hpp | 15 --
src/cpp_token/cpp_highlight.cpp | 113 +++++++++++
src/cpp_token/cpp_highlight.hpp | 11 ++
src/{c_token => cpp_token}/token.hpp | 6 +
src/cpp_token/token_color.hpp | 31 +++
src/cpp_token/token_db.cpp | 158 +++++++++++++++
src/cpp_token/token_db.hpp | 17 ++
src/cpp_token/ts_test.cpp | 21 ++
src/states/game_state.cpp | 141 ++++++++++++--
src/states/game_state.hpp | 16 +-
src/systems/cpp_highlight_system.hpp | 74 +++++++
src/systems/wall_connection.cpp | 224 +++++++++++++---------
src/systems/wall_connection.hpp | 3 +-
src/systems/word_ai_system.cpp | 11 +-
src/text_3d/text_3d.cpp | 12 +-
src/text_3d/text_3d.hpp | 4 +-
src/text_3d/text_file.hpp | 84 ++++++++
src/wall/editor_wall.cpp | 30 +++
src/wall/editor_wall.hpp | 3 +
src/wall/text_wall.cpp | 36 ++--
src/wall/text_wall.hpp | 2 +-
26 files changed, 913 insertions(+), 566 deletions(-)
diff --git a/.gitignore b/.gitignore
index 1db59fe..b1102d9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,4 @@ compile_commands.json
build/
build-prof/
+external/treesitter*
diff --git a/CMakeLists.txt b/CMakeLists.txt
index fa73236..2b62ecc 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -59,6 +59,8 @@ function(apply_common_settings target)
OpenGL::GL
m
${CMAKE_DL_LIBS}
+ tree_sitter_runtime
+ tree_sitter_cpp
)
# Profiler flag.
@@ -98,6 +100,58 @@ function(apply_common_settings target)
)
endfunction()
+# ─────────────────────────────────────────────────────────────────────────────
+# tree-sitter (C++ syntax highlighting) — vendored at configure time.
+# Runtime (MIT) + C++ grammar (MIT). We compile the committed generated C
+# sources directly; no tree-sitter CLI / generation step is involved.
+# ─────────────────────────────────────────────────────────────────────────────
+set(FETCHCONTENT_BASE_DIR "${CMAKE_SOURCE_DIR}/external")
+
+include(FetchContent)
+
+FetchContent_Declare(treesitter
+ GIT_REPOSITORY https://github.com/tree-sitter/tree-sitter.git
+ GIT_TAG v0.26.9
+ GIT_SHALLOW TRUE
+)
+FetchContent_Declare(treesitter_cpp
+ GIT_REPOSITORY https://github.com/tree-sitter/tree-sitter-cpp.git
+ GIT_TAG v0.23.4
+ GIT_SHALLOW TRUE
+)
+
+message(STATUS "Fetching tree-sitter... <<-- Do i need that ?")
+FetchContent_MakeAvailable(treesitter)
+message(STATUS "tree-sitter ready")
+
+message(STATUS "Fetching tree-sitter-cpp...")
+FetchContent_MakeAvailable(treesitter_cpp)
+message(STATUS "tree-sitter-cpp ready")
+
+# The tree-sitter runtime: compile the amalgamated lib.c.
+add_library(tree_sitter_runtime STATIC
+ ${treesitter_SOURCE_DIR}/lib/src/lib.c
+)
+target_include_directories(tree_sitter_runtime PUBLIC
+ ${treesitter_SOURCE_DIR}/lib/include
+)
+target_include_directories(tree_sitter_runtime PRIVATE
+ ${treesitter_SOURCE_DIR}/lib/src
+)
+
+# The C++ grammar: generated parser + external scanner.
+add_library(tree_sitter_cpp STATIC
+ ${treesitter_cpp_SOURCE_DIR}/src/parser.c
+ ${treesitter_cpp_SOURCE_DIR}/src/scanner.c
+)
+target_include_directories(tree_sitter_cpp PRIVATE
+ ${treesitter_cpp_SOURCE_DIR}/src
+)
+# Expose the function declaration tree_sitter_cpp() to our C++ code.
+target_include_directories(tree_sitter_cpp PUBLIC
+ ${treesitter_cpp_SOURCE_DIR}/bindings/c
+)
+
# ─────────────────────────────────────────────────────────────────────────────
# Main executable
# ─────────────────────────────────────────────────────────────────────────────
diff --git a/README.md b/README.md
index 433f24c..1102f10 100644
--- a/README.md
+++ b/README.md
@@ -22,6 +22,10 @@ git submodule update --remote --recursive
> Better replace my own smartpointer with std ones^^
+When compiling we fetch [tree-sitter](https://github.com/tree-sitter/tree-sitter) and [tree-sitter-pp](https://github.com/tree-sitter/tree-sitter-cpp) to lex CPP
+
+
+
---
The following packages need to be findable by cmake's find\_package:
@@ -34,6 +38,13 @@ The following packages need to be findable by cmake's find\_package:
## Compilation
+Check that the font we use is existent at your system or fix the font path to an existing font which is actually getting set in [src/text\_3d/text\_3d.hpp](src/text_3d/text_3d.hpp)
+
+```
+sdl_runtime_compiler > grep -rI "/usr/sh" src/
+src/text_3d/text_3d.hpp: "/usr/share/fonts/dejavu/DejaVuSansMono.ttf";
+```
+
Smash a
```bash
diff --git a/src/c_token/token_color.hpp b/src/c_token/token_color.hpp
deleted file mode 100644
index 70594f6..0000000
--- a/src/c_token/token_color.hpp
+++ /dev/null
@@ -1,39 +0,0 @@
-#pragma once
-#include <SDL3/SDL.h>
-#include "token.hpp"
-#include "token_db.hpp"
-#include "text_3d/text_3d.hpp"
-
-namespace TokenColor
-{
-inline SDL_FColor
-get(TokenType t)
-{
- switch (t) {
- case TokenType::Keyword: return ConfigColor::orange; // if, for, return
- case TokenType::Type: return ConfigColor::green; // int, void
- case TokenType::Identifier: return ConfigColor::light_gray; // main, printf, i, x …
- case TokenType::Literal: return ConfigColor::red; // 0, 1, "\n", %s
- case TokenType::Operator: return ConfigColor::light_gray; // = == + - * / ++
- case TokenType::Delimiter: return ConfigColor::light_gray; // ( ) { } ; , [ ]
- case TokenType::Preprocessor:return ConfigColor::pink; // #define #if
- }
- return ConfigColor::white;
-}
-
-inline void
-apply(Text3d* word)
-{
- if (!word) return;
- const Token* tok = TokenDB::find_OrSynthesize(word->text);
- if (tok) word->set_Color(get(tok->type));
-}
-
-inline SDL_FColor
-get_ByString(std::string token_text)
-{
- const Token* tok = TokenDB::find_OrSynthesize(token_text);
- return tok ? get(tok->type) : ConfigColor::white;
-}
-
-}
diff --git a/src/c_token/token_db.cpp b/src/c_token/token_db.cpp
deleted file mode 100644
index 1a0b403..0000000
--- a/src/c_token/token_db.cpp
+++ /dev/null
@@ -1,362 +0,0 @@
-#include "token_db.hpp"
-#include <unordered_map>
-
-
-static const DynArray<Token>
-all_tokens = {
- { "int", TokenType::Type, "Integer type", "Declares a 32-bit integer variable" },
- { "float", TokenType::Type, "Float type", "Declares a floating point variable" },
- { "char", TokenType::Type, "Character type", "Declares a single character" },
- { "void", TokenType::Type, "No value type", "Function returns nothing" },
- { "bool", TokenType::Type, "Boolean type", "Stores true or false values" },
- { "short", TokenType::Type, "Short integer type", "Declares a short integer variable" },
- { "long", TokenType::Type, "Long integer type", "Declares a long integer variable" },
- { "unsigned", TokenType::Type, "Unsigned modifier", "Declares an unsigned integer type" },
- { "signed", TokenType::Type, "Signed modifier", "Declares a signed integer type" },
- { "double", TokenType::Type, "Double type", "Declares a double-precision floating point variable" },
- { "wchar_t", TokenType::Type, "Wide character type", "Stores wide character values" },
- { "char8_t", TokenType::Type, "UTF-8 character type", "Stores a UTF-8 code unit" },
- { "char16_t", TokenType::Type, "UTF-16 character type", "Stores a UTF-16 code unit" },
- { "char32_t", TokenType::Type, "UTF-32 character type", "Stores a UTF-32 code unit" },
- { "size_t", TokenType::Type, "Size type", "Stores object sizes and array indices" },
- { "struct", TokenType::Type, "Structure type", "Defines a structure type" },
- { "union", TokenType::Type, "Union type", "Defines a union type" },
- { "enum", TokenType::Type, "Enumeration type", "Defines an enumeration type" },
- { "typedef", TokenType::Type, "Type alias", "Creates an alias for a type" },
- { "extern", TokenType::Type, "External linkage", "Declares a symbol defined elsewhere" },
- { "static", TokenType::Type, "Static storage", "Declares static storage duration" },
- { "register", TokenType::Type, "Register storage", "Hints that a variable is frequently used" },
- { "volatile", TokenType::Type, "Volatile qualifier", "Prevents certain compiler optimizations" },
- { "const", TokenType::Type, "Constant qualifier", "Prevents modification of an object" },
-
- { "return", TokenType::Keyword, "Return statement", "Ends function and returns a value" },
- { "if", TokenType::Keyword, "Conditional branch", "Executes code if condition is true" },
- { "else", TokenType::Keyword, "Alternative branch", "Executes if 'if' condition is false" },
- { "while", TokenType::Keyword, "While loop", "Repeats while condition is true" },
- { "for", TokenType::Keyword, "For loop", "Loop with init, condition, increment"},
- { "switch", TokenType::Keyword, "Switch statement", "Selects execution path based on a value" },
- { "case", TokenType::Keyword, "Case label", "Marks a branch in a switch statement" },
- { "default", TokenType::Keyword, "Default label", "Fallback branch in a switch statement" },
- { "break", TokenType::Keyword, "Break statement", "Exits a loop or switch statement" },
- { "continue", TokenType::Keyword, "Continue statement", "Skips to the next loop iteration" },
- { "goto", TokenType::Keyword, "Goto statement", "Jumps to a labeled statement" },
- { "do", TokenType::Keyword, "Do loop", "Begins a do-while loop" },
- { "sizeof", TokenType::Keyword, "Size operator", "Returns the size of a type or object" },
-
- { "main", TokenType::Identifier, "Entry point", "First function called by the OS" },
- { "x", TokenType::Identifier, "Variable name", "Stores a value" },
- { "y", TokenType::Identifier, "Variable name", "Stores a value" },
- { "z", TokenType::Identifier, "Variable name", "Stores a value" },
- { "func", TokenType::Identifier, "Function name", "A callable block of code" },
- { "printf", TokenType::Identifier, "Print formatted", "Outputs formatted text to stdout" },
- { "strlen", TokenType::Identifier, "String length", "Returns length of a string" },
- { "src", TokenType::Identifier, "Source variable", "Input data" },
- { "dst", TokenType::Identifier, "Destination variable","Output data" },
- { "i", TokenType::Identifier, "Loop counter", "Common loop index variable" },
- { "j", TokenType::Identifier, "Loop counter", "Common loop index variable" },
- { "k", TokenType::Identifier, "Loop counter", "Common loop index variable" },
-
- { "NULL", TokenType::Literal, "Null pointer constant", "Macro representing a null pointer" },
- { "0", TokenType::Literal, "Integer zero", "The integer constant 0" },
- { "1", TokenType::Literal, "Integer one", "The integer constant 1" },
- { "\"\\n\"",TokenType::Literal, "Newline string", "String containing a newline character"},
- { "%s", TokenType::Literal, "String format", "Printf format specifier for strings" },
- { "\"", TokenType::Literal, "Double quote character","Quotation mark character (\")" },
- { "'", TokenType::Literal, "Single quote character","Apostrophe or character literal delimiter" },
- { "true", TokenType::Literal, "Boolean true", "Boolean constant true" },
- { "false", TokenType::Literal, "Boolean false", "Boolean constant false" },
- { "%d", TokenType::Literal, "Integer format", "Printf format specifier for integers" },
- { "%u", TokenType::Literal, "Unsigned format", "Printf format specifier for unsigned integers" },
- { "%f", TokenType::Literal, "Float format", "Printf format specifier for floating-point values" },
- { "%c", TokenType::Literal, "Character format", "Printf format specifier for characters" },
- { "%p", TokenType::Literal, "Pointer format", "Printf format specifier for pointers" },
- { "%%", TokenType::Literal, "Percent format", "Printf escape sequence for percent sign" },
- { "%ld", TokenType::Literal, "Long integer format", "Printf format specifier for long integers" },
- { "%lld", TokenType::Literal, "Long long format", "Printf format specifier for long long integers" },
- { "%lu", TokenType::Literal, "Unsigned long format", "Printf format specifier for unsigned long integers" },
- { "%llu", TokenType::Literal, "Unsigned long long format", "Printf format specifier for unsigned long long integers" },
- { "%x", TokenType::Literal, "Hex lowercase", "Printf format specifier for hexadecimal (lowercase)" },
- { "%X", TokenType::Literal, "Hex uppercase", "Printf format specifier for hexadecimal (uppercase)" },
- { "%o", TokenType::Literal, "Octal format", "Printf format specifier for octal integers" },
- { "%e", TokenType::Literal, "Scientific format", "Printf format specifier for scientific notation (lowercase e)" },
- { "%E", TokenType::Literal, "Scientific format", "Printf format specifier for scientific notation (uppercase E)" },
- { "%g", TokenType::Literal, "Shortest float format", "Printf format specifier for shortest representation (float/double)" },
- { "%G", TokenType::Literal, "Shortest float format", "Printf format specifier for shortest representation (uppercase)" },
- { "%s", TokenType::Literal, "String format", "Printf format specifier for null-terminated strings" },
- { "%n", TokenType::Literal, "Write-back count", "Printf specifier that writes number of characters printed so far" },
-
- { "+", TokenType::Operator, "Addition", "Adds two operands" },
- { "-", TokenType::Operator, "Subtraction", "Subtracts right operand from left" },
- //{ "*", TokenType::Operator, "Multiplication", "Multiplies two operands" },
- { "/", TokenType::Operator, "Division", "Divides left operand by right" },
-
- { "++", TokenType::Operator, "Increment", "Increases value by 1" },
- { "--", TokenType::Operator, "Decrement", "Decreases value by 1" },
- { "!=", TokenType::Operator, "Not equal", "Checks whether two values differ" },
- { "<", TokenType::Operator, "Less than", "Checks whether left is less than right" },
- { ">", TokenType::Operator, "Greater than", "Checks whether left is greater than right" },
- { "<=", TokenType::Operator, "Less or equal", "Checks whether left is less than or equal to right" },
- { ">=", TokenType::Operator, "Greater or equal", "Checks whether left is greater than or equal to right" },
- { "+=", TokenType::Operator, "Addition assignment", "Adds and assigns the result" },
- { "-=", TokenType::Operator, "Subtraction assignment","Subtracts and assigns the result" },
- { "*=", TokenType::Operator, "Multiplication assignment", "Multiplies and assigns the result" },
- { "/=", TokenType::Operator, "Division assignment", "Divides and assigns the result" },
- { "%=", TokenType::Operator, "Modulo assignment", "Calculates remainder and assigns it" },
- { "%", TokenType::Operator, "Modulo", "Returns the division remainder" },
- { "&&", TokenType::Operator, "Logical AND", "True if both operands are true" },
- { "||", TokenType::Operator, "Logical OR", "True if either operand is true" },
- { "!", TokenType::Operator, "Logical NOT", "Inverts a boolean value" },
- { "&", TokenType::Operator, "Bitwise AND", "Performs bitwise conjunction" },
- { "|", TokenType::Operator, "Bitwise OR", "Performs bitwise disjunction" },
- { "^", TokenType::Operator, "Bitwise XOR", "Performs bitwise exclusive OR" },
- { "~", TokenType::Operator, "Bitwise NOT", "Inverts all bits" },
- { "<<", TokenType::Operator, "Left shift", "Shifts bits to the left" },
- { ">>", TokenType::Operator, "Right shift", "Shifts bits to the right" },
- { "<<=", TokenType::Operator, "Left shift assignment", "Left shifts and assigns the result" },
- { ">>=", TokenType::Operator, "Right shift assignment","Right shifts and assigns the result" },
- { "&=", TokenType::Operator, "AND assignment", "Performs bitwise AND and assigns" },
- { "|=", TokenType::Operator, "OR assignment", "Performs bitwise OR and assigns" },
- { "^=", TokenType::Operator, "XOR assignment", "Performs bitwise XOR and assigns" },
- { "->", TokenType::Operator, "Member access", "Accesses a member through a pointer" },
- { ".", TokenType::Operator, "Member access", "Accesses a member of an object" },
- { "?", TokenType::Operator, "Conditional operator", "Begins a ternary conditional expression" },
- { ":", TokenType::Operator, "Conditional separator", "Separates true and false branches" },
- { "*", TokenType::Operator, "Dereference", "Accesses the object pointed to by a pointer" },
-
- { "(", TokenType::Delimiter, "Open parenthesis", "Starts a function call or condition" },
- { ")", TokenType::Delimiter, "Close parenthesis", "Ends a function call or condition" },
- { "{", TokenType::Delimiter, "Open brace", "Starts a code block" },
- { "}", TokenType::Delimiter, "Close brace", "Ends a code block" },
- { ";", TokenType::Delimiter, "Statement terminator","Ends a statement" },
- { ",", TokenType::Delimiter, "Separator", "Separates function arguments" },
- { "[", TokenType::Delimiter, "Open bracket", "Start array subscript" },
- { "]", TokenType::Delimiter, "Close bracket", "End array subscript" },
-
- { "#include", TokenType::Preprocessor, "Include directive", "Includes a header file" },
- { "#define", TokenType::Preprocessor, "Macro definition", "Defines a macro" },
- { "#undef", TokenType::Preprocessor, "Macro undefinition","Removes a macro definition" },
-
- { "#ifdef", TokenType::Preprocessor, "If defined", "Compiles code if macro is defined" },
- { "#ifndef", TokenType::Preprocessor, "If not defined", "Compiles code if macro is not defined" },
- { "#if", TokenType::Preprocessor, "If condition", "Conditional compilation block" },
- { "#elif", TokenType::Preprocessor, "Else if", "Alternative conditional branch" },
- { "#else", TokenType::Preprocessor, "Else branch", "Fallback conditional branch" },
- { "#endif", TokenType::Preprocessor, "End conditional", "Ends conditional compilation block" },
-};
-
-
-static const std::unordered_map<std::string, const Token*>
-build_index()
-{
- std::unordered_map<std::string, const Token*> m;
- for (const Token& t : all_tokens)
- m[t.text] = &t;
- return m;
-}
-
-
-static const std::unordered_map<std::string, const Token*>
-token_index = build_index();
-
-
-DynArray<std::string>
-TokenDB::get_TestArrayAllKeywords()
-{
- return { "float", "return", "main", "0", "++", "(", ",", ";", "\"", "\"", "#define" };
-}
-
-
-const Token*
-TokenDB::find(const std::string& text)
-{
- auto it = token_index.find(text);
- return (it != token_index.end()) ? it->second : nullptr;
-}
-
-
-DynArray<const Token*>
-TokenDB::get_AllOfType(TokenType type)
-{
- DynArray<const Token*> result;
- for (const auto& t : all_tokens)
- if (t.type == type)
- result.push_back(&t);
- return result;
-}
-
-
-const char*
-TokenDB::get_TypeName(TokenType t)
-{
- switch (t) {
- case TokenType::Keyword: return "Keyword";
- case TokenType::Type: return "Type";
- case TokenType::Identifier: return "Identifier";
- case TokenType::Literal: return "Literal";
- case TokenType::Operator: return "Operator";
- case TokenType::Delimiter: return "Delimiter";
- case TokenType::Preprocessor:return "Preprocessor";
- }
- return "Unknown";
-}
-
-
-bool
-TokenDB::known(const std::string& text)
-{
- return token_index.find(text) != token_index.end();
-}
-
-
-// ---------------------------------------------------------------------------
-// Pattern helpers
-// ---------------------------------------------------------------------------
-
-static bool
-is_number(const std::string& s)
-{
- if (s.empty()) return false;
- size_t i = (s[0] == '-' || s[0] == '+') ? 1 : 0;
- if (i == s.size()) return false;
-
- // hex: 0x / 0X
- if (i + 1 < s.size() && s[i] == '0' && (s[i+1] == 'x' || s[i+1] == 'X')) {
- for (size_t j = i + 2; j < s.size(); ++j) {
- char c = s[j];
- if (std::isxdigit(c)) continue;
- if (std::string("uUlL").find(c) != std::string::npos) continue;
- return false;
- }
- return i + 2 < s.size();
- }
-
- bool has_dot = false, has_exp = false;
- for (; i < s.size(); ++i) {
- char c = s[i];
- if (std::isdigit(c)) continue;
- if (c == '.' && !has_dot && !has_exp) { has_dot = true; continue; }
- if ((c == 'e' || c == 'E') && !has_exp && i > 0) {
- has_exp = true;
- if (i + 1 < s.size() && (s[i+1] == '+' || s[i+1] == '-')) ++i;
- continue;
- }
- if (std::string("fFuUlL").find(c) != std::string::npos && i == s.size() - 1) continue;
- return false;
- }
- return true;
-}
-
-static bool
-is_string_literal(const std::string& s)
-{
- if (s.size() < 2) return false;
- if (s.front() != '"' || s.back() != '"') return false;
- // ensure closing " is not escaped
- size_t backslashes = 0;
- for (size_t i = s.size() - 2; i > 0 && s[i] == '\\'; --i)
- ++backslashes;
- return (backslashes % 2) == 0;
-}
-
-static bool
-is_char_literal(const std::string& s)
-{
- // 'a' '\n' '\0' '\x41'
- if (s.size() < 3) return false;
- if (s.front() != '\'' || s.back() != '\'') return false;
- std::string inner = s.substr(1, s.size() - 2);
- if (inner.empty() || inner.size() > 6) return false;
- if (inner[0] != '\\') return inner.size() == 1; // plain char
- return inner.size() >= 2; // escape sequence
-}
-
-
-// ---------------------------------------------------------------------------
-// Token synthesizers
-// ---------------------------------------------------------------------------
-
-static const Token*
-synth_number(const std::string& s)
-{
- bool is_hex = s.size() > 2 && s[0] == '0' && (s[1] == 'x' || s[1] == 'X');
-
- // collect suffix characters (trailing u U l L f F)
- size_t suffix_start = s.size();
- while (suffix_start > 0 && std::string("uUlLfF").find(s[suffix_start - 1]) != std::string::npos)
- --suffix_start;
- std::string suffix = s.substr(suffix_start);
-
- // lowercase the suffix for easier comparison
- std::string suf_low;
- for (char c : suffix) suf_low += static_cast<char>(std::tolower(c));
-
- bool has_f_suffix = suf_low == "f";
- bool has_fp_content = s.find('.') != std::string::npos ||
- s.find('e') != std::string::npos ||
- s.find('E') != std::string::npos;
- bool is_fp = has_fp_content || has_f_suffix;
-
- bool is_unsigned = suf_low.find('u') != std::string::npos;
- bool is_long = suf_low.find('l') != std::string::npos;
-
- thread_local Token t;
- if (is_hex) {
- if (is_unsigned && is_long)
- t = { s, TokenType::Literal, "Hex literal", "Unsigned long hexadecimal constant" };
- else if (is_unsigned)
- t = { s, TokenType::Literal, "Hex literal", "Unsigned hexadecimal constant" };
- else
- t = { s, TokenType::Literal, "Hex literal", "Hexadecimal integer constant" };
- }
- else if (is_fp) {
- if (has_f_suffix)
- t = { s, TokenType::Literal, "Float literal", "Single-precision floating-point (float)" };
- else if (is_long)
- t = { s, TokenType::Literal, "Long double literal", "Extended-precision floating-point (long double)" };
- else
- t = { s, TokenType::Literal, "Double literal", "Double-precision floating-point (double)" };
- }
- else {
- if (is_unsigned && is_long)
- t = { s, TokenType::Literal, "Unsigned long literal","Unsigned long integer constant" };
- else if (is_unsigned)
- t = { s, TokenType::Literal, "Unsigned literal", "Unsigned integer constant" };
- else if (is_long)
- t = { s, TokenType::Literal, "Long literal", "Long integer constant" };
- else
- t = { s, TokenType::Literal, "Integer literal", "Integer constant" };
- }
- return &t;
-}
-
-static const Token*
-synth_string(const std::string& s)
-{
- thread_local Token t;
- t = { s, TokenType::Literal, "String literal", "Null-terminated character string" };
- return &t;
-}
-
-static const Token*
-synth_char(const std::string& s)
-{
- thread_local Token t;
- t = { s, TokenType::Literal, "Char literal", "Single character constant" };
- return &t;
-}
-
-
-// ---------------------------------------------------------------------------
-// Public API
-// ---------------------------------------------------------------------------
-
-const Token*
-TokenDB::find_OrSynthesize(const std::string& text)
-{
- const Token* t = find(text);
- if (t) return t;
- if (is_string_literal(text)) return synth_string(text);
- if (is_char_literal(text)) return synth_char(text);
- if (is_number(text)) return synth_number(text);
- return nullptr;
-}
diff --git a/src/c_token/token_db.hpp b/src/c_token/token_db.hpp
deleted file mode 100644
index e0a0355..0000000
--- a/src/c_token/token_db.hpp
+++ /dev/null
@@ -1,15 +0,0 @@
-#pragma once
-
-#include "token.hpp"
-#include "utils/globals.hpp"
-
-namespace TokenDB
-{
- bool known(const std::string& text);
- const Token* find (const std::string& text);
- const Token* find_OrSynthesize(const std::string& text);
-
- const char* get_TypeName (TokenType type);
- DynArray<const Token*> get_AllOfType(TokenType type);
- DynArray<std::string> get_TestArrayAllKeywords();
-}
diff --git a/src/cpp_token/cpp_highlight.cpp b/src/cpp_token/cpp_highlight.cpp
new file mode 100644
index 0000000..07c3645
--- /dev/null
+++ b/src/cpp_token/cpp_highlight.cpp
@@ -0,0 +1,113 @@
+#include <cstring>
+#include <mutex>
+#include <tree_sitter/api.h>
+#include <unordered_map>
+
+#include "cpp_token/cpp_highlight.hpp"
+
+extern "C" const TSLanguage* tree_sitter_cpp(void);
+
+namespace {
+
+// Map a node's type string (+ its parent's) to our TokenType.
+// Returns false if this node should not emit a span (recurse into children).
+bool
+classify_Node(TSNode node, TokenType& out)
+{
+ const char* t = ts_node_type(node);
+
+ // Leaf categories that color directly.
+ if (std::strcmp(t, "comment") == 0) { out = TokenType::Comment; return true; }
+ if (std::strcmp(t, "string_literal") == 0 ||
+ std::strcmp(t, "raw_string_literal") == 0 ||
+ std::strcmp(t, "char_literal") == 0 ||
+ std::strcmp(t, "system_lib_string") == 0) { out = TokenType::StringLit; return true; }
+ if (std::strcmp(t, "number_literal") == 0) { out = TokenType::NumberLit; return true; }
+ if (std::strcmp(t, "primitive_type") == 0 ||
+ std::strcmp(t, "type_identifier") == 0 ||
+ std::strcmp(t, "sized_type_specifier") == 0) { out = TokenType::Type; return true; }
+ if (std::strcmp(t, "namespace_identifier") == 0) { out = TokenType::Namespace; return true; }
+ if (std::strcmp(t, "field_identifier") == 0) { out = TokenType::Variable; return true; }
+
+ // Identifiers: depend on parent.
+ if (std::strcmp(t, "identifier") == 0) {
+ TSNode parent = ts_node_parent(node);
+ const char* p = ts_node_is_null(parent) ? "" : ts_node_type(parent);
+ if (std::strcmp(p, "function_declarator") == 0 ||
+ std::strcmp(p, "call_expression") == 0)
+ { out = TokenType::Function; return true; }
+ out = TokenType::Variable; return true;
+ }
+
+ // Preprocessor: the #include/#define lines.
+ if (std::strncmp(t, "preproc", 7) == 0) { out = TokenType::Preprocessor; return true; }
+
+ return false; // not a terminal we color — recurse into children
+}
+
+void
+walk(TSNode node, DynArray<HiSpan>& out)
+{
+ TokenType type;
+ if (ts_node_child_count(node) == 0) {
+ // leaf — classify and emit if recognized
+ if (classify_Node(node, type))
+ out.push_back({ ts_node_start_byte(node), ts_node_end_byte(node), type });
+ return;
+ }
+ // named-but-classifiable interior nodes (comments, strings can be interior)
+ if (classify_Node(node, type)) {
+ out.push_back({ ts_node_start_byte(node), ts_node_end_byte(node), type });
+ return; // don't descend into a classified span
+ }
+ const uint32_t n = ts_node_child_count(node);
+ for (uint32_t i = 0; i < n; ++i)
+ walk(ts_node_child(node, i), out);
+}
+
+} // namespace
+
+DynArray<HiSpan>
+CppHighlight::classify(const std::string& source)
+{
+ DynArray<HiSpan> spans;
+ TSParser* p = ts_parser_new();
+ ts_parser_set_language(p, tree_sitter_cpp());
+ TSTree* tree = ts_parser_parse_string(p, nullptr, source.c_str(),
+ (uint32_t)source.size());
+ walk(ts_tree_root_node(tree), spans);
+ ts_tree_delete(tree);
+ ts_parser_delete(p);
+ return spans;
+}
+
+
+TokenType
+CppHighlight::classify_Word(const std::string& word)
+{
+ static std::unordered_map<std::string, TokenType> cache;
+ static std::mutex mtx;
+
+ {
+ std::lock_guard<std::mutex> lk(mtx);
+ auto it = cache.find(word);
+ if (it != cache.end()) return it->second;
+ }
+
+ // First sight: classify this lone token with tree-sitter.
+ DynArray<HiSpan> spans = classify(word);
+
+ // Pick the span covering the most of the word (usually the only one).
+ TokenType type = TokenType::Identifier;
+ size_t best_len = 0;
+ for (const HiSpan& s : spans) {
+ size_t len = s.byte_end - s.byte_begin;
+ if (len > best_len) { best_len = len; type = s.type; }
+ }
+
+ {
+ std::lock_guard<std::mutex> lk(mtx);
+ cache[word] = type;
+ }
+ return type;
+}
diff --git a/src/cpp_token/cpp_highlight.hpp b/src/cpp_token/cpp_highlight.hpp
new file mode 100644
index 0000000..13e65e1
--- /dev/null
+++ b/src/cpp_token/cpp_highlight.hpp
@@ -0,0 +1,11 @@
+#pragma once
+#include <string>
+#include "cpp_token/token.hpp"
+#include "utils/globals.hpp"
+
+struct HiSpan { size_t byte_begin; size_t byte_end; TokenType type; };
+
+namespace CppHighlight {
+ DynArray<HiSpan> classify(const std::string& source);
+ TokenType classify_Word(const std::string& word);
+}
diff --git a/src/c_token/token.hpp b/src/cpp_token/token.hpp
similarity index 78%
rename from src/c_token/token.hpp
rename to src/cpp_token/token.hpp
index 88ea12b..5a1aa3a 100644
--- a/src/c_token/token.hpp
+++ b/src/cpp_token/token.hpp
@@ -11,6 +11,12 @@ enum class TokenType : uint8_t {
Operator,
Preprocessor,
Type,
+ Function,
+ Variable,
+ Comment,
+ StringLit,
+ NumberLit,
+ Namespace,
};
struct Token {
diff --git a/src/cpp_token/token_color.hpp b/src/cpp_token/token_color.hpp
new file mode 100644
index 0000000..1127b25
--- /dev/null
+++ b/src/cpp_token/token_color.hpp
@@ -0,0 +1,31 @@
+#pragma once
+#include <SDL3/SDL.h>
+#include "token.hpp"
+#include "token_db.hpp"
+#include "text_3d/text_3d.hpp"
+
+namespace TokenColor
+{
+
+inline SDL_FColor
+get(TokenType t)
+{
+ switch (t) {
+ case TokenType::Keyword: return ConfigColor::orange;
+ case TokenType::Type: return ConfigColor::green;
+ case TokenType::Identifier: return ConfigColor::light_gray;
+ case TokenType::Literal: return ConfigColor::red;
+ case TokenType::Operator: return ConfigColor::soft_blue;
+ case TokenType::Delimiter: return ConfigColor::slate_blue;
+ case TokenType::Preprocessor:return ConfigColor::pink;
+ case TokenType::Function: return ConfigColor::blue;
+ case TokenType::Variable: return ConfigColor::pale_sky;
+ case TokenType::Comment: return ConfigColor::gray;
+ case TokenType::StringLit: return ConfigColor::lime;
+ case TokenType::NumberLit: return ConfigColor::amber;
+ case TokenType::Namespace: return ConfigColor::teal;
+ }
+ return ConfigColor::white;
+}
+
+}
diff --git a/src/cpp_token/token_db.cpp b/src/cpp_token/token_db.cpp
new file mode 100644
index 0000000..457993b
--- /dev/null
+++ b/src/cpp_token/token_db.cpp
@@ -0,0 +1,158 @@
+#include "token_db.hpp"
+#include <unordered_map>
+
+namespace {
+
+struct Entry { const char* short_desc; const char* long_desc; };
+
+// Curated, hand-written help text. Keyed by exact token text.
+const std::unordered_map<std::string, Entry>&
+table()
+{
+ static const std::unordered_map<std::string, Entry> m = {
+ { "int", { "Integer type", "Declares a 32-bit integer variable" } },
+ { "float", { "Float type", "Declares a floating point variable" } },
+ { "char", { "Character type", "Declares a single character" } },
+ { "void", { "No value type", "Function returns nothing" } },
+ { "bool", { "Boolean type", "Stores true or false values" } },
+ { "short", { "Short integer type", "Declares a short integer variable" } },
+ { "long", { "Long integer type", "Declares a long integer variable" } },
+ { "unsigned", { "Unsigned modifier", "Declares an unsigned integer type" } },
+ { "signed", { "Signed modifier", "Declares a signed integer type" } },
+ { "double", { "Double type", "Declares a double-precision floating point variable" } },
+ { "size_t", { "Size type", "Stores object sizes and array indices" } },
+ { "struct", { "Structure type", "Defines a structure type" } },
+ { "union", { "Union type", "Defines a union type" } },
+ { "enum", { "Enumeration type", "Defines an enumeration type" } },
+ { "typedef", { "Type alias", "Creates an alias for a type" } },
+ { "const", { "Constant qualifier", "Prevents modification of an object" } },
+ { "static", { "Static storage", "Declares static storage duration" } },
+ { "extern", { "External linkage", "Declares a symbol defined elsewhere" } },
+ { "volatile", { "Volatile qualifier", "Prevents certain compiler optimizations" } },
+
+ { "return", { "Return statement", "Ends function and returns a value" } },
+ { "if", { "Conditional branch", "Executes code if condition is true" } },
+ { "else", { "Alternative branch", "Executes if 'if' condition is false" } },
+ { "while", { "While loop", "Repeats while condition is true" } },
+ { "for", { "For loop", "Loop with init, condition, increment" } },
+ { "switch", { "Switch statement", "Selects execution path based on a value" } },
+ { "case", { "Case label", "Marks a branch in a switch statement" } },
+ { "default", { "Default label", "Fallback branch in a switch statement" } },
+ { "break", { "Break statement", "Exits a loop or switch statement" } },
+ { "continue", { "Continue statement", "Skips to the next loop iteration" } },
+ { "goto", { "Goto statement", "Jumps to a labeled statement" } },
+ { "do", { "Do loop", "Begins a do-while loop" } },
+ { "sizeof", { "Size operator", "Returns the size of a type or object" } },
+
+ { "main", { "Entry point", "First function called by the OS" } },
+ { "printf", { "Print formatted", "Outputs formatted text to stdout" } },
+ { "strlen", { "String length", "Returns length of a string" } },
+ { "i", { "Loop counter", "Common loop index variable" } },
+ { "j", { "Loop counter", "Common loop index variable" } },
+ { "k", { "Loop counter", "Common loop index variable" } },
+
+ { "NULL", { "Null pointer constant", "Macro representing a null pointer" } },
+ { "true", { "Boolean true", "Boolean constant true" } },
+ { "false", { "Boolean false", "Boolean constant false" } },
+ { "%d", { "Integer format", "Printf format specifier for integers" } },
+ { "%s", { "String format", "Printf format specifier for strings" } },
+ { "%f", { "Float format", "Printf format specifier for floating-point values" } },
+ { "%c", { "Character format", "Printf format specifier for characters" } },
+ { "%p", { "Pointer format", "Printf format specifier for pointers" } },
+ { "%x", { "Hex format", "Printf format specifier for hexadecimal" } },
+ { "\\n", { "Newline", "Newline escape sequence" } },
+
+ { "#include", { "Include directive", "Includes a header file" } },
+ { "#define", { "Macro definition", "Defines a macro" } },
+ { "#ifndef", { "If not defined", "Compiles code if macro is not defined" } },
+ { "#endif", { "End conditional", "Ends conditional compilation block" } },
+
+ { "+", { "Addition", "Adds two operands" } },
+ { "-", { "Subtraction", "Subtracts right operand from left" } },
+ { "*", { "Dereference / multiply","Dereferences a pointer or multiplies operands" } },
+ { "/", { "Division", "Divides left operand by right" } },
+ { "=", { "Assignment", "Assigns the right value to the left operand" } },
+ { "==", { "Equality", "Checks whether two values are equal" } },
+ { "!=", { "Not equal", "Checks whether two values differ" } },
+ { "++", { "Increment", "Increases value by 1" } },
+ { "--", { "Decrement", "Decreases value by 1" } },
+ { "&&", { "Logical AND", "True if both operands are true" } },
+ { "||", { "Logical OR", "True if either operand is true" } },
+ { "->", { "Member access", "Accesses a member through a pointer" } },
+
+ { "(", { "Open parenthesis", "Starts a function call or condition" } },
+ { ")", { "Close parenthesis", "Ends a function call or condition" } },
+ { "{", { "Open brace", "Starts a code block" } },
+ { "}", { "Close brace", "Ends a code block" } },
+ { ";", { "Statement terminator","Ends a statement" } },
+ { ",", { "Separator", "Separates function arguments" } },
+ { "[", { "Open bracket", "Start array subscript" } },
+ { "]", { "Close bracket", "End array subscript" } },
+ };
+ return m;
+}
+
+// Generic fallback description per token type, for tokens not in the table
+// (e.g. user-named variables, arbitrary literals).
+TokenDB::Desc
+generic_For(TokenType type)
+{
+ switch (type) {
+ case TokenType::Keyword: return { "Keyword", "A C/C++ language keyword" };
+ case TokenType::Type: return { "Type", "A data type" };
+ case TokenType::Identifier: return { "Identifier", "A name in the program" };
+ case TokenType::Literal: return { "Literal", "A literal value" };
+ case TokenType::Operator: return { "Operator", "An operator" };
+ case TokenType::Delimiter: return { "Delimiter", "A delimiter" };
+ case TokenType::Preprocessor: return { "Preprocessor", "A preprocessor directive" };
+ case TokenType::Function: return { "Function", "A function" };
+ case TokenType::Variable: return { "Variable", "A variable" };
+ case TokenType::Comment: return { "Comment", "A source comment" };
+ case TokenType::StringLit: return { "String", "A string literal" };
+ case TokenType::NumberLit: return { "Number", "A numeric literal" };
+ case TokenType::Namespace: return { "Namespace", "A namespace" };
+ }
+ return { "", "" };
+}
+
+} // namespace
+
+
+TokenDB::Desc
+TokenDB::describe(const std::string& text, TokenType type)
+{
+ const auto& t = table();
+ auto it = t.find(text);
+ if (it != t.end())
+ return { it->second.short_desc, it->second.long_desc };
+ return generic_For(type);
+}
+
+
+const char*
+TokenDB::type_Name(TokenType t)
+{
+ switch (t) {
+ case TokenType::Keyword: return "Keyword";
+ case TokenType::Type: return "Type";
+ case TokenType::Identifier: return "Identifier";
+ case TokenType::Literal: return "Literal";
+ case TokenType::Operator: return "Operator";
+ case TokenType::Delimiter: return "Delimiter";
+ case TokenType::Preprocessor: return "Preprocessor";
+ case TokenType::Function: return "Function";
+ case TokenType::Variable: return "Variable";
+ case TokenType::Comment: return "Comment";
+ case TokenType::StringLit: return "String";
+ case TokenType::NumberLit: return "Number";
+ case TokenType::Namespace: return "Namespace";
+ }
+ return "Unknown";
+}
+
+
+DynArray<std::string>
+TokenDB::get_TestArrayAllKeywords()
+{
+ return { "float", "return", "main", "0", "++", "(", ",", ";", "\"", "\"", "#define" };
+}
diff --git a/src/cpp_token/token_db.hpp b/src/cpp_token/token_db.hpp
new file mode 100644
index 0000000..8e2d59e
--- /dev/null
+++ b/src/cpp_token/token_db.hpp
@@ -0,0 +1,17 @@
+#pragma once
+
+#include <string>
+
+#include "token.hpp"
+#include "utils/globals.hpp"
+
+namespace TokenDB
+{
+ struct Desc { std::string short_desc; std::string long_desc; };
+
+ Desc describe(const std::string& text, TokenType type);
+
+ const char* type_Name(TokenType type);
+
+ DynArray<std::string> get_TestArrayAllKeywords();
+}
diff --git a/src/cpp_token/ts_test.cpp b/src/cpp_token/ts_test.cpp
new file mode 100644
index 0000000..c78c0a6
--- /dev/null
+++ b/src/cpp_token/ts_test.cpp
@@ -0,0 +1,21 @@
+#include <tree_sitter/api.h>
+#include <SDL3/SDL.h>
+#include <string>
+
+extern "C" const TSLanguage* tree_sitter_cpp(void);
+
+void ts_LinkTest()
+{
+ TSParser* p = ts_parser_new();
+ ts_parser_set_language(p, tree_sitter_cpp());
+ const std::string src = "int main(){ return 0; }";
+ TSTree* tree = ts_parser_parse_string(p, nullptr, src.c_str(),
+ (uint32_t)src.size());
+ TSNode root = ts_tree_root_node(tree);
+ SDL_Log("tree-sitter root node type: %s", ts_node_type(root));
+ char* s = ts_node_string(root);
+ SDL_Log("tree-sitter tree: %s", s);
+ free(s);
+ ts_tree_delete(tree);
+ ts_parser_delete(p);
+}
diff --git a/src/states/game_state.cpp b/src/states/game_state.cpp
index f0d6240..8612639 100644
--- a/src/states/game_state.cpp
+++ b/src/states/game_state.cpp
@@ -3,7 +3,6 @@
#include <unordered_set>
-#include "c_token/token_color.hpp"
#include "core/game.hpp"
#include "entities/solid_box.hpp"
#include "entities/world.hpp"
@@ -25,6 +24,23 @@
#include "wall/text_wall_scroll.hpp"
+
+static DynArray<std::string>
+normalize_EditorLines(const DynArray<std::string>& lines)
+{
+ DynArray<std::string> out;
+ for (const std::string& line : lines) {
+ if (line.find_first_not_of(' ') == std::string::npos) continue;
+ std::istringstream in(line);
+ std::string joined, tk;
+ while (in >> tk) { if (!joined.empty()) joined += ' '; joined += tk; }
+ out.push_back(joined);
+ }
+ return out;
+}
+
+
+
GameState::GameState(size_t starting_level)
{
@@ -482,6 +498,9 @@ GameState::projectile_LandOnWall(ProjectileResult& result)
for (TextWallPtr& wt : placed)
_ctx.wall_texts.push_back(std::move(wt));
+
+ if (!editor_group.empty())
+ editor_SyncFile();
}
@@ -512,9 +531,11 @@ void
GameState::walltext_HandleShootDown(ProjectileResult& result,
DynArray<size_t>& to_erase)
{
+ bool removed_editor = false;
if (result.wall_text_index < _ctx.wall_texts.size()) {
TextWallPtr& wt = _ctx.wall_texts[result.wall_text_index];
if (wt && wt->text3d) {
+ removed_editor = (wt->role == Wall::Role::Editor);
_editor_wall.on_WordRemoved(wt->text3d.get());
Text3dPtr orphan = std::move(wt->text3d);
@@ -529,6 +550,9 @@ GameState::walltext_HandleShootDown(ProjectileResult& result,
if (!w) continue;
_ctx.drop_WordToFloor(std::move(w), result.vel);
}
+
+ if (removed_editor)
+ editor_SyncFile();
}
@@ -567,21 +591,21 @@ GameState::handle_Input(FrameInput& input, double dt) {
input.vim_keys.clear();
switch (req) {
case WallConnection::Request::Write:
- editor_CommitToWall(_wall_connection.lines());
+ editor_CommitToWall(_wall_connection.lines(), false);
_wall_connection.mark_Saved();
break;
case WallConnection::Request::WriteQuit:
- editor_CommitToWall(_wall_connection.lines());
+ editor_CommitToWall(_wall_connection.lines(), true);
_wall_connection.end();
_frame_input.capture_vim = false;
break;
case WallConnection::Request::Exit:
- editor_CommitToWall(_wall_connection.lines());
+ editor_CommitToWall(_wall_connection.lines(), true);
_wall_connection.end();
_frame_input.capture_vim = false;
break;
case WallConnection::Request::QuitDiscard:
- editor_CommitToWall(_wall_connection.lines());
+ editor_CommitToWall(_wall_connection.lines(), true);
_wall_connection.end();
_frame_input.capture_vim = false;
break;
@@ -688,6 +712,13 @@ GameState::update(double dt, const GlRenderer* gl)
level_TryAdvance(dt);
}
+ { FP_ZONE("update.highlight");
+ DynArray<HiSpan> spans;
+ uint64_t gen;
+ if (_highlight_sys.poll(spans, gen) && gen == _editor_gen)
+ _editor_file.apply_Spans(spans);
+ }
+
input.look_x = 0;
input.look_y = 0;
input.commands.clear();
@@ -713,7 +744,11 @@ GameState::render(SDL_Window& window, GlRenderer& renderer)
{ FP_ZONE("render.set_Light"); renderer.set_Light(*_ctx.light); }
{ FP_ZONE("render.world"); _ctx.world ->draw(renderer, *_ctx.camera, *_ctx.light); }
- { FP_ZONE("render._editor_wall"); _editor_wall.draw(renderer, *_ctx.camera, *_ctx.light); }
+
+ { FP_ZONE("render._editor_wall");
+ if (!_editor_hidden)
+ _editor_wall.draw(renderer, *_ctx.camera, *_ctx.light);
+ }
{ FP_ZONE("render.scenery");
for (const GameObjectPtr& s : _ctx.scenery)
@@ -726,8 +761,10 @@ GameState::render(SDL_Window& window, GlRenderer& renderer)
}
{ FP_ZONE("render.wall_texts");
- for (const TextWallPtr& wt : _ctx.wall_texts)
+ for (const TextWallPtr& wt : _ctx.wall_texts) {
+ if (_editor_hidden && wt && wt->role == Wall::Role::Editor) continue;
wt->draw(renderer, *_ctx.camera, *_ctx.light);
+ }
}
{ FP_ZONE("render.wall_labels");
@@ -1120,7 +1157,11 @@ GameState::wallconnect_BeginProgram()
void
GameState::wallconnect_BeginEditor()
{
- DynArray<std::string> lines = editor_AbsorbLines();
+ DynArray<std::string> lines = editor_ReadLines();
+ _editor_absorbed = normalize_EditorLines(lines);
+
+ _editor_hidden = true;
+
_frame_input.capture_vim = true;
_frame_input.vim_keys.clear();
@@ -1185,6 +1226,21 @@ GameState::editor_AbsorbLines()
}
+DynArray<std::string>
+GameState::editor_ReadLines() const
+{
+ std::string src = TextWall::extract_Text(_ctx.wall_texts, Wall::Role::Editor);
+ DynArray<std::string> lines;
+ std::string cur;
+ for (char ch : src) {
+ if (ch == '\n') { lines.push_back(cur); cur.clear(); }
+ else { cur.push_back(ch); }
+ }
+ if (!cur.empty()) lines.push_back(cur);
+ return lines;
+}
+
+
void
GameState::editor_PlaceLines(const DynArray<std::string>& lines)
{
@@ -1204,12 +1260,17 @@ GameState::editor_PlaceLines(const DynArray<std::string>& lines)
const double bottom_y = floor_y + 1.2;
double y = bottom_y + static_cast<double>(n_lines - 1) * lh;
+ struct Tok { Text3dPtr t; double half; };
+ struct Row { DynArray<Tok> toks; double width; double y; };
+ DynArray<Row> rows;
+
+ double max_width = 0.0;
for (const std::string& line : lines) {
if (line.find_first_not_of(' ') == std::string::npos)
continue;
- struct Tok { Text3dPtr t; double half; };
- DynArray<Tok> toks;
+ Row row;
+ row.y = y;
double total_w = 0.0;
std::istringstream in(line);
@@ -1218,31 +1279,69 @@ GameState::editor_PlaceLines(const DynArray<std::string>& lines)
Text3dPtr t = make_Text3d(tk, Text3d::depth_wall, 0.003f,
Vec3{ x_wall, y, 0.0 }, ConfigColor::wall);
t->yaw = tf.yaw;
- TokenColor::apply(t.get());
const double half = static_cast<double>(t->half_extent);
total_w += 2.0 * half;
- toks.push_back({ std::move(t), half });
+ row.toks.push_back({ std::move(t), half });
}
- if (toks.empty()) { y -= lh; continue; }
- total_w += gap * static_cast<double>(toks.size() - 1);
+ if (row.toks.empty()) { y -= lh; continue; }
+ total_w += gap * static_cast<double>(row.toks.size() - 1);
+ row.width = total_w;
+ max_width = SDL_max(max_width, total_w);
+ rows.push_back(std::move(row));
+ y -= lh;
+ }
+ if (rows.empty()) return;
+
+ const double left_z = max_width * 0.5;
+
+ for (Row& row : rows) {
DynArray<Text3d*> placed;
- double cursor = total_w * 0.5;
- for (Tok& tok : toks) {
+ double cursor = left_z;
+ for (Tok& tok : row.toks) {
const double center_z = cursor - tok.half;
- tok.t->pos = Vec3{ x_wall, y, center_z };
+ tok.t->pos = Vec3{ x_wall, row.y, center_z };
placed.push_back(tok.t.get());
_ctx.add_WallText(Wall::Position::NegX, Wall::Role::Editor, std::move(tok.t));
cursor = center_z - tok.half - gap;
}
- _editor_wall.add_PlacedLine(placed, y);
-
- y -= lh;
+ _editor_wall.add_PlacedLine(placed, row.y);
}
+
+ editor_SyncFile();
}
+
void
-GameState::editor_CommitToWall(const DynArray<std::string>& lines)
+GameState::editor_CommitToWall(const DynArray<std::string>& lines, bool ending)
{
+ DynArray<std::string> incoming = normalize_EditorLines(lines);
+
+ bool same = (incoming.size() == _editor_absorbed.size());
+ if (same)
+ for (size_t i = 0; i < incoming.size(); ++i)
+ if (incoming[i] != _editor_absorbed[i]) { same = false; break; }
+
+ if (ending) _editor_hidden = false;
+
+ if (same) return;
+
+ editor_Clear();
editor_PlaceLines(lines);
+ _editor_absorbed = incoming;
+}
+
+void
+GameState::editor_SyncFile()
+{
+ _editor_file.clear();
+ for (const DynArray<Text3d*>& line : _editor_wall.lines_InOrder()) {
+ TextFile::Line fl;
+ for (Text3d* w : line)
+ if (w) fl.tokens.push_back(TextFile::Token{ w->text, 0, 0, w });
+ _editor_file.lines.push_back(std::move(fl));
+ }
+
+ ++_editor_gen;
+ _highlight_sys.request(_editor_file.render_Source(), _editor_gen);
}
diff --git a/src/states/game_state.hpp b/src/states/game_state.hpp
index fa84f45..8d6a19e 100644
--- a/src/states/game_state.hpp
+++ b/src/states/game_state.hpp
@@ -12,6 +12,7 @@
#include "systems/carry_system.hpp"
#include "systems/celebration_system.hpp"
#include "systems/compiler_system.hpp"
+#include "systems/cpp_highlight_system.hpp"
#include "systems/dyn_loader_system.hpp"
#include "systems/freelook_system.hpp"
#include "systems/hint_system.hpp"
@@ -26,6 +27,7 @@
#include "systems/wall_connection.hpp"
#include "systems/word_ai_system.hpp"
#include "systems/word_anim.hpp"
+#include "text_3d/text_file.hpp"
#include "text_3d/vim_buffer.hpp"
#include "utils/carry_math.hpp"
#include "utils/globals.hpp"
@@ -54,8 +56,10 @@ class GameState : public IState
cpputils::unique_ptr<WordAiSystem> _word_ai_sys;
cpputils::unique_ptr<WordAnimSystem> _word_anim_sys;
- double _pick_cooldown { 0.0 };
- double _shoot_cooldown { 0.0 };
+ bool _editor_hidden { false };
+ uint64_t _editor_gen { 0 };
+ double _pick_cooldown { 0.0 };
+ double _shoot_cooldown { 0.0 };
std::string _typing_last;
@@ -64,11 +68,14 @@ class GameState : public IState
FrameInput _frame_input;
GameContext _ctx;
WallConnection _wall_connection;
+ TextFile _editor_file;
+ CppHighlightSystem _highlight_sys;
DynArray<GameObject*> _dyn_scenery;
DynArray<Text3d*> _typing_words;
DynArray<Vec3> _freelook_return_from;
+ DynArray<std::string> _editor_absorbed;
DynArray<Projectile> _projectiles;
DynArray<WallLabel> _wall_labels;
DynArray<WordAnim_Add> _anims_add;
@@ -81,9 +88,12 @@ class GameState : public IState
DynArray<std::string>
editor_AbsorbLines ();
+ DynArray<std::string>
+ editor_ReadLines () const;
void editor_Clear ();
- void editor_CommitToWall (const DynArray<std::string>& lines);
+ void editor_CommitToWall (const DynArray<std::string>& lines, bool ending);
void editor_PlaceLines (const DynArray<std::string>& lines);
+ void editor_SyncFile ();
bool on_Pick ();
diff --git a/src/systems/cpp_highlight_system.hpp b/src/systems/cpp_highlight_system.hpp
new file mode 100644
index 0000000..baf3268
--- /dev/null
+++ b/src/systems/cpp_highlight_system.hpp
@@ -0,0 +1,74 @@
+#pragma once
+
+#include <atomic>
+#include <mutex>
+#include <string>
+#include <thread>
+
+#include "cpp_token/cpp_highlight.hpp"
+#include "utils/globals.hpp"
+
+class CppHighlightSystem {
+public:
+ ~CppHighlightSystem() { if (_worker.joinable()) _worker.join(); }
+
+ // Request a lex of `source` tagged with `gen`. No-op if source unchanged
+ // since last request. Single-flight: coalesces while a lex is in flight.
+ void request(const std::string& source, uint64_t gen)
+ {
+ if (source == _last_requested) return; // content unchanged → skip
+ _last_requested = source;
+
+ if (_running.load()) { _redo = true; return; } // coalesce
+ spawn(source, gen);
+ }
+
+ // Drain finished results. Returns true + fills out if a lex completed this
+ // call; caller checks gen against current content before applying.
+ bool poll(DynArray<HiSpan>& out, uint64_t& out_gen)
+ {
+ if (!_done.load()) return false;
+ {
+ std::lock_guard<std::mutex> lk(_mtx);
+ out = _pending;
+ out_gen = _result_gen;
+ }
+ _done.store(false);
+ if (_worker.joinable()) _worker.join();
+ _running.store(false);
+
+ if (_redo) { // content changed mid-lex → relex
+ _redo = false;
+ spawn(_last_requested, _last_gen_pending);
+ }
+ return true;
+ }
+
+private:
+ void spawn(const std::string& source, uint64_t gen)
+ {
+ if (_worker.joinable()) _worker.join();
+ _running.store(true);
+ _done.store(false);
+ _last_gen_pending = gen;
+ _worker = std::thread([this, source, gen]() {
+ DynArray<HiSpan> spans = CppHighlight::classify(source);
+ {
+ std::lock_guard<std::mutex> lk(_mtx);
+ _pending = std::move(spans);
+ _result_gen = gen;
+ }
+ _done.store(true);
+ });
+ }
+
+ std::thread _worker;
+ std::mutex _mtx;
+ std::atomic<bool> _running { false };
+ std::atomic<bool> _done { false };
+ DynArray<HiSpan> _pending;
+ uint64_t _result_gen { 0 };
+ uint64_t _last_gen_pending { 0 };
+ std::string _last_requested;
+ bool _redo { false };
+};
diff --git a/src/systems/wall_connection.cpp b/src/systems/wall_connection.cpp
index 23b2e63..c44a937 100644
--- a/src/systems/wall_connection.cpp
+++ b/src/systems/wall_connection.cpp
@@ -2,7 +2,8 @@
#include <cstdlib>
#include <sstream>
-#include "c_token/token_color.hpp"
+#include "cpp_token/cpp_highlight.hpp"
+#include "cpp_token/token_color.hpp"
#include "entities/camera.hpp"
#include "entities/world.hpp"
#include "rendering/gl_renderer.hpp"
@@ -13,6 +14,31 @@
#include "wall/wall.hpp"
+static double
+mono_CharWidth()
+{
+ static double w = 0.0;
+ if (w == 0.0) {
+ Text3dPtr m1 = make_Text3d("M", Text3d::depth_wall, 0.003f, Vec3{0,0,0}, ConfigColor::white);
+ Text3dPtr m2 = make_Text3d("MM", Text3d::depth_wall, 0.003f, Vec3{0,0,0}, ConfigColor::white);
+ const double w1 = 2.0 * static_cast<double>(m1->half_extent);
+ const double w2 = 2.0 * static_cast<double>(m2->half_extent);
+ w = w2 - w1;
+ }
+ return w;
+}
+static double
+mono_HalfExtentY()
+{
+ static double h = 0.0;
+ if (h == 0.0) {
+ Text3dPtr m = make_Text3d("M", Text3d::depth_wall, 0.003f, Vec3{0,0,0}, ConfigColor::white);
+ h = static_cast<double>(m->half_extent_y);
+ }
+ return h;
+}
+
+
void
WallConnection::begin_Program(const Vec3& pult_pos)
{
@@ -126,9 +152,6 @@ WallConnection::lines() const
void
WallConnection::rebuild_Editor()
{
- _rows.clear();
- _linenums.clear();
-
const float ws = World::get_BoxScale();
Wall::Transform tf = Wall::get_Transform(Wall::Role::Editor, ws);
const double lh = ConfigWall::line_height;
@@ -142,124 +165,153 @@ WallConnection::rebuild_Editor()
const auto& lines = _vim.lines();
- // ── Pass 1: build line-number labels (orange), measure gutter width. ──
- struct NumRow { Text3dPtr num; };
- DynArray<NumRow> nums;
- nums.reserve(lines.size());
+ const double char_w = mono_CharWidth();
+ const double he_y = mono_HalfExtentY();
+
+ // Can we reuse cached meshes? Only when the line COUNT is unchanged — then
+ // a line whose text is identical keeps its existing token meshes (we only
+ // reposition). Lines whose text changed are rebuilt. Count change (enter,
+ // dd) → full rebuild (rare vs. per-char typing).
+ const bool can_reuse = (_cached_line_text.size() == lines.size());
+
+ // Move old state aside so we can salvage rows from it.
+ DynArray<EditorRow> old_rows = std::move(_rows);
+ DynArray<Text3dPtr> old_nums = std::move(_linenums);
+ _rows.clear();
+ _linenums.clear();
+ // ── gutter width: build/reuse line-number meshes ──
+ // Numbers only change when the line count changes, so on reuse we keep them.
+ DynArray<Text3dPtr> nums;
+ nums.reserve(lines.size());
double gutter_w = 0.0;
- for (size_t i = 0; i < lines.size(); ++i) {
- Text3dPtr num = make_Text3d(std::to_string(i + 1), Text3d::depth_wall, 0.0022f,
- tf.position, ConfigColor::orange);
- num->yaw = tf.yaw;
- num->set_Color(ConfigColor::orange); // digit would token-lookup to red; force orange
- gutter_w = SDL_max(gutter_w, 2.0 * static_cast<double>(num->half_extent));
- nums.push_back({ std::move(num) });
+ if (can_reuse && old_nums.size() == lines.size()) {
+ for (size_t i = 0; i < lines.size(); ++i) {
+ gutter_w = SDL_max(gutter_w, 2.0 * static_cast<double>(old_nums[i]->half_extent));
+ nums.push_back(std::move(old_nums[i]));
+ }
+ } else {
+ for (size_t i = 0; i < lines.size(); ++i) {
+ Text3dPtr num = make_Text3d(std::to_string(i + 1), Text3d::depth_wall, 0.0022f,
+ tf.position, ConfigColor::orange);
+ num->yaw = tf.yaw;
+ num->set_Color(ConfigColor::orange);
+ gutter_w = SDL_max(gutter_w, 2.0 * static_cast<double>(num->half_extent));
+ nums.push_back(std::move(num));
+ }
}
const double gutter_gap = 0.6;
const double code_u0 = gutter_w + gutter_gap;
- // ── Pass 2: build per-token meshes for each line; measure block width. ──
- // We need a uniform char width per row so the block-relative cursor lands
- // correctly. Measure each line by rendering one hidden full-line mesh just
- // to get its total width; tokens are then laid out within that width.
- struct BuiltRow {
- DynArray<Text3dPtr> toks; // colored token meshes
- DynArray<double> tok_half; // half-extent per token
- double line_w { 0.0 }; // full line render width (incl. spaces)
- double char_w { 0.18 };
- double he_y { 0.2 };
- size_t n_chars { 0 };
- };
+ // ── Per-line token meshes + canonical source for lexing. ──
+ struct BuiltTok { Text3dPtr t; size_t col; size_t len; size_t byte_begin; size_t byte_end; };
+ struct BuiltRow { DynArray<BuiltTok> toks; };
DynArray<BuiltRow> built;
built.reserve(lines.size());
+ std::string source; // canonical buffer for tree-sitter (token-joined)
+
double block_code_w = 0.0;
for (size_t i = 0; i < lines.size(); ++i) {
- BuiltRow br;
const std::string& s = lines[i];
- br.n_chars = s.size();
-
- // Measure the full line (spaces included) for a faithful char width.
- const bool drawable = s.find_first_not_of(' ') != std::string::npos;
- if (drawable) {
- Text3dPtr meas = make_Text3d(s, Text3d::depth_wall, 0.003f, tf.position, ConfigColor::white);
- br.line_w = 2.0 * static_cast<double>(meas->half_extent);
- br.he_y = static_cast<double>(meas->half_extent_y);
- br.char_w = (s.empty() ? 0.18 : br.line_w / static_cast<double>(s.size()));
- // meas is discarded — measurement only.
-
- // Build per-token colored meshes.
- std::istringstream in(s);
- std::string tk;
- while (in >> tk) {
- Text3dPtr t = make_Text3d(tk, Text3d::depth_wall, 0.003f, tf.position, ConfigColor::white);
+ block_code_w = SDL_max(block_code_w, char_w * static_cast<double>(s.size()));
+
+ BuiltRow br;
+ const bool reuse_line = can_reuse
+ && i < _cached_line_text.size()
+ && _cached_line_text[i] == s
+ && i < old_rows.size();
+
+ // Walk tokens once; build meshes (reuse or fresh) AND record byte ranges
+ // in the canonical source (space-joined tokens, newline-joined lines).
+ size_t pos = 0, ti = 0;
+ bool first_tok = true;
+ while (pos < s.size()) {
+ while (pos < s.size() && s[pos] == ' ') ++pos;
+ if (pos >= s.size()) break;
+ size_t start = pos; std::string tk;
+ while (pos < s.size() && s[pos] != ' ') { tk += s[pos]; ++pos; }
+
+ if (!first_tok) source += ' ';
+ first_tok = false;
+ const size_t b0 = source.size();
+ source += tk;
+ const size_t b1 = source.size();
+
+ Text3dPtr t;
+ if (reuse_line && ti < old_rows[i].toks.size()) {
+ t = std::move(old_rows[i].toks[ti]);
+ } else {
+ t = make_Text3d(tk, Text3d::depth_wall, 0.003f, tf.position, ConfigColor::white);
t->yaw = tf.yaw;
- TokenColor::apply(t.get());
- br.tok_half.push_back(static_cast<double>(t->half_extent));
- br.toks.push_back(std::move(t));
+ // NO TokenColor::apply — coloring comes solely from tree-sitter below.
}
+ br.toks.push_back({ std::move(t), start, tk.size(), b0, b1 });
+ ++ti;
}
- block_code_w = SDL_max(block_code_w, br.line_w);
+ source += '\n';
+
built.push_back(std::move(br));
}
const double block_w = code_u0 + block_code_w;
const double z_block_left = block_w * 0.5;
- _code_left_z = z_block_left - code_u0; // left edge of the CODE area (char 0 starts here)
+ _code_left_z = z_block_left - code_u0;
- // ── Pass 3: place numbers + tokens, fill _rows for the cursor. ──
+ // ── Place numbers + tokens (always reposition — cheap). ──
for (size_t i = 0; i < lines.size(); ++i) {
const double row_y = _row0_y - static_cast<double>(i) * lh;
- // line number in the gutter (right-edge of number at z_block_left)
- if (nums[i].num) {
- const double he = static_cast<double>(nums[i].num->half_extent);
- nums[i].num->pos = tf.position;
- nums[i].num->pos.y = row_y;
- nums[i].num->pos.z = z_block_left - he;
- _linenums.push_back(std::move(nums[i].num));
+ if (nums[i]) {
+ const double he = static_cast<double>(nums[i]->half_extent);
+ nums[i]->pos = tf.position;
+ nums[i]->pos.y = row_y;
+ nums[i]->pos.z = z_block_left - he;
+ _linenums.push_back(std::move(nums[i]));
}
EditorRow er;
er.left_z = _code_left_z;
- er.char_w = built[i].char_w;
- er.he_y = built[i].he_y;
+ er.char_w = char_w;
+ er.he_y = he_y;
er.line_x = tf.position.x;
er.has_text = !built[i].toks.empty();
- // Lay tokens left→right from the code-left edge, advancing by token
- // width + one space-width so columns stay aligned with char_w.
- const std::string& s = lines[i];
- double cursor_z = _code_left_z; // left edge of next token
- size_t char_pos = 0; // running character column
- std::istringstream in(s);
- std::string tk;
- size_t ti = 0;
- // Walk the string to honor leading/internal spaces between tokens.
- {
- size_t pos = 0;
- while (ti < built[i].toks.size()) {
- // advance over spaces
- while (pos < s.size() && s[pos] == ' ') { cursor_z -= built[i].char_w; ++pos; ++char_pos; }
- // place token ti at cursor_z (left edge), center it
- const double half = built[i].tok_half[ti];
- Text3dPtr& t = built[i].toks[ti];
- t->pos = tf.position;
- t->pos.y = row_y;
- t->pos.z = cursor_z - half; // center = left edge − half
- // advance cursor by the token's character count
- size_t tok_len = 0;
- while (pos < s.size() && s[pos] != ' ') { ++pos; ++tok_len; ++char_pos; }
- cursor_z -= built[i].char_w * static_cast<double>(tok_len);
- er.toks.push_back(std::move(t));
- ++ti;
- }
+ for (BuiltTok& bt : built[i].toks) {
+ const double left_z = _code_left_z - static_cast<double>(bt.col) * char_w;
+ const double tok_w = char_w * static_cast<double>(bt.len);
+ bt.t->pos = tf.position;
+ bt.t->pos.y = row_y;
+ bt.t->pos.z = left_z - tok_w * 0.5;
+ er.toks.push_back(std::move(bt.t));
}
_rows.push_back(std::move(er));
}
+ // Snapshot line text for next reuse check.
+ _cached_line_text.clear();
+ for (const std::string& s : lines) _cached_line_text.push_back(s);
+
+ // ── Color every token via tree-sitter (same classifier as the wall). ──
+ {
+ DynArray<HiSpan> spans = CppHighlight::classify(source);
+ // For each span, color the tokens whose byte range overlaps it.
+ // built[] holds byte ranges; _rows[] holds the meshes in the same order.
+ for (const HiSpan& span : spans) {
+ for (size_t i = 0; i < built.size() && i < _rows.size(); ++i) {
+ for (size_t j = 0; j < built[i].toks.size() && j < _rows[i].toks.size(); ++j) {
+ const BuiltTok& bt = built[i].toks[j];
+ // overlap test: [bt.byte_begin,bt.byte_end) ∩ [span.begin,span.end)
+ if (bt.byte_begin < span.byte_end && span.byte_begin < bt.byte_end) {
+ if (_rows[i].toks[j])
+ _rows[i].toks[j]->set_Color(TokenColor::get(span.type));
+ }
+ }
+ }
+ }
+ }
+
rebuild_Cmdline();
}
diff --git a/src/systems/wall_connection.hpp b/src/systems/wall_connection.hpp
index 48cbc00..1c4487c 100644
--- a/src/systems/wall_connection.hpp
+++ b/src/systems/wall_connection.hpp
@@ -69,7 +69,8 @@ private:
VimBuffer _vim;
- DynArray<EditorRow> _rows;
+ DynArray<std::string> _cached_line_text;
+ DynArray<EditorRow> _rows;
DynArray<Text3dPtr> _linenums;
DynArray<std::string> _status;
diff --git a/src/systems/word_ai_system.cpp b/src/systems/word_ai_system.cpp
index 6ceec1f..5cbc296 100644
--- a/src/systems/word_ai_system.cpp
+++ b/src/systems/word_ai_system.cpp
@@ -1,11 +1,12 @@
#include <SDL3/SDL.h>
#include <cstdlib>
-#include "c_token/token_color.hpp"
+#include "cpp_token/token_color.hpp"
#include "entities/player.hpp"
#include "entities/world.hpp"
#include "systems/carry_system.hpp"
#include "systems/collision.hpp"
+#include "systems/cpp_highlight_system.hpp"
#include "utils/config.hpp"
#include "word_ai_system.hpp"
@@ -46,12 +47,12 @@ WordAiSystem::update(DynArray<Text3dPtr>& words,
if (!s) continue;
s->body.mode = PhysicsMode::Dynamic;
s->body.vel = w->body.vel * 0.5;
- s->set_Color(s->token_info ? TokenColor::get(s->token_info->type) : ConfigColor::floor);
+ s->set_Color(TokenColor::get(CppHighlight::classify_Word(s->text)));
released.push_back(std::move(s));
}
w->ai.stolen.clear();
w->ai.state = Text3dAI::State::Idle;
- w->set_Color(w->token_info ? TokenColor::get(w->token_info->type) : ConfigColor::floor);
+ w->set_Color(TokenColor::get(CppHighlight::classify_Word(w->text)));
continue;
}
@@ -270,7 +271,7 @@ WordAiSystem::release_StolenWords(Text3d& w, DynArray<Text3dPtr>& out)
if (!s) continue;
s->body.mode = PhysicsMode::Dynamic;
s->body.vel = { w.body.vel.x * 0.5, 2.0, w.body.vel.z * 0.5 };
- s->set_Color(s->token_info ? TokenColor::get(s->token_info->type) : ConfigColor::floor);
+ s->set_Color(TokenColor::get(CppHighlight::classify_Word(s->text)));
out.push_back(std::move(s));
}
w.ai.stolen.clear();
@@ -324,7 +325,7 @@ WordAiSystem::enter_Idle(Text3d& w)
w.ai.state_timer = 0.0;
w.body.mode = PhysicsMode::Hover;
w.body.hover_height = w.ai.hover_height_saved;
- w.set_Color(w.token_info ? TokenColor::get(w.token_info->type) : ConfigColor::floor);
+ w.set_Color(TokenColor::get(CppHighlight::classify_Word(w.text)));
}
diff --git a/src/text_3d/text_3d.cpp b/src/text_3d/text_3d.cpp
index 2b26986..b89b565 100644
--- a/src/text_3d/text_3d.cpp
+++ b/src/text_3d/text_3d.cpp
@@ -1,10 +1,10 @@
#include <SDL3/SDL.h>
-#include "c_token/token_color.hpp"
-#include "c_token/token_db.hpp"
+#include "cpp_token/token_color.hpp"
#include "entities/camera.hpp"
#include "entities/world.hpp"
#include "rendering/gl_renderer.hpp"
+#include "systems/cpp_highlight_system.hpp"
#include "text_3d.hpp"
#include "utils/frame_profiler.hpp"
#include "utils/globals.hpp"
@@ -81,10 +81,10 @@ Text3d::Text3d(const std::string& txt,
: GameObject(position, ObjectType::Text,
CollisionShape{}, RigidBody{PhysicsMode::Hover},
true, true, true),
- text(txt), color(colr), depth(dpth), scale(scle), token_info(TokenDB::find_OrSynthesize(text))
+ text(txt), color(colr), depth(dpth), scale(scle)
{
- if (token_info)
- set_Color(TokenColor::get(token_info->type));
+ set_Color(TokenColor::get(CppHighlight::classify_Word(text)));
+
rebuild_Mesh();
body.hover_phase = Random::unit() * SDL_PI_D * 2.0;
@@ -149,7 +149,7 @@ void
Text3d::set_Look(SDL_FColor c, float new_depth)
{
if (c.r == -1.0f)
- color = TokenColor::get_ByString(text);
+ color = TokenColor::get(CppHighlight::classify_Word(text));
else if (c.g == -1.0f) { }
else color = c;
diff --git a/src/text_3d/text_3d.hpp b/src/text_3d/text_3d.hpp
index d6492a1..9d85683 100644
--- a/src/text_3d/text_3d.hpp
+++ b/src/text_3d/text_3d.hpp
@@ -3,7 +3,7 @@
#include <SDL3/SDL.h>
#include <string>
-#include "c_token/token_db.hpp"
+#include "cpp_token/token_db.hpp"
#include "cpputils/unique_ptr.hpp"
#include "entities/game_object.hpp"
#include "text_3d/text_mesh.hpp"
@@ -89,8 +89,6 @@ struct Text3d final : public GameObject {
float half_extent{0.0f};
float pitch{0.0f};
Vec3 ribbon_local{};
- const Token* token_info;
-
Text3d(const std::string& txt,
float depth, float scale,
diff --git a/src/text_3d/text_file.hpp b/src/text_3d/text_file.hpp
new file mode 100644
index 0000000..21d2b93
--- /dev/null
+++ b/src/text_3d/text_file.hpp
@@ -0,0 +1,84 @@
+#pragma once
+
+#include <string>
+#include "cpp_token/cpp_highlight.hpp" // HiSpan
+#include "cpp_token/token_color.hpp" // TokenColor::get
+#include "text_3d/text_3d.hpp"
+#include "utils/globals.hpp"
+
+struct TextFile {
+ struct Token {
+ std::string text;
+ size_t byte_begin { 0 };
+ size_t byte_end { 0 };
+ Text3d* view { nullptr };
+ };
+ struct Line {
+ DynArray<Token> tokens;
+ };
+
+ DynArray<Line> lines;
+
+ void clear() { lines.clear(); }
+
+ std::string render_Source()
+ {
+ std::string out;
+ for (Line& line : lines) {
+ for (size_t ti = 0; ti < line.tokens.size(); ++ti) {
+ if (ti) out += ' ';
+ Token& tk = line.tokens[ti];
+ tk.byte_begin = out.size();
+ out += tk.text;
+ tk.byte_end = out.size();
+ }
+ out += '\n';
+ }
+ return out;
+ }
+
+ Token* token_At(size_t off)
+ {
+ for (Line& line : lines)
+ for (Token& tk : line.tokens)
+ if (off >= tk.byte_begin && off < tk.byte_end)
+ return &tk;
+ return nullptr;
+ }
+
+ void apply_Spans(const DynArray<HiSpan>& spans)
+ {
+ for (Line& line : lines) {
+ for (Token& tk : line.tokens) {
+ if (!tk.view) continue;
+
+ size_t best_overlap = 0;
+ TokenType best_type = TokenType::Identifier;
+ bool found = false;
+ for (const HiSpan& s : spans) {
+ size_t lo = (tk.byte_begin > s.byte_begin) ? tk.byte_begin : s.byte_begin;
+ size_t hi = (tk.byte_end < s.byte_end) ? tk.byte_end : s.byte_end;
+ if (lo < hi) {
+ size_t ov = hi - lo;
+ if (ov > best_overlap) { best_overlap = ov; best_type = s.type; found = true; }
+ }
+ }
+ if (found) tk.view->set_Color(TokenColor::get(best_type));
+ }
+ }
+ }
+
+ DynArray<std::string> as_Lines() const
+ {
+ DynArray<std::string> out;
+ for (const Line& line : lines) {
+ std::string s;
+ for (size_t ti = 0; ti < line.tokens.size(); ++ti) {
+ if (ti) s += ' ';
+ s += line.tokens[ti].text;
+ }
+ out.push_back(s);
+ }
+ return out;
+ }
+};
diff --git a/src/wall/editor_wall.cpp b/src/wall/editor_wall.cpp
index a6905ea..9017def 100644
--- a/src/wall/editor_wall.cpp
+++ b/src/wall/editor_wall.cpp
@@ -435,6 +435,34 @@ EditorWall::update(double dt, const Vec3& eye_pos)
}
+DynArray<DynArray<Text3d*>>
+EditorWall::lines_InOrder() const
+{
+ // Order rows top→bottom (largest y first), matching update()'s numbering.
+ DynArray<size_t> order;
+ order.reserve(_lines.size());
+ for (size_t i = 0; i < _lines.size(); ++i)
+ if (!_lines[i].words.empty()) order.push_back(i);
+
+ std::sort(order.begin(), order.end(),
+ [&](size_t a, size_t b){ return _lines[a].y > _lines[b].y; });
+
+ DynArray<DynArray<Text3d*>> out;
+ out.reserve(order.size());
+ for (size_t oi : order) {
+ // Within a row, visual left→right is decreasing z (left = larger z).
+ DynArray<Text3d*> ws;
+ ws.reserve(_lines[oi].words.size());
+ for (Text3d* w : _lines[oi].words)
+ if (w) ws.push_back(w);
+ std::sort(ws.begin(), ws.end(),
+ [](const Text3d* a, const Text3d* b){ return a->pos.z > b->pos.z; });
+ out.push_back(std::move(ws));
+ }
+ return out;
+}
+
+
DynArray<EditorWall::NumberHit>
EditorWall::number_Hitboxes() const
{
@@ -491,6 +519,7 @@ EditorWall::draw(GlRenderer& gl, const Camera& cam, const Light& light) const
verts .reserve(_lines.size() * 4 * 9);
indices.reserve(_lines.size() * 6);
+ /*
const float ws = World::get_BoxScale();
const float x_front = -ws + 0.02f;
@@ -548,6 +577,7 @@ EditorWall::draw(GlRenderer& gl, const Camera& cam, const Light& light) const
gl.set_ColorUseVertex(false);
}
+ */
// Orange line numbers (own meshes; positioned/numbered in update()).
for (const Line& l : _lines) {
diff --git a/src/wall/editor_wall.hpp b/src/wall/editor_wall.hpp
index e64baa1..feb666c 100644
--- a/src/wall/editor_wall.hpp
+++ b/src/wall/editor_wall.hpp
@@ -49,6 +49,7 @@ public:
static bool is_LineNumberHit(size_t idx) { return idx >= HIT_BASE; }
static size_t line_Of (size_t idx) { return idx - HIT_BASE; }
+
struct NumberHit {
size_t index;
Vec3 aabb_min;
@@ -70,6 +71,8 @@ public:
double snap_RowY (double y) const;
double scroll_DeltaToEye(size_t line_id, double eye_y) const;
size_t line_Count () const { return _lines.size(); }
+ DynArray<DynArray<Text3d*>>
+ lines_InOrder() const;
DynArray<NumberHit>
number_Hitboxes() const;
diff --git a/src/wall/text_wall.cpp b/src/wall/text_wall.cpp
index afbf9f7..7abb641 100644
--- a/src/wall/text_wall.cpp
+++ b/src/wall/text_wall.cpp
@@ -1,4 +1,5 @@
#include "entities/world.hpp"
+#include "systems/cpp_highlight_system.hpp"
#include "text_wall.hpp"
#include "utils/config.hpp"
@@ -128,43 +129,30 @@ TextWall::draw(GlRenderer& g, const Camera& c, const Light& l) const
void
-TextWall::show_TokenInfo(TextWallList& texts,
- const std::string& token_text)
+TextWall::show_TokenInfo(TextWallList& texts, const std::string& token_text)
{
- const Token* tok = TokenDB::find_OrSynthesize(token_text);
-
- // Clear previous info wall content
texts.erase(
std::remove_if(texts.begin(), texts.end(),
[](const TextWallPtr& wt){ return wt->role == Wall::Role::Info; }),
texts.end());
- if (!tok) return;
-
Wall::Transform tf = Wall::get_Transform(Wall::Role::Info, World::get_BoxScale());
- // Line 1: token text + type
- std::string line1 = tok->text + " [" + TokenDB::get_TypeName(tok->type) + "]";
- // Line 2: short description
- std::string line2 = tok->short_desc;
- // Line 3: long description
- std::string line3 = tok->long_desc;
+ TokenType tt = CppHighlight::classify_Word(token_text);
+ TokenDB::Desc d = TokenDB::describe(token_text, tt);
+
+ std::string line1 = token_text + " [" + TokenDB::type_Name(tt) + "]";
+ std::string line2 = d.short_desc;
+ std::string line3 = d.long_desc;
float y = -14.0f;
for (const std::string& line : { line1, line2, line3 }) {
- Vec3 pos = tf.position;
- pos.y = y;
-
- Text3dPtr t = make_Text3d(line, Text3d::depth_wall, 0.003f,
- pos, ConfigColor::white);
+ if (line.empty()) { y -= ConfigWall::text_line_spacing; continue; }
+ Vec3 pos = tf.position; pos.y = y;
+ Text3dPtr t = make_Text3d(line, Text3d::depth_wall, 0.003f, pos, ConfigColor::white);
t->yaw = tf.yaw;
-
texts.push_back(cpputils::make_unique<TextWall>(TextWall{
- tf.wall_position,
- Wall::Role::Info,
- std::move(t)
- }));
-
+ tf.wall_position, Wall::Role::Info, std::move(t) }));
y -= ConfigWall::text_line_spacing;
}
}
diff --git a/src/wall/text_wall.hpp b/src/wall/text_wall.hpp
index a359ca6..636c425 100644
--- a/src/wall/text_wall.hpp
+++ b/src/wall/text_wall.hpp
@@ -3,7 +3,7 @@
#include "wall/wall.hpp"
#include "text_3d/text_3d.hpp"
#include "utils/globals.hpp"
-#include "c_token/token_db.hpp"
+#include "cpp_token/token_db.hpp"
class GlRenderer;