diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/ui/source_view.cpp | 124 | ||||
| -rw-r--r-- | src/ui/source_view.h | 5 | ||||
| -rw-r--r-- | src/ui/ui.cpp | 796 | ||||
| -rw-r--r-- | src/ui/ui.h | 66 |
4 files changed, 830 insertions, 161 deletions
diff --git a/src/ui/source_view.cpp b/src/ui/source_view.cpp index 0a6a230..3c15c08 100644 --- a/src/ui/source_view.cpp +++ b/src/ui/source_view.cpp @@ -274,12 +274,12 @@ namespace Hsdbg macros.find(word) != nullptr) kind = SyntaxKind::Macro; - if (kind != SyntaxKind::Default) - { - flush_default(start); - emit(start, at - start, kind); - run = at; - } + // every word gets its own span, including plain identifiers: + // isolating them lets the source view hit-test a name under + // the cursor and show its live value + flush_default(start); + emit(start, at - start, kind); + run = at; continue; } @@ -324,11 +324,64 @@ namespace Hsdbg const ImU32 BREAKPOINT_DISABLED_COLOR = IM_COL32(120, 90, 90, 255); const ImU32 BREAKPOINT_HOVER_COLOR = IM_COL32(226, 84, 84, 90); + // muted blue-grey for the live values shown after a line, distinct from the + // green of comments so they do not read as part of the code + const ImU32 INLINE_VALUE_COLOR = IM_COL32(122, 140, 170, 235); + auto line_number_width(size_t line_count) -> float { return ImGui::CalcTextSize(std::to_string(line_count).c_str()).x; } + // the frame local named exactly `word`, or null; only names with a value to + // show qualify, so pure aggregates without a summary are skipped + auto find_local(std::span<const Variable> locals, std::string_view word) -> const Variable* + { + const auto match = std::ranges::find_if(locals, [&](const Variable& local) + { + return !local.value.empty() && local.name == word; + }); + + return match != locals.end() ? &*match : nullptr; + } + + // whether `name` appears in `line` as a whole identifier rather than as a + // fragment of a longer word + auto contains_word(const std::string& line, const std::string& name) -> bool + { + if (name.empty()) + return false; + + for (size_t at = line.find(name); at != std::string::npos; at = line.find(name, at + 1)) + { + const bool left = at > 0 && is_word(line[at - 1]); + const bool right = at + name.size() < line.size() && is_word(line[at + name.size()]); + + if (!left && !right) + return true; + } + + return false; + } + + // a single-line, length-capped rendering of a value for the inline annotation + auto inline_value(const Variable& local) -> std::string + { + std::string text = local.name + " = " + local.value; + + if (const auto newline = text.find('\n'); newline != std::string::npos) + text.resize(newline); + + constexpr size_t cap = 48; + if (text.size() > cap) + { + text.resize(cap - 3); + text += "..."; + } + + return text; + } + // grab a macro invocation out of a line starting at the name: the name // alone for an object-like use, or the name plus a balanced argument list // for a function-like one. a call that runs off the end of the line falls @@ -430,6 +483,13 @@ namespace Hsdbg return request; } + auto SourceView::take_watch_request() -> std::optional<std::string> + { + std::optional<std::string> request = std::move(m_watch_request); + m_watch_request.reset(); + return request; + } + auto SourceView::set_highlighted_line(uint32_t line) -> void { m_highlighted_line = line; @@ -495,6 +555,13 @@ namespace Hsdbg m_scroll_to_highlight = false; } + // the selected frame's locals, but only when we are stopped in this very + // file (a non-zero highlight means show_frame put us here); used both to + // annotate lines and to answer a hover over a name + const std::span<const Variable> locals = + (debugger.is_stopped() && m_highlighted_line != 0) ? debugger.locals() + : std::span<const Variable>{}; + ImGuiListClipper clipper; clipper.Begin(static_cast<int>(m_lines.size()), text_height); @@ -596,6 +663,23 @@ namespace Hsdbg if (span.kind == SyntaxKind::Default) { ImGui::TextUnformatted(begin, end); + + const std::string_view word(begin, static_cast<size_t>(end - begin)); + const bool is_name = !word.empty() && is_word_start(word.front()); + + // hovering a name that is a live local shows its value + if (!locals.empty() && ImGui::IsItemHovered()) + { + if (const Variable* local = find_local(locals, word)) + ImGui::SetTooltip("%s = %s%s%s", local->name.c_str(), + local->value.c_str(), + local->type.empty() ? "" : "\n", + local->type.c_str()); + } + + // right-click any name to add it to the watch list + if (is_name && ImGui::IsItemClicked(ImGuiMouseButton_Right)) + m_watch_request = std::string(word); } else if (span.kind == SyntaxKind::Macro) { @@ -610,6 +694,34 @@ namespace Hsdbg } } } + + // trailing live values: the locals that appear on this line, shown + // only up to the line the pc sits on, since anything past it has + // not run yet and would read as a stale or unset value + if (!locals.empty() && line_number <= m_highlighted_line) + { + int shown = 0; + + for (const Variable& local : locals) + { + if (shown >= 4) + break; + + if (local.value.empty() || !contains_word(text, local.name)) + continue; + + ImGui::SameLine(0.0f, shown == 0 ? GUTTER_MARGIN * 3.0f : GUTTER_MARGIN); + ImGui::PushStyleColor(ImGuiCol_Text, INLINE_VALUE_COLOR); + ImGui::TextUnformatted(inline_value(local).c_str()); + ImGui::PopStyleColor(); + + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s = %s%s%s", local.name.c_str(), local.value.c_str(), + local.type.empty() ? "" : "\n", local.type.c_str()); + + ++shown; + } + } } } diff --git a/src/ui/source_view.h b/src/ui/source_view.h index 0c10efa..4c2cbe4 100644 --- a/src/ui/source_view.h +++ b/src/ui/source_view.h @@ -66,6 +66,10 @@ namespace Hsdbg // hand to the macros panel; cleared once taken auto take_macro_request() -> std::optional<std::string>; + // right-clicking a name in the source stashes it here for the ui to add to + // the watch list; cleared once taken + auto take_watch_request() -> std::optional<std::string>; + private: auto draw_open_bar() -> void; auto draw_lines(Debugger& debugger) -> void; @@ -80,6 +84,7 @@ namespace Hsdbg std::vector<std::vector<SourceSpan>> m_spans; MacroTable m_macros; std::optional<std::string> m_macro_request; + std::optional<std::string> m_watch_request; bool m_highlight = false; bool m_highlighting_enabled = true; bool m_line_numbers = true; diff --git a/src/ui/ui.cpp b/src/ui/ui.cpp index 14fd1d8..a82bfb6 100644 --- a/src/ui/ui.cpp +++ b/src/ui/ui.cpp @@ -34,12 +34,12 @@ namespace Hsdbg constexpr const char* PANEL_THREADS = "threads"; constexpr const char* PANEL_SOURCE_TREE = "source tree"; constexpr const char* PANEL_LOCALS = "locals"; + constexpr const char* PANEL_WATCH = "watch"; constexpr const char* PANEL_REGISTERS = "registers"; constexpr const char* PANEL_SYMBOLS = "symbols"; constexpr const char* PANEL_DISASSEMBLY = "disassembly"; constexpr const char* PANEL_CONSOLE = "console"; constexpr const char* PANEL_PROFILER = "profiler"; - constexpr const char* PANEL_TIMELINE = "timeline"; constexpr const char* PANEL_MACROS = "macros"; constexpr const char* LOAD_TARGET_POPUP = "load target"; @@ -239,6 +239,19 @@ namespace Hsdbg }); } + // whether the file lives anywhere under this node, so a folder can tell if + // it is on the path to the open source and should reveal itself + auto node_contains(const SourceNode& node, const std::filesystem::path& target) -> bool + { + if (!node.path.empty()) + return node.path == target; + + return std::ranges::any_of(node.children, [&](const SourceNode& child) + { + return node_contains(child, target); + }); + } + auto preferred_source(std::span<const std::filesystem::path> files) -> std::filesystem::path { const auto named_main = std::ranges::find_if(files, [](const std::filesystem::path& file) @@ -512,6 +525,11 @@ namespace Hsdbg m_scroll_to_program_counter = true; m_scroll_to_symbol = true; + follow_selected_frame(debugger); + } + + auto Ui::follow_selected_frame(Debugger& debugger) -> void + { const std::span<const StackFrame> stack = debugger.call_stack(); // the innermost frames are often runtime internals with no source, and @@ -628,32 +646,57 @@ namespace Hsdbg m_focus_macros = true; } + // right-clicking a name in the source adds it to the watch list + if (std::optional<std::string> request = m_source_view.take_watch_request()) + add_watch(debugger, std::move(*request)); + + // let the profiling views appear on their own the moment work starts: + // the profiler (which now holds the flame chart, timings and graphs) when + // something is being sampled, traced or instrumented. the latch means it + // only springs up on the rising edge, so closing it makes it stay closed + const bool profiling_active = debugger.sampling_enabled() || + debugger.instrumentation_active() || + !debugger.traces().empty(); + + if (profiling_active && !m_profiler_revealed) + m_visible.profiler = true; + m_profiler_revealed = profiling_active; + draw_breakpoints_panel(debugger); draw_call_stack_panel(debugger); draw_source_tree_panel(debugger); draw_threads_panel(debugger); draw_locals_panel(debugger); + draw_watch_panel(debugger); draw_registers_panel(debugger); draw_symbols_panel(debugger); draw_disassembly_panel(debugger); draw_console_panel(debugger); draw_profiler_panel(debugger); - draw_timeline_panel(debugger); draw_macros_panel(debugger); if (m_visible.demo) ImGui::ShowDemoWindow(&m_visible.demo); draw_preferences_window(); + draw_command_palette(debugger); // a window claims its tab when it is first submitted, so this can only be - // asked for once every panel in the node exists + // asked for once every panel in the node exists. within each tabbed node + // the later focus wins, so raise the tab we want in front last; the very + // last call also lands the keyboard focus, which belongs on the source if (m_select_default_tabs) { + ImGui::SetWindowFocus(PANEL_SYMBOLS); + ImGui::SetWindowFocus(PANEL_SOURCE_TREE); + ImGui::SetWindowFocus(PANEL_THREADS); + ImGui::SetWindowFocus(PANEL_CALL_STACK); + ImGui::SetWindowFocus(PANEL_WATCH); ImGui::SetWindowFocus(PANEL_LOCALS); + ImGui::SetWindowFocus(PANEL_DISASSEMBLY); ImGui::SetWindowFocus(PANEL_BREAKPOINTS); + ImGui::SetWindowFocus(PANEL_CONSOLE); ImGui::SetWindowFocus(PANEL_SOURCE); - ImGui::SetWindowFocus(PANEL_SOURCE_TREE); m_select_default_tabs = false; } @@ -679,6 +722,12 @@ namespace Hsdbg ImGui::SetWindowFocus(PANEL_MACROS); m_focus_macros = false; } + + if (m_focus_profiler) + { + ImGui::SetWindowFocus(PANEL_PROFILER); + m_focus_profiler = false; + } } auto Ui::build_default_layout(uint32_t dockspace_id) -> void @@ -687,28 +736,43 @@ namespace Hsdbg ImGui::DockBuilderAddNode(dockspace_id, ImGuiDockNodeFlags_DockSpace); ImGui::DockBuilderSetNodeSize(dockspace_id, ImGui::GetMainViewport()->WorkSize); + // four regions frame a central code view, each holding one stage of the + // debugging loop so related panels sit together instead of scattered: + // left navigator - where to go (files, symbols) over where execution + // currently is (call stack, threads) + // center code - source and disassembly, both tracking the pc + // right inspector - the selected frame's state: locals over registers + // bottom output - the console and the auxiliary read-outs 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.22f, nullptr, ¢er_id); - const ImGuiID bottom_id = ImGui::DockBuilderSplitNode(center_id, ImGuiDir_Down, 0.24f, nullptr, ¢er_id); + const ImGuiID right_id = ImGui::DockBuilderSplitNode(center_id, ImGuiDir_Right, 0.24f, nullptr, ¢er_id); + const ImGuiID bottom_id = ImGui::DockBuilderSplitNode(center_id, ImGuiDir_Down, 0.26f, 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); ImGuiID right_top_id = right_id; - const ImGuiID right_bottom_id = ImGui::DockBuilderSplitNode(right_top_id, ImGuiDir_Down, 0.5f, nullptr, &right_top_id); + const ImGuiID right_bottom_id = ImGui::DockBuilderSplitNode(right_top_id, ImGuiDir_Down, 0.45f, nullptr, &right_top_id); + + // navigator: where to go, then where execution is + ImGui::DockBuilderDockWindow(PANEL_SOURCE_TREE, left_top_id); + ImGui::DockBuilderDockWindow(PANEL_SYMBOLS, left_top_id); + ImGui::DockBuilderDockWindow(PANEL_CALL_STACK, left_bottom_id); + ImGui::DockBuilderDockWindow(PANEL_THREADS, left_bottom_id); + // code ImGui::DockBuilderDockWindow(PANEL_SOURCE, center_id); ImGui::DockBuilderDockWindow(PANEL_DISASSEMBLY, center_id); - ImGui::DockBuilderDockWindow(PANEL_TIMELINE, center_id); - ImGui::DockBuilderDockWindow(PANEL_SOURCE_TREE, left_top_id); - ImGui::DockBuilderDockWindow(PANEL_THREADS, left_top_id); - ImGui::DockBuilderDockWindow(PANEL_SYMBOLS, left_bottom_id); + + // inspector: locals and the watch list share the top, the frame's state + // over the frame's registers ImGui::DockBuilderDockWindow(PANEL_LOCALS, right_top_id); - ImGui::DockBuilderDockWindow(PANEL_CALL_STACK, right_top_id); + ImGui::DockBuilderDockWindow(PANEL_WATCH, right_top_id); ImGui::DockBuilderDockWindow(PANEL_REGISTERS, right_bottom_id); - ImGui::DockBuilderDockWindow(PANEL_BREAKPOINTS, bottom_id); + + // output drawer ImGui::DockBuilderDockWindow(PANEL_CONSOLE, bottom_id); + ImGui::DockBuilderDockWindow(PANEL_BREAKPOINTS, bottom_id); ImGui::DockBuilderDockWindow(PANEL_PROFILER, bottom_id); ImGui::DockBuilderDockWindow(PANEL_MACROS, bottom_id); @@ -735,6 +799,9 @@ namespace Hsdbg ImGui::Separator(); + if (ImGui::MenuItem("command palette...", "cmd+k")) + m_palette_request = true; + ImGui::MenuItem("preferences...", nullptr, &m_show_preferences); ImGui::Separator(); @@ -788,12 +855,12 @@ namespace Hsdbg ImGui::MenuItem(PANEL_CALL_STACK, nullptr, &m_visible.call_stack); ImGui::MenuItem(PANEL_THREADS, nullptr, &m_visible.threads); ImGui::MenuItem(PANEL_LOCALS, nullptr, &m_visible.locals); + ImGui::MenuItem(PANEL_WATCH, nullptr, &m_visible.watch); ImGui::MenuItem(PANEL_REGISTERS, nullptr, &m_visible.registers); ImGui::MenuItem(PANEL_SYMBOLS, nullptr, &m_visible.symbols); ImGui::MenuItem(PANEL_DISASSEMBLY, nullptr, &m_visible.disassembly); ImGui::MenuItem(PANEL_CONSOLE, nullptr, &m_visible.console); ImGui::MenuItem(PANEL_PROFILER, nullptr, &m_visible.profiler); - ImGui::MenuItem(PANEL_TIMELINE, nullptr, &m_visible.timeline); ImGui::MenuItem(PANEL_MACROS, nullptr, &m_visible.macros); ImGui::Separator(); @@ -882,6 +949,30 @@ namespace Hsdbg ImGui::TextDisabled("|"); ImGui::SameLine(); + // the way in to profiling, and the way back out: the panel only exists + // while this is on, and lights up to show that it is. the button toggles + // that state, so the tint is keyed off a copy taken before the click or + // the push and pop would not balance + const bool profiler_open = m_visible.profiler; + + if (profiler_open) + ImGui::PushStyleColor(ImGuiCol_Button, ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive)); + + if (ImGui::Button("profiler")) + { + m_visible.profiler = !m_visible.profiler; + + if (m_visible.profiler) + m_focus_profiler = true; + } + + if (profiler_open) + ImGui::PopStyleColor(); + + ImGui::SameLine(); + ImGui::TextDisabled("|"); + ImGui::SameLine(); + ImGui::AlignTextToFramePadding(); ImGui::TextColored(state_color(debugger.state()), "%s", to_string(debugger.state()).data()); @@ -1193,8 +1284,7 @@ namespace Hsdbg else { constexpr ImGuiTableFlags flags = ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV | - ImGuiTableFlags_SizingStretchProp | - ImGuiTableFlags_ScrollY; + ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY; if (ImGui::BeginTable("##breakpoints", 8, flags)) { @@ -1232,9 +1322,25 @@ namespace Hsdbg } else if (breakpoint.line != 0) { - ImGui::Text("%s:%u", - breakpoint.file.filename().string().c_str(), - breakpoint.line); + // clicking the location takes the source view there, so + // the breakpoints list doubles as a jump list. plain + // text with a click test, not a Selectable: a full-width + // Selectable in this stretch-sized column feeds its own + // width back into the column solver and lands on NaN + const std::string location = std::format( + "{}:{}", breakpoint.file.filename().string(), breakpoint.line); + + ImGui::TextUnformatted(location.c_str()); + + if (ImGui::IsItemHovered()) + ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + + if (ImGui::IsItemClicked() && !breakpoint.file.empty()) + { + open_source(breakpoint.file); + m_source_view.set_highlighted_line(breakpoint.line); + ImGui::SetWindowFocus(PANEL_SOURCE); + } } else { @@ -1364,7 +1470,11 @@ namespace Hsdbg { debugger.select_thread(thread.id); + // land the source, disassembly and inspector on this + // thread's own frame instead of leaving them on the last m_scroll_to_program_counter = true; + m_scroll_to_symbol = true; + follow_selected_frame(debugger); } ImGui::SameLine(); @@ -1397,6 +1507,11 @@ namespace Hsdbg const SourceNode tree = build_source_tree(files); const std::filesystem::path& open = m_source_view.path(); + // folders stay collapsed until they are needed. when the open file + // changes, the one frame where it differs from what we last showed + // expands the chain down to it; after that the user is in control + const bool reveal = open != m_revealed_source; + ImDrawList* const draw_list = ImGui::GetWindowDrawList(); const auto draw_node = [&](this const auto& self, const SourceNode& node) -> void @@ -1428,12 +1543,17 @@ namespace Hsdbg return; } - ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_SpanAvailWidth | - ImGuiTreeNodeFlags_OpenOnArrow | - ImGuiTreeNodeFlags_OpenOnDoubleClick; + constexpr ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_SpanAvailWidth | + ImGuiTreeNodeFlags_OpenOnArrow | + ImGuiTreeNodeFlags_OpenOnDoubleClick; - if (m_source_filter.empty()) - flags |= ImGuiTreeNodeFlags_DefaultOpen; + // a filter opens every folder so its matches are visible; with + // no filter, only the chain down to the open file unfurls, and + // only on the frame it changed so manual collapses survive + if (!m_source_filter.empty()) + ImGui::SetNextItemOpen(true, ImGuiCond_Always); + else if (reveal && node_contains(node, open)) + ImGui::SetNextItemOpen(true, ImGuiCond_Always); ImGui::PushStyleColor(ImGuiCol_Text, SOURCE_FOLDER_COLOR); const bool open_node = ImGui::TreeNodeEx(node.name.c_str(), flags); @@ -1479,6 +1599,8 @@ namespace Hsdbg ImGui::TextDisabled("no source files"); else draw_node(tree); + + m_revealed_source = open; } } @@ -1520,6 +1642,148 @@ namespace Hsdbg ImGui::End(); } + auto Ui::add_watch(Debugger& debugger, std::string expression) -> void + { + if (expression.empty()) + return; + + if (std::ranges::any_of(m_watches, [&](const Watch& watch) + { return watch.expression == expression; })) + { + m_visible.watch = true; + return; + } + + Watch watch; + watch.expression = std::move(expression); + + if (debugger.is_stopped()) + { + if (const auto result = debugger.evaluate(watch.expression)) + { + watch.value = *result; + watch.ok = true; + } + else + { + watch.value = result.error(); + } + } + + m_watches.push_back(std::move(watch)); + m_visible.watch = true; + } + + auto Ui::draw_watch_panel(Debugger& debugger) -> void + { + if (!m_visible.watch) + return; + + if (ImGui::Begin(PANEL_WATCH, &m_visible.watch)) + { + // re-run the expressions whenever the target stops again or the frame + // the ui is looking at moves, but never every frame: each call jits and + // runs code in the target + const bool stopped = debugger.is_stopped(); + const bool moved = debugger.stop_count() != m_watch_stop || + debugger.selected_thread() != m_watch_thread || + debugger.selected_frame() != m_watch_frame; + + if (stopped && (moved || !m_watch_evaluated)) + { + m_watch_stop = debugger.stop_count(); + m_watch_thread = debugger.selected_thread(); + m_watch_frame = debugger.selected_frame(); + m_watch_evaluated = true; + + for (Watch& watch : m_watches) + { + if (const auto result = debugger.evaluate(watch.expression)) + { + watch.value = *result; + watch.ok = true; + } + else + { + watch.value = result.error(); + watch.ok = false; + } + } + } + + if (!stopped) + m_watch_evaluated = false; + + ImGui::SetNextItemWidth(-60.0f); + const bool submitted = ImGui::InputTextWithHint("##watch_input", "expression to watch", + &m_watch_input, + ImGuiInputTextFlags_EnterReturnsTrue); + + ImGui::SameLine(); + const bool add_clicked = ImGui::Button("watch", ImVec2(-1.0f, 0.0f)); + + if ((submitted || add_clicked) && !m_watch_input.empty()) + { + add_watch(debugger, m_watch_input); + m_watch_input.clear(); + } + + if (m_watches.empty()) + { + ImGui::TextDisabled("watch an expression; it re-evaluates on every stop"); + } + else + { + constexpr ImGuiTableFlags flags = ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV | + ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY; + + if (ImGui::BeginTable("##watches", 3, flags)) + { + ImGui::TableSetupColumn("expression", ImGuiTableColumnFlags_WidthFixed, 140.0f); + ImGui::TableSetupColumn("value"); + ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, 24.0f); + ImGui::TableSetupScrollFreeze(0, 1); + ImGui::TableHeadersRow(); + + size_t remove_index = m_watches.size(); + + for (size_t index = 0; index < m_watches.size(); ++index) + { + const Watch& watch = m_watches[index]; + + ImGui::TableNextRow(); + ImGui::PushID(static_cast<int>(index)); + + ImGui::TableNextColumn(); + ImGui::TextUnformatted(watch.expression.c_str()); + + ImGui::TableNextColumn(); + if (!m_watch_evaluated && !stopped) + ImGui::TextDisabled("-"); + else if (watch.ok) + ImGui::TextUnformatted(watch.value.c_str()); + else + ImGui::TextColored(ImVec4(0.85f, 0.45f, 0.45f, 1.0f), "%s", + watch.value.c_str()); + + ImGui::TableNextColumn(); + if (ImGui::SmallButton("x")) + remove_index = index; + + ImGui::PopID(); + } + + ImGui::EndTable(); + + if (remove_index < m_watches.size()) + m_watches.erase(m_watches.begin() + static_cast<std::ptrdiff_t>(remove_index)); + } + } + } + + ImGui::End(); + } + auto Ui::draw_registers_panel(const Debugger& debugger) -> void { if (!m_visible.registers) @@ -1742,61 +2006,59 @@ namespace Hsdbg if (ImGui::Begin(PANEL_PROFILER, &m_visible.profiler)) { - bool paused = m_profiler.paused(); - if (ImGui::Checkbox("pause", &paused)) - m_profiler.set_paused(paused); - - ImGui::SameLine(); - if (ImGui::Button("reset")) - m_profiler.reset(); - - ImGui::Separator(); + const bool instrumented = debugger.instrumentation_active(); + const std::span<const FunctionTrace> traces = debugger.traces(); - if (!debugger.has_target()) + // compact header: how calls are being gathered. instrumentation, when + // the target carries it, is exact and needs nothing; otherwise sampling + // approximates on any binary. either way calls flow into the flame chart + if (instrumented) { - ImGui::TextDisabled("no target: load and run a process to profile it"); + ImGui::TextDisabled("instrumented target — every function timed automatically"); } else { - const TimeSeries& memory = m_profiler.target_memory_mb(); - - const std::string memory_overlay = - std::format("{:.1f} MB (peak {:.1f})", memory.latest(), memory.maximum()); + bool sampling = debugger.sampling_enabled(); + if (ImGui::Checkbox("sample while running", &sampling)) + debugger.set_sampling_enabled(sampling); - ImGui::TextUnformatted("resident memory"); - ImGui::PlotLines("##target_memory", memory.values(), memory.count(), memory.offset(), - memory_overlay.c_str(), 0.0f, FLT_MAX, ImVec2(-1.0f, 90.0f)); + ImGui::SameLine(); + ImGui::TextDisabled("(any binary, approximate)"); } - ImGui::Separator(); - ImGui::TextUnformatted("function tracing"); + // the hero view: the call flame chart, given the lion's share of the + // panel, with the detail sections collapsed beneath it + const float flame_height = std::max(ImGui::GetContentRegionAvail().y * 0.6f, 150.0f); - ImGui::SetNextItemWidth(-70.0f); - const bool submitted = ImGui::InputTextWithHint("##trace_input", "function to time", - &m_trace_input, - ImGuiInputTextFlags_EnterReturnsTrue); + if (ImGui::BeginChild("##flamegraph", ImVec2(0.0f, flame_height), ImGuiChildFlags_Borders)) + draw_flamegraph(debugger); - ImGui::SameLine(); - const bool add_clicked = ImGui::Button("trace", ImVec2(-1.0f, 0.0f)); + ImGui::EndChild(); - if ((submitted || add_clicked) && !m_trace_input.empty()) + // exact per-function timings, and the box to add another + if (ImGui::CollapsingHeader("function timings", ImGuiTreeNodeFlags_DefaultOpen)) { - debugger.add_trace(m_trace_input); - m_trace_input.clear(); - } + ImGui::SetNextItemWidth(-70.0f); + const bool submitted = ImGui::InputTextWithHint("##trace_input", "function to time", + &m_trace_input, + ImGuiInputTextFlags_EnterReturnsTrue); - const std::span<const FunctionTrace> traces = debugger.traces(); + ImGui::SameLine(); + const bool add_clicked = ImGui::Button("trace", ImVec2(-1.0f, 0.0f)); - if (traces.empty()) - { - ImGui::TextDisabled("no traced functions yet"); - } - else - { - constexpr ImGuiTableFlags flags = ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV | - ImGuiTableFlags_SizingStretchProp; + if ((submitted || add_clicked) && !m_trace_input.empty()) + { + debugger.add_trace(m_trace_input); + m_trace_input.clear(); + } - if (ImGui::BeginTable("##traces", 6, flags)) + if (traces.empty()) + { + ImGui::TextDisabled("name a function above to count and time its calls"); + } + else if (ImGui::BeginTable("##traces", 6, + ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV | + ImGuiTableFlags_Resizable)) { ImGui::TableSetupColumn("function"); ImGui::TableSetupColumn("calls", ImGuiTableColumnFlags_WidthFixed, 52.0f); @@ -1855,8 +2117,32 @@ namespace Hsdbg } } - if (ImGui::CollapsingHeader("debugger self-timing")) + // the live footprint of the process under debug + if (debugger.has_target() && + ImGui::CollapsingHeader("resident memory", ImGuiTreeNodeFlags_DefaultOpen)) { + const TimeSeries& memory = m_profiler.target_memory_mb(); + + ImGui::Text("%.1f MB", memory.latest()); + ImGui::SameLine(); + ImGui::TextDisabled("peak %.1f MB", memory.maximum()); + + ImGui::PlotLines("##target_memory", memory.values(), memory.count(), memory.offset(), + nullptr, 0.0f, FLT_MAX, ImVec2(-1.0f, 60.0f)); + } + + // hsdbg's own frame cost, tucked away: useful when the ui itself feels + // slow, not part of profiling the target + if (ImGui::CollapsingHeader("hsdbg self-timing")) + { + bool paused = m_profiler.paused(); + if (ImGui::Checkbox("pause graphs", &paused)) + m_profiler.set_paused(paused); + + ImGui::SameLine(); + if (ImGui::SmallButton("reset")) + m_profiler.reset(); + const TimeSeries& frame_ms = m_profiler.frame_times(); const TimeSeries& fps = m_profiler.frame_rates(); @@ -1880,119 +2166,102 @@ namespace Hsdbg ImGui::End(); } - auto Ui::draw_timeline_panel(Debugger& debugger) -> void + auto Ui::draw_flamegraph(const Debugger& debugger) -> void { - if (!m_visible.timeline) - return; + const std::span<const TimelineSpan> spans = debugger.timeline(); - if (ImGui::Begin(PANEL_TIMELINE, &m_visible.timeline)) + if (spans.empty()) { - const std::span<const TimelineSpan> spans = debugger.timeline(); - if (debugger.instrumentation_active()) - { - ImGui::TextDisabled("instrumented: every function traced automatically"); - } + ImGui::TextDisabled("instrumented target — run it to lay out its calls here"); + else if (debugger.sampling_enabled()) + ImGui::TextDisabled("sampling — run the target and the calls will appear here"); else - { - bool sampling = debugger.sampling_enabled(); - if (ImGui::Checkbox("sample while running", &sampling)) - debugger.set_sampling_enabled(sampling); + ImGui::TextDisabled("turn on sampling, or trace a function below, then run"); - ImGui::SameLine(); - ImGui::TextDisabled("(any binary, approximate)"); - } - - if (spans.empty()) - { - ImGui::TextDisabled("run an instrumented target, or trace functions, then stop to see calls"); - } - else - { - // resolve a trace id to its function name for labels and tooltips - const auto name_of = [&](uint32_t trace_id) -> const char* - { - return debugger.span_label(trace_id); - }; + return; + } - // the time range to fit and how tall the call stack gets - double t_min = spans.front().start; - double t_max = t_min; - uint32_t max_depth = 0; + // resolve a trace id to its function name for labels and tooltips + const auto name_of = [&](uint32_t trace_id) -> const char* + { + return debugger.span_label(trace_id); + }; - for (const TimelineSpan& span : spans) - { - t_min = std::min(t_min, span.start); - t_max = std::max(t_max, span.start + span.duration); - max_depth = std::max(max_depth, span.depth); - } + // the time range to fit and how tall the call stack gets + double t_min = spans.front().start; + double t_max = t_min; + uint32_t max_depth = 0; - const double range = std::max(t_max - t_min, 1.0e-6); + for (const TimelineSpan& span : spans) + { + t_min = std::min(t_min, span.start); + t_max = std::max(t_max, span.start + span.duration); + max_depth = std::max(max_depth, span.depth); + } - ImGui::Text("%.3f ms total %zu calls", range * 1000.0, spans.size()); + const double range = std::max(t_max - t_min, 1.0e-6); - constexpr float row_height = 20.0f; - const float rows = static_cast<float>(max_depth + 1); + ImGui::Text("%.3f ms total %zu calls", range * 1000.0, spans.size()); - const ImVec2 origin = ImGui::GetCursorScreenPos(); - const ImVec2 avail = ImGui::GetContentRegionAvail(); + constexpr float row_height = 20.0f; + const float rows = static_cast<float>(max_depth + 1); - const float canvas_w = std::max(avail.x, 1.0f); - const float canvas_h = std::max(avail.y, rows * row_height + 4.0f); + const ImVec2 origin = ImGui::GetCursorScreenPos(); + const ImVec2 avail = ImGui::GetContentRegionAvail(); - // claim the region so hovering resolves against it - ImGui::InvisibleButton("##timeline_canvas", ImVec2(canvas_w, canvas_h)); - const bool canvas_hovered = ImGui::IsItemHovered(); + const float canvas_w = std::max(avail.x, 1.0f); + const float canvas_h = std::max(avail.y, rows * row_height + 4.0f); - ImDrawList* draw_list = ImGui::GetWindowDrawList(); - draw_list->PushClipRect(origin, ImVec2(origin.x + canvas_w, origin.y + canvas_h), true); + // claim the region so hovering resolves against it + ImGui::InvisibleButton("##flame_canvas", ImVec2(canvas_w, canvas_h)); + const bool canvas_hovered = ImGui::IsItemHovered(); - const float scale = canvas_w / static_cast<float>(range); // pixels per second - const float baseline = origin.y + canvas_h; // row 0 rests on the bottom + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + draw_list->PushClipRect(origin, ImVec2(origin.x + canvas_w, origin.y + canvas_h), true); - const ImVec2 mouse = ImGui::GetMousePos(); + const float scale = canvas_w / static_cast<float>(range); // pixels per second + const float baseline = origin.y + canvas_h; // row 0 rests on the bottom - for (const TimelineSpan& span : spans) - { - const float x0 = origin.x + static_cast<float>((span.start - t_min) * scale); - const float width = std::max(1.0f, static_cast<float>(span.duration * scale)); - const float y1 = baseline - static_cast<float>(span.depth) * row_height; - const float y0 = y1 - (row_height - 2.0f); + const ImVec2 mouse = ImGui::GetMousePos(); - const ImU32 fill = ImColor::HSV(span.trace_id * 0.13f, 0.55f, 0.78f); + for (const TimelineSpan& span : spans) + { + const float x0 = origin.x + static_cast<float>((span.start - t_min) * scale); + const float width = std::max(1.0f, static_cast<float>(span.duration * scale)); + const float y1 = baseline - static_cast<float>(span.depth) * row_height; + const float y0 = y1 - (row_height - 2.0f); - draw_list->AddRectFilled(ImVec2(x0, y0), ImVec2(x0 + width, y1), fill, 2.0f); - draw_list->AddRect(ImVec2(x0, y0), ImVec2(x0 + width, y1), IM_COL32(0, 0, 0, 90), 2.0f); + const ImU32 fill = ImColor::HSV(span.trace_id * 0.13f, 0.55f, 0.78f); - if (width > 24.0f) - { - draw_list->PushClipRect(ImVec2(x0 + 2.0f, y0), ImVec2(x0 + width - 2.0f, y1), true); - draw_list->AddText(ImVec2(x0 + 4.0f, y0 + 2.0f), IM_COL32(20, 20, 20, 255), - name_of(span.trace_id)); - draw_list->PopClipRect(); - } + draw_list->AddRectFilled(ImVec2(x0, y0), ImVec2(x0 + width, y1), fill, 2.0f); + draw_list->AddRect(ImVec2(x0, y0), ImVec2(x0 + width, y1), IM_COL32(0, 0, 0, 90), 2.0f); - const bool over = canvas_hovered && mouse.x >= x0 && mouse.x <= x0 + width && - mouse.y >= y0 && mouse.y <= y1; + if (width > 24.0f) + { + draw_list->PushClipRect(ImVec2(x0 + 2.0f, y0), ImVec2(x0 + width - 2.0f, y1), true); + draw_list->AddText(ImVec2(x0 + 4.0f, y0 + 2.0f), IM_COL32(20, 20, 20, 255), + name_of(span.trace_id)); + draw_list->PopClipRect(); + } - if (over) - { - ImGui::BeginTooltip(); - ImGui::TextUnformatted(name_of(span.trace_id)); - ImGui::Text("start %.3f ms", (span.start - t_min) * 1000.0); - if (span.duration > 0.0) - ImGui::Text("duration %.3f ms", span.duration * 1000.0); - else - ImGui::TextDisabled("running..."); - ImGui::EndTooltip(); - } - } + const bool over = canvas_hovered && mouse.x >= x0 && mouse.x <= x0 + width && + mouse.y >= y0 && mouse.y <= y1; - draw_list->PopClipRect(); + if (over) + { + ImGui::BeginTooltip(); + ImGui::TextUnformatted(name_of(span.trace_id)); + ImGui::Text("start %.3f ms", (span.start - t_min) * 1000.0); + if (span.duration > 0.0) + ImGui::Text("duration %.3f ms", span.duration * 1000.0); + else + ImGui::TextDisabled("running..."); + ImGui::EndTooltip(); } } - ImGui::End(); + draw_list->PopClipRect(); } auto Ui::draw_macros_panel(Debugger& /*debugger*/) -> void @@ -2110,6 +2379,231 @@ namespace Hsdbg ImGui::End(); } + auto Ui::draw_command_palette(Debugger& debugger) -> void + { + // cmd/ctrl+k anywhere, or the menu item, opens it; opening resets the query + // so it always starts clean and asks for the text field's focus next frame + const bool open_requested = + ImGui::IsKeyChordPressed(ImGuiMod_Ctrl | ImGuiKey_K) || m_palette_request; + + m_palette_request = false; + + if (open_requested) + { + m_palette_open = true; + m_palette_focus = true; + m_palette_selection = 0; + m_palette_query.clear(); + ImGui::OpenPopup("##command_palette"); + } + + if (!m_palette_open) + return; + + const ImGuiViewport* viewport = ImGui::GetMainViewport(); + const ImVec2 size(std::min(viewport->WorkSize.x - 80.0f, 640.0f), 0.0f); + + ImGui::SetNextWindowPos(ImVec2(viewport->WorkPos.x + viewport->WorkSize.x * 0.5f, + viewport->WorkPos.y + 90.0f), + ImGuiCond_Always, ImVec2(0.5f, 0.0f)); + ImGui::SetNextWindowSize(size); + + constexpr ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings; + + if (!ImGui::BeginPopup("##command_palette", flags)) + { + // esc or a click outside closed it + m_palette_open = false; + return; + } + + // every command the palette can run; only the ones that make sense for the + // current state are offered, so the list never dangles a dead action + enum class Kind : uint8_t { Command, File, Symbol }; + enum Command : uint8_t + { + Run, Continue, Pause, Stop, StepOver, StepInto, StepOut, ToggleProfiler + }; + + struct Entry + { + std::string label; + const char* hint = ""; + Kind kind = Kind::Command; + Command command = Run; + std::filesystem::path file; + uint64_t symbol = 0; + }; + + const bool has_target = debugger.has_target(); + const bool running = debugger.is_running(); + const bool stopped = debugger.is_stopped(); + + std::vector<Entry> entries; + + const auto add_command = [&](bool enabled, Command command, const char* label) + { + if (enabled && matches_filter(label, m_palette_query)) + entries.push_back({ label, "command", Kind::Command, command, {}, 0 }); + }; + + add_command(has_target && !running, Run, "run"); + add_command(stopped, Continue, "continue"); + add_command(running, Pause, "pause"); + add_command(has_target, Stop, "stop"); + add_command(stopped, StepOver, "step over"); + add_command(stopped, StepInto, "step into"); + add_command(stopped, StepOut, "step out"); + add_command(true, ToggleProfiler, m_visible.profiler ? "hide profiler" : "show profiler"); + + // files and symbols only clutter the list once there is a query to match + if (!m_palette_query.empty()) + { + for (const std::filesystem::path& file : debugger.source_files()) + { + if (entries.size() >= 60) + break; + + if (matches_filter(file.filename().string(), m_palette_query)) + entries.push_back({ file.filename().string(), "file", Kind::File, Run, file, 0 }); + } + + for (const Symbol& symbol : debugger.symbols()) + { + if (entries.size() >= 120) + break; + + if (matches_filter(symbol.name, m_palette_query)) + entries.push_back({ symbol.name, "symbol", Kind::Symbol, Run, {}, symbol.file_address }); + } + } + + if (entries.empty()) + m_palette_selection = 0; + else + m_palette_selection = std::clamp(m_palette_selection, 0, static_cast<int>(entries.size()) - 1); + + // arrow keys walk the results; the input field ignores up/down itself, so + // reading them here does not fight the cursor + if (ImGui::IsKeyPressed(ImGuiKey_DownArrow) && !entries.empty()) + m_palette_selection = (m_palette_selection + 1) % static_cast<int>(entries.size()); + + if (ImGui::IsKeyPressed(ImGuiKey_UpArrow) && !entries.empty()) + { + m_palette_selection = (m_palette_selection - 1 + static_cast<int>(entries.size())) % + static_cast<int>(entries.size()); + } + + const auto activate = [&](const Entry& entry) + { + switch (entry.kind) + { + case Kind::File: + open_source(entry.file); + ImGui::SetWindowFocus(PANEL_SOURCE); + break; + + case Kind::Symbol: + if (debugger.select_symbol(entry.symbol)) + { + m_focus_disassembly = true; + m_scroll_to_program_counter = true; + } + break; + + case Kind::Command: + switch (entry.command) + { + case Run: + { + LaunchSpec spec; + spec.executable = debugger.target_path(); + spec.stop_at_entry = m_preferences.stop_at_entry; + report(debugger.launch(spec), "run"); + break; + } + case Continue: report(debugger.resume(), "continue"); break; + case Pause: report(debugger.pause(), "pause"); break; + case Stop: report(debugger.terminate(), "stop"); break; + case StepOver: report(debugger.step_over(), "step over"); break; + case StepInto: report(debugger.step_into(), "step into"); break; + case StepOut: report(debugger.step_out(), "step out"); break; + case ToggleProfiler: + m_visible.profiler = !m_visible.profiler; + if (m_visible.profiler) + m_focus_profiler = true; + break; + } + break; + } + + m_palette_open = false; + ImGui::CloseCurrentPopup(); + }; + + if (m_palette_focus) + { + ImGui::SetKeyboardFocusHere(); + m_palette_focus = false; + } + + ImGui::SetNextItemWidth(-FLT_MIN); + const bool submitted = ImGui::InputTextWithHint("##palette_query", + "jump to a file or symbol, or run a command", + &m_palette_query, + ImGuiInputTextFlags_EnterReturnsTrue); + + if (submitted && !entries.empty()) + { + activate(entries[static_cast<size_t>(m_palette_selection)]); + ImGui::EndPopup(); + return; + } + + ImGui::Separator(); + + if (entries.empty()) + { + ImGui::TextDisabled("no matches"); + } + else + { + const float row = ImGui::GetTextLineHeightWithSpacing(); + ImGui::BeginChild("##palette_results", ImVec2(0.0f, std::min(row * 10.0f + 4.0f, + row * static_cast<float>(entries.size()) + 4.0f))); + + for (int index = 0; index < static_cast<int>(entries.size()); ++index) + { + const Entry& entry = entries[static_cast<size_t>(index)]; + const bool selected = index == m_palette_selection; + + ImGui::PushID(index); + + if (ImGui::Selectable("##row", selected, ImGuiSelectableFlags_SpanAllColumns)) + activate(entry); + + // keep the keyboard-picked row in view as it moves + if (selected && (ImGui::IsKeyPressed(ImGuiKey_UpArrow) || + ImGui::IsKeyPressed(ImGuiKey_DownArrow) || ImGui::IsWindowAppearing())) + ImGui::SetScrollHereY(0.5f); + + ImGui::SameLine(0.0f, 0.0f); + ImGui::TextUnformatted(entry.label.c_str()); + + const float hint_width = ImGui::CalcTextSize(entry.hint).x; + ImGui::SameLine(ImGui::GetContentRegionAvail().x - hint_width); + ImGui::TextDisabled("%s", entry.hint); + + ImGui::PopID(); + } + + ImGui::EndChild(); + } + + ImGui::EndPopup(); + } + auto Ui::push_console(std::string line) -> void { m_console_lines.push_back(std::move(line)); diff --git a/src/ui/ui.h b/src/ui/ui.h index 3365e51..2bc758d 100644 --- a/src/ui/ui.h +++ b/src/ui/ui.h @@ -46,19 +46,28 @@ namespace Hsdbg bool threads = true; bool source_tree = true; bool locals = true; + bool watch = true; bool registers = true; bool symbols = true; bool disassembly = true; bool console = true; - bool profiler = true; - bool timeline = true; - bool macros = true; + + // these two are not standing panels: they earn their place only once + // there is something to show. the profiler opens when you go to profile + // (its flame chart, timings and graphs all live inside it), the macros + // view when you unroll a macro from source + bool profiler = false; + bool macros = false; bool demo = false; }; auto apply_style() -> void; static auto build_default_layout(uint32_t dockspace_id) -> void; + // a fuzzy omnibar (cmd/ctrl+k) over files, symbols and execution commands, + // so navigating and driving the target never means hunting for a panel + auto draw_command_palette(Debugger& debugger) -> void; + auto draw_menu_bar(Debugger& debugger) -> void; auto draw_toolbar(Debugger& debugger) -> void; auto draw_status_bar(const Debugger& debugger) -> void; @@ -72,12 +81,20 @@ namespace Hsdbg auto draw_threads_panel(Debugger& debugger) -> void; auto draw_source_tree_panel(const Debugger& debugger) -> void; auto draw_locals_panel(const Debugger& debugger) -> void; + auto draw_watch_panel(Debugger& debugger) -> void; auto draw_registers_panel(const Debugger& debugger) -> void; + + // adds an expression to the watch list and evaluates it right away if the + // target is stopped, so it never shows a blank until the next stop + auto add_watch(Debugger& debugger, std::string expression) -> void; auto draw_symbols_panel(Debugger& debugger) -> void; auto draw_disassembly_panel(Debugger& debugger) -> void; auto draw_console_panel(Debugger& debugger) -> void; auto draw_profiler_panel(Debugger& debugger) -> void; - auto draw_timeline_panel(Debugger& debugger) -> void; + + // the call flame chart, the hero of the profiler panel: draws into whatever + // region the caller has opened, no window of its own + static auto draw_flamegraph(const Debugger& debugger) -> void; auto draw_macros_panel(Debugger& debugger) -> void; auto push_console(std::string line) -> void; @@ -85,6 +102,11 @@ namespace Hsdbg auto follow_stop(Debugger& debugger) -> void; auto follow_target(Debugger& debugger) -> void; + + // selects the innermost frame that has source and shows it, so a new + // thread or stop lands the whole ui on real code rather than a runtime + // internal frame with nothing to display + auto follow_selected_frame(Debugger& debugger) -> void; auto show_frame(const StackFrame& frame) -> void; Window* m_window = nullptr; @@ -119,6 +141,33 @@ namespace Hsdbg std::string m_source_filter; std::string m_trace_input; + // one user-entered watch expression and its last evaluation + struct Watch + { + std::string expression; + std::string value; + bool ok = false; + }; + + std::vector<Watch> m_watches; + std::string m_watch_input; + + // the (stop, thread, frame) the watches were last evaluated against, so + // they re-run when execution stops again or the selection moves, but not + // every frame (each evaluation jits code into the target) + uint64_t m_watch_stop = 0; + uint64_t m_watch_thread = 0; + uint32_t m_watch_frame = 0; + bool m_watch_evaluated = false; + + // command palette: whether it is up, a one-shot request to grab the text + // field's focus, the query, and which result the keyboard has landed on + bool m_palette_open = false; + bool m_palette_request = false; + bool m_palette_focus = false; + std::string m_palette_query; + int m_palette_selection = 0; + // macros panel: the invocation being unrolled, the current unroll depth, // and the read-only buffer that shows the tokens at that depth std::string m_macro_input; @@ -128,12 +177,21 @@ namespace Hsdbg uint64_t m_followed_stop = 0; std::filesystem::path m_followed_target; + // the source file whose folder chain the tree last expanded; when the + // open file moves away from it the tree reveals the new path once + std::filesystem::path m_revealed_source; + + // rising-edge latch so the profiler surfaces itself the first time there is + // profiling activity, without fighting the user if they then close it + bool m_profiler_revealed = false; + 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; bool m_focus_macros = false; + bool m_focus_profiler = false; bool m_focus_symbols = false; bool m_focus_disassembly = false; bool m_scroll_to_program_counter = false; |
