aboutsummaryrefslogtreecommitdiff
path: root/src/ui/source_view.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/ui/source_view.cpp')
-rw-r--r--src/ui/source_view.cpp288
1 files changed, 255 insertions, 33 deletions
diff --git a/src/ui/source_view.cpp b/src/ui/source_view.cpp
index 205fbca..af389f8 100644
--- a/src/ui/source_view.cpp
+++ b/src/ui/source_view.cpp
@@ -2,6 +2,8 @@
#include "core/log.h"
#include "debugger/debugger.h"
+#include "ui/icons.h"
+#include "ui/widgets.h"
#include <imgui.h>
#include <imgui_internal.h>
@@ -9,10 +11,14 @@
#include <algorithm>
#include <cctype>
+#include <cfloat>
+#include <format>
#include <fstream>
+#include <iterator>
#include <optional>
#include <span>
#include <string_view>
+#include <unordered_map>
#include <unordered_set>
namespace Hsdbg
@@ -21,6 +27,9 @@ namespace Hsdbg
{
constexpr float BREAKPOINT_RADIUS = 5.0f;
+ constexpr float MIN_FONT_SIZE = 9.0f;
+ constexpr float MAX_FONT_SIZE = 32.0f;
+ constexpr float DEFAULT_FONT_SIZE = 16.0f;
enum class Language : uint8_t
{
@@ -325,6 +334,24 @@ namespace Hsdbg
// green of comments so they do not read as part of the code
const ImU32 INLINE_VALUE_COLOR = IM_COL32(122, 140, 170, 235);
+ // the accent bar down the left edge of the line execution is on
+ constexpr float EXECUTION_BAR_WIDTH = 3.0f;
+
+ // the directory of a path cut to its last two components, so a deep path
+ // leaves room for the file name in the location bar
+ auto short_directory(const std::filesystem::path& path) -> std::string
+ {
+ const std::filesystem::path parent = path.parent_path();
+ std::vector<std::string> parts;
+ std::ranges::transform(parent, std::back_inserter(parts),
+ [](const std::filesystem::path& part) { return part.string(); });
+
+ if (parts.size() <= 3)
+ return parent.generic_string() + "/";
+
+ return "…/" + parts[parts.size() - 2] + "/" + parts.back() + "/";
+ }
+
auto line_number_width(size_t line_count) -> float
{
return ImGui::CalcTextSize(std::to_string(line_count).c_str()).x;
@@ -361,6 +388,22 @@ namespace Hsdbg
return false;
}
+ // whether the line holds `name` as an identifier: by its spans when it
+ // has them (so a mention inside a comment or string does not count),
+ // else by the raw text
+ auto line_mentions(const std::string& line, const std::vector<SourceSpan>& spans,
+ const std::string& name) -> bool
+ {
+ if (spans.empty())
+ return contains_word(line, name);
+
+ return std::ranges::any_of(spans, [&](const SourceSpan& span)
+ {
+ return span.kind == SyntaxKind::Default && span.length == name.size() &&
+ line.compare(span.start, span.length, name) == 0;
+ });
+ }
+
// a single-line, length-capped rendering of a value for the inline annotation
auto inline_value(const Variable& local) -> std::string
{
@@ -491,51 +534,153 @@ namespace Hsdbg
m_scroll_to_highlight = line != 0;
}
+ auto SourceView::set_frame_extent(uint32_t first, uint32_t last) -> void
+ {
+ m_frame_first_line = first;
+ m_frame_last_line = last;
+ }
+
auto SourceView::draw(Debugger& debugger) -> void
{
draw_open_bar();
- ImGui::Separator();
-
if (m_lines.empty())
{
- ImGui::TextDisabled("No source file open");
+ Widgets::empty_state(ICON_PH_FILE_CODE, "No source file open");
return;
}
- // the code and its line-number gutter render in the monospace face; the
- // open bar above stays in the ui font
- if (m_mono_font != nullptr)
- ImGui::PushFont(m_mono_font, 0.0f);
-
+ // the code and its line-number gutter render in the monospace face at the
+ // zoomable size; the location bar above stays in the ui font
+ ImGui::PushFont(m_mono_font, m_font_size);
draw_lines(debugger);
+ ImGui::PopFont();
+ }
- if (m_mono_font != nullptr)
- ImGui::PopFont();
+ auto SourceView::handle_zoom() -> void
+ {
+ // cmd/ctrl + wheel over the code resizes it, and cmd/ctrl + 0 resets it.
+ // owning the wheel while the modifier is down keeps the view from
+ // scrolling too (imgui only holds off on its own for the real ctrl key)
+ const ImGuiIO& io = ImGui::GetIO();
+ if (!ImGui::IsWindowHovered() || !(io.KeyCtrl || io.KeySuper))
+ {
+ m_zoom_accumulator = 0.0f;
+ return;
+ }
+
+ ImGui::SetKeyOwner(ImGuiKey_MouseWheelY, ImGui::GetID("##zoom"));
+
+ // a mouse notch is a whole step; a trackpad gets there in small increments
+ float size = m_font_size;
+ m_zoom_accumulator += io.MouseWheel;
+ while (m_zoom_accumulator >= 1.0f)
+ {
+ size += 1.0f;
+ m_zoom_accumulator -= 1.0f;
+ }
+ while (m_zoom_accumulator <= -1.0f)
+ {
+ size -= 1.0f;
+ m_zoom_accumulator += 1.0f;
+ }
+
+ if (ImGui::IsKeyPressed(ImGuiKey_0))
+ size = DEFAULT_FONT_SIZE;
+
+ size = std::clamp(size, MIN_FONT_SIZE, MAX_FONT_SIZE);
+ if (size != m_font_size)
+ {
+ m_font_size = size;
+ m_zoom_badge_until = ImGui::GetTime() + 1.2;
+ }
}
auto SourceView::draw_open_bar() -> void
{
- ImGui::SetNextItemWidth(-ImGui::CalcTextSize("Open").x - ImGui::GetStyle().FramePadding.x * 4.0f);
-
- const bool submitted = ImGui::InputTextWithHint("##source_path",
- "Path to a source file",
- &m_path_input,
- ImGuiInputTextFlags_EnterReturnsTrue);
+ // with a file open the bar is a breadcrumb (dim directory, bright name)
+ // that a click turns into the path field; with none, the field is there
+ // from the start
+ if (m_editing_path || m_path.empty())
+ {
+ if (m_focus_path)
+ {
+ ImGui::SetKeyboardFocusHere();
+ m_focus_path = false;
+ }
- ImGui::SameLine();
+ ImGui::SetNextItemWidth(-FLT_MIN);
+ const bool submitted = Widgets::input_with_icon("##source_path", ICON_PH_FILE_CODE,
+ "Path to a source file, Enter to open",
+ &m_path_input,
+ ImGuiInputTextFlags_EnterReturnsTrue);
- if (ImGui::Button("Open") || submitted)
+ if (submitted)
+ {
+ if (const auto result = open(m_path_input); !result)
+ {
+ m_error = result.error();
+ Log::error("{}", m_error);
+ }
+ else
+ {
+ m_editing_path = false;
+ }
+ }
+ else if (m_editing_path && (ImGui::IsItemDeactivated() || ImGui::IsKeyPressed(ImGuiKey_Escape)))
+ {
+ m_editing_path = false;
+ m_path_input = m_path.string();
+ }
+ }
+ else
{
- if (const auto result = open(m_path_input); !result)
+ const ImGuiStyle& style = ImGui::GetStyle();
+ const ImVec2 pos = ImGui::GetCursorScreenPos();
+ const float height = ImGui::GetFrameHeight();
+ const float width = ImGui::GetContentRegionAvail().x;
+
+ ImGui::InvisibleButton("##path_row", ImVec2(width, height));
+ const bool hovered = ImGui::IsItemHovered();
+
+ ImDrawList* draw_list = ImGui::GetWindowDrawList();
+ if (hovered)
+ {
+ draw_list->AddRectFilled(pos, ImVec2(pos.x + width, pos.y + height),
+ ImGui::GetColorU32(ImGuiCol_FrameBg), style.FrameRounding);
+ ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
+ ImGui::SetTooltip("%s\nClick to type a path", m_path.string().c_str());
+ }
+
+ const ImU32 dim = ImGui::GetColorU32(ImGuiCol_TextDisabled);
+ const ImU32 bright = ImGui::GetColorU32(ImGuiCol_Text);
+ const std::string directory = short_directory(m_path);
+ const std::string name = m_path.filename().string();
+
+ float x = pos.x + style.FramePadding.x;
+ const float y = pos.y + style.FramePadding.y;
+ draw_list->AddText(ImVec2(x, y), dim, ICON_PH_FILE_CODE);
+ x += ImGui::CalcTextSize(ICON_PH_FILE_CODE).x + 6.0f;
+ draw_list->AddText(ImVec2(x, y), dim, directory.c_str());
+ x += ImGui::CalcTextSize(directory.c_str()).x;
+ draw_list->AddText(ImVec2(x, y), bright, name.c_str());
+
+ if (hovered)
+ {
+ const float pencil = ImGui::CalcTextSize(ICON_PH_PENCIL).x;
+ draw_list->AddText(ImVec2(pos.x + width - pencil - style.FramePadding.x, y), dim, ICON_PH_PENCIL);
+ }
+
+ if (ImGui::IsItemClicked())
{
- m_error = result.error();
- Log::error("{}", m_error);
+ m_editing_path = true;
+ m_focus_path = true;
+ m_path_input = m_path.string();
}
}
if (!m_error.empty())
- ImGui::TextColored(ImVec4(0.89f, 0.33f, 0.33f, 1.0f), "%s", m_error.c_str());
+ ImGui::TextColored(ImVec4(0.89f, 0.33f, 0.33f, 1.0f), ICON_PH_WARNING " %s", m_error.c_str());
}
auto SourceView::draw_lines(Debugger& debugger) -> void
@@ -551,6 +696,8 @@ namespace Hsdbg
return;
}
+ handle_zoom();
+
if (m_scroll_to_highlight && m_highlighted_line != 0)
{
ImGui::SetScrollY(static_cast<float>(m_highlighted_line - 1) * text_height -
@@ -564,6 +711,40 @@ namespace Hsdbg
? debugger.locals()
: std::span<const Variable>{};
+ // each local is annotated once, on the line that declares it: the
+ // declaration the debug info names (a parameter's is the signature), else
+ // its first mention inside the current function. never past the pc, as
+ // anything there has not run and would read stale
+ std::unordered_map<std::string, uint32_t> pinned;
+ if (!locals.empty())
+ {
+ for (const Variable& local : locals)
+ {
+ if (!local.value.empty() && local.declaration_line != 0 &&
+ local.declaration_line <= m_highlighted_line && local.declaration_line <= m_lines.size())
+ pinned.emplace(local.name, local.declaration_line);
+ }
+
+ const uint32_t first = std::max(m_frame_first_line, 1u);
+ const uint32_t last = std::min({ m_highlighted_line,
+ m_frame_last_line != 0 ? m_frame_last_line : m_highlighted_line,
+ static_cast<uint32_t>(m_lines.size()) });
+
+ for (uint32_t line = first; line <= last; ++line)
+ {
+ for (const Variable& local : locals)
+ {
+ if (local.value.empty() || pinned.contains(local.name))
+ continue;
+
+ const size_t at = line - 1;
+ const std::vector<SourceSpan>& spans = at < m_spans.size() ? m_spans[at] : std::vector<SourceSpan>{};
+ if (line_mentions(m_lines[at], spans, local.name))
+ pinned.emplace(local.name, line);
+ }
+ }
+ }
+
ImGuiListClipper clipper;
clipper.Begin(static_cast<int>(m_lines.size()), text_height);
@@ -574,12 +755,18 @@ namespace Hsdbg
const uint32_t line_number = static_cast<uint32_t>(index) + 1;
const ImVec2 row_start = ImGui::GetCursorScreenPos();
+ // the line execution sits on: a full-bleed tint plus an accent bar
+ // down the panel's left edge
if (line_number == m_highlighted_line && m_highlight_current_line)
{
- draw_list->AddRectFilled(row_start,
- ImVec2(row_start.x + ImGui::GetContentRegionAvail().x,
- row_start.y + text_height),
+ const float left = ImGui::GetWindowPos().x + 1.0f;
+ const float right = ImGui::GetWindowPos().x + ImGui::GetWindowWidth() - 1.0f;
+ draw_list->AddRectFilled(ImVec2(left, row_start.y),
+ ImVec2(right, row_start.y + text_height),
m_current_line_color);
+ draw_list->AddRectFilled(ImVec2(left, row_start.y),
+ ImVec2(left + EXECUTION_BAR_WIDTH, row_start.y + text_height),
+ ImGui::ColorConvertFloat4ToU32(Widgets::accent()));
}
ImGui::PushID(index);
@@ -636,7 +823,11 @@ namespace Hsdbg
ImGui::SameLine(0.0f, 0.0f);
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + number_width -
ImGui::CalcTextSize(number_text.c_str()).x);
- ImGui::TextDisabled("%s", number_text.c_str());
+
+ if (line_number == m_highlighted_line)
+ ImGui::TextUnformatted(number_text.c_str());
+ else
+ ImGui::TextDisabled("%s", number_text.c_str());
}
ImGui::SameLine(0.0f, GUTTER_MARGIN * 2.0f);
@@ -697,9 +888,7 @@ namespace Hsdbg
}
}
- // live values for locals on this line, shown only up to the pc —
- // anything past it has not run yet and would read stale
- if (!locals.empty() && line_number <= m_highlighted_line)
+ if (!pinned.empty())
{
int shown = 0;
@@ -708,18 +897,37 @@ namespace Hsdbg
if (shown >= 4)
break;
- if (local.value.empty() || !contains_word(text, local.name))
+ const auto pin = pinned.find(local.name);
+ if (pin == pinned.end() || pin->second != line_number)
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());
+ const bool changed = m_changed_locals != nullptr && m_changed_locals->contains(local.name);
+
+ // set in a dim pill so it reads as the debugger's note on
+ // the line rather than as more code
+ const std::string label = inline_value(local);
+ ImGui::SameLine(0.0f, shown == 0 ? GUTTER_MARGIN * 4.0f : GUTTER_MARGIN);
+
+ constexpr float pad = 5.0f;
+ const ImVec2 pos = ImGui::GetCursorScreenPos();
+ const ImVec2 size = ImGui::CalcTextSize(label.c_str());
+ draw_list->AddRectFilled(ImVec2(pos.x, pos.y - 1.0f),
+ ImVec2(pos.x + size.x + pad * 2.0f, pos.y + size.y + 1.0f),
+ ImGui::GetColorU32(ImGuiCol_FrameBg, 0.85f), 4.0f);
+
+ ImGui::SetCursorScreenPos(ImVec2(pos.x + pad, pos.y));
+ ImGui::PushStyleColor(ImGuiCol_Text, changed ? ImGui::ColorConvertFloat4ToU32(Widgets::accent())
+ : INLINE_VALUE_COLOR);
+ ImGui::TextUnformatted(label.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());
+ ImGui::SameLine(0.0f, 0.0f);
+ ImGui::Dummy(ImVec2(pad, 0.0f));
+
++shown;
}
}
@@ -728,6 +936,20 @@ namespace Hsdbg
clipper.End();
+ // a passing badge with the new size, top right, after a zoom
+ if (ImGui::GetTime() < m_zoom_badge_until)
+ {
+ const std::string label = std::format("{:.0f} px", m_font_size);
+ const ImVec2 ts = ImGui::CalcTextSize(label.c_str());
+ const ImVec2 win = ImGui::GetWindowPos();
+ const ImVec2 end(win.x + ImGui::GetWindowWidth() - 16.0f, win.y + 10.0f + ts.y + 10.0f);
+ const ImVec2 start(end.x - ts.x - 20.0f, win.y + 10.0f);
+ ImDrawList* overlay = ImGui::GetWindowDrawList();
+ overlay->AddRectFilled(start, end, ImGui::GetColorU32(ImGuiCol_FrameBg), 6.0f);
+ overlay->AddRect(start, end, ImGui::GetColorU32(ImGuiCol_Border), 6.0f);
+ overlay->AddText(ImVec2(start.x + 10.0f, start.y + 5.0f), ImGui::GetColorU32(ImGuiCol_Text), label.c_str());
+ }
+
ImGui::EndChild();
}