单步与表达式
断点让程序停下来,单步让程序按你的节奏走,表达式让调试器变成可交互的 REPL。这三类能力对应 DAP 的执行控制与求值请求。
执行控制请求
四个请求控制程序执行流程:
| 请求 | 作用 | stopped 的 reason |
|---|---|---|
continue | 继续运行直到下一个断点 | 无(发 continued 事件) |
next | 单步跳过(不进入函数) | step |
stepIn | 步入(进入函数内部) | step |
stepOut | 步出(跳出当前函数) | step |
typescript
function handleNext(msg: DAPMessage, socket: any) {
const threadId = msg.arguments.threadId;
// 单步跳过:执行到当前栈帧的下一行
performNext(threadId);
// 编辑器需要知道程序已恢复执行
sendResponse(socket, msg, {});
sendEvent(socket, 'continued', {
threadId,
allThreadsContinued: true
});
// 单步结束(到达下一行)时再发 stopped
executeWhenPaused(() => {
sendEvent(socket, 'stopped', { reason: 'step', threadId });
});
}三个单步请求结构几乎相同,差异只在执行的语义:
typescript
function handleStepIn(msg: DAPMessage, socket: any) {
const threadId = msg.arguments.threadId;
performStepIn(threadId); // 进入当前行调用的函数
sendResponse(socket, msg, {});
sendEvent(socket, 'continued', { threadId, allThreadsContinued: true });
sendEvent(socket, 'stopped', { reason: 'step', threadId });
}
function handleStepOut(msg: DAPMessage, socket: any) {
const threadId = msg.arguments.threadId;
performStepOut(threadId); // 执行完当前函数并返回到调用处
sendResponse(socket, msg, {});
sendEvent(socket, 'continued', { threadId, allThreadsContinued: true });
sendEvent(socket, 'stopped', { reason: 'step', threadId });
}单步的内部实现
真实调试器的单步由目标运行时完成。以 Node.js inspector 为例:
typescript
// 基于 CDP(Chrome DevTools Protocol)的单步
import { Session } from 'inspector';
const session = new Session();
session.connect();
// 注册调试器事件
session.on('Debugger.paused', (params) => {
// 解析暂停位置 → 构建栈帧
const callFrames = params.callFrames.map((frame, index) => ({
id: index,
name: frame.functionName || '<anonymous>',
line: frame.location.lineNumber + 1,
column: frame.location.columnNumber,
source: { path: frame.url.replace('file://', '') }
}));
// 通知 VS Code 已暂停
sendEvent(socket, 'stopped', {
reason: params.reason === 'breakpoint' ? 'breakpoint' : 'step',
threadId: 1
});
});
// 单步跳过
function performNext() {
session.post('Debugger.stepOver');
}暂停与继续
暂停
VS Code 的暂停按钮发出 pause 请求,适配器中断目标程序:
typescript
function handlePause(msg: DAPMessage, socket: any) {
const threadId = msg.arguments.threadId;
interruptExecution(threadId); // 向目标运行时发送中断
sendResponse(socket, msg, {});
// 中断成功时发送 stopped(reason: pause)
sendEvent(socket, 'stopped', { reason: 'pause', threadId });
}继续
typescript
function handleContinue(msg: DAPMessage, socket: any) {
const threadId = msg.arguments.threadId;
resumeExecution(threadId);
sendResponse(socket, msg, { allThreadsContinued: true });
sendEvent(socket, 'continued', { threadId, allThreadsContinued: true });
}EvaluateRequest 表达式求值
evaluate 请求是调试器交互能力的关键,同一请求被三个 UI 位置使用,通过 context 区分:
| context | 触发位置 | 用途 |
|---|---|---|
watch | 监视面板 | 用户输入的监视表达式,持续求值 |
hover | 编辑器悬停 | 鼠标悬停变量时的值 |
repl | 调试控制台 | 用户直接输入表达式执行 |
typescript
function handleEvaluate(msg: DAPMessage, socket: any) {
const args = msg.arguments;
const expression = args.expression; // 表达式字符串
const frameId = args.frameId; // 当前栈帧(求值上下文)
const context = args.context || 'repl'; // watch / hover / repl
try {
const result = evaluateExpression(expression, frameId);
sendResponse(socket, msg, {
result: result.value,
type: result.type,
variablesReference: result.variablesReference,
// hover 场景可附带 markdown 提升显示效果
...(context === 'hover' ? { presentationHint: { kind: 'data' } } : {})
});
} catch (err) {
sendResponse(socket, msg, {
result: err.message,
type: 'string'
}, false);
}
}表达式求值实现
typescript
function evaluateExpression(expression: string, frameId: number): any {
const frame = findFrameById(frameId);
if (!frame) throw new Error('无效的栈帧');
// 1. 先在局部变量中查找简单标识符
const locals = getFrameLocals(frame);
const direct = locals.find((v) => v.name === expression);
if (direct) {
return {
value: direct.value,
type: direct.type,
variablesReference: direct.variablesReference
};
}
// 2. 复杂表达式交给目标运行时求值(以 Node 为例)
// 使用 inspector 的 Runtime.evaluate 或 Debugger.evaluateOnCallFrame
const session = getInspectorSession(frame);
const result = session.evaluateOnCallFrame({
callFrameId: frame.inspectorId,
expression
});
// 3. 包装结果
const value = result.result.value;
const isObject = result.result.objectId !== undefined;
return {
value: isObject ? `{Object}` : String(value),
type: result.result.type,
variablesReference: isObject ? registerRemoteObject(result.result.objectId) : 0
};
}悬停提示增强
presentationHint 可以控制变量在 UI 中的展示方式:
typescript
// 数组/对象可标记分组展示
sendResponse(socket, msg, {
result: '[1, 2, 3]',
type: 'array',
variablesReference: 10,
presentationHint: {
kind: 'data', // data / property / method
attributes: ['readOnly']
}
});SetVariableRequest 运行时改值
在变量视图中直接编辑值,触发 setVariable 请求:
typescript
function handleSetVariable(msg: DAPMessage, socket: any) {
const args = msg.arguments;
const variablesReference = args.variablesReference; // 父容器
const name = args.name; // 变量名
const value = args.value; // 新值(字符串)
try {
const newValue = applyNewValue(variablesReference, name, value);
sendResponse(socket, msg, {
value: newValue.value,
type: newValue.type,
variablesReference: newValue.variablesReference
});
} catch (err) {
sendResponse(socket, msg, { result: err.message }, false);
}
}
function applyNewValue(ref: number, name: string, rawValue: string) {
const container = variablesById.get(ref);
const variable = container.find((v) => v.name === name);
if (!variable) throw new Error(`找不到变量 ${name}`);
// 类型推断:数字/布尔/字符串
const parsed = parsePrimitive(rawValue);
variable.value = parsed.raw;
// 若目标运行时支持写回,调用底层 API
setInspectorVariable(variable, parsed);
return {
value: parsed.raw,
type: parsed.type,
variablesReference: 0
};
}声明能力时需开启 supportsSetVariable,否则变量视图不提供编辑入口。
调试控制台输出
调试控制台同时显示程序输出与用户输入。程序输出通过 output 事件送达:
typescript
// category 决定样式与图标
function outputStdout(text: string) {
sendEvent(socket, 'output', {
category: 'stdout', // stdout / stderr / console / telemetry
output: text,
// 可附加 ANSI 颜色
source: { path: currentSourcePath }
});
}控制台用户输入会触发带 context: repl 的 evaluate 请求,适配器可以支持交互式求值。多行输入与命令历史由编辑器侧处理。
组合:交互式求值示例
typescript
// 支持在控制台执行任意表达式并打印结果
function handleEvaluate(msg: DAPMessage, socket: any) {
const args = msg.arguments;
if (args.context === 'repl') {
// 先打印输入回显
sendEvent(socket, 'output', {
category: 'console',
output: '> ' + args.expression + '\n'
});
const result = evaluateExpression(args.expression, args.frameId);
sendEvent(socket, 'output', {
category: 'console',
output: result.value + '\n'
});
return;
}
// watch / hover 走标准响应
...
}调试状态机
适配器需要维护清晰的执行状态,避免收到矛盾请求:
typescript
type DebugState = 'initializing' | 'running' | 'paused' | 'terminated';
let state: DebugState = 'initializing';
// 每个请求入口校验状态
function handleRequest(msg: any, socket: any) {
if (msg.command === 'initialize' || msg.command === 'disconnect') {
// 无条件接受
} else if (state === 'terminated') {
return; // 会话已结束,忽略所有请求
}
switch (msg.command) {
case 'next':
case 'stepIn':
case 'stepOut':
case 'continue':
// 只有 paused 状态可执行
if (state !== 'paused') {
sendResponse(socket, msg, { result: '程序未暂停' }, false);
return;
}
state = 'running';
break;
}
}
// stopped 事件发出后状态回到 paused
function onPaused() {
state = 'paused';
}单步、求值、改值构成了调试器的交互核心。状态机 + 明确的请求处理,为最终搭建一个完整的脚本调试器铺平道路。