diff options
| -rw-r--r-- | README.md | 35 | ||||
| -rw-r--r-- | branding/peepocry.gif | bin | 0 -> 163610 bytes | |||
| -rw-r--r-- | src/ui/source_view.cpp | 299 | ||||
| -rw-r--r-- | src/ui/source_view.h | 22 |
4 files changed, 353 insertions, 3 deletions
@@ -1,3 +1,34 @@ -# hsdbg -hachem's stupid debugger. +<h1 align="center"> + <img src="branding/peepocry.gif" alt="" width="56" align="middle" /> + hsdbg: hachem's stupid debugger +</h1> +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-<system>/hsdbg`. + + +## License +hsdbg is released under the [MIT License](LICENSE). diff --git a/branding/peepocry.gif b/branding/peepocry.gif Binary files differnew file mode 100644 index 0000000..56d5b95 --- /dev/null +++ b/branding/peepocry.gif 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; |
