From 1200df8a9a60490ac6b783dc8dd11f67b1ba0721 Mon Sep 17 00:00:00 2001 From: hachem Date: Thu, 13 Feb 2025 22:33:29 +0100 Subject: feat: syntax highlighting --- README.md | 35 +++++- branding/peepocry.gif | Bin 0 -> 163610 bytes src/ui/source_view.cpp | 294 ++++++++++++++++++++++++++++++++++++++++++++++++- src/ui/source_view.h | 20 ++++ 4 files changed, 346 insertions(+), 3 deletions(-) create mode 100644 branding/peepocry.gif diff --git a/README.md b/README.md index 4274f6b..9087876 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,34 @@ -# hsdbg -hachem's stupid debugger. +

+ +  hsdbg: hachem's stupid debugger +

+hsdbg is a cross-platform GUI debugger for C and C++. I wrote it because I didn't like the options I had. Debugging is a visual thing, at least to me. I want to see the source, the state of the program, the stack, my variables, registers, and whatever else is going on, all at once. Most of the good C/C++ debuggers are either terminal programs or tied to one platform. The only ones I've actually liked are RemedyBG and Visual Studio. Both are Windows-only. I'm not on Windows, so I made my own. + +hsdbg is meant to be a debugger, not a full IDE. It should start quickly, work on everything I use, and have the stuff I actually reach for without turning into an entire development environment. Underneath, it's an LLDB frontend. LLDB does the debugging; hsdbg is the UI I wish I had on top of it. + +## Features +The usual debugger stuff is there. You can launch or attach to processes, step through code, and set breakpoints by line, function, or address. Breakpoints can have conditions and ignore counts. + There's a source view with syntax highlighting and a breakpoint gutter, alongside locals, registers, threads, and the call stack. There's also disassembly, symbols, and raw memory for when you need to go further down. + +The console lets you type C or C++ directly into the debugger, which LLDB JIT-compiles and runs inside the debugged process. So if you do something like `a = 60` you actually changed `a`, and that change sticks when you continue. + +## Build +hsdbg builds on macOS, Linux, and Windows. You'll need [premake5](https://premake.github.io/) and a C++23 compiler. hsdbg links against LLVM/LLDB. By default, it pulls the pinned LLVM release (22.1.0) for your platform. Alternatively, you can point it at an existing LLVM installation that ships the LLDB headers with `LLVM_PREFIX`, or have `llvm-config` available on your `PATH`. + +```bash +$ git clone --recursive https://github.com/hachem-wtf/hsdbg.git +$ cd hsdbg + +$ premake5 fetch-llvm +$ premake5 gmake2 +$ make config=dist + +$ ./bin/dist-macosx/hsdbg ./a.out +``` + +The binary lands in `bin/dist-/hsdbg`. + + +## License +hsdbg is released under the [MIT License](LICENSE). diff --git a/branding/peepocry.gif b/branding/peepocry.gif new file mode 100644 index 0000000..56d5b95 Binary files /dev/null and b/branding/peepocry.gif differ diff --git a/src/ui/source_view.cpp b/src/ui/source_view.cpp index 652acc0..06cc983 100644 --- a/src/ui/source_view.cpp +++ b/src/ui/source_view.cpp @@ -8,7 +8,10 @@ #include #include +#include #include +#include +#include namespace Hsdbg { @@ -16,6 +19,259 @@ namespace Hsdbg { constexpr float BREAKPOINT_RADIUS = 5.0f; + 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(c)) != 0 || c == '_'; + } + + auto is_word(char c) -> bool + { + return std::isalnum(static_cast(c)) != 0 || c == '_'; + } + + auto classify_word(std::string_view word) -> SyntaxKind + { + static const std::unordered_set 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 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(line[index])) == 0) + return false; + } + + return true; + } + + auto highlight_lines(const std::vector& lines) + -> std::vector> + { + std::vector> out(lines.size()); + bool in_block = false; + + for (size_t number = 0; number < lines.size(); ++number) + { + const std::string& line = lines[number]; + std::vector& spans = out[number]; + const uint32_t size = static_cast(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(end) + 2, SyntaxKind::Comment); + at = static_cast(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(end) + 2 - at, SyntaxKind::Comment); + at = static_cast(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(c)) != 0 || + (c == '.' && at + 1 < size && + std::isdigit(static_cast(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(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 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(std::tolower(ch)); + }); + + return extensions.contains(ext); + } + constexpr float GUTTER_MARGIN = 6.0f; constexpr float GUTTER_WIDTH = BREAKPOINT_RADIUS * 2.0f + GUTTER_MARGIN * 2.0f; @@ -49,6 +305,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>{}; m_path_input = path.string(); m_error.clear(); m_highlighted_line = 0; @@ -62,6 +320,8 @@ namespace Hsdbg { m_path.clear(); m_lines.clear(); + m_spans.clear(); + m_highlight = false; m_error.clear(); m_highlighted_line = 0; } @@ -211,7 +471,39 @@ namespace Hsdbg ImGui::TextDisabled("%s", number_text.c_str()); ImGui::SameLine(0.0f, GUTTER_MARGIN * 2.0f); - ImGui::TextUnformatted(m_lines[static_cast(index)].c_str()); + + const std::string& text = m_lines[static_cast(index)]; + + if (!m_highlight || m_spans[static_cast(index)].empty()) + { + ImGui::TextUnformatted(text.c_str()); + } + else + { + bool first = true; + + for (const SourceSpan& span : m_spans[static_cast(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..9bea9d8 100644 --- a/src/ui/source_view.h +++ b/src/ui/source_view.h @@ -11,6 +11,24 @@ namespace Hsdbg { class Debugger; + 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 +49,8 @@ namespace Hsdbg std::filesystem::path m_path; std::vector m_lines; + std::vector> m_spans; + bool m_highlight = false; std::string m_path_input; std::string m_error; uint32_t m_highlighted_line = 0; -- cgit v1.3