hachem.wtf
aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorhachem <im@hachem.wtf>2025-02-13 22:33:29 +0100
committerhachem <im@hachem.wtf>2025-02-13 22:33:29 +0100
commit1200df8a9a60490ac6b783dc8dd11f67b1ba0721 (patch)
treed48905f4faad20271db6ff52d5962c0ed8f7eeeb
parent1cdd9d221a9cbee38ae52fe88b5f65d82fe319c0 (diff)
feat: syntax highlighting
-rw-r--r--README.md35
-rw-r--r--branding/peepocry.gifbin0 -> 163610 bytes
-rw-r--r--src/ui/source_view.cpp294
-rw-r--r--src/ui/source_view.h20
4 files changed, 346 insertions, 3 deletions
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.
+<h1 align="center">
+ <img src="branding/peepocry.gif" alt="" width="56" align="middle" />
+ &nbsp;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
new file mode 100644
index 0000000..56d5b95
--- /dev/null
+++ b/branding/peepocry.gif
Binary files 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 <misc/cpp/imgui_stdlib.h>
#include <algorithm>
+#include <cctype>
#include <fstream>
+#include <string_view>
+#include <unordered_set>
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<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;
+ }
+
+ 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);
+ }
+
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<std::vector<SourceSpan>>{};
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<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..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<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;