aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorhachem <im@hachem.wtf>2026-08-29 01:59:56 +0200
committerhachem <im@hachem.wtf>2026-08-29 01:59:56 +0200
commitf581953b0f2bc092cd8ca5d8243eeb61622d89d9 (patch)
tree6a9ccefaa6e2ec646f8d3eaccd905508dcbe5209
parentba64bcc9c38c04bd48e79ac59788b946454f4acb (diff)
feat: source tree and symbols panel
-rw-r--r--.gitignore2
-rw-r--r--src/debugger/debugger.cpp92
-rw-r--r--src/debugger/debugger.h3
-rw-r--r--src/ui/ui.cpp243
-rw-r--r--src/ui/ui.h4
5 files changed, 334 insertions, 10 deletions
diff --git a/.gitignore b/.gitignore
index 1677882..bff048e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,3 +23,5 @@ imgui.ini
ext/llvm/
ext/llvm-staging/
ext/*.tar.xz
+
+test/
diff --git a/src/debugger/debugger.cpp b/src/debugger/debugger.cpp
index 9bc36da..ae96ca2 100644
--- a/src/debugger/debugger.cpp
+++ b/src/debugger/debugger.cpp
@@ -7,6 +7,7 @@
#include <algorithm>
#include <array>
+#include <set>
#include <source_location>
// no standard way to read or write the environment
@@ -314,6 +315,78 @@ namespace Hsdbg
return symbols;
}
+ auto is_system_source(const std::filesystem::path& path) -> bool
+ {
+ const std::string text = path.generic_string();
+
+ constexpr std::string_view markers[] = {
+ "/usr/",
+ "/opt/",
+ "/Library/",
+ "/Applications/",
+ "/System/",
+ "/c++/v1/",
+ "\\Program Files",
+ "\\Windows Kits",
+ };
+
+ return std::ranges::any_of(markers, [&](std::string_view marker)
+ {
+ return text.find(marker) != std::string::npos;
+ });
+ }
+
+ auto normalize_source(const std::filesystem::path& path) -> std::filesystem::path
+ {
+ std::error_code error;
+ std::filesystem::path canonical = std::filesystem::weakly_canonical(path, error);
+
+ return error ? path.lexically_normal() : canonical;
+ }
+
+ auto module_source_files(lldb::SBTarget& target) -> std::vector<std::filesystem::path>
+ {
+ std::set<std::filesystem::path> files;
+
+ lldb::SBModule module = target.FindModule(target.GetExecutable());
+
+ if (!module.IsValid())
+ return {};
+
+ for (uint32_t index = 0; index < module.GetNumCompileUnits(); ++index)
+ {
+ lldb::SBCompileUnit unit = module.GetCompileUnitAtIndex(index);
+
+ if (!unit.IsValid())
+ continue;
+
+ if (std::filesystem::path cu = path_of(unit.GetFileSpec()); !cu.empty())
+ {
+ cu = normalize_source(cu);
+
+ if (std::filesystem::is_regular_file(cu))
+ files.insert(cu);
+ }
+
+ for (uint32_t support = 0; support < unit.GetNumSupportFiles(); ++support)
+ {
+ std::filesystem::path path = path_of(unit.GetSupportFileAtIndex(support));
+
+ if (path.empty() || is_system_source(path))
+ continue;
+
+ path = normalize_source(path);
+
+ if (!std::filesystem::is_regular_file(path))
+ continue;
+
+ files.insert(std::move(path));
+ }
+ }
+
+ return { files.begin(), files.end() };
+ }
+
auto display_of(lldb::SBValue& value) -> std::string
{
const char* text = value.GetValue();
@@ -458,6 +531,7 @@ namespace Hsdbg
m_disassembly.clear();
m_disassembly_name.clear();
m_selected_symbol = 0;
+ refresh_source_files();
refresh_disassembly();
return {};
@@ -480,6 +554,7 @@ namespace Hsdbg
m_locals.clear();
m_registers.clear();
m_symbols.clear();
+ m_source_files.clear();
m_disassembly.clear();
m_disassembly_name.clear();
m_console_output.clear();
@@ -642,6 +717,9 @@ namespace Hsdbg
Log::info("debugger: attached to pid {}", m_process_id);
+ if (m_source_files.empty())
+ refresh_source_files();
+
sync_after_start();
return {};
@@ -1392,6 +1470,16 @@ namespace Hsdbg
m_symbols = module_symbols(m_session->target);
}
+ auto Debugger::refresh_source_files() -> void
+ {
+ m_source_files.clear();
+
+ if (!m_session->target.IsValid())
+ return;
+
+ m_source_files = module_source_files(m_session->target);
+ }
+
auto Debugger::load_disassembly(uint64_t file_address) -> void
{
m_disassembly.clear();
@@ -1446,6 +1534,7 @@ namespace Hsdbg
if (!m_session->target.IsValid())
{
m_symbols.clear();
+ m_source_files.clear();
m_disassembly.clear();
m_disassembly_name.clear();
m_selected_symbol = 0;
@@ -1455,6 +1544,9 @@ namespace Hsdbg
if (m_symbols.empty())
refresh_symbols();
+ if (m_source_files.empty())
+ refresh_source_files();
+
for (Symbol& symbol : m_symbols)
{
lldb::SBAddress address = m_session->target.ResolveFileAddress(symbol.file_address);
diff --git a/src/debugger/debugger.h b/src/debugger/debugger.h
index 175683e..e2277df 100644
--- a/src/debugger/debugger.h
+++ b/src/debugger/debugger.h
@@ -58,6 +58,7 @@ namespace Hsdbg
auto locals() const -> std::span<const Variable> { return m_locals; }
auto registers() const -> std::span<const Register> { return m_registers; }
auto symbols() const -> std::span<const Symbol> { return m_symbols; }
+ auto source_files() const -> std::span<const std::filesystem::path> { return m_source_files; }
auto disassembly() const -> std::span<const Instruction> { return m_disassembly; }
auto disassembly_name() const -> std::string_view { return m_disassembly_name; }
auto evaluate(std::string_view expression) -> Result<std::string>;
@@ -106,6 +107,7 @@ namespace Hsdbg
auto refresh_call_stack() -> void;
auto refresh_frame_data() -> void;
auto refresh_symbols() -> void;
+ auto refresh_source_files() -> void;
auto refresh_disassembly() -> void;
auto load_disassembly(uint64_t file_address) -> void;
@@ -125,6 +127,7 @@ namespace Hsdbg
std::vector<Variable> m_locals;
std::vector<Register> m_registers;
std::vector<Symbol> m_symbols;
+ std::vector<std::filesystem::path> m_source_files;
std::vector<Instruction> m_disassembly;
std::string m_disassembly_name;
std::vector<std::string> m_console_output;
diff --git a/src/ui/ui.cpp b/src/ui/ui.cpp
index b79da17..c3a7fa2 100644
--- a/src/ui/ui.cpp
+++ b/src/ui/ui.cpp
@@ -26,6 +26,7 @@ namespace Hsdbg
constexpr const char* PANEL_BREAKPOINTS = "breakpoints";
constexpr const char* PANEL_CALL_STACK = "call stack";
constexpr const char* PANEL_THREADS = "threads";
+ constexpr const char* PANEL_SOURCE_TREE = "source tree";
constexpr const char* PANEL_LOCALS = "locals";
constexpr const char* PANEL_REGISTERS = "registers";
constexpr const char* PANEL_SYMBOLS = "symbols";
@@ -100,6 +101,144 @@ namespace Hsdbg
return std::ranges::search(name, filter, equal).begin() != name.end();
}
+ struct SourceNode
+ {
+ std::string name;
+ std::filesystem::path path;
+ std::vector<SourceNode> children;
+ };
+
+ auto find_or_add_child(std::vector<SourceNode>& nodes, std::string_view name) -> SourceNode&
+ {
+ for (SourceNode& node : nodes)
+ {
+ if (node.name == name)
+ return node;
+ }
+
+ nodes.push_back({ std::string(name), {}, {} });
+ return nodes.back();
+ }
+
+ auto common_directory(std::span<const std::filesystem::path> files) -> std::filesystem::path
+ {
+ if (files.empty())
+ return {};
+
+ std::filesystem::path prefix = files.front().parent_path();
+
+ for (const std::filesystem::path& file : files)
+ {
+ while (true)
+ {
+ if (file.parent_path() == prefix)
+ break;
+
+ std::error_code error;
+ const std::filesystem::path relative =
+ std::filesystem::relative(file.parent_path(), prefix, error);
+ const std::string text = relative.generic_string();
+
+ if (!error && (text.empty() || text == "." || !text.starts_with("..")))
+ break;
+
+ const std::filesystem::path parent = prefix.parent_path();
+
+ if (parent == prefix)
+ break;
+
+ prefix = parent;
+ }
+ }
+
+ return prefix;
+ }
+
+ auto build_source_tree(std::span<const std::filesystem::path> files) -> SourceNode
+ {
+ SourceNode root;
+ const std::filesystem::path prefix = common_directory(files);
+ root.name = prefix.empty() ? "/" : prefix.filename().string();
+
+ if (root.name.empty())
+ root.name = prefix.string();
+
+ for (const std::filesystem::path& file : files)
+ {
+ std::error_code error;
+ std::filesystem::path relative = std::filesystem::relative(file, prefix, error);
+
+ if (error || relative.empty())
+ relative = file.filename();
+
+ SourceNode* current = &root;
+
+ const std::vector<std::filesystem::path> parts(relative.begin(), relative.end());
+
+ for (size_t index = 0; index < parts.size(); ++index)
+ {
+ SourceNode& child = find_or_add_child(current->children, parts[index].string());
+
+ if (index + 1 == parts.size())
+ child.path = file;
+ else
+ current = &child;
+ }
+ }
+
+ const auto sort_nodes = [](this const auto& self, std::vector<SourceNode>& nodes) -> void
+ {
+ std::ranges::sort(nodes, [](const SourceNode& left, const SourceNode& right)
+ {
+ const bool left_directory = left.path.empty();
+ const bool right_directory = right.path.empty();
+
+ if (left_directory != right_directory)
+ return left_directory;
+
+ return left.name < right.name;
+ });
+
+ for (SourceNode& node : nodes)
+ self(node.children);
+ };
+
+ sort_nodes(root.children);
+
+ return root;
+ }
+
+ auto source_node_matches(const SourceNode& node, std::string_view filter) -> bool
+ {
+ if (filter.empty())
+ return true;
+
+ if (!node.path.empty())
+ return matches_filter(node.name, filter);
+
+ return std::ranges::any_of(node.children, [&](const SourceNode& child)
+ {
+ return source_node_matches(child, filter);
+ });
+ }
+
+ auto preferred_source(std::span<const std::filesystem::path> files) -> std::filesystem::path
+ {
+ for (const std::filesystem::path& file : files)
+ {
+ if (file.stem() == "main" && std::filesystem::exists(file))
+ return file;
+ }
+
+ for (const std::filesystem::path& file : files)
+ {
+ if (std::filesystem::exists(file))
+ return file;
+ }
+
+ return {};
+ }
+
auto draw_instruction_table(Debugger& debugger,
std::span<const Instruction> instructions,
bool scroll_to_current) -> void
@@ -353,9 +492,17 @@ namespace Hsdbg
return;
m_followed_target = debugger.target_path();
- m_focus_symbols = true;
m_scroll_to_symbol = true;
m_scroll_to_program_counter = true;
+
+ if (m_source_view.path().empty())
+ {
+ if (const std::filesystem::path source = preferred_source(debugger.source_files());
+ !source.empty())
+ {
+ open_source(source);
+ }
+ }
}
auto Ui::show_frame(const StackFrame& frame) -> void
@@ -408,9 +555,7 @@ namespace Hsdbg
{
build_default_layout(dockspace_id);
m_layout_built = true;
-
- // otherwise whichever panel happens to be submitted last wins the tab
- m_focus_breakpoints = true;
+ m_select_default_tabs = true;
}
draw_status_bar(debugger);
@@ -421,6 +566,7 @@ namespace Hsdbg
draw_source_panel(debugger);
draw_breakpoints_panel(debugger);
draw_call_stack_panel(debugger);
+ draw_source_tree_panel(debugger);
draw_threads_panel(debugger);
draw_locals_panel(debugger);
draw_registers_panel(debugger);
@@ -433,19 +579,27 @@ namespace Hsdbg
// a window claims its tab when it is first submitted, so this can only be
// asked for once every panel in the node exists
+ if (m_select_default_tabs)
+ {
+ ImGui::SetWindowFocus(PANEL_LOCALS);
+ ImGui::SetWindowFocus(PANEL_BREAKPOINTS);
+ ImGui::SetWindowFocus(PANEL_SOURCE);
+ ImGui::SetWindowFocus(PANEL_SOURCE_TREE);
+ m_select_default_tabs = false;
+ }
+
if (m_focus_symbols)
{
ImGui::SetWindowFocus(PANEL_SYMBOLS);
m_focus_symbols = false;
- m_focus_breakpoints = false;
}
else if (m_focus_disassembly)
{
ImGui::SetWindowFocus(PANEL_DISASSEMBLY);
m_focus_disassembly = false;
- m_focus_breakpoints = false;
}
- else if (m_focus_breakpoints)
+
+ if (m_focus_breakpoints)
{
ImGui::SetWindowFocus(PANEL_BREAKPOINTS);
m_focus_breakpoints = false;
@@ -460,8 +614,8 @@ namespace Hsdbg
ImGuiID center_id = dockspace_id;
const ImGuiID left_id = ImGui::DockBuilderSplitNode(center_id, ImGuiDir_Left, 0.20f, nullptr, &center_id);
- const ImGuiID right_id = ImGui::DockBuilderSplitNode(center_id, ImGuiDir_Right, 0.28f, nullptr, &center_id);
- const ImGuiID bottom_id = ImGui::DockBuilderSplitNode(center_id, ImGuiDir_Down, 0.28f, nullptr, &center_id);
+ const ImGuiID right_id = ImGui::DockBuilderSplitNode(center_id, ImGuiDir_Right, 0.22f, nullptr, &center_id);
+ const ImGuiID bottom_id = ImGui::DockBuilderSplitNode(center_id, ImGuiDir_Down, 0.24f, nullptr, &center_id);
ImGuiID left_top_id = left_id;
const ImGuiID left_bottom_id = ImGui::DockBuilderSplitNode(left_top_id, ImGuiDir_Down, 0.5f, nullptr, &left_top_id);
@@ -471,10 +625,11 @@ namespace Hsdbg
ImGui::DockBuilderDockWindow(PANEL_SOURCE, center_id);
ImGui::DockBuilderDockWindow(PANEL_DISASSEMBLY, center_id);
+ ImGui::DockBuilderDockWindow(PANEL_SOURCE_TREE, left_top_id);
ImGui::DockBuilderDockWindow(PANEL_THREADS, left_top_id);
ImGui::DockBuilderDockWindow(PANEL_SYMBOLS, left_bottom_id);
- ImGui::DockBuilderDockWindow(PANEL_CALL_STACK, left_bottom_id);
ImGui::DockBuilderDockWindow(PANEL_LOCALS, right_top_id);
+ ImGui::DockBuilderDockWindow(PANEL_CALL_STACK, right_top_id);
ImGui::DockBuilderDockWindow(PANEL_REGISTERS, right_bottom_id);
ImGui::DockBuilderDockWindow(PANEL_BREAKPOINTS, bottom_id);
ImGui::DockBuilderDockWindow(PANEL_CONSOLE, bottom_id);
@@ -543,6 +698,7 @@ namespace Hsdbg
if (ImGui::BeginMenu("view"))
{
ImGui::MenuItem(PANEL_SOURCE, nullptr, &m_visible.source);
+ ImGui::MenuItem(PANEL_SOURCE_TREE, nullptr, &m_visible.source_tree);
ImGui::MenuItem(PANEL_BREAKPOINTS, nullptr, &m_visible.breakpoints);
ImGui::MenuItem(PANEL_CALL_STACK, nullptr, &m_visible.call_stack);
ImGui::MenuItem(PANEL_THREADS, nullptr, &m_visible.threads);
@@ -910,6 +1066,73 @@ namespace Hsdbg
ImGui::End();
}
+ auto Ui::draw_source_tree_panel(Debugger& debugger) -> void
+ {
+ if (!m_visible.source_tree)
+ return;
+
+ if (ImGui::Begin(PANEL_SOURCE_TREE, &m_visible.source_tree))
+ {
+ const std::span<const std::filesystem::path> files = debugger.source_files();
+
+ if (files.empty())
+ {
+ ImGui::TextDisabled("no source files");
+ }
+ else
+ {
+ ImGui::SetNextItemWidth(-1.0f);
+ ImGui::InputTextWithHint("##source_filter", "filter files", &m_source_filter);
+
+ const SourceNode tree = build_source_tree(files);
+ const std::filesystem::path& open = m_source_view.path();
+
+ const auto draw_node = [&](this const auto& self, const SourceNode& node) -> void
+ {
+ if (!source_node_matches(node, m_source_filter))
+ return;
+
+ if (!node.path.empty())
+ {
+ const bool selected = node.path == open;
+
+ if (ImGui::Selectable(node.name.c_str(), selected))
+ {
+ open_source(node.path);
+ ImGui::SetWindowFocus(PANEL_SOURCE);
+ }
+
+ if (ImGui::IsItemHovered())
+ ImGui::SetTooltip("%s", node.path.string().c_str());
+
+ return;
+ }
+
+ ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_SpanAvailWidth |
+ ImGuiTreeNodeFlags_OpenOnArrow;
+
+ if (m_source_filter.empty())
+ flags |= ImGuiTreeNodeFlags_DefaultOpen;
+
+ if (!ImGui::TreeNodeEx(node.name.c_str(), flags))
+ return;
+
+ for (const SourceNode& child : node.children)
+ self(child);
+
+ ImGui::TreePop();
+ };
+
+ if (tree.children.empty())
+ ImGui::TextDisabled("no source files");
+ else
+ draw_node(tree);
+ }
+ }
+
+ ImGui::End();
+ }
+
auto Ui::draw_locals_panel(Debugger& debugger) -> void
{
if (!m_visible.locals)
diff --git a/src/ui/ui.h b/src/ui/ui.h
index 39493dc..8cc573e 100644
--- a/src/ui/ui.h
+++ b/src/ui/ui.h
@@ -40,6 +40,7 @@ namespace Hsdbg
bool breakpoints = true;
bool call_stack = true;
bool threads = true;
+ bool source_tree = true;
bool locals = true;
bool registers = true;
bool symbols = true;
@@ -60,6 +61,7 @@ namespace Hsdbg
auto draw_breakpoints_panel(Debugger& debugger) -> void;
auto draw_call_stack_panel(Debugger& debugger) -> void;
auto draw_threads_panel(Debugger& debugger) -> void;
+ auto draw_source_tree_panel(Debugger& debugger) -> void;
auto draw_locals_panel(Debugger& debugger) -> void;
auto draw_registers_panel(Debugger& debugger) -> void;
auto draw_symbols_panel(Debugger& debugger) -> void;
@@ -81,11 +83,13 @@ namespace Hsdbg
std::string m_console_input;
std::string m_target_input;
std::string m_symbol_filter;
+ std::string m_source_filter;
uint64_t m_followed_stop = 0;
std::filesystem::path m_followed_target;
bool m_layout_built = false;
+ bool m_select_default_tabs = false;
bool m_console_scroll_pending = false;
bool m_load_target_pending = false;
bool m_focus_breakpoints = false;