aboutsummaryrefslogtreecommitdiff
path: root/src/ui/source_view.cpp
diff options
context:
space:
mode:
authorhachem <im@hachem.wtf>2026-08-29 22:33:29 +0200
committerhachem <im@hachem.wtf>2026-08-29 22:33:29 +0200
commit73c781e9856ba13e397418a9eed2c7c7a3daec1d (patch)
treeef0a492129f1bc122cebf43cd3ce562b673e1286 /src/ui/source_view.cpp
parent27181ae7dc81499dd3912849be7642a6f2fbceeb (diff)
feat: syntax highlighting
Diffstat (limited to 'src/ui/source_view.cpp')
-rw-r--r--src/ui/source_view.cpp299
1 files changed, 298 insertions, 1 deletions
diff --git a/src/ui/source_view.cpp b/src/ui/source_view.cpp
index 7f80aa8..c7fb606 100644
--- a/src/ui/source_view.cpp
+++ b/src/ui/source_view.cpp
@@ -8,7 +8,10 @@
#include <misc/cpp/imgui_stdlib.h>
#include <algorithm>
+#include <cctype>
#include <fstream>
+#include <string_view>
+#include <unordered_set>
namespace Hsdbg
{
@@ -16,6 +19,264 @@ namespace Hsdbg
{
constexpr float BREAKPOINT_RADIUS = 5.0f;
+ // a dark palette in the spirit of the source view's existing greens; the
+ // default kind carries no colour so ordinary text keeps the theme's own
+ auto color_of(SyntaxKind kind) -> ImU32
+ {
+ switch (kind)
+ {
+ case SyntaxKind::Keyword: return IM_COL32(86, 156, 214, 255);
+ case SyntaxKind::Type: return IM_COL32(78, 201, 176, 255);
+ case SyntaxKind::String: return IM_COL32(206, 145, 120, 255);
+ case SyntaxKind::Number: return IM_COL32(181, 206, 168, 255);
+ case SyntaxKind::Comment: return IM_COL32(106, 153, 85, 255);
+ case SyntaxKind::Preprocessor: return IM_COL32(197, 134, 192, 255);
+ case SyntaxKind::Default: break;
+ }
+
+ return 0;
+ }
+
+ auto is_word_start(char c) -> bool
+ {
+ return std::isalpha(static_cast<unsigned char>(c)) != 0 || c == '_';
+ }
+
+ auto is_word(char c) -> bool
+ {
+ return std::isalnum(static_cast<unsigned char>(c)) != 0 || c == '_';
+ }
+
+ auto classify_word(std::string_view word) -> SyntaxKind
+ {
+ static const std::unordered_set<std::string_view> keywords = {
+ "alignas", "alignof", "and", "and_eq", "asm", "bitand", "bitor", "break",
+ "case", "catch", "class", "co_await", "co_return", "co_yield", "compl",
+ "concept", "const", "consteval", "constexpr", "constinit", "const_cast",
+ "continue", "decltype", "default", "delete", "do", "dynamic_cast", "else",
+ "enum", "explicit", "export", "extern", "false", "final", "for", "friend",
+ "goto", "if", "inline", "mutable", "namespace", "new", "noexcept", "not",
+ "not_eq", "nullptr", "operator", "or", "or_eq", "override", "private",
+ "protected", "public", "register", "reinterpret_cast", "requires", "return",
+ "sizeof", "static", "static_assert", "static_cast", "struct", "switch",
+ "template", "this", "thread_local", "throw", "true", "try", "typedef",
+ "typeid", "typename", "union", "using", "virtual", "volatile", "while",
+ "xor", "xor_eq",
+ };
+
+ static const std::unordered_set<std::string_view> types = {
+ "auto", "bool", "char", "char8_t", "char16_t", "char32_t", "double", "float",
+ "int", "int8_t", "int16_t", "int32_t", "int64_t", "intptr_t", "long",
+ "ptrdiff_t", "short", "signed", "size_t", "ssize_t", "uint8_t", "uint16_t",
+ "uint32_t", "uint64_t", "uintptr_t", "unsigned", "void", "wchar_t",
+ };
+
+ if (keywords.contains(word))
+ return SyntaxKind::Keyword;
+
+ if (types.contains(word))
+ return SyntaxKind::Type;
+
+ return SyntaxKind::Default;
+ }
+
+ auto only_space_before(const std::string& line, uint32_t upto) -> bool
+ {
+ for (uint32_t index = 0; index < upto; ++index)
+ {
+ if (std::isspace(static_cast<unsigned char>(line[index])) == 0)
+ return false;
+ }
+
+ return true;
+ }
+
+ // splits every line into contiguous coloured spans. the whole file is
+ // walked in order so a block comment opened on one line stays open on the
+ // next, which a per-line pass could not know
+ auto highlight_lines(const std::vector<std::string>& lines)
+ -> std::vector<std::vector<SourceSpan>>
+ {
+ std::vector<std::vector<SourceSpan>> out(lines.size());
+ bool in_block = false;
+
+ for (size_t number = 0; number < lines.size(); ++number)
+ {
+ const std::string& line = lines[number];
+ std::vector<SourceSpan>& spans = out[number];
+ const uint32_t size = static_cast<uint32_t>(line.size());
+
+ uint32_t at = 0;
+ uint32_t run = 0;
+
+ const auto emit = [&](uint32_t start, uint32_t length, SyntaxKind kind)
+ {
+ if (length != 0)
+ spans.push_back({ start, length, kind });
+ };
+
+ const auto flush_default = [&](uint32_t upto)
+ {
+ emit(run, upto - run, SyntaxKind::Default);
+ };
+
+ if (in_block)
+ {
+ const size_t end = line.find("*/");
+
+ if (end == std::string::npos)
+ {
+ emit(0, size, SyntaxKind::Comment);
+ continue;
+ }
+
+ emit(0, static_cast<uint32_t>(end) + 2, SyntaxKind::Comment);
+ at = static_cast<uint32_t>(end) + 2;
+ run = at;
+ in_block = false;
+ }
+
+ while (at < size)
+ {
+ const char c = line[at];
+
+ if (c == '/' && at + 1 < size && line[at + 1] == '/')
+ {
+ flush_default(at);
+ emit(at, size - at, SyntaxKind::Comment);
+ run = size;
+ at = size;
+ break;
+ }
+
+ if (c == '/' && at + 1 < size && line[at + 1] == '*')
+ {
+ flush_default(at);
+ const size_t end = line.find("*/", at + 2);
+
+ if (end == std::string::npos)
+ {
+ emit(at, size - at, SyntaxKind::Comment);
+ in_block = true;
+ at = size;
+ }
+ else
+ {
+ emit(at, static_cast<uint32_t>(end) + 2 - at, SyntaxKind::Comment);
+ at = static_cast<uint32_t>(end) + 2;
+ }
+
+ run = at;
+ continue;
+ }
+
+ if (c == '"' || c == '\'')
+ {
+ flush_default(at);
+ const uint32_t start = at++;
+
+ while (at < size)
+ {
+ if (line[at] == '\\' && at + 1 < size)
+ {
+ at += 2;
+ continue;
+ }
+
+ if (line[at] == c)
+ {
+ ++at;
+ break;
+ }
+
+ ++at;
+ }
+
+ emit(start, at - start, SyntaxKind::String);
+ run = at;
+ continue;
+ }
+
+ if (c == '#' && only_space_before(line, at))
+ {
+ flush_default(at);
+ const uint32_t start = at++;
+
+ while (at < size && is_word(line[at]))
+ ++at;
+
+ emit(start, at - start, SyntaxKind::Preprocessor);
+ run = at;
+ continue;
+ }
+
+ if (std::isdigit(static_cast<unsigned char>(c)) != 0 ||
+ (c == '.' && at + 1 < size &&
+ std::isdigit(static_cast<unsigned char>(line[at + 1])) != 0))
+ {
+ flush_default(at);
+ const uint32_t start = at++;
+
+ while (at < size)
+ {
+ const char d = line[at];
+
+ if (std::isalnum(static_cast<unsigned char>(d)) != 0 ||
+ d == '.' || d == '\'')
+ ++at;
+ else
+ break;
+ }
+
+ emit(start, at - start, SyntaxKind::Number);
+ run = at;
+ continue;
+ }
+
+ if (is_word_start(c))
+ {
+ const uint32_t start = at++;
+
+ while (at < size && is_word(line[at]))
+ ++at;
+
+ if (const SyntaxKind kind =
+ classify_word(std::string_view(line).substr(start, at - start));
+ kind != SyntaxKind::Default)
+ {
+ flush_default(start);
+ emit(start, at - start, kind);
+ run = at;
+ }
+
+ continue;
+ }
+
+ ++at;
+ }
+
+ flush_default(size);
+ }
+
+ return out;
+ }
+
+ auto is_highlightable(const std::filesystem::path& path) -> bool
+ {
+ static const std::unordered_set<std::string_view> extensions = {
+ ".c", ".h", ".cc", ".cp", ".cpp", ".cxx", ".c++", ".hh", ".hpp", ".hxx",
+ ".h++", ".inl", ".ipp", ".m", ".mm", ".cu", ".cuh",
+ };
+
+ std::string ext = path.extension().string();
+ std::ranges::transform(ext, ext.begin(), [](unsigned char ch)
+ {
+ return static_cast<char>(std::tolower(ch));
+ });
+
+ return extensions.contains(ext);
+ }
+
// the dot sits in the gutter with this much room on either side, and the
// line numbers pick the same gap up again so the column reads evenly
constexpr float GUTTER_MARGIN = 6.0f;
@@ -51,6 +312,8 @@ namespace Hsdbg
m_path = path;
m_lines = std::move(lines);
+ m_highlight = is_highlightable(path);
+ m_spans = m_highlight ? highlight_lines(m_lines) : std::vector<std::vector<SourceSpan>>{};
m_path_input = path.string();
m_error.clear();
m_highlighted_line = 0;
@@ -64,6 +327,8 @@ namespace Hsdbg
{
m_path.clear();
m_lines.clear();
+ m_spans.clear();
+ m_highlight = false;
m_error.clear();
m_highlighted_line = 0;
}
@@ -214,7 +479,39 @@ namespace Hsdbg
ImGui::TextDisabled("%s", number_text.c_str());
ImGui::SameLine(0.0f, GUTTER_MARGIN * 2.0f);
- ImGui::TextUnformatted(m_lines[static_cast<size_t>(index)].c_str());
+
+ const std::string& text = m_lines[static_cast<size_t>(index)];
+
+ if (!m_highlight || m_spans[static_cast<size_t>(index)].empty())
+ {
+ ImGui::TextUnformatted(text.c_str());
+ }
+ else
+ {
+ bool first = true;
+
+ for (const SourceSpan& span : m_spans[static_cast<size_t>(index)])
+ {
+ if (!first)
+ ImGui::SameLine(0.0f, 0.0f);
+
+ first = false;
+
+ const char* const begin = text.c_str() + span.start;
+ const char* const end = begin + span.length;
+
+ if (span.kind == SyntaxKind::Default)
+ {
+ ImGui::TextUnformatted(begin, end);
+ }
+ else
+ {
+ ImGui::PushStyleColor(ImGuiCol_Text, color_of(span.kind));
+ ImGui::TextUnformatted(begin, end);
+ ImGui::PopStyleColor();
+ }
+ }
+ }
}
}