diff --git a/src/backend/repl-main-gen.ts b/src/backend/repl-main-gen.ts index 6c4dc41..2c28942 100644 --- a/src/backend/repl-main-gen.ts +++ b/src/backend/repl-main-gen.ts @@ -283,6 +283,7 @@ function emitProgramDescriptorsAndMain( ' if (std::string(argv[i]) == "--print-vars") print_vars = true;', ); lines.push(" }"); + lines.push(""); lines.push(" if (cyclic) {"); lines.push( ` strucpp::cyclic_run(programs, ${programs.length}, print_vars);`, diff --git a/src/runtime/repl/iec_cyclic.hpp b/src/runtime/repl/iec_cyclic.hpp index a53de2b..a43850c 100644 --- a/src/runtime/repl/iec_cyclic.hpp +++ b/src/runtime/repl/iec_cyclic.hpp @@ -94,8 +94,14 @@ inline void cyclic_run(ProgramDescriptor* programs, size_t program_count, fflush(stdout); } - // Sleep until next tick (drift-free) + // Sleep until next tick. + // If we've fallen behind (e.g., resumed from a debugger breakpoint), + // reset to now to prevent a burst of catch-up cycles. next_tick += std::chrono::nanoseconds(common_ticktime); + auto now = std::chrono::steady_clock::now(); + if (next_tick < now) { + next_tick = now; + } std::this_thread::sleep_until(next_tick); } diff --git a/src/runtime/repl/iec_repl.hpp b/src/runtime/repl/iec_repl.hpp index fc4ec30..607109b 100644 --- a/src/runtime/repl/iec_repl.hpp +++ b/src/runtime/repl/iec_repl.hpp @@ -323,6 +323,161 @@ inline bool parse_qualified_name(const std::string& input, std::string& prog_nam return true; } +// ============================================================================= +// Shared Command Processor +// ============================================================================= +// Processes force/unforce/get/set/list commands and returns a plain-text response. +// Used by both the interactive REPL loop and the IPC command server. +// Returns "OK: " on success or "ERR: " on failure. + +inline std::string process_command( + const std::string& cmd_line, + ProgramDescriptor* programs, + size_t program_count) +{ + // Parse command and arguments + std::string line = cmd_line; + size_t start = line.find_first_not_of(" \t\r\n"); + if (start == std::string::npos) return "ERR: Empty command"; + size_t end = line.find_last_not_of(" \t\r\n"); + line = line.substr(start, end - start + 1); + if (line.empty()) return "ERR: Empty command"; + + size_t sp = line.find(' '); + std::string cmd = (sp == std::string::npos) ? line : line.substr(0, sp); + std::string args_str = (sp == std::string::npos) ? "" : line.substr(sp + 1); + size_t astart = args_str.find_first_not_of(" \t"); + if (astart != std::string::npos) args_str = args_str.substr(astart); + else args_str.clear(); + + // --- get . --- + if (cmd == "get") { + if (args_str.empty()) return "ERR: Usage: get ."; + std::string pn, vn; + if (!parse_qualified_name(args_str, pn, vn)) return "ERR: Invalid format. Use: program.variable"; + auto* prog = find_program(programs, program_count, pn); + if (!prog) return "ERR: Unknown program: " + pn; + auto* var = find_var(prog, vn); + if (!var) return "ERR: Unknown variable: " + vn + " in " + pn; + bool forced = var_is_forced(var->type, var->var_ptr); + std::string val = var_value_to_string(var->type, var->var_ptr); + return std::string("OK: ") + pn + "." + vn + " : " + + var_type_name(var->type) + " = " + val + (forced ? " [FORCED]" : ""); + } + + // --- set . --- + if (cmd == "set") { + size_t vsp = args_str.find(' '); + if (vsp == std::string::npos) return "ERR: Usage: set . "; + std::string qname = args_str.substr(0, vsp); + std::string val = args_str.substr(vsp + 1); + size_t vs = val.find_first_not_of(" \t"); + if (vs != std::string::npos) val = val.substr(vs); + std::string pn, vn; + if (!parse_qualified_name(qname, pn, vn)) return "ERR: Invalid format. Use: program.variable"; + auto* prog = find_program(programs, program_count, pn); + if (!prog) return "ERR: Unknown program: " + pn; + auto* var = find_var(prog, vn); + if (!var) return "ERR: Unknown variable: " + vn + " in " + pn; + if (var_set_value(var->type, var->var_ptr, val)) { + return std::string("OK: ") + pn + "." + vn + " = " + var_value_to_string(var->type, var->var_ptr); + } + return std::string("ERR: Invalid value for ") + var_type_name(var->type) + ": " + val; + } + + // --- force . --- + if (cmd == "force") { + size_t vsp = args_str.find(' '); + if (vsp == std::string::npos) return "ERR: Usage: force . "; + std::string qname = args_str.substr(0, vsp); + std::string val = args_str.substr(vsp + 1); + size_t vs = val.find_first_not_of(" \t"); + if (vs != std::string::npos) val = val.substr(vs); + std::string pn, vn; + if (!parse_qualified_name(qname, pn, vn)) return "ERR: Invalid format. Use: program.variable"; + auto* prog = find_program(programs, program_count, pn); + if (!prog) return "ERR: Unknown program: " + pn; + auto* var = find_var(prog, vn); + if (!var) return "ERR: Unknown variable: " + vn + " in " + pn; + if (var_force_value(var->type, var->var_ptr, val)) { + return std::string("OK: ") + pn + "." + vn + " FORCED = " + var_value_to_string(var->type, var->var_ptr); + } + return std::string("ERR: Invalid value for ") + var_type_name(var->type) + ": " + val; + } + + // --- unforce . --- + if (cmd == "unforce") { + if (args_str.empty()) return "ERR: Usage: unforce ."; + std::string pn, vn; + if (!parse_qualified_name(args_str, pn, vn)) return "ERR: Invalid format. Use: program.variable"; + auto* prog = find_program(programs, program_count, pn); + if (!prog) return "ERR: Unknown program: " + pn; + auto* var = find_var(prog, vn); + if (!var) return "ERR: Unknown variable: " + vn + " in " + pn; + var_unforce(var->type, var->var_ptr); + return std::string("OK: ") + pn + "." + vn + " unforced. Value: " + var_value_to_string(var->type, var->var_ptr); + } + + // --- unforce_all --- + if (cmd == "unforce_all") { + int count = 0; + for (size_t p = 0; p < program_count; ++p) { + for (size_t i = 0; i < programs[p].var_count; ++i) { + if (var_is_forced(programs[p].vars[i].type, programs[p].vars[i].var_ptr)) { + var_unforce(programs[p].vars[i].type, programs[p].vars[i].var_ptr); + ++count; + } + } + } + return "OK: Unforced " + std::to_string(count) + " variable(s)"; + } + + // --- list_vars [program] --- + if (cmd == "list_vars") { + std::string result = "OK:"; + for (size_t p = 0; p < program_count; ++p) { + if (!args_str.empty() && args_str != programs[p].name) continue; + for (size_t i = 0; i < programs[p].var_count; ++i) { + auto& v = programs[p].vars[i]; + bool forced = var_is_forced(v.type, v.var_ptr); + result += std::string("\n") + programs[p].name + "." + v.name + + " : " + var_type_name(v.type) + " = " + + var_value_to_string(v.type, v.var_ptr) + + (forced ? " [FORCED]" : ""); + } + } + return result; + } + + // --- list_forced --- + if (cmd == "list_forced") { + std::string result = "OK:"; + for (size_t p = 0; p < program_count; ++p) { + for (size_t i = 0; i < programs[p].var_count; ++i) { + auto& v = programs[p].vars[i]; + if (var_is_forced(v.type, v.var_ptr)) { + result += std::string("\n") + programs[p].name + "." + v.name + + " : " + var_type_name(v.type) + " = " + + var_value_to_string(v.type, v.var_ptr); + } + } + } + return result; + } + + // --- programs --- + if (cmd == "programs") { + std::string result = "OK:"; + for (size_t p = 0; p < program_count; ++p) { + result += std::string("\n") + programs[p].name + + " (" + std::to_string(programs[p].var_count) + " vars)"; + } + return result; + } + + return "ERR: Unknown command: " + cmd; +} + // ============================================================================= // Colored Output Helpers // ============================================================================= @@ -702,100 +857,34 @@ inline void repl_run(ProgramDescriptor* programs, size_t program_count, continue; } - // --- vars [program] --- - if (cmd == "vars") { - if (!args_str.empty()) { - auto* prog = find_program(programs, program_count, args_str); - if (!prog) { ic_printf("[red]Unknown program: %s[/]\n", args_str.c_str()); continue; } - for (size_t i = 0; i < prog->var_count; ++i) { - print_var_line(*prog, prog->vars[i]); - } - } else { - for (size_t p = 0; p < program_count; ++p) { - for (size_t i = 0; i < programs[p].var_count; ++i) { - print_var_line(programs[p], programs[p].vars[i]); - } - } - } - continue; - } - - // --- get . --- - if (cmd == "get") { - if (args_str.empty()) { ic_println("[red]Usage: get .[/]"); continue; } - std::string pn, vn; - if (!parse_qualified_name(args_str, pn, vn)) { ic_println("[red]Invalid format. Use: program.variable[/]"); continue; } - auto* prog = find_program(programs, program_count, pn); - if (!prog) { ic_printf("[red]Unknown program: %s[/]\n", pn.c_str()); continue; } - auto* var = find_var(prog, vn); - if (!var) { ic_printf("[red]Unknown variable: %s in %s[/]\n", vn.c_str(), pn.c_str()); continue; } - print_var_line(*prog, *var); - continue; - } - - // --- set . --- - if (cmd == "set") { - size_t vsp = args_str.find(' '); - if (vsp == std::string::npos) { ic_println("[red]Usage: set . [/]"); continue; } - std::string qname = args_str.substr(0, vsp); - std::string val = args_str.substr(vsp + 1); - // Trim val - size_t vs = val.find_first_not_of(" \t"); - if (vs != std::string::npos) val = val.substr(vs); - - std::string pn, vn; - if (!parse_qualified_name(qname, pn, vn)) { ic_println("[red]Invalid format. Use: program.variable[/]"); continue; } - auto* prog = find_program(programs, program_count, pn); - if (!prog) { ic_printf("[red]Unknown program: %s[/]\n", pn.c_str()); continue; } - auto* var = find_var(prog, vn); - if (!var) { ic_printf("[red]Unknown variable: %s in %s[/]\n", vn.c_str(), pn.c_str()); continue; } - if (var_set_value(var->type, var->var_ptr, val)) { - ic_printf(" [b]%s[/].%s = [green]%s[/]\n", pn.c_str(), vn.c_str(), var_value_to_string(var->type, var->var_ptr).c_str()); - } else { - ic_printf("[red]Invalid value for %s: %s[/]\n", var_type_name(var->type), val.c_str()); + // --- vars, get, set, force, unforce, programs --- + // Delegate to shared process_command() for data commands + if (cmd == "vars" || cmd == "get" || cmd == "set" || + cmd == "force" || cmd == "unforce" || cmd == "programs" || + cmd == "list_vars" || cmd == "list_forced" || cmd == "unforce_all") { + // For interactive "vars", map to "list_vars" to use the shared processor + std::string effective_line = line; + if (cmd == "vars") { + effective_line = "list_vars" + (args_str.empty() ? "" : " " + args_str); } - continue; - } - - // --- force . --- - if (cmd == "force") { - size_t vsp = args_str.find(' '); - if (vsp == std::string::npos) { ic_println("[red]Usage: force . [/]"); continue; } - std::string qname = args_str.substr(0, vsp); - std::string val = args_str.substr(vsp + 1); - size_t vs = val.find_first_not_of(" \t"); - if (vs != std::string::npos) val = val.substr(vs); - - std::string pn, vn; - if (!parse_qualified_name(qname, pn, vn)) { ic_println("[red]Invalid format. Use: program.variable[/]"); continue; } - auto* prog = find_program(programs, program_count, pn); - if (!prog) { ic_printf("[red]Unknown program: %s[/]\n", pn.c_str()); continue; } - auto* var = find_var(prog, vn); - if (!var) { ic_printf("[red]Unknown variable: %s in %s[/]\n", vn.c_str(), pn.c_str()); continue; } - if (var_force_value(var->type, var->var_ptr, val)) { - ic_printf(" [b]%s[/].%s [yellow b]FORCED[/] = [green]%s[/]\n", - pn.c_str(), vn.c_str(), var_value_to_string(var->type, var->var_ptr).c_str()); + std::string result = process_command(effective_line, programs, program_count); + // Display with isocline coloring + if (result.substr(0, 3) == "OK:") { + std::string msg = result.substr(3); + if (!msg.empty() && msg[0] == ' ') msg = msg.substr(1); + if (!msg.empty()) { + ic_printf("[green]%s[/]\n", msg.c_str()); + } + } else if (result.substr(0, 4) == "ERR:") { + std::string msg = result.substr(4); + if (!msg.empty() && msg[0] == ' ') msg = msg.substr(1); + ic_printf("[red]%s[/]\n", msg.c_str()); } else { - ic_printf("[red]Invalid value for %s: %s[/]\n", var_type_name(var->type), val.c_str()); + ic_println(result.c_str()); } continue; } - // --- unforce . --- - if (cmd == "unforce") { - if (args_str.empty()) { ic_println("[red]Usage: unforce .[/]"); continue; } - std::string pn, vn; - if (!parse_qualified_name(args_str, pn, vn)) { ic_println("[red]Invalid format. Use: program.variable[/]"); continue; } - auto* prog = find_program(programs, program_count, pn); - if (!prog) { ic_printf("[red]Unknown program: %s[/]\n", pn.c_str()); continue; } - auto* var = find_var(prog, vn); - if (!var) { ic_printf("[red]Unknown variable: %s in %s[/]\n", vn.c_str(), pn.c_str()); continue; } - var_unforce(var->type, var->var_ptr); - ic_printf(" [b]%s[/].%s [green]unforced[/]. Value: [green]%s[/]\n", - pn.c_str(), vn.c_str(), var_value_to_string(var->type, var->var_ptr).c_str()); - continue; - } - // --- code [line] [end] --- if (cmd == "code") { if (source_lines.empty()) { diff --git a/vscode-extension/client/src/debug-adapter-tracker.ts b/vscode-extension/client/src/debug-adapter-tracker.ts index 81c0071..5aee12e 100644 --- a/vscode-extension/client/src/debug-adapter-tracker.ts +++ b/vscode-extension/client/src/debug-adapter-tracker.ts @@ -39,6 +39,20 @@ export class StrucppDebugTrackerFactory } class StrucppDebugTracker implements vscode.DebugAdapterTracker { + /** + * Cache: maps (variablesReference, name) → evaluateName. + * Populated from variables responses, used to resolve setVariable requests. + */ + private varEvalNameCache = new Map(); + /** Track pending variables requests: seq → variablesReference */ + private pendingVarRequests = new Map(); + /** Last seen top frame ID (for evaluate requests that need a frame context) */ + private lastFrameId: number | undefined; + + private cacheKey(variablesReference: number, name: string): string { + return `${variablesReference}:${name}`; + } + /** * Intercept requests FROM VSCode TO the debug adapter. * Transform watch/REPL expressions from ST naming to C++ naming. @@ -47,23 +61,29 @@ class StrucppDebugTracker implements vscode.DebugAdapterTracker { const msg = message as { type?: string; command?: string; - arguments?: { - expression?: string; - context?: string; - }; + seq?: number; + arguments?: Record; }; + // Track variables requests so we can cache evaluateNames from responses + if (msg.type === "request" && msg.command === "variables" && msg.seq !== undefined) { + const varRef = msg.arguments?.variablesReference as number | undefined; + if (varRef !== undefined) { + this.pendingVarRequests.set(msg.seq, varRef); + } + } + if ( msg.type === "request" && msg.command === "evaluate" && msg.arguments?.expression ) { - const ctx = msg.arguments.context; + const ctx = msg.arguments.context as string | undefined; // Transform watch and REPL expressions. // Skip "hover" — handled by EvaluatableExpressionProvider which // also consults the language server for type-aware .value_ appending. if (ctx === "watch" || ctx === "repl") { - const original = msg.arguments.expression; + const original = msg.arguments.expression as string; const transformed = transformStExpression(original); if (transformed !== original) { log.appendLine(`[tracker] ${ctx}: "${original}" → "${transformed}"`); @@ -71,6 +91,46 @@ class StrucppDebugTracker implements vscode.DebugAdapterTracker { } } } + + // Intercept "Set Value" (setVariable/setExpression) for IECVar types. + if ( + msg.type === "request" && + (msg.command === "setVariable" || msg.command === "setExpression") + ) { + log.appendLine(`[tracker] Intercepted ${msg.command}: ${JSON.stringify(msg.arguments)}`); + + if (msg.command === "setExpression" && msg.arguments?.expression && msg.arguments?.value !== undefined) { + const expr = transformStExpression(msg.arguments.expression as string); + const val = msg.arguments.value as string; + // Rewrite: setExpression → evaluate with .value_ assignment + // Use uppercase C++ name and "variables" context to bypass further transformation + msg.command = "evaluate"; + msg.arguments.expression = `${expr}.value_ = ${val}`; + msg.arguments.context = "variables"; + delete msg.arguments.value; + log.appendLine(`[tracker] setExpression → evaluate: ${msg.arguments.expression}`); + } + + if (msg.command === "setVariable" && msg.arguments?.name && msg.arguments?.value !== undefined) { + const varRef = msg.arguments.variablesReference as number | undefined; + const name = msg.arguments.name as string; + const val = msg.arguments.value as string; + const evalName = varRef !== undefined + ? this.varEvalNameCache.get(this.cacheKey(varRef, name)) + : undefined; + log.appendLine(`[tracker] setVariable for "${name}" = "${val}" (ref=${varRef}, evalName=${evalName ?? "NOT CACHED"})`); + if (evalName) { + // Rewrite: setVariable → evaluate with .value_ field assignment + msg.command = "evaluate"; + msg.arguments = { + expression: `${evalName}.value_ = ${val}`, + context: "variables", + ...(this.lastFrameId !== undefined ? { frameId: this.lastFrameId } : {}), + }; + log.appendLine(`[tracker] Rewrote to evaluate: ${msg.arguments.expression} (frameId=${this.lastFrameId})`); + } + } + } } /** @@ -81,6 +141,7 @@ class StrucppDebugTracker implements vscode.DebugAdapterTracker { const msg = message as { type?: string; command?: string; + request_seq?: number; body?: { variables?: DAPVariable[]; // evaluate response fields @@ -92,10 +153,38 @@ class StrucppDebugTracker implements vscode.DebugAdapterTracker { }; }; + // Clear caches when the target resumes — cached references become stale. + const evt = message as { type?: string; event?: string }; + if (evt.type === "event" && evt.event === "continued") { + this.varEvalNameCache.clear(); + this.pendingVarRequests.clear(); + return; + } + if (msg.type !== "response" || !msg.body) return; + // Cache the top frame ID from stackTrace responses + if (msg.command === "stackTrace") { + const frames = (msg.body as Record)?.stackFrames as Array<{ id: number }> | undefined; + if (frames && frames.length > 0) { + this.lastFrameId = frames[0].id; + } + } + // Handle variables responses (Variables pane) if (msg.command === "variables" && msg.body.variables) { + // Cache evaluateNames for setVariable rewriting + const varRef = msg.request_seq !== undefined + ? this.pendingVarRequests.get(msg.request_seq) + : undefined; + if (varRef !== undefined) { + this.pendingVarRequests.delete(msg.request_seq!); + for (const v of msg.body.variables) { + if (v.evaluateName) { + this.varEvalNameCache.set(this.cacheKey(varRef, v.name), v.evaluateName); + } + } + } this.transformVariables(msg.body.variables); } diff --git a/vscode-extension/client/src/debug-config-builder.ts b/vscode-extension/client/src/debug-config-builder.ts index 0247a7d..1d74615 100644 --- a/vscode-extension/client/src/debug-config-builder.ts +++ b/vscode-extension/client/src/debug-config-builder.ts @@ -137,6 +137,7 @@ export function buildDebugConfig( overrides?: DebugConfigOverrides, ): vscode.DebugConfiguration { const name = overrides?.name || "Debug ST Program"; + const args = ["--cyclic"]; if (debugType === "lldb") { return { @@ -144,7 +145,7 @@ export function buildDebugConfig( request: "launch", name, program: build.binaryPath, - args: ["--cyclic"], + args, cwd: build.outputDir, __strucpp: true, initCommands: getLLDBInitCommands(), @@ -158,7 +159,7 @@ export function buildDebugConfig( request: "launch", name, program: build.binaryPath, - args: ["--cyclic"], + args, cwd: build.outputDir, __strucpp: true, MIMode: miMode, diff --git a/vscode-extension/client/src/force-variable.ts b/vscode-extension/client/src/force-variable.ts index 01ece4b..f886fcb 100644 --- a/vscode-extension/client/src/force-variable.ts +++ b/vscode-extension/client/src/force-variable.ts @@ -3,22 +3,100 @@ /** * Force/Unforce variable commands and Forced Variables panel. * - * Uses IECVar::force(value) and IECVar::unforce() C++ methods - * via the debug adapter's evaluate request. + * Forces variables by writing directly to IECVar internal fields via + * the debug adapter's evaluate request. GDB/LLDB can set struct fields + * without needing to call template methods: + * + * evaluateName.forced_ = true + * evaluateName.forced_value_ = + * evaluateName.value_ = + * + * This works when stopped at a breakpoint (the primary use case) because + * GDB has full access to process memory and ignores C++ access specifiers. */ import * as vscode from "vscode"; /** A variable that has been forced to a specific value. */ export interface ForcedVariableEntry { - /** C++ evaluate path (e.g. "TICK_TIMER.IN") */ + /** GDB evaluateName for the variable */ evaluateName: string; /** Display name shown in the panel */ displayName: string; /** The value the variable is forced to */ forcedValue: string; - /** C++ type (for display) */ - type?: string; +} + +/** + * Execute a GDB/LLDB expression via the debug adapter. + * Uses the topmost stack frame of the active thread for context. + * Returns the result string, or throws on failure. + */ +async function debugEvaluate( + session: vscode.DebugSession, + expression: string, +): Promise { + // Get the active thread's topmost stack frame for evaluation context + let frameId: number | undefined; + try { + const threads = await session.customRequest("threads"); + if (threads?.threads?.length > 0) { + const threadId = threads.threads[0].id; + const stack = await session.customRequest("stackTrace", { + threadId, + startFrame: 0, + levels: 1, + }); + if (stack?.stackFrames?.length > 0) { + frameId = stack.stackFrames[0].id; + } + } + } catch { + // If we can't get a frame, try without one + } + + // Use context "variables" to bypass the debug adapter tracker's + // ST-to-C++ expression transformation (which uppercases identifiers + // and would break C++ namespace resolution like strucpp::). + const result = await session.customRequest("evaluate", { + expression, + context: "variables", + ...(frameId !== undefined ? { frameId } : {}), + }); + return result?.result ?? ""; +} + +/** + * Force a variable by writing to its IECVar internal fields via GDB. + * Sets forced_ = true, forced_value_ = value, value_ = value. + */ +async function forceViaDebugger( + session: vscode.DebugSession, + evaluateName: string, + value: string, +): Promise { + // Write all three fields — GDB ignores C++ access specifiers + await debugEvaluate(session, `${evaluateName}.forced_ = true`); + await debugEvaluate(session, `${evaluateName}.forced_value_ = ${value}`); + await debugEvaluate(session, `${evaluateName}.value_ = ${value}`); + + // Trigger a Variables pane refresh by re-reading the value we just set. + // This causes cppdbg to invalidate its cached variable display. + try { + await debugEvaluate(session, `${evaluateName}.value_`); + } catch { + // Best-effort — the force already succeeded + } +} + +/** + * Unforce a variable by clearing its IECVar forced_ flag via GDB. + */ +async function unforceViaDebugger( + session: vscode.DebugSession, + evaluateName: string, +): Promise { + await debugEvaluate(session, `${evaluateName}.forced_ = false`); } /** @@ -51,10 +129,7 @@ export async function forceVariableCommand( if (value === undefined) return; // cancelled try { - await session.customRequest("evaluate", { - expression: `${evaluateName}.force(${value})`, - context: "repl", - }); + await forceViaDebugger(session, evaluateName, value); provider.addForced({ evaluateName, @@ -82,7 +157,6 @@ export async function unforceVariableCommand( return; } - // From Forced Variables panel (TreeItem) or Variables pane context menu const evaluateName = args?.entry?.evaluateName ?? args?.variable?.evaluateName; if (!evaluateName) { vscode.window.showWarningMessage("Cannot unforce this variable — no evaluate path available."); @@ -90,11 +164,7 @@ export async function unforceVariableCommand( } try { - await session.customRequest("evaluate", { - expression: `${evaluateName}.unforce()`, - context: "repl", - }); - + await unforceViaDebugger(session, evaluateName); provider.removeForced(evaluateName); } catch (err) { vscode.window.showErrorMessage( @@ -120,10 +190,7 @@ export async function unforceAllCommand( for (const entry of entries) { try { - await session.customRequest("evaluate", { - expression: `${entry.evaluateName}.unforce()`, - context: "repl", - }); + await unforceViaDebugger(session, entry.evaluateName); } catch (err) { errors.push(`${entry.displayName}: ${err instanceof Error ? err.message : String(err)}`); } @@ -168,10 +235,8 @@ export class ForcedVariablesProvider vscode.TreeItemCollapsibleState.None, ); item.iconPath = new vscode.ThemeIcon("lock"); - item.tooltip = `${element.evaluateName} forced to ${element.forcedValue}`; + item.tooltip = `Forced to ${element.forcedValue}`; item.contextValue = "forcedVariable"; - // Pass the entry as command argument for inline unforce button - item.command = undefined; return item; } @@ -180,11 +245,6 @@ export class ForcedVariablesProvider return this.entries; } - /** Get the entry for a tree item (used by unforce command). */ - getEntryByEvaluateName(evaluateName: string): ForcedVariableEntry | undefined { - return this.entries.find((e) => e.evaluateName === evaluateName); - } - /** Get all entries (used by unforce all). */ getEntries(): readonly ForcedVariableEntry[] { return this.entries;