diff options
Diffstat (limited to 'src/ui')
| -rw-r--r-- | src/ui/source_view.cpp | 299 | ||||
| -rw-r--r-- | src/ui/source_view.h | 22 |
2 files changed, 320 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(); + } + } + } } } diff --git a/src/ui/source_view.h b/src/ui/source_view.h index 94855e8..d3b3ed5 100644 --- a/src/ui/source_view.h +++ b/src/ui/source_view.h @@ -11,6 +11,26 @@ namespace Hsdbg { class Debugger; + // one coloured run inside a source line; start and length index into the + // line's own string so the spans stay valid as long as the line does + enum class SyntaxKind : uint8_t + { + Default, + Keyword, + Type, + String, + Number, + Comment, + Preprocessor, + }; + + struct SourceSpan + { + uint32_t start; + uint32_t length; + SyntaxKind kind; + }; + class SourceView { public: @@ -31,6 +51,8 @@ namespace Hsdbg std::filesystem::path m_path; std::vector<std::string> m_lines; + std::vector<std::vector<SourceSpan>> m_spans; + bool m_highlight = false; std::string m_path_input; std::string m_error; uint32_t m_highlighted_line = 0; |
