diff options
| author | hachem <im@hachem.wtf> | 2026-08-29 01:59:56 +0200 |
|---|---|---|
| committer | hachem <im@hachem.wtf> | 2026-08-29 01:59:56 +0200 |
| commit | f581953b0f2bc092cd8ca5d8243eeb61622d89d9 (patch) | |
| tree | 6a9ccefaa6e2ec646f8d3eaccd905508dcbe5209 | |
| parent | ba64bcc9c38c04bd48e79ac59788b946454f4acb (diff) | |
feat: source tree and symbols panel
| -rw-r--r-- | .gitignore | 2 | ||||
| -rw-r--r-- | src/debugger/debugger.cpp | 92 | ||||
| -rw-r--r-- | src/debugger/debugger.h | 3 | ||||
| -rw-r--r-- | src/ui/ui.cpp | 243 | ||||
| -rw-r--r-- | src/ui/ui.h | 4 |
5 files changed, 334 insertions, 10 deletions
@@ -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, ¢er_id); - const ImGuiID right_id = ImGui::DockBuilderSplitNode(center_id, ImGuiDir_Right, 0.28f, nullptr, ¢er_id); - const ImGuiID bottom_id = ImGui::DockBuilderSplitNode(center_id, ImGuiDir_Down, 0.28f, nullptr, ¢er_id); + const ImGuiID right_id = ImGui::DockBuilderSplitNode(center_id, ImGuiDir_Right, 0.22f, nullptr, ¢er_id); + const ImGuiID bottom_id = ImGui::DockBuilderSplitNode(center_id, ImGuiDir_Down, 0.24f, nullptr, ¢er_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; |
