vscode: F5 debugs with step back; timeline slider in the emulator panel

This commit is contained in:
Steven Hugg committed 2026-09-28 18:57:04 +02:00
1 parent 7b9829dde3
commit 972b712ec6
11 files changed
+272 -60

No files matched your search

+1 -1
View File
@@ -86,7 +86,7 @@ also appear in the editor title bar, the Explorer, or **Project Options**.
| **Change Main File...** | Pick the main file from the workspace |
| **Change Platform...** | Build the project for another platform |
| **Detect Projects** | Scan the folder for projects and platforms |
| **Add Launch Configuration** | Write a run configuration to `.vscode/launch.json` |
| **Add Launch Configuration** | Write a run configuration to `.vscode/launch.json`; <kbd>F5</kbd> debugs it, with step back |
| **Project Options** | Open the project menu |
| **Build** | Build without running |
| **Run** | Build and run in the emulator |
+1 -1
View File
@@ -88,7 +88,7 @@ back, click the status bar and choose **Run Main File**.
To keep a list of programs to switch between, click the status bar and
choose **Add Launch Configuration**. The program then appears in the
**Run and Debug** view's list, and <kbd>F5</kbd> runs the one you chose.
**Run and Debug** view's list, and <kbd>F5</kbd> debugs the one you chose.
**A folder of small programs**, like a set of exercises, has no main
file. **Run** runs the program in the editor you're looking at. The
+43
View File
@@ -43,6 +43,49 @@ A build that fails leaves the last good version running.
Some programs end, such as a BASIC program. Then the panel says
**Halted** and the reason. Click **Run** to start again.
## Go back in time
The emulator records as it runs. While it's paused, a timeline appears
under the screen. Drag it to see any recorded frame. **Resume** runs on
from the frame you're looking at. Pressing a key there starts a new
future and drops the one that was recorded.
The recording keeps about the last 50 seconds. The NES saves bigger
states, so it keeps about 10 seconds.
## Debug
Press <kbd>F5</kbd> to build the program and debug it. Press
<kbd>Ctrl</kbd>+<kbd>F5</kbd> (**Run Without Debugging**) to just run it.
- **Breakpoints:** click left of a line number in C or assembly. You can
also add a breakpoint on a function by name (**+** in the Breakpoints
view), or on an address in the Disassembly view. A condition such as
`A == 3` or `[score] > 100` makes the breakpoint stop only when it's true.
- **Stepping:** Step Over, Step Into and Step Out go by source line. In the
Disassembly view they go by instruction.
- **Step Back** and **Reverse Continue** run backwards through the
recording, to the previous line or the previous breakpoint hit.
- **Variables** shows the CPU registers. **Watch** and hovers take the
same expressions as conditions: registers, symbols, `[addr]` for a byte
of memory, `#mem16[addr]` for a word.
- The **Debug Console** takes the commands of `8bws run -e`, such as
`mem $0200 32`, `hist 10`, `back 5` and `rbreak main`. Type `help` to
list them.
A launch configuration can also set:
| Setting | What it does |
|---|---|
| `stopOnEntry` | Stop before the first instruction instead of running |
| `script` | Debug Console commands to run once the program loads, such as `"break main"` |
While you debug, a rebuild doesn't restart the program. Press
<kbd>F5</kbd> again to debug the new build.
Some platforms, such as the Atari 2600 and Verilog, can only stop
between frames. There, breakpoints and steps work a frame at a time.
## Sound
The emulator plays sound through the panel. Audio starts on your first click
+3 -2
View File
@@ -78,7 +78,7 @@ To switch between programs with <kbd>F5</kbd>:
2. Click the status bar and choose **Add Launch Configuration**.
The program appears in the **Run and Debug** view's list. Choose it there
and press <kbd>F5</kbd> to build and run it. A launch configuration can
and press <kbd>F5</kbd> to build and debug it. A launch configuration can
also name another platform, so one program can run on `c64` and `vic20`:
```json
@@ -91,7 +91,8 @@ also name another platform, so one program can run on `c64` and `vic20`:
}
```
<kbd>F5</kbd> runs the program but doesn't debug it yet.
<kbd>F5</kbd> builds and debugs the program; <kbd>Ctrl</kbd>+<kbd>F5</kbd>
runs it without the debugger. See [Debug](emulator.md#debug).
## Where your choices are saved
+5 -2
View File
@@ -1647,8 +1647,11 @@ debugger shouldn't become a third.
`DebugService` and answers `debug(method, ...)`; its frame loop runs
through the controller, and every stop is a `stopped` event. The adapter
is `src/tools/dapsession.ts`, shared with `8bws dap`;
`extension/src/debugbackend.ts` is its RPC backend. Not yet wired into
`extension.ts`.)* The Debug Adapter (next
`extension/src/debugbackend.ts` is its RPC backend. F5 debugs; Ctrl+F5
(`noDebug`) runs as before. A debug launch builds, starts the worker
paused, and sends the listings with `setBuild`. Rebuilds don't reload
under the debugger; a plain Run ends the session. The panel's timeline
slider (shown while stopped) calls `seekFrame`.)* The Debug Adapter (next
section) runs in the extension host and translates DAP to RPC. DAP has
reverse debugging built in: report `supportsStepBack`, and handle
`stepBack` and `reverseContinue`. Scrubbing (`seekFrame`) is a timeline
+16
View File
@@ -434,6 +434,13 @@
"contents": "Make a program for a classic console or computer.\n[New 8bitworkshop Project](command:8bitworkshop.newProject)\n[Open 8bitworkshop Example](command:8bitworkshop.openExample)"
}
],
"breakpoints": [
{ "language": "c" },
{ "language": "cpp" },
{ "language": "8bws-6502" },
{ "language": "8bws-z80" },
{ "language": "8bws-6809" }
],
"debuggers": [
{
"type": "8bitworkshop",
@@ -453,6 +460,15 @@
"tool": {
"type": "string",
"description": "Tool for the main file, if not the usual one."
},
"stopOnEntry": {
"type": "boolean",
"description": "Stop before the program's first instruction instead of running.",
"default": false
},
"script": {
"type": "string",
"description": "Commands to run once the program is loaded, as in the Debug Console (e.g. \"break main; key SPACE\")."
}
}
}
+42
View File
@@ -11,6 +11,8 @@ export interface PanelEvents {
onKey(key: number, code: number, flags: number): void;
/** the user showed or hid the controls bar */
onControlsVisible(visible: boolean): void;
/** the user dragged the timeline to a recorded frame */
onSeek?(frame: number): void;
onVisible(visible: boolean): void;
onDispose(): void;
}
@@ -29,6 +31,7 @@ export class EmulatorPanel {
if (msg.type === 'key') this.events.onKey(msg.key, msg.code, msg.flags);
else if (msg.type === 'frameDone') this.frameInFlight = false;
else if (msg.type === 'controlsVisible') this.events.onControlsVisible(!!msg.visible);
else if (msg.type === 'seek') this.events.onSeek?.(msg.frame);
});
this.panel.onDidChangeViewState(e => this.events.onVisible(e.webviewPanel.visible));
this.panel.onDidDispose(() => this.events.onDispose());
@@ -102,11 +105,19 @@ function getHtml(controlsVisible: boolean) {
#show { position: absolute; right: 6px; bottom: 4px; display: none; cursor: pointer; font: 11px var(--vscode-font-family);
color: #ccc; background: rgba(0,0,0,0.5); border: 0; border-radius: 3px; padding: 2px 6px; }
body.hascontrols:not(.controls) #show { display: block; }
/* the timeline: shown while stopped, to scrub through the recording */
#timeline { position: absolute; left: 0; right: 0; display: none; align-items: center; gap: 8px;
padding: 2px 8px; box-sizing: border-box; background: rgba(0,0,0,0.7);
font: 11px var(--vscode-font-family); color: #ccc; }
body.stopped.haslog #timeline { display: flex; }
#scrub { flex: 1; accent-color: var(--vscode-focusBorder); }
#when { white-space: nowrap; font-variant-numeric: tabular-nums; }
</style>
</head>
<body class="${controlsVisible ? 'controls' : ''}">
<div id="wrap"><canvas id="screen" tabindex="0" width="1" height="1"></canvas></div>
<div id="status"></div>
<div id="timeline"><input id="scrub" type="range" min="0" max="0" value="0" title="Recorded frames: drag to go back in time"><span id="when"></span></div>
<div id="bar"><span id="hints"></span><button id="hide" title="Hide controls">&times;</button></div>
<button id="show" title="Show controls">Controls</button>
<script nonce="${nonce}">
@@ -223,6 +234,31 @@ function getHtml(controlsVisible: boolean) {
fit();
}
// the timeline: where the shown frame is in the recording. Dragging asks
// the host to seek, one request at a time; the next frame answers it.
const timelineEl = document.getElementById('timeline');
const scrub = document.getElementById('scrub');
const whenEl = document.getElementById('when');
let seekPending = false, seekWanted = null;
function showTimeline(t) {
document.body.classList.toggle('haslog', !!t);
if (!t) return;
scrub.min = t.first;
scrub.max = t.last;
if (!seekPending && seekWanted == null) scrub.value = t.now.frame;
whenEl.textContent = 'frame ' + t.now.frame + (t.now.step ? ':' + t.now.step : '') + ' / ' + t.last;
}
function requestSeek(frame) {
if (seekPending) { seekWanted = frame; return; }
seekPending = true;
vscode.postMessage({ type: 'seek', frame });
}
function seekDone() {
seekPending = false;
if (seekWanted != null) { const f = seekWanted; seekWanted = null; requestSeek(f); }
}
scrub.addEventListener('input', () => requestSeek(Number(scrub.value)));
// scale the canvas to fill the panel, keeping its aspect ratio
function fit() {
const { w, h, rotate, aspect } = layout;
@@ -231,6 +267,7 @@ function getHtml(controlsVisible: boolean) {
let ratio = aspect || w / h;
if (sideways) ratio = 1 / ratio;
const barH = document.body.classList.contains('controls') ? bar.offsetHeight : 0;
timelineEl.style.bottom = barH + 'px';
document.getElementById('wrap').style.bottom = barH + 'px';
const W = window.innerWidth, H = window.innerHeight - barH;
let dw = W, dh = W / ratio;
@@ -257,6 +294,8 @@ function getHtml(controlsVisible: boolean) {
image.data.set(msg.pixels);
ctx.putImageData(image, 0, 0);
vscode.postMessage({ type: 'frameDone' });
if (msg.timeline) showTimeline(msg.timeline);
seekDone();
} else if (msg.type === 'status') {
const s = msg.status;
if (s && JSON.stringify(s.controls) !== lastControls) {
@@ -264,6 +303,9 @@ function getHtml(controlsVisible: boolean) {
showControls(s.controls);
}
statusEl.textContent = !s ? '' : s.state == 'running' ? '' : s.state == 'halted' ? 'Halted: ' + (s.message || '') : 'Paused';
document.body.classList.toggle('stopped', !!s && s.state != 'running');
showTimeline(s && s.timeline);
seekDone(); // a seek always ends with a status, even if its frame was dropped
} else if (msg.type === 'audio') {
onAudio(msg);
} else if (msg.type === 'audioReset') {
+85 -12
View File
@@ -11,6 +11,8 @@ import type { BuildArgs } from './buildworker';
import type { AudioChunk, EmuStatus } from './emuworker';
import { WorkerHandle } from './engine';
import { EmulatorPanel } from './emulatorpanel';
import { WorkerDebugBackend } from './debugbackend';
import { EmuDebugSession, LaunchArgs } from '../../src/tools/dapsession';
import { Project, findRootDir, isHeaderFile, isInside, isOwnExtension, isSourceFile } from './projectinfo';
import { CONFIG, ProjectScope } from './projectscope';
import { BuildReason, BuildScheduler } from './autobuild';
@@ -54,6 +56,8 @@ let autoTarget: Target | undefined;
let heldDiagnostics: { timer: NodeJS.Timeout, show: () => void } | undefined;
/** Sound is muted; remembered across runs and webviews. */
let muted = false;
/** The debug session driving the emulator, if one is. */
let debugging: { session: vscode.DebugSession, backend: WorkerDebugBackend } | undefined;
export function activate(ctx: vscode.ExtensionContext) {
context = ctx;
@@ -102,13 +106,24 @@ export function activate(ctx: vscode.ExtensionContext) {
muted = ctx.globalState.get<boolean>('muted', false);
vscode.commands.executeCommand('setContext', '8bitworkshop.muted', muted);
// F5 on an 8bitworkshop launch configuration runs it (no debugger yet)
// F5 on an 8bitworkshop launch configuration debugs it; Ctrl+F5 (Run
// Without Debugging) just runs it in the emulator panel
ctx.subscriptions.push(vscode.debug.registerDebugConfigurationProvider('8bitworkshop', {
resolveDebugConfiguration: async (folder, config) => {
await runLaunchConfiguration(folder, config);
return undefined; // no debug session; the emulator panel runs it
if (config.noDebug) {
await runLaunchConfiguration(folder, config);
return undefined; // no debug session; the emulator panel runs it
}
// F5 with no launch.json: debug the current target
return { type: '8bitworkshop', request: 'launch', name: 'Debug', ...config };
},
}));
ctx.subscriptions.push(vscode.debug.registerDebugAdapterDescriptorFactory('8bitworkshop', {
createDebugAdapterDescriptor: session => new vscode.DebugAdapterInlineImplementation(newDebugSession(session)),
}));
ctx.subscriptions.push(vscode.debug.onDidTerminateDebugSession(session => {
if (debugging?.session === session) debugging = undefined;
}));
ctx.subscriptions.push(
vscode.workspace.onDidSaveTextDocument(doc => onSave(doc)),
@@ -229,6 +244,7 @@ async function getEmu(platform: string): Promise<WorkerHandle> {
emu.on('frame', frame => panel?.showFrame(frame));
emu.on('audio', (chunk: AudioChunk) => panel?.showAudio(chunk));
emu.on('audioReset', () => panel?.resetAudio());
emu.on('stopped', e => debugging?.backend.handleStop(e));
emu.on('status', (s: EmuStatus | null) => {
emuStatus = s;
panel?.showStatus(s);
@@ -425,7 +441,10 @@ async function followActiveEditor() {
await scope.setTargetChoice(project, 'follow');
}
async function startEmulator(target: Target, build: BuildOutcome) {
/** Start `build` in the emulator panel. Returns false if it didn't start. */
async function startEmulator(target: Target, build: BuildOutcome, opts: { paused?: boolean } = {}): Promise<boolean> {
// a plain Run replaces what the debugger was looking at
if (debugging && !opts.paused) vscode.debug.stopDebugging(debugging.session);
// each run gets a fresh worker: platforms keep global state (see emuworker)
emu?.dispose();
emu = undefined;
@@ -437,7 +456,10 @@ async function startEmulator(target: Target, build: BuildOutcome) {
onControlsVisible: visible => context.globalState.update('controlsVisible', visible),
// don't burn CPU on a hidden screen
onVisible: visible => { emu?.call('setVisible', visible); },
onSeek: frame => { emu?.call('seekFrame', frame); },
onDispose: () => {
// closing the emulator ends the program, and any session debugging it
if (debugging) vscode.debug.stopDebugging(debugging.session);
panel = undefined;
emuStatus = null;
running = undefined;
@@ -454,15 +476,17 @@ async function startEmulator(target: Target, build: BuildOutcome) {
panel.setTitle(`${title} (${target.platform})`);
panel.setMuted(muted);
try {
emuStatus = await worker.call<EmuStatus>('start', target.platform, build.output, build.files);
emuStatus = await worker.call<EmuStatus>('start', target.platform, build.output, build.files, opts);
worker.call('setMuted', muted);
panel.showStatus(emuStatus);
running = target;
runningBuild = build;
output.appendLine(`Running ${title} on ${target.platform}`);
return true;
} catch (e) {
output.appendLine(`Emulator failed to start: ${e && e.stack || e}`);
output.show(true);
return false;
}
}
@@ -470,6 +494,8 @@ async function startEmulator(target: Target, build: BuildOutcome) {
async function reloadEmulator(target: Target, reason: BuildReason, result: BuildOutcome) {
if (!panel || !emuStatus || !running || !result.output) return;
if (running.main.toString() !== target.main.toString()) return;
// the code under the debugger doesn't change beneath it; F5 again rebuilds
if (debugging) return;
var mode = config(target.main).get<string>('reloadOnBuild', 'always');
if (mode === 'never' || (mode === 'onSave' && reason === 'type')) return;
if (emuStatus.platform !== target.platform) {
@@ -1102,18 +1128,65 @@ async function addLaunchConfiguration() {
}
async function runLaunchConfiguration(folder: vscode.WorkspaceFolder | undefined, cfg: vscode.DebugConfiguration) {
if (!cfg.mainFile) {
// Ctrl+F5 with no launch.json: run the current target
return buildCommand(true);
}
var target = await launchTarget(folder, cfg);
await buildAndMaybeRun(target, true, 'command');
}
/** What a launch configuration runs: its mainFile, or the current target. */
async function launchTarget(folder: vscode.WorkspaceFolder | undefined, cfg: { name?: string, mainFile?: string, platform?: string, tool?: string }): Promise<Target> {
var base = folder?.uri.fsPath || vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
if (!cfg.mainFile || !base) {
// F5 with no launch.json: run the current target
return buildCommand(true);
var current = currentTarget();
if (!current) throw new Error('open a program to debug, or set mainFile in the launch configuration');
return current;
}
var file = path.resolve(base, cfg.mainFile);
var project = scope.projectFor(vscode.Uri.file(file));
var platform = cfg.platform || project?.platform;
if (!platform) {
vscode.window.showErrorMessage(`8bitworkshop: launch configuration "${cfg.name}" needs a platform.`);
return;
}
if (!platform) throw new Error(`launch configuration "${cfg.name}" needs a platform`);
if (project) await scope.setTargetChoice(project, file);
await buildAndMaybeRun({ platform, main: vscode.Uri.file(file), tool: cfg.tool, project }, true, 'command');
return { platform, main: vscode.Uri.file(file), tool: cfg.tool, project };
}
/**
* A debug session over the emulator worker. The adapter (shared with
* `8bws dap`) runs here in the extension host; the debugger itself runs in
* the worker, next to the emulator.
*/
function newDebugSession(session: vscode.DebugSession): EmuDebugSession {
// the worker this session launched; a later Run starts another
var worker: WorkerHandle | undefined;
var backend = new WorkerDebugBackend(
(method, ...args) => {
if (!worker || worker !== emu) throw new Error('the emulator this session started has stopped');
return worker.call('debug', method, ...args);
},
async args => {
var r = await launchForDebug(session.workspaceFolder, args);
worker = emu;
return r;
},
async () => {
if (debugging?.session === session) debugging = undefined;
// stopping the session stops its program, not one started since
if (worker && worker === emu) panel?.dispose();
});
debugging = { session, backend };
return new EmuDebugSession(backend);
}
/** Build the program and load it stopped, with its listings for the debugger. */
async function launchForDebug(folder: vscode.WorkspaceFolder | undefined, args: LaunchArgs): Promise<{ root: string }> {
var target = await launchTarget(folder, args);
var result = await runBuild(target, 'command');
if (!result || !result.success) throw new Error('the build failed; see Problems');
if (!result.output) throw new Error('the build produced no ROM to run');
if (!await startEmulator(target, result, { paused: true })) throw new Error('the emulator failed to start; see Output');
var mainPath = path.posix.basename(target.main.path);
await emu!.call('debug', 'setBuild', { listings: result.listings, symbols: result.symbolmap, mainPath, paths: result.paths });
return { root: path.dirname(target.main.fsPath) };
}
+30
View File
@@ -7,6 +7,9 @@ import { Rpc } from '../src/rpc';
import { findRootDir } from '../src/projectinfo';
import { Builder, ProjectFileProvider } from '../src/buildcore';
import type { AudioChunk, EmuStatus, FrameEvent } from '../src/emuworker';
import { WorkerDebugBackend } from '../src/debugbackend';
import { EmuDebugSession } from '../../src/tools/dapsession';
import { DapClient } from '../../test/unit/dapclient';
// Drives out/emuworker.js in a worker thread, the way the extension host does.
// Mocha runs from extension/, so BIOS fetches must resolve against ROOT.
@@ -230,6 +233,33 @@ describe('extension emuworker', function () {
assert.equal(statuses[statuses.length - 1].state, 'paused');
});
it('drives the debug adapter through the worker, as the extension does', async function () {
var backend = new WorkerDebugBackend((method, ...args) => rpc.call('debug', method, ...args), async () => {
await rpc.call('start', 'mw8080bw', built.output, built.files, { paused: true });
await rpc.call('debug', 'setBuild', { listings: built.listings, symbols: built.symbolmap, mainPath: 'game2.c', paths: built.paths });
return { root: '/proj' };
}, async () => { });
rpc.on('stopped', e => backend.handleStop(e));
var c = new DapClient(new EmuDebugSession(backend));
await c.request('initialize', { adapterID: '8bitworkshop', pathFormat: 'path', linesStartAt1: true, columnsStartAt1: true });
await c.request('launch', { mainFile: 'game2.c' });
await c.event('initialized');
var { breakpoints } = await c.request('setBreakpoints', { source: { path: '/proj/game2.c' }, breakpoints: [{ line: 166 }] });
assert.ok(breakpoints[0].verified);
await c.request('configurationDone');
assert.equal((await c.event('stopped')).reason, 'breakpoint');
var frame = await c.where();
assert.equal(frame.source.path, '/proj/game2.c');
assert.equal(frame.line, 166);
await c.request('next', { threadId: 1 });
await c.event('stopped');
assert.equal((await c.where()).line, 167);
await c.request('stepBack', { threadId: 1 });
await c.event('stopped');
assert.equal((await c.where()).line, 166);
assert.match((await c.request('evaluate', { expression: 'now', context: 'repl' })).result, /past/);
});
it('pauses with a stop, and seeks to a recorded frame', async function () {
await rpc.call('start', 'mw8080bw', built.output, built.files);
await waitForFrames(10);
+43
View File
@@ -0,0 +1,43 @@
// A test client for the debug adapter, shared by the root and extension tests.
import { DebugProtocol } from "@vscode/debugprotocol";
import type { EmuDebugSession } from "../../src/tools/dapsession";
/** Just enough of a DAP client: requests by sequence number, events by name. */
export class DapClient {
private seq = 1;
private pending = new Map<number, (r: DebugProtocol.Response) => void>();
private events: DebugProtocol.Event[] = [];
private waiting: { name: string, resolve: (e: DebugProtocol.Event) => void }[] = [];
constructor(readonly session: EmuDebugSession) {
session.onDidSendMessage((m: any) => {
if (m.type === 'response') this.pending.get(m.request_seq)?.(m);
else if (m.type === 'event') {
const w = this.waiting.findIndex(w => w.name === m.event);
if (w >= 0) this.waiting.splice(w, 1)[0].resolve(m);
else this.events.push(m);
}
});
}
request(command: string, args: any = {}): Promise<any> {
const seq = this.seq++;
return new Promise((resolve, reject) => {
this.pending.set(seq, (r) => r.success ? resolve(r.body) : reject(new Error(r.message)));
this.session.handleMessage({ seq, type: 'request', command, arguments: args } as any);
});
}
/** The next event of this name, including one that already came. */
event(name: string): Promise<any> {
const i = this.events.findIndex(e => e.event === name);
if (i >= 0) return Promise.resolve(this.events.splice(i, 1)[0].body);
return new Promise(resolve => this.waiting.push({ name, resolve: e => resolve(e.body) }));
}
async where() {
const { stackFrames } = await this.request('stackTrace', { threadId: 1 });
return stackFrames[0];
}
}
+3 -42
View File
@@ -1,7 +1,7 @@
import assert from "assert";
import { before, describe, it } from "mocha";
import { DebugProtocol } from "@vscode/debugprotocol";
import { EmuDebugSession } from "../../src/tools/dapsession";
import { DapClient } from "./dapclient";
import { LocalDebugBackend } from "../../src/tools/daplocal";
import { loadPlatform } from "../../src/tools/emutarget";
import { compileSourceFile, preload } from "../../src/tools/testlib";
@@ -12,53 +12,14 @@ import { compileSourceFile, preload } from "../../src/tools/testlib";
const ROOT = '/proj';
let build: any;
/** Just enough of a DAP client: requests by sequence number, events by name. */
class Client {
private seq = 1;
private pending = new Map<number, (r: DebugProtocol.Response) => void>();
private events: DebugProtocol.Event[] = [];
private waiting: { name: string, resolve: (e: DebugProtocol.Event) => void }[] = [];
constructor(readonly session: EmuDebugSession) {
session.onDidSendMessage((m: any) => {
if (m.type === 'response') this.pending.get(m.request_seq)?.(m);
else if (m.type === 'event') {
const w = this.waiting.findIndex(w => w.name === m.event);
if (w >= 0) this.waiting.splice(w, 1)[0].resolve(m);
else this.events.push(m);
}
});
}
request(command: string, args: any = {}): Promise<any> {
const seq = this.seq++;
return new Promise((resolve, reject) => {
this.pending.set(seq, (r) => r.success ? resolve(r.body) : reject(new Error(r.message)));
this.session.handleMessage({ seq, type: 'request', command, arguments: args } as any);
});
}
/** The next event of this name, including one that already came. */
event(name: string): Promise<any> {
const i = this.events.findIndex(e => e.event === name);
if (i >= 0) return Promise.resolve(this.events.splice(i, 1)[0].body);
return new Promise(resolve => this.waiting.push({ name, resolve: e => resolve(e.body) }));
}
async where() {
const { stackFrames } = await this.request('stackTrace', { threadId: 1 });
return stackFrames[0];
}
}
async function launch(): Promise<Client> {
async function launch(): Promise<DapClient> {
const backend = new LocalDebugBackend(async () => {
const target = await loadPlatform('mw8080bw');
await target.start();
await target.loadROM(build.output);
return { target, root: ROOT, debugInfo: { listings: build.listings, symbols: build.symbolmap, mainPath: 'game2.c' } };
});
const c = new Client(new EmuDebugSession(backend));
const c = new DapClient(new EmuDebugSession(backend));
const caps = await c.request('initialize', { adapterID: '8bitworkshop', linesStartAt1: true, columnsStartAt1: true, pathFormat: 'path' });
assert.ok(caps.supportsDisassembleRequest);
await c.request('launch', { program: 'game2.c' });