OXYGENHUB
◆ main enhanced-x16-emulator / src / debug_ui / panels / source_panel.cpp
32.7 KiB · C++ · last changed chore(): initial commit 25 days ago source blame history raw
1
// Commander X16 Emulator — ImGui "Source" panel.
2
//
3
// Source-level view for the in-process Dear ImGui debugger (-imgui). Maps the
4
// CPU PC -> source file+line via the cc65 .dbg info and shows the source with
5
// the current line highlighted, following the PC as you step.
6
//
7
// Multiple source files can be open at once as TABS along the top: the file the
8
// PC is in is added + selected automatically (in Follow mode), and you can open
9
// any other file the debug info knows about via "Open..." to set breakpoints /
10
// run-to before the PC ever gets there. "Open..." can also load another .dbg
11
// module found on disk (the .dbg directory / fsroot) so code that will be LOADed
12
// later can be prepared ahead of time.
13
//
14
// Only this file is owned by the SOURCE panel. C emulator headers are pulled in
15
// via extern "C" (they lack C++ guards); `regs` + execution control come from
16
// debug_ui_bridge.h.
17
#include "imgui.h"
18
#include "debug_ui_panels.h"
19
#include "debug_ui_bridge.h"  // regs, DEBUGRunTo/DEBUGIsPaused, breakpoints, dbg_info_label_to_addr
20
#include "debug_ui_widgets.h" // dbgui_value_lines
21
#include "debug_ui_insn_tooltip.h" // dbgui_instruction_tooltip_body
22
23
#include <stdio.h>
24
#include <stdlib.h>
25
#include <string.h>
26
#include <stdint.h>
27
#include <ctype.h>
28
#include <string>
29
#include <vector>
30
31
// --- C emulator core surface (headers have no C++ guards) ---------------------
32
extern "C" {
33
#include "dbg_info.h"    // dbg_info_* : PC<->source mapping + file enumeration
34
#include "source_view.h" // source_view_* : locate + cache source files on disk
35
#include "insn_info.h"   // insn_info_* : what an instruction does, for hover help
36
#include "disasm.h"      // disasm() : re-decode at the PC for the live preview
37
extern uint8_t *fsroot_path; // host FS root (main.c) — scanned for .dbg modules
38
}
39
40
// Breakpoint + bank surface (struct breakpoint, DEBUGAddBreakPoint,
41
// DEBUGRemoveBreakPoint, breakPoints, numBreakpoints, memory_get_ram_bank,
42
// memory_get_rom_bank) comes from debug_ui_bridge.h — do NOT redeclare it here.
43
44
// Case-insensitive compare; MSVC spells the POSIX one differently.
45
static int
46
dbgui_stricmp(const char *a, const char *b)
47
{
48
    for (;; a++, b++) {
49
        int ca = tolower((unsigned char)*a);
50
        int cb = tolower((unsigned char)*b);
51
        if (ca != cb || !ca)
52
            return ca - cb;
53
    }
54
}
55
56
// --- Panel state --------------------------------------------------------------
57
static std::vector<std::string> s_tabs;          // open source-file tabs (by .dbg name)
58
static std::string              s_pcFile;         // file the PC currently maps to
59
static int                      s_pcLine     = 0; // 1-based PC line within s_pcFile
60
static std::string              s_lastPcFile;     // for edge-detecting PC movement
61
static int                      s_lastPcLine = 0;
62
static bool                     s_follow       = true; // auto-follow the PC while stepping
63
static bool                     s_addedDbgPath = false;
64
65
// Scroll request applied to whichever tab body renders this frame (only the
66
// selected tab renders). Follow requests it on the PC tab; Goto forces it.
67
//
68
// The request is tagged with the file it was issued for. Only the matching tab
69
// may consume it, so a request raised for a tab that isn't rendering yet (a
70
// cross-panel goto, or Go to PC while another tab is selected) survives until
71
// that tab actually draws instead of being swallowed by whichever tab happened
72
// to render first. It is also dropped if the target tab never appears, rather
73
// than being applied later to the wrong file.
74
static bool s_scrollRequest = false;
75
static bool s_scrollForce   = false;
76
static int  s_scrollTarget  = 0;
77
static std::string s_scrollFile;  // file the request belongs to ("" = any/current)
78
static int  s_scrollAge     = 0;  // frames the request has waited for its tab
79
static std::string s_selectFile;  // force-select this tab next frame (cross-panel goto)
80
static int  s_gotoLine      = 0;
81
static std::string s_activeFile;  // file whose body rendered last frame (the visible tab)
82
83
// Destination of the most recent deliberate navigation, highlighted on arrival
84
// so you can see where you were sent, then faded out so it does not linger as a
85
// second "current line".
86
static std::string s_flashFile;
87
static int         s_flashLine = 0;
88
static double      s_flashTime = 0.0;
89
constexpr double   SRC_FLASH_SECONDS = 3.0;
90
91
// Raise a scroll request for `file` (nullptr/"" = whichever tab is showing).
92
// `force` centres unconditionally rather than only when the line is off-screen.
93
// `flash` briefly highlights the destination; pass false when the line already
94
// carries the current-line highlight, which would make the flash redundant.
95
static void
96
src_request_scroll(const char *file, int line, bool force, bool flash = true)
97
{
98
    if (line < 1)
99
        return;
100
    s_scrollRequest = true;
101
    s_scrollForce   = force;
102
    s_scrollTarget  = line;
103
    s_scrollFile    = file ? file : "";
104
    s_scrollAge     = 0;
105
106
    // Deliberate navigation (goto line, go to PC, clicking a symbol or a call
107
    // frame) drops you in the middle of a file with nothing marking where you
108
    // landed. Flag the destination so it can be highlighted on arrival.
109
    if (force && flash) {
110
        s_flashFile = file ? file : "";
111
        s_flashLine = line;
112
        s_flashTime = ImGui::GetTime();
113
    }
114
}
115
116
// Basename of a path (after the last / or \).
117
static const char *
118
src_basename(const char *f)
119
{
120
    const char *b = f;
121
    for (const char *p = f; *p; ++p)
122
        if (*p == '/' || *p == '\\')
123
            b = p + 1;
124
    return b;
125
}
126
127
// Add a file as a tab if not already open.
128
static void
129
src_ensure_tab(const char *file)
130
{
131
    if (!file || !file[0])
132
        return;
133
    for (const std::string &t : s_tabs)
134
        if (t == file)
135
            return;
136
    s_tabs.push_back(file);
137
}
138
139
// Find a user breakpoint at (addr, bank 0). Returns index or -1.
140
static int
141
src_find_bp(uint16_t addr)
142
{
143
    for (int i = 0; i < numBreakpoints; i++) {
144
        if (breakPoints[i].pc == (int)addr && breakPoints[i].bank == 0)
145
            return i;
146
    }
147
    return -1;
148
}
149
150
// Does a source line map to an address that currently has a breakpoint?
151
static bool
152
src_line_has_bp(const char *file, int line)
153
{
154
    if (numBreakpoints <= 0)
155
        return false;
156
    uint16_t addr;
157
    if (!debug_ui_source_to_addr(file, line, &addr))
158
        return false;
159
    return src_find_bp(addr) >= 0;
160
}
161
162
// Toggle a breakpoint on a 1-based source line.
163
static void
164
src_toggle_bp(const char *file, int line)
165
{
166
    uint16_t addr;
167
    if (!file || !file[0] || !debug_ui_source_to_addr(file, line, &addr))
168
        return; // line has no generated code — nothing to toggle
169
    // Resolve the bank first: it keys the remove as well as the add.
170
    int x16Bank = (addr >= 0xA000)
171
                      ? (addr < 0xC000 ? (int)memory_get_ram_bank()
172
                                       : (int)memory_get_rom_bank())
173
                      : -1;
174
    if (!DEBUGRemoveBreakPoint((int)addr, 0, x16Bank)) {
175
        struct breakpoint bp = {};
176
        bp.pc      = (int)addr;
177
        bp.bank    = 0;
178
        bp.x16Bank = x16Bank;
179
        DEBUGAddBreakPoint(bp);
180
    }
181
}
182
183
// Copy source text expanding tabs to 4-col stops and sanitising control bytes.
184
static void
185
src_expand_tabs(const char *s, char *out, size_t outsz)
186
{
187
    size_t col = 0;
188
    while (*s && col + 1 < outsz) {
189
        if (*s == '\t') {
190
            size_t next = (col + 4) & ~(size_t)3;
191
            while (col < next && col + 1 < outsz)
192
                out[col++] = ' ';
193
            s++;
194
        } else {
195
            unsigned char c = (unsigned char)*s++;
196
            out[col++] = (c < 0x20 || c > 0x7E) ? ' ' : (char)c;
197
        }
198
    }
199
    out[col] = '\0';
200
}
201
202
// The "Open..." quick-pick popup: source files known to the loaded .dbg, plus
203
// any other .dbg modules found on disk (which can be loaded so their sources
204
// become available before the code LOADs them).
205
static void
206
src_render_open_popup(void)
207
{
208
    if (!ImGui::BeginPopup("src_open"))
209
        return;
210
211
    ImGui::TextDisabled("Source files in the loaded debug info:");
212
    ImGui::Separator();
213
    int fc    = dbg_info_file_count();
214
    int shown = 0;
215
    for (int i = 0; i < fc; i++) {
216
        const char *nm = nullptr;
217
        if (!dbg_info_file_at(i, &nm) || !nm)
218
            continue;
219
        bool isOpen = false;
220
        for (const std::string &t : s_tabs)
221
            if (t == nm) { isOpen = true; break; }
222
        char lbl[300];
223
        snprintf(lbl, sizeof lbl, "%s%s##openf%d", nm, isOpen ? "   (open)" : "", i);
224
        if (ImGui::Selectable(lbl))
225
            src_ensure_tab(nm);
226
        shown++;
227
    }
228
    if (shown == 0)
229
        ImGui::TextDisabled("  (none)");
230
231
    // Other .dbg modules on disk (in the .dbg directory and the fsroot).
232
    static char paths[64][DBG_INFO_PATH_MAX];
233
    int         np = 0;
234
    np += dbg_info_scan_dbg_files(dbg_info_get_dbg_dir(), paths + np, 64 - np);
235
    if (fsroot_path)
236
        np += dbg_info_scan_dbg_files((const char *)fsroot_path, paths + np, 64 - np);
237
    if (np > 0) {
238
        ImGui::Separator();
239
        ImGui::TextDisabled("Load another .dbg module (for not-yet-loaded code):");
240
        for (int i = 0; i < np; i++) {
241
            char lbl[DBG_INFO_PATH_MAX + 24];
242
            snprintf(lbl, sizeof lbl, "Load %s##loaddbg%d", src_basename(paths[i]), i);
243
            if (ImGui::Selectable(lbl)) {
244
                if (dbg_info_load(paths[i]) == 0)
245
                    source_view_invalidate();
246
            }
247
            if (ImGui::IsItemHovered())
248
                ImGui::SetTooltip("%s", paths[i]);
249
        }
250
    }
251
252
    ImGui::EndPopup();
253
}
254
255
// Resolve the identifier/number token under the mouse on a source line and, if
256
// it is a known label or a numeric literal, show a tooltip with its address and
257
// live value (or multi-base for a literal). `line` is the tab-expanded display
258
// string; `text_x` is its left edge; `glyph_w` the monospace advance.
259
static void
260
src_hover_token_tooltip(const char *line, float text_x, float glyph_w, bool is_pc_line)
261
{
262
    ImVec2 mp  = ImGui::GetIO().MousePos;
263
    float  rel = mp.x - text_x;
264
    if (rel < 0 || glyph_w <= 0)
265
        return;
266
    int col = (int)(rel / glyph_w);
267
    int len = (int)strlen(line);
268
    if (col < 0 || col >= len)
269
        return;
270
271
    auto is_ident = [](char c) {
272
        return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
273
               c == '_' || c == '$';
274
    };
275
    if (!is_ident(line[col]))
276
        return;
277
278
    int s = col, e = col;
279
    while (s > 0 && is_ident(line[s - 1]))
280
        s--;
281
    while (e < len && is_ident(line[e]))
282
        e++;
283
    char tok[64];
284
    int  tl = 0;
285
    for (int k = s; k < e && tl < 63; k++)
286
        tok[tl++] = line[k];
287
    tok[tl] = '\0';
288
    if (tl == 0)
289
        return;
290
291
    // Numeric literal ($hex or decimal)?
292
    uint32_t num    = 0;
293
    bool     is_num = false;
294
    if (tok[0] == '$') {
295
        char *endp = nullptr;
296
        num        = (uint32_t)strtoul(tok + 1, &endp, 16);
297
        is_num     = (endp && *endp == '\0' && endp != tok + 1);
298
    } else if (tok[0] >= '0' && tok[0] <= '9') {
299
        char *endp = nullptr;
300
        num        = (uint32_t)strtoul(tok, &endp, 10);
301
        is_num     = (endp && *endp == '\0');
302
    }
303
    if (is_num) {
304
        ImGui::BeginTooltip();
305
        ImGui::TextUnformatted(tok);
306
        ImGui::Separator();
307
        dbgui_value_lines(num, num > 0xFF ? 2 : 1);
308
        ImGui::EndTooltip();
309
        return;
310
    }
311
312
    // An instruction mnemonic? Only treat it as one when it is the first token
313
    // on the line (after optional whitespace/label), which is where a mnemonic
314
    // lives in assembly source, so a variable called "and" isn't misread.
315
    const insn_info_t *insn = insn_info_lookup(tok);
316
    if (insn) {
317
        int first = 0;
318
        while (first < len && (line[first] == ' ' || line[first] == '\t'))
319
            first++;
320
        // Skip a leading "label:" if present.
321
        if (first < len) {
322
            int c = first;
323
            while (c < len && is_ident(line[c]))
324
                c++;
325
            if (c < len && line[c] == ':') {
326
                c++;
327
                while (c < len && (line[c] == ' ' || line[c] == '\t'))
328
                    c++;
329
                first = c;
330
            }
331
        }
332
        if (s != first)
333
            insn = nullptr; // not in mnemonic position
334
    }
335
    if (insn) {
336
        ImGui::BeginTooltip();
337
        dbgui_instruction_tooltip_body(tok, regs.pc, regs.k, is_pc_line);
338
        ImGui::EndTooltip();
339
        return;
340
    }
341
342
    // A name the debug info knows: a program label, or an equate (KERNAL entry
343
    // points, hardware registers, constants). Equates were previously invisible
344
    // here, which is why things like JOYGET resolved to nothing.
345
    uint16_t addr;
346
    int      kind = DBG_NAME_LABEL;
347
    if (debug_ui_name_to_value(tok, &addr, &kind)) {
348
        const bool is_equ = (kind == DBG_NAME_EQUATE);
349
        uint8_t    b      = debug_ui_read6502(addr, 0, DEBUG_UI_CURRENT_BANK);
350
        uint8_t    b2     = debug_ui_read6502((uint16_t)(addr + 1), 0, DEBUG_UI_CURRENT_BANK);
351
        uint16_t   w      = (uint16_t)(b | (b2 << 8));
352
353
        ImGui::BeginTooltip();
354
        ImGui::Text("%s  =  $%04X", tok, addr);
355
        ImGui::SameLine();
356
        ImGui::TextDisabled(is_equ ? "(equate)" : "(label)");
357
358
        // A label the disassembler also knows about at that address is worth
359
        // naming, since equates often alias a routine entry point.
360
        const char *at = nullptr;
361
        if (dbg_info_addr_to_label(addr, &at) && at && dbgui_stricmp(at, tok) != 0) {
362
            ImGui::TextDisabled("also known as %s", at);
363
        }
364
365
        ImGui::Separator();
366
        ImGui::TextDisabled("byte [$%04X]", addr);
367
        dbgui_value_lines(b, 1);
368
        ImGui::Separator();
369
        ImGui::TextDisabled("word [$%04X] (LE)", addr);
370
        dbgui_value_lines(w, 2);
371
        ImGui::EndTooltip();
372
    }
373
}
374
375
// Render one file's source body inside its tab. `isPcFile` marks the tab the PC
376
// is currently in (gets the current-line highlight + follow scrolling).
377
static void
378
src_render_body(const char *file, bool isPcFile, int pcLine, bool pcMoved)
379
{
380
    s_activeFile = file ? file : "";
381
    const ImVec4 orange(1.0f, 0.78f, 0.35f, 1.0f);
382
383
    const source_file_t *sf        = source_view_get(file);
384
    int                  lineCount = (sf && sf->found) ? sf->count : 0;
385
386
    if (!sf || !sf->found) {
387
        ImGui::TextColored(orange, "%s   (source file not found)", src_basename(file));
388
        ImGui::TextWrapped("Could not locate '%s' on disk. Add a search directory "
389
                           "with -srcpath <dir>.",
390
                           file);
391
        return;
392
    }
393
394
    if (isPcFile)
395
        ImGui::TextColored(orange, "%s   line %d / %d", src_basename(file), pcLine, lineCount);
396
    else
397
        ImGui::TextColored(orange, "%s   %d lines", src_basename(file), lineCount);
398
    ImGui::Separator();
399
400
    // Follow: recenter on the current line when the PC moves (PC tab only).
401
    //
402
    // Stepping and breaking want different things. While stepping, the line
403
    // creeps along and yanking the view to centre on every step is disorienting,
404
    // so only scroll once it would leave the window. But arriving at a
405
    // breakpoint, a break-from-code (STP / DEBUGBreakToDebugger), a watchpoint
406
    // or a manual pause is a jump to somewhere new - there the line must be
407
    // centred, or it turns up wherever the old scroll position happened to leave
408
    // it and you have to hunt for the highlight.
409
    if (isPcFile && s_follow && pcMoved) {
410
        const char *why = debug_server_last_stop_reason();
411
        const bool  stepping = (why && strcmp(why, "step") == 0);
412
        src_request_scroll(file, pcLine, !stepping, /*flash=*/false);
413
    }
414
415
    ImGui::BeginChild("srcbody", ImVec2(0, 0), 0, ImGuiWindowFlags_None);
416
    ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0));
417
418
    ImDrawList *dl   = ImGui::GetWindowDrawList();
419
    float       rowH = ImGui::GetTextLineHeight();
420
421
    char maxnum[16];
422
    snprintf(maxnum, sizeof maxnum, "%d", lineCount > 0 ? lineCount : 1);
423
    float markerW = ImGui::CalcTextSize("0").x * 1.8f;
424
    float digitsW = ImGui::CalcTextSize(maxnum).x;
425
    float spaceW  = ImGui::CalcTextSize(" ").x;
426
    float glyphW  = ImGui::CalcTextSize("0").x; // monospace advance, for hover hit-testing
427
    float textX   = markerW + digitsW + spaceW * 2.0f;
428
429
    const ImU32 colNum  = IM_COL32(130, 130, 130, 255);
430
    const ImU32 colText = IM_COL32(210, 210, 210, 255);
431
    const ImU32 colCur  = IM_COL32(255, 240, 120, 255);
432
    const ImU32 curBg   = IM_COL32(38, 60, 120, 255);
433
    const ImU32 hoverBg = IM_COL32(70, 70, 70, 150);
434
    const ImU32 bpCol   = IM_COL32(230, 70, 70, 255);
435
436
    // Reveal a requested line (follow recenters only when off-screen; goto forces).
437
    // Only the tab the request was raised for may consume it, so a request for a
438
    // tab that hasn't rendered yet isn't swallowed here. Clamp to the file's
439
    // length so an out-of-range line still lands somewhere sensible.
440
    if (s_scrollRequest && s_scrollTarget >= 1 &&
441
        (s_scrollFile.empty() || s_scrollFile == file)) {
442
        int target_line = s_scrollTarget;
443
        if (target_line > lineCount)
444
            target_line = lineCount > 0 ? lineCount : 1;
445
446
        float childH  = ImGui::GetWindowHeight();
447
        float sY      = ImGui::GetScrollY();
448
        float top     = (target_line - 1) * rowH;
449
        bool  visible = (top >= sY + rowH) && (top <= sY + childH - 2 * rowH);
450
        if (s_scrollForce || !visible) {
451
            float target = top - childH * 0.5f + rowH * 0.5f;
452
            if (target < 0)
453
                target = 0;
454
            ImGui::SetScrollY(target);
455
        }
456
        s_scrollRequest = false;
457
        s_scrollFile.clear();
458
    }
459
460
    ImGuiListClipper clipper;
461
    clipper.Begin(lineCount, rowH);
462
    while (clipper.Step()) {
463
        for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; ++i) {
464
            int  lineNo = i + 1;
465
            bool isCur  = isPcFile && (lineNo == pcLine);
466
            // Recently navigated-to line in this file (fades out).
467
            const double flashAge = ImGui::GetTime() - s_flashTime;
468
            const bool   isFlash  = (s_flashLine == lineNo) && (s_flashFile == file) &&
469
                                  (flashAge >= 0.0) && (flashAge < SRC_FLASH_SECONDS);
470
471
            ImGui::PushID(i);
472
            ImVec2 p0   = ImGui::GetCursorScreenPos();
473
            float  rowW = ImGui::GetContentRegionAvail().x;
474
            if (rowW < textX + 10.0f)
475
                rowW = textX + 10.0f;
476
477
            bool pressed = ImGui::InvisibleButton("row", ImVec2(rowW, rowH));
478
            bool hovered = ImGui::IsItemHovered();
479
480
            // Right-click a line: run-to / toggle breakpoint (works before the
481
            // PC ever reaches this file, so breakpoints can be pre-armed).
482
            if (ImGui::BeginPopupContextItem("srcctx")) {
483
                uint16_t addr;
484
                bool     mappable = debug_ui_source_to_addr(file, lineNo, &addr);
485
                ImGui::BeginDisabled(!mappable || !DEBUGIsPaused());
486
                if (ImGui::MenuItem("Run to here"))
487
                    DEBUGRunTo(addr, 0, DEBUG_OWNER_UI);
488
                ImGui::EndDisabled();
489
                ImGui::BeginDisabled(!mappable);
490
                if (ImGui::MenuItem("Toggle breakpoint"))
491
                    src_toggle_bp(file, lineNo);
492
                ImGui::EndDisabled();
493
                ImGui::EndPopup();
494
            }
495
496
            // Row background: current line, then the just-navigated-to line,
497
            // then hover feedback. The navigation flash fades so it can't be
498
            // mistaken for a second current line.
499
            if (isCur) {
500
                dl->AddRectFilled(p0, ImVec2(p0.x + rowW, p0.y + rowH), curBg);
501
            } else if (isFlash) {
502
                const float k = 1.0f - (float)(flashAge / SRC_FLASH_SECONDS);
503
                const int   a = (int)(40.0f + 150.0f * k);
504
                dl->AddRectFilled(p0, ImVec2(p0.x + rowW, p0.y + rowH),
505
                                  IM_COL32(150, 110, 30, a));
506
                dl->AddRect(p0, ImVec2(p0.x + rowW, p0.y + rowH),
507
                            IM_COL32(240, 200, 90, (int)(80.0f + 175.0f * k)));
508
            } else if (hovered) {
509
                dl->AddRectFilled(p0, ImVec2(p0.x + rowW, p0.y + rowH), hoverBg);
510
            }
511
512
            // Breakpoint marker (filled) / hover hint (hollow) in the gutter.
513
            // The hint only appears while the mouse is actually over the gutter,
514
            // since that is the only place a click sets a breakpoint.
515
            ImVec2 center(p0.x + markerW * 0.5f, p0.y + rowH * 0.5f);
516
            float  radius = rowH * 0.30f;
517
            const float mouseX  = ImGui::GetIO().MousePos.x - p0.x;
518
            const bool  inGutter = hovered && mouseX >= 0.0f && mouseX < markerW;
519
            if (src_line_has_bp(file, lineNo)) {
520
                dl->AddCircleFilled(center, radius, bpCol);
521
            } else if (inGutter) {
522
                uint16_t tmp;
523
                if (debug_ui_source_to_addr(file, lineNo, &tmp))
524
                    dl->AddCircle(center, radius, IM_COL32(230, 70, 70, 150));
525
            }
526
527
            // Right-aligned line number.
528
            char num[16];
529
            snprintf(num, sizeof num, "%d", lineNo);
530
            float numRight = p0.x + markerW + digitsW;
531
            float numW     = ImGui::CalcTextSize(num).x;
532
            dl->AddText(ImVec2(numRight - numW, p0.y), colNum, num);
533
534
            // Source text (current line highlighted).
535
            char buf[1024];
536
            src_expand_tabs(sf->lines[lineNo - 1], buf, sizeof buf);
537
            dl->AddText(ImVec2(p0.x + textX, p0.y), isCur ? colCur : colText, buf);
538
539
            // Hover a mnemonic/label/number token → tooltip explaining it. On
540
            // the PC's line the instruction tooltip also predicts the result.
541
            if (hovered)
542
                src_hover_token_tooltip(buf, p0.x + textX, glyphW, isCur);
543
544
            ImGui::PopID();
545
546
            // Only a click in the gutter toggles a breakpoint. The row-wide
547
            // button stays (it drives hover feedback, the context menu and the
548
            // hover tooltips), but clicking the text itself must not arm a
549
            // breakpoint - that made it far too easy to set one by accident
550
            // while just clicking around the source.
551
            if (pressed) {
552
                const float clickX = ImGui::GetIO().MousePos.x - p0.x;
553
                if (clickX >= 0.0f && clickX < markerW)
554
                    src_toggle_bp(file, lineNo);
555
            }
556
        }
557
    }
558
    clipper.End();
559
560
    ImGui::PopStyleVar();
561
    ImGui::EndChild();
562
}
563
564
static void
565
source_panel_render(bool *p_open)
566
{
567
    if (!ImGui::Begin("Source", p_open)) {
568
        dbgui_window_end();
569
        return;
570
    }
571
    dbgui_window_zoom("source"); // Ctrl+wheel zooms this window's text only
572
573
    const ImVec4 orange(1.0f, 0.78f, 0.35f, 1.0f);
574
575
    // No debug info at all: explain how to get some, render nothing else.
576
    if (!dbg_info_is_loaded()) {
577
        ImGui::TextColored(orange, "No debug info loaded.");
578
        ImGui::Spacing();
579
        ImGui::TextWrapped(
580
            "The Source view needs cc65 debug info (a .dbg file) that maps "
581
            "addresses to source lines.");
582
        ImGui::Spacing();
583
        ImGui::BulletText("Start the emulator with:  -dbgfile <program.dbg>");
584
        ImGui::BulletText("If the .s/.c files aren't beside the .dbg:  -srcpath <dir>");
585
        ImGui::BulletText("A LOADed program's matching .dbg is picked up automatically.");
586
        dbgui_window_end();
587
        return;
588
    }
589
590
    // Resolve PC -> source. Only the non-banked program bank (regs.k == 0) is
591
    // covered by the 16-bit .dbg map; banked/ROM code is not.
592
    const char *pcf     = nullptr;
593
    int         pcl     = 0;
594
    bool        mapping = (regs.k == 0) &&
595
                   dbg_info_addr_to_source_banked(regs.pc, (int)memory_get_ram_bank(), &pcf, &pcl);
596
597
    // The PC often sits in code with no debug info: the KERNAL, or a small
598
    // trampoline copied into low RAM (e.g. JSRFAR at $02xx used for far calls).
599
    // Showing nothing at all leaves no clue where execution is, so fall back to
600
    // the nearest return address on the stack that DOES map and display that,
601
    // clearly flagged as the calling line rather than the PC.
602
    bool     viaCaller = false;
603
    uint16_t callerPc  = 0;
604
    if (!mapping) {
605
        const bool native = regs.is65c816 && !regs.e;
606
        uint32_t   sp     = regs.sp;
607
        uint32_t   from   = native ? (sp + 1) : (0x100 + (sp & 0xFF) + 1);
608
        uint32_t   to     = native ? (sp + 1 + 256) : 0x200;
609
        for (uint32_t a = from; a + 1 < to && !viaCaller; a++) {
610
            uint8_t  lo     = debug_ui_read6502((uint16_t)a, 0, DEBUG_UI_CURRENT_BANK);
611
            uint8_t  hi     = debug_ui_read6502((uint16_t)(a + 1), 0, DEBUG_UI_CURRENT_BANK);
612
            uint16_t pushed = (uint16_t)(lo | (hi << 8));
613
            uint8_t  opc    = debug_ui_read6502((uint16_t)(pushed - 2), regs.k, DEBUG_UI_CURRENT_BANK);
614
            if (opc != 0x20 && opc != 0xFC)
615
                continue;                       // not a JSR return frame
616
            uint16_t ret = (uint16_t)(pushed + 1);
617
            const char *cf = nullptr;
618
            int         cl = 0;
619
            if (dbg_info_addr_to_source_banked(ret, (int)memory_get_ram_bank(), &cf, &cl) && cf) {
620
                pcf = cf; pcl = cl; callerPc = ret;
621
                viaCaller = true;
622
            }
623
        }
624
    }
625
626
    if ((mapping || viaCaller) && pcf) {
627
        if (!s_addedDbgPath) {
628
            source_view_add_path(dbg_info_get_dbg_dir());
629
            s_addedDbgPath = true;
630
        }
631
        // A caller-derived file is a hint about where execution came from, not
632
        // where it is. Opening a tab for one that is not on disk replaces
633
        // whatever the user was reading with an error page -- which is exactly
634
        // what a C program does the moment it returns to BASIC: the PC lands in
635
        // the KERNAL and the stack still holds the C runtime's frames, whose
636
        // sources (cbm/write.s and friends) ship with cc65 rather than sitting
637
        // beside the program. Decline the hint entirely in that case, so the
638
        // header goes on saying there is no source rather than claiming to show
639
        // a caller it is not showing. The PC's OWN file is still opened when it
640
        // cannot be found, because there "missing" is the answer to "where am I".
641
        if (viaCaller) {
642
            const source_file_t *sf = source_view_get(pcf);
643
            if (!sf || !sf->found) {
644
                viaCaller = false;
645
                pcf       = nullptr;
646
            }
647
        }
648
        if (pcf) {
649
            s_pcFile = pcf;
650
            s_pcLine = pcl;
651
            src_ensure_tab(s_pcFile.c_str()); // keep the PC's file open as a tab
652
        }
653
    }
654
655
    bool pcMoved = (mapping || viaCaller) && (s_pcLine != s_lastPcLine || s_pcFile != s_lastPcFile);
656
657
    // Cross-panel goto (clicking a Call Stack / Symbols / Breakpoints entry):
658
    // open that address's source file and jump to its line, so selecting a
659
    // frame lands you in the code rather than only moving the memory view.
660
    {
661
        uint16_t gaddr;
662
        uint8_t  gbank;
663
        if (debug_ui_peek_goto(&gaddr, &gbank)) {
664
            const char *gf = nullptr;
665
            int         gl = 0;
666
            if (dbg_info_addr_to_source_banked(gaddr, (int)memory_get_ram_bank(), &gf, &gl) && gf) {
667
                if (!s_addedDbgPath) {
668
                    source_view_add_path(dbg_info_get_dbg_dir());
669
                    s_addedDbgPath = true;
670
                }
671
                src_ensure_tab(gf);
672
                s_selectFile    = gf;
673
                src_request_scroll(gf, gl, true);
674
            }
675
        }
676
    }
677
678
    // --- Toolbar ---
679
    ImGui::Checkbox("Follow PC", &s_follow);
680
    if (ImGui::IsItemHovered())
681
        ImGui::SetTooltip("Select and scroll to the PC's file and line on every stop.");
682
    if (dbg_info_has_high_level()) {
683
        // Only offered where a line is more than an instruction. For assembly
684
        // the two are the same thing, and stepping a whole macro expansion per
685
        // keypress would change how every existing project steps.
686
        ImGui::SameLine();
687
        bool byLine = DEBUGGetSourceStep();
688
        if (ImGui::Checkbox("Step by line", &byLine))
689
            DEBUGSetSourceStep(byLine);
690
        if (ImGui::IsItemHovered())
691
            ImGui::SetTooltip("F10/F11 run a whole C statement instead of one\n"
692
                              "6502 instruction. Off steps by instruction, which\n"
693
                              "is what the Disassembly panel shows.");
694
    }
695
    ImGui::SameLine();
696
    if (ImGui::Button("Go to PC")) {
697
        // Jump to wherever the PC maps right now, without changing Follow PC.
698
        if ((mapping || viaCaller) && !s_pcFile.empty()) {
699
            src_ensure_tab(s_pcFile.c_str());
700
            s_selectFile    = s_pcFile;
701
            src_request_scroll(s_pcFile.c_str(), s_pcLine, true);
702
        }
703
    }
704
    if (ImGui::IsItemHovered())
705
        ImGui::SetTooltip("Jump to the line the PC maps to (or its nearest\n"
706
                          "mapped caller when the PC has no source).");
707
    ImGui::SameLine(0, 16);
708
    if (ImGui::Button("Open..."))
709
        ImGui::OpenPopup("src_open");
710
    src_render_open_popup();
711
    ImGui::SameLine(0, 16);
712
    ImGui::TextUnformatted("Goto line:");
713
    ImGui::SameLine();
714
    ImGui::SetNextItemWidth(90);
715
    bool go = false;
716
    ImGui::InputInt("##gotoln", &s_gotoLine, 0, 0);
717
    if (dbgui_committed_with_enter())
718
        go = true;
719
    ImGui::SameLine();
720
    if (ImGui::Button("Go"))
721
        go = true;
722
    if (go && s_gotoLine >= 1) {
723
        // Target the tab that is actually showing (recorded when it last
724
        // rendered), so the jump lands in the file the user is looking at
725
        // instead of whichever tab happens to draw next. Fall back to the PC's
726
        // file when nothing has rendered yet.
727
        const std::string &target = !s_activeFile.empty() ? s_activeFile : s_pcFile;
728
        if (!target.empty()) {
729
            src_ensure_tab(target.c_str());
730
            s_selectFile = target;
731
            src_request_scroll(target.c_str(), s_gotoLine, true);
732
        } else {
733
            src_request_scroll(nullptr, s_gotoLine, true);
734
        }
735
    }
736
    if (!mapping && regs.k != 0) {
737
        ImGui::SameLine(0, 16);
738
        ImGui::TextDisabled("PC in bank %02X (no source map)", regs.k);
739
    } else if (viaCaller) {
740
        ImGui::SameLine(0, 16);
741
        ImGui::TextColored(ImVec4(0.95f, 0.75f, 0.35f, 1.0f),
742
                           "PC $%04X has no source (KERNAL/trampoline) — showing caller $%04X",
743
                           regs.pc, callerPc);
744
    } else if (!mapping) {
745
        ImGui::SameLine(0, 16);
746
        ImGui::TextDisabled("PC $%04X — no source mapping", regs.pc);
747
    }
748
749
    ImGui::Separator();
750
751
    if (s_tabs.empty()) {
752
        ImGui::TextWrapped("No source file open yet. Use \"Open...\" to pick one, or "
753
                           "step into code covered by the loaded .dbg.");
754
        s_lastPcLine = s_pcLine;
755
        s_lastPcFile = s_pcFile;
756
        dbgui_window_end();
757
        return;
758
    }
759
760
    // --- Tabs: one per open source file ---
761
    int closeIdx = -1;
762
    if (ImGui::BeginTabBar("src_tabs",
763
                           ImGuiTabBarFlags_AutoSelectNewTabs |
764
                               ImGuiTabBarFlags_Reorderable |
765
                               ImGuiTabBarFlags_DrawSelectedOverline |
766
                               ImGuiTabBarFlags_TabListPopupButton)) {
767
        for (int i = 0; i < (int)s_tabs.size(); i++) {
768
            const std::string &f    = s_tabs[i];
769
            bool               isPc = (mapping || viaCaller) && (f == s_pcFile);
770
771
            ImGuiTabItemFlags flags = 0;
772
            if (s_follow && isPc && pcMoved)
773
                flags |= ImGuiTabItemFlags_SetSelected; // snap to PC while following
774
            if (!s_selectFile.empty() && f == s_selectFile)
775
                flags |= ImGuiTabItemFlags_SetSelected; // cross-panel goto target
776
777
            // Display shows a "> " marker on the PC's file; the ID (after ###)
778
            // is the stable full name so toggling the marker never recreates it.
779
            char label[300];
780
            snprintf(label, sizeof label, "%s%s###srctab%s", isPc ? "> " : "",
781
                     src_basename(f.c_str()), f.c_str());
782
783
            bool open = true;
784
            ImGui::PushID(f.c_str());
785
            if (ImGui::BeginTabItem(label, &open, flags)) {
786
                src_render_body(f.c_str(), isPc, s_pcLine, pcMoved);
787
                ImGui::EndTabItem();
788
            }
789
            ImGui::PopID();
790
            if (!open)
791
                closeIdx = i;
792
        }
793
        ImGui::EndTabBar();
794
    }
795
    s_selectFile.clear(); // one-shot: only force tab selection for this frame
796
    if (closeIdx >= 0)
797
        s_tabs.erase(s_tabs.begin() + closeIdx);
798
799
    // Drop a scroll request whose target tab never rendered (closed, or never
800
    // opened) instead of letting it fire later against an unrelated file.
801
    if (s_scrollRequest && ++s_scrollAge > 4) {
802
        s_scrollRequest = false;
803
        s_scrollFile.clear();
804
    }
805
806
    s_lastPcLine = s_pcLine;
807
    s_lastPcFile = s_pcFile;
808
809
    dbgui_window_end();
810
}
811
812
static DebugPanelRegistration s_reg("Source", source_panel_render, true);