Compare commits
2
Commits
2026.8.200
...
2026.8.300
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c3d5116885 | ||
|
|
07ccd2d26a |
+5
-1
@@ -9,6 +9,7 @@ vite.config.js
|
||||
.prettierrc.json
|
||||
esbuild.js
|
||||
.gitea
|
||||
**/.ruff_cache/**
|
||||
.venv/**
|
||||
.nox/**
|
||||
server/.venv/**
|
||||
@@ -18,4 +19,7 @@ server/.nox/**
|
||||
**/requirements.in
|
||||
**/server/src/_debug_server.py
|
||||
server/noxfile.py
|
||||
client/**
|
||||
server/.claude/**
|
||||
server/tests/**
|
||||
client/**
|
||||
dist/**/*.map
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
## Unreleased
|
||||
|
||||
- Integrate the NX Tcl Remote Debugger directly into NX Postprocessor Support
|
||||
- Add `nx-tcl` attach configurations and breakpoint support for TCL and DEF files
|
||||
- Support breakpoints, stepping, stack frames, variables, watches, evaluation, logpoints, hit conditions, and Tcl error stops
|
||||
- Add generation-safe stack and variable references so delayed VS Code requests cannot address stale NX frames
|
||||
- Add command-buffer breakpoint handling for `LIB_GE_command_buffer_edit_*` source bodies
|
||||
- Extend the README with launch configuration, path mapping, and debugger usage
|
||||
- Prevent truncated TCL inlay hints and add configurable parameter hint modes
|
||||
- Add inlay hints for built-in NX procedures, variadic arguments, and visible ranges
|
||||
- Add inlay hint documentation and navigation to custom procedure definitions
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# NX Postprocessor Support
|
||||
|
||||
A comprehensive VS Code extension providing language support for NX CAM postprocessor development, including CDL, TCL, and DEF files.
|
||||
A comprehensive VS Code extension providing language support and remote debugging for NX CAM postprocessor development, including CDL, TCL, and DEF files.
|
||||
|
||||
## Features
|
||||
|
||||
@@ -10,6 +10,7 @@ A comprehensive VS Code extension providing language support for NX CAM postproc
|
||||
- **Intelligent Code Analysis** - Linting and error detection for postprocessor code
|
||||
- **Auto-completion** - Context-aware code completion for faster development
|
||||
- **Signature Help** - Shows parameters and documentation for custom and NX procedures
|
||||
- **NX Tcl Remote Debugger** - Breakpoints, stepping, call stack, scopes, variables, watches, evaluation, logpoints, hit conditions, and Tcl error stops directly in a running NX Post process
|
||||
|
||||
## Supported File Types
|
||||
|
||||
@@ -24,6 +25,10 @@ A comprehensive VS Code extension providing language support for NX CAM postproc
|
||||
3. Open any `.cdl`, `.tcl`, or `.def` file
|
||||
4. The extension will automatically activate and provide language support
|
||||
|
||||
The former standalone `NX Tcl Remote Debugger` extension is no longer required. Disable or
|
||||
uninstall `local-nx.nx-tcl-debug` before using the integrated debugger because both extensions
|
||||
register the same `nx-tcl` debug type.
|
||||
|
||||
## Configuration
|
||||
|
||||
The extension can be configured through VS Code settings:
|
||||
@@ -38,6 +43,49 @@ TCL files default to unlimited inlay hint length so that VS Code does not
|
||||
truncate later parameter names on a line. An explicit user setting for
|
||||
`editor.inlayHints.maximumLength` still takes precedence.
|
||||
|
||||
## NX Tcl Remote Debugger
|
||||
|
||||
### Add a VS Code attach configuration
|
||||
|
||||
Create `.vscode/launch.json` through **Run and Debug: create a launch.json file** and select
|
||||
**NX Tcl: Attach to NX Post**, or use this configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "nx-tcl",
|
||||
"request": "attach",
|
||||
"name": "Attach to NX Post Tcl",
|
||||
"host": "127.0.0.1",
|
||||
"port": 4711,
|
||||
"connectTimeout": 120000,
|
||||
"stopOnEntry": false,
|
||||
"breakOnError": true,
|
||||
"localRoot": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
When VS Code and NX see the source through different roots, set `remoteRoot` to the root used by
|
||||
NX and keep `localRoot` as the corresponding workspace root.
|
||||
|
||||
### Start debugging
|
||||
|
||||
1. Set breakpoints on executable Tcl or DEF commands.
|
||||
2. Start **Attach to NX Post Tcl** in VS Code before running the postprocessor.
|
||||
3. Start postprocessing in NX.
|
||||
4. Use Continue, Step Over, Step Into, Step Out, Pause, Variables, Watch, and the Debug Console as
|
||||
with a normal source debugger.
|
||||
|
||||
Breakpoints remain active and stop at every real invocation. Use a hit condition such as `1` for
|
||||
a one-time stop. Blank lines, comments, declarations, and multiline Tcl commands may be relocated
|
||||
to the nearest executable command; VS Code shows the resolved line. Dynamic
|
||||
`LIB_GE_command_buffer_edit_*` bodies are mapped back to their original source and owning Tcl
|
||||
procedure.
|
||||
|
||||
## Usage
|
||||
|
||||
Simply open any supported file type and enjoy:
|
||||
@@ -48,6 +96,7 @@ Simply open any supported file type and enjoy:
|
||||
- Code formatting (Format Document command)
|
||||
- Hover information
|
||||
- Signature help while entering procedure arguments
|
||||
- Remote NX Tcl debugging with breakpoints and full stepping
|
||||
|
||||
## Development and debugging
|
||||
|
||||
@@ -56,6 +105,7 @@ Install the root and client dependencies before the first debug session:
|
||||
```powershell
|
||||
npm install
|
||||
npm install --prefix client
|
||||
npm run test:debugger
|
||||
```
|
||||
|
||||
Use one of the checked-in VS Code launch configurations:
|
||||
@@ -78,4 +128,5 @@ This extension is actively maintained. For issues or feature requests, please vi
|
||||
|
||||
## License
|
||||
|
||||
AGPL-3.0 License - see LICENSE file for details.
|
||||
AGPL-3.0 License - see LICENSE file for details. The embedded NX Tcl Remote Debugger adapter
|
||||
retains its MIT notice in `debugger/NX_TCL_DEBUGGER_LICENSE.txt`.
|
||||
|
||||
@@ -0,0 +1,650 @@
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
const { RuntimeConnection, encodeHex, decodeHex } = require('./protocol');
|
||||
|
||||
const THREAD_ID = 1;
|
||||
|
||||
class NxDebugAdapter {
|
||||
constructor(input = process.stdin, output = process.stdout) {
|
||||
this.input = input;
|
||||
this.output = output;
|
||||
this.inputBuffer = Buffer.alloc(0);
|
||||
this.sequence = 1;
|
||||
this.runtime = null;
|
||||
this.terminated = false;
|
||||
this.explicitDisconnect = false;
|
||||
this.localRoot = '';
|
||||
this.remoteRoot = '';
|
||||
this.nextBreakpointId = 1;
|
||||
// VS Code can expose the same physical file through more than one source
|
||||
// identity (for example, drive mappings or differently cased workspace
|
||||
// roots). SETBPS replaces all breakpoints for a runtime path, so retain
|
||||
// each DAP source group and send their union to NX.
|
||||
this.sourceBreakpointGroups = new Map();
|
||||
this.pendingStartRequest = null;
|
||||
this.resumeInProgress = 0;
|
||||
this.pendingStoppedEvents = [];
|
||||
this.isPaused = false;
|
||||
// DAP frame/variable IDs can outlive a stop in the VS Code UI. Runtime
|
||||
// levels and references, however, are valid only for the current Tcl
|
||||
// pause. Give every stop adapter-owned IDs so a late request can never
|
||||
// accidentally address a same-numbered frame from a later pause.
|
||||
this.nextFrameId = 1;
|
||||
this.frameReferences = new Map();
|
||||
this.runtimeToFrameReference = new Map();
|
||||
this.nextVariablesReference = 1;
|
||||
this.variableReferences = new Map();
|
||||
this.runtimeToVariableReference = new Map();
|
||||
|
||||
input.on('data', (chunk) => this.onData(chunk));
|
||||
input.on('end', () => this.dispose());
|
||||
input.resume();
|
||||
}
|
||||
|
||||
onData(chunk) {
|
||||
this.inputBuffer = Buffer.concat([this.inputBuffer, chunk]);
|
||||
while (true) {
|
||||
const headerEnd = this.inputBuffer.indexOf('\r\n\r\n');
|
||||
if (headerEnd < 0) return;
|
||||
const header = this.inputBuffer.subarray(0, headerEnd).toString('ascii');
|
||||
const match = /Content-Length:\s*(\d+)/i.exec(header);
|
||||
if (!match) throw new Error('missing DAP Content-Length header');
|
||||
const contentLength = Number(match[1]);
|
||||
const messageEnd = headerEnd + 4 + contentLength;
|
||||
if (this.inputBuffer.length < messageEnd) return;
|
||||
const payload = this.inputBuffer.subarray(headerEnd + 4, messageEnd).toString('utf8');
|
||||
this.inputBuffer = this.inputBuffer.subarray(messageEnd);
|
||||
const message = JSON.parse(payload);
|
||||
if (message.type === 'request') {
|
||||
this.handleRequest(message).catch((error) => this.sendErrorResponse(message, error));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
send(message) {
|
||||
message.seq = this.sequence++;
|
||||
const json = JSON.stringify(message);
|
||||
this.output.write(`Content-Length: ${Buffer.byteLength(json, 'utf8')}\r\n\r\n${json}`);
|
||||
}
|
||||
|
||||
sendResponse(request, body = {}) {
|
||||
this.send({
|
||||
type: 'response',
|
||||
request_seq: request.seq,
|
||||
success: true,
|
||||
command: request.command,
|
||||
body,
|
||||
});
|
||||
}
|
||||
|
||||
sendErrorResponse(request, error) {
|
||||
this.send({
|
||||
type: 'response',
|
||||
request_seq: request.seq,
|
||||
success: false,
|
||||
command: request.command,
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
|
||||
sendEvent(event, body = {}) {
|
||||
this.send({ type: 'event', event, body });
|
||||
}
|
||||
|
||||
async handleRequest(request) {
|
||||
const args = request.arguments || {};
|
||||
switch (request.command) {
|
||||
case 'initialize':
|
||||
this.sendResponse(request, {
|
||||
supportsConfigurationDoneRequest: true,
|
||||
supportsConditionalBreakpoints: true,
|
||||
supportsHitConditionalBreakpoints: true,
|
||||
supportsLogPoints: true,
|
||||
supportsEvaluateForHovers: true,
|
||||
supportsSetVariable: true,
|
||||
supportsExceptionFilterOptions: true,
|
||||
exceptionBreakpointFilters: [
|
||||
{ filter: 'tclError', label: 'Tcl errors', description: 'Break when a Tcl command returns TCL_ERROR' },
|
||||
],
|
||||
supportsTerminateRequest: true,
|
||||
});
|
||||
break;
|
||||
|
||||
case 'attach':
|
||||
case 'launch':
|
||||
await this.attach(args);
|
||||
this.pendingStartRequest = request;
|
||||
this.sendEvent('initialized');
|
||||
break;
|
||||
|
||||
case 'configurationDone':
|
||||
await this.runtime.request('CONFIGDONE');
|
||||
this.sendResponse(request);
|
||||
if (this.pendingStartRequest) {
|
||||
this.sendResponse(this.pendingStartRequest);
|
||||
this.pendingStartRequest = null;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'setBreakpoints':
|
||||
await this.setBreakpoints(request, args);
|
||||
break;
|
||||
|
||||
case 'setExceptionBreakpoints': {
|
||||
const filters = args.filters || [];
|
||||
await this.runtime.request('SETEXCEPTIONS', filters);
|
||||
this.sendResponse(request, {
|
||||
breakpoints: filters.map(() => ({ verified: true })),
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'threads':
|
||||
this.sendResponse(request, { threads: [{ id: THREAD_ID, name: 'NX Post Tcl' }] });
|
||||
break;
|
||||
|
||||
case 'stackTrace':
|
||||
await this.stackTrace(request);
|
||||
break;
|
||||
|
||||
case 'scopes':
|
||||
await this.scopes(request, args);
|
||||
break;
|
||||
|
||||
case 'variables':
|
||||
await this.variables(request, args);
|
||||
break;
|
||||
|
||||
case 'evaluate':
|
||||
await this.evaluate(request, args);
|
||||
break;
|
||||
|
||||
case 'setVariable':
|
||||
await this.setVariable(request, args);
|
||||
break;
|
||||
|
||||
case 'continue':
|
||||
await this.resume(request, 'CONTINUE', { allThreadsContinued: true });
|
||||
break;
|
||||
|
||||
case 'next':
|
||||
await this.resume(request, 'NEXT');
|
||||
break;
|
||||
|
||||
case 'stepIn':
|
||||
await this.resume(request, 'STEPIN');
|
||||
break;
|
||||
|
||||
case 'stepOut':
|
||||
await this.resume(request, 'STEPOUT');
|
||||
break;
|
||||
|
||||
case 'pause':
|
||||
await this.runtime.request('PAUSE');
|
||||
this.sendResponse(request);
|
||||
break;
|
||||
|
||||
case 'disconnect':
|
||||
case 'terminate':
|
||||
await this.disconnect(request);
|
||||
break;
|
||||
|
||||
default:
|
||||
this.sendResponse(request);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async attach(args) {
|
||||
const host = args.host || '127.0.0.1';
|
||||
const port = Number(args.port || 4711);
|
||||
const timeout = Number(args.connectTimeout || 120000);
|
||||
this.localRoot = args.localRoot ? path.resolve(args.localRoot) : '';
|
||||
this.remoteRoot = args.remoteRoot ? this.normalizePath(args.remoteRoot) : '';
|
||||
this.sourceBreakpointGroups.clear();
|
||||
this.isPaused = false;
|
||||
this.resetStoppedReferences(true);
|
||||
|
||||
this.runtime = new RuntimeConnection();
|
||||
this.runtime.on('event', (name, fields) => this.onRuntimeEvent(name, fields));
|
||||
this.runtime.on('runtimeError', (error) => {
|
||||
this.sendEvent('output', { category: 'stderr', output: `NX: ${error.message}\n` });
|
||||
});
|
||||
this.runtime.on('close', () => {
|
||||
if (!this.explicitDisconnect) this.sendTerminated();
|
||||
});
|
||||
|
||||
const started = Date.now();
|
||||
let lastError;
|
||||
while (Date.now() - started < timeout) {
|
||||
try {
|
||||
await this.runtime.connect(host, port);
|
||||
lastError = null;
|
||||
break;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||
}
|
||||
}
|
||||
if (lastError) {
|
||||
throw new Error(`could not connect to NX at ${host}:${port}: ${lastError.message}`);
|
||||
}
|
||||
await this.runtime.request('CONFIG', [args.stopOnEntry ? 1 : 0, args.breakOnError ? 1 : 0]);
|
||||
}
|
||||
|
||||
async setBreakpoints(request, args) {
|
||||
const sourcePath = args.source && args.source.path ? args.source.path : '';
|
||||
const remotePath = this.toRemotePath(sourcePath);
|
||||
const sourceKey = this.breakpointSourceKey(args.source, sourcePath);
|
||||
const remoteKey = this.breakpointRemoteKey(remotePath);
|
||||
const requested = args.breakpoints || (args.lines || []).map((line) => ({ line }));
|
||||
const records = requested.map((breakpoint) => ({
|
||||
id: this.nextBreakpointId++,
|
||||
line: breakpoint.line,
|
||||
condition: breakpoint.condition || '',
|
||||
hitCondition: breakpoint.hitCondition || '',
|
||||
logMessage: breakpoint.logMessage || '',
|
||||
}));
|
||||
if (!this.isTclSource(sourcePath)) {
|
||||
this.sourceBreakpointGroups.delete(sourceKey);
|
||||
// VS Code forwards breakpoints from every language to every active debug
|
||||
// session. Clear any stale runtime entry left by an older adapter, but do
|
||||
// not make the NX Tcl runtime parse or instrument a Python source file.
|
||||
await this.runtime.request('SETBPS', [encodeHex(remotePath), 0]);
|
||||
this.sendResponse(request, {
|
||||
breakpoints: records.map((breakpoint) => ({
|
||||
id: breakpoint.id,
|
||||
verified: false,
|
||||
line: breakpoint.line,
|
||||
source: args.source,
|
||||
message: 'NX Tcl Remote Debugger supports breakpoints only in .tcl and .def files',
|
||||
})),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.sourceBreakpointGroups.set(sourceKey, { remotePath, remoteKey, records });
|
||||
|
||||
// Collapse exact duplicates shared by aliases, while remembering which
|
||||
// runtime breakpoint represents every DAP breakpoint ID. This keeps hit
|
||||
// counts and log points from firing twice for one Tcl command.
|
||||
const mergedBreakpoints = [];
|
||||
const mergedBySignature = new Map();
|
||||
const representativeById = new Map();
|
||||
for (const group of this.sourceBreakpointGroups.values()) {
|
||||
if (group.remoteKey !== remoteKey) continue;
|
||||
for (const breakpoint of group.records) {
|
||||
const signature = this.breakpointSignature(breakpoint);
|
||||
let representative = mergedBySignature.get(signature);
|
||||
if (!representative) {
|
||||
representative = breakpoint;
|
||||
mergedBySignature.set(signature, representative);
|
||||
mergedBreakpoints.push(representative);
|
||||
}
|
||||
representativeById.set(breakpoint.id, representative.id);
|
||||
}
|
||||
}
|
||||
|
||||
const fields = [encodeHex(remotePath), mergedBreakpoints.length];
|
||||
for (const breakpoint of mergedBreakpoints) {
|
||||
fields.push(
|
||||
breakpoint.id,
|
||||
breakpoint.line,
|
||||
encodeHex(breakpoint.condition),
|
||||
encodeHex(breakpoint.hitCondition),
|
||||
encodeHex(breakpoint.logMessage),
|
||||
);
|
||||
}
|
||||
const runtimeResponse = await this.runtime.request('SETBPS', fields);
|
||||
const responseFields = runtimeResponse.fields || [];
|
||||
const responseCount = Number(responseFields[0] || 0);
|
||||
const responseStride = responseFields.length >= 1 + responseCount * 4 ? 4 : 2;
|
||||
const runtimeBreakpoints = new Map();
|
||||
let responseCursor = 1;
|
||||
for (let index = 0; index < responseCount; index++) {
|
||||
const id = Number(responseFields[responseCursor++]);
|
||||
const line = Number(responseFields[responseCursor++]);
|
||||
let verified = true;
|
||||
let message = '';
|
||||
if (responseStride === 4) {
|
||||
verified = responseFields[responseCursor++] === '1';
|
||||
message = decodeHex(responseFields[responseCursor++]);
|
||||
}
|
||||
runtimeBreakpoints.set(id, { line, verified, message });
|
||||
}
|
||||
this.sendResponse(request, {
|
||||
breakpoints: records.map((breakpoint) => {
|
||||
const representativeId = representativeById.get(breakpoint.id);
|
||||
const runtimeBreakpoint = runtimeBreakpoints.get(representativeId);
|
||||
const result = {
|
||||
id: breakpoint.id,
|
||||
verified: runtimeBreakpoint ? runtimeBreakpoint.verified : false,
|
||||
line: runtimeBreakpoint && runtimeBreakpoint.line ? runtimeBreakpoint.line : breakpoint.line,
|
||||
source: args.source,
|
||||
};
|
||||
if (runtimeBreakpoint && runtimeBreakpoint.message) result.message = runtimeBreakpoint.message;
|
||||
return result;
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
async stackTrace(request) {
|
||||
if (!this.isPaused) {
|
||||
this.sendResponse(request, { stackFrames: [], totalFrames: 0 });
|
||||
return;
|
||||
}
|
||||
const { fields } = await this.runtime.request('STACK');
|
||||
const count = Number(fields[0] || 0);
|
||||
const stackFrames = [];
|
||||
let cursor = 1;
|
||||
for (let index = 0; index < count; index++) {
|
||||
const runtimeId = Number(fields[cursor++]);
|
||||
const name = decodeHex(fields[cursor++]);
|
||||
const remoteFile = decodeHex(fields[cursor++]);
|
||||
const line = Number(fields[cursor++]);
|
||||
const column = Number(fields[cursor++]);
|
||||
const localFile = this.toLocalPath(remoteFile);
|
||||
const frame = {
|
||||
id: this.toAdapterFrameId(runtimeId),
|
||||
name: name || '<Tcl>',
|
||||
line: line || 1,
|
||||
column: column || 1,
|
||||
};
|
||||
if (localFile) frame.source = { name: path.basename(localFile), path: localFile };
|
||||
stackFrames.push(frame);
|
||||
}
|
||||
this.sendResponse(request, { stackFrames, totalFrames: stackFrames.length });
|
||||
}
|
||||
|
||||
async scopes(request, args) {
|
||||
const runtimeFrameId = this.frameReferences.get(Number(args.frameId));
|
||||
if (!this.isPaused || runtimeFrameId === undefined) {
|
||||
this.sendResponse(request, { scopes: [] });
|
||||
return;
|
||||
}
|
||||
const { fields } = await this.runtime.request('SCOPES', [runtimeFrameId]);
|
||||
const count = Number(fields[0] || 0);
|
||||
const scopes = [];
|
||||
let cursor = 1;
|
||||
for (let index = 0; index < count; index++) {
|
||||
const variablesReference = this.toAdapterVariablesReference(Number(fields[cursor++]));
|
||||
const name = decodeHex(fields[cursor++]);
|
||||
cursor++; // wire label: expensive
|
||||
const expensive = fields[cursor++] === '1';
|
||||
scopes.push({ name, variablesReference, expensive });
|
||||
}
|
||||
this.sendResponse(request, { scopes });
|
||||
}
|
||||
|
||||
async variables(request, args) {
|
||||
const runtimeReference = this.variableReferences.get(Number(args.variablesReference));
|
||||
if (!this.isPaused || runtimeReference === undefined) {
|
||||
this.sendResponse(request, { variables: [] });
|
||||
return;
|
||||
}
|
||||
const { fields } = await this.runtime.request('VARIABLES', [
|
||||
runtimeReference,
|
||||
args.start || 0,
|
||||
args.count || 0,
|
||||
encodeHex(args.filter || ''),
|
||||
]);
|
||||
const count = Number(fields[0] || 0);
|
||||
const variables = [];
|
||||
let cursor = 1;
|
||||
for (let index = 0; index < count; index++) {
|
||||
variables.push({
|
||||
name: decodeHex(fields[cursor++]),
|
||||
value: decodeHex(fields[cursor++]),
|
||||
type: decodeHex(fields[cursor++]),
|
||||
variablesReference: this.toAdapterVariablesReference(Number(fields[cursor++])),
|
||||
indexedVariables: Number(fields[cursor++]) || undefined,
|
||||
namedVariables: Number(fields[cursor++]) || undefined,
|
||||
});
|
||||
}
|
||||
this.sendResponse(request, { variables });
|
||||
}
|
||||
|
||||
async evaluate(request, args) {
|
||||
// VS Code can refresh Watch/Hover expressions after Continue but before
|
||||
// it has processed the next stopped event. Do not forward that transient
|
||||
// request to the running NX interpreter (which correctly rejects it).
|
||||
if (!this.isPaused) {
|
||||
this.sendResponse(request, {
|
||||
result: '<NX is running; evaluation is available while paused>',
|
||||
type: 'unavailable',
|
||||
variablesReference: 0,
|
||||
});
|
||||
return;
|
||||
}
|
||||
let runtimeFrameId = 0;
|
||||
if (Number(args.frameId) > 0) {
|
||||
runtimeFrameId = this.frameReferences.get(Number(args.frameId));
|
||||
if (runtimeFrameId === undefined) {
|
||||
this.sendResponse(request, {
|
||||
result: '<stack frame is no longer available>',
|
||||
type: 'unavailable',
|
||||
variablesReference: 0,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
const { fields } = await this.runtime.request('EVALUATE', [runtimeFrameId, encodeHex(args.expression || '')]);
|
||||
this.sendResponse(request, {
|
||||
result: decodeHex(fields[0]),
|
||||
type: decodeHex(fields[1]),
|
||||
variablesReference: this.toAdapterVariablesReference(Number(fields[2])),
|
||||
indexedVariables: Number(fields[3]) || undefined,
|
||||
namedVariables: Number(fields[4]) || undefined,
|
||||
});
|
||||
}
|
||||
|
||||
async setVariable(request, args) {
|
||||
const runtimeReference = this.variableReferences.get(Number(args.variablesReference));
|
||||
if (!this.isPaused || runtimeReference === undefined) {
|
||||
throw new Error('variable is no longer available');
|
||||
}
|
||||
const { fields } = await this.runtime.request('SETVARIABLE', [
|
||||
runtimeReference,
|
||||
encodeHex(args.name),
|
||||
encodeHex(args.value),
|
||||
]);
|
||||
this.sendResponse(request, {
|
||||
value: decodeHex(fields[0]),
|
||||
type: decodeHex(fields[1]),
|
||||
variablesReference: this.toAdapterVariablesReference(Number(fields[2])),
|
||||
indexedVariables: Number(fields[3]) || undefined,
|
||||
namedVariables: Number(fields[4]) || undefined,
|
||||
});
|
||||
}
|
||||
|
||||
async resume(request, command, responseBody = {}) {
|
||||
this.isPaused = false;
|
||||
this.resetStoppedReferences();
|
||||
this.resumeInProgress++;
|
||||
try {
|
||||
await this.runtime.request(command);
|
||||
// The runtime can hit the next breakpoint immediately and place its
|
||||
// STOPPED event in the same TCP packet as this response. Always publish
|
||||
// the DAP resume transition before releasing such a queued stop.
|
||||
this.sendResponse(request, responseBody);
|
||||
this.sendEvent('continued', { threadId: THREAD_ID, allThreadsContinued: true });
|
||||
} finally {
|
||||
this.resumeInProgress--;
|
||||
if (this.resumeInProgress === 0) this.flushStoppedEvents();
|
||||
}
|
||||
}
|
||||
|
||||
async disconnect(request) {
|
||||
this.explicitDisconnect = true;
|
||||
this.isPaused = false;
|
||||
this.resetStoppedReferences();
|
||||
if (this.runtime && !this.runtime.closed) {
|
||||
try {
|
||||
await this.runtime.request('DISCONNECT', [], 2000);
|
||||
} catch (_) {
|
||||
// The runtime may close immediately after acknowledging detach.
|
||||
}
|
||||
this.runtime.close();
|
||||
}
|
||||
this.sendResponse(request);
|
||||
this.sendTerminated();
|
||||
}
|
||||
|
||||
onRuntimeEvent(name, fields) {
|
||||
switch (name) {
|
||||
case 'HELLO':
|
||||
this.sendEvent('output', {
|
||||
category: 'console',
|
||||
output: `NX runtime ${decodeHex(fields[0])}, Tcl ${decodeHex(fields[1])}, PID ${fields[2]}\n`,
|
||||
});
|
||||
break;
|
||||
case 'STOPPED':
|
||||
this.isPaused = true;
|
||||
this.resetStoppedReferences();
|
||||
this.publishStoppedEvent({
|
||||
reason: decodeHex(fields[0]) || 'pause',
|
||||
threadId: THREAD_ID,
|
||||
allThreadsStopped: true,
|
||||
text: decodeHex(fields[3]) || undefined,
|
||||
});
|
||||
break;
|
||||
case 'OUTPUT':
|
||||
this.sendEvent('output', { category: 'stdout', output: decodeHex(fields[0]) });
|
||||
break;
|
||||
default:
|
||||
this.sendEvent('output', { category: 'console', output: `NX event ${name}\n` });
|
||||
}
|
||||
}
|
||||
|
||||
publishStoppedEvent(body) {
|
||||
if (this.resumeInProgress > 0) {
|
||||
this.pendingStoppedEvents.push(body);
|
||||
return;
|
||||
}
|
||||
this.sendEvent('stopped', body);
|
||||
}
|
||||
|
||||
flushStoppedEvents() {
|
||||
const queued = this.pendingStoppedEvents;
|
||||
this.pendingStoppedEvents = [];
|
||||
for (const body of queued) this.sendEvent('stopped', body);
|
||||
}
|
||||
|
||||
resetStoppedReferences(resetCounters = false) {
|
||||
this.frameReferences.clear();
|
||||
this.runtimeToFrameReference.clear();
|
||||
this.variableReferences.clear();
|
||||
this.runtimeToVariableReference.clear();
|
||||
if (resetCounters) {
|
||||
this.nextFrameId = 1;
|
||||
this.nextVariablesReference = 1;
|
||||
}
|
||||
}
|
||||
|
||||
toAdapterFrameId(runtimeId) {
|
||||
const normalized = Number(runtimeId);
|
||||
if (!Number.isFinite(normalized) || normalized <= 0) return 0;
|
||||
let adapterId = this.runtimeToFrameReference.get(normalized);
|
||||
if (adapterId !== undefined) return adapterId;
|
||||
adapterId = this.nextFrameId++;
|
||||
this.runtimeToFrameReference.set(normalized, adapterId);
|
||||
this.frameReferences.set(adapterId, normalized);
|
||||
return adapterId;
|
||||
}
|
||||
|
||||
toAdapterVariablesReference(runtimeReference) {
|
||||
const normalized = Number(runtimeReference);
|
||||
if (!Number.isFinite(normalized) || normalized <= 0) return 0;
|
||||
let adapterReference = this.runtimeToVariableReference.get(normalized);
|
||||
if (adapterReference !== undefined) return adapterReference;
|
||||
adapterReference = this.nextVariablesReference++;
|
||||
this.runtimeToVariableReference.set(normalized, adapterReference);
|
||||
this.variableReferences.set(adapterReference, normalized);
|
||||
return adapterReference;
|
||||
}
|
||||
|
||||
isTclSource(sourcePath) {
|
||||
const extension = path.extname(String(sourcePath || '')).toLowerCase();
|
||||
return extension === '.tcl' || extension === '.def';
|
||||
}
|
||||
|
||||
breakpointSourceKey(source = {}, sourcePath = '') {
|
||||
if (source && Number(source.sourceReference) > 0) {
|
||||
return `reference:${source.sourceReference}`;
|
||||
}
|
||||
// Deliberately preserve spelling and case here: they are precisely what
|
||||
// distinguish two VS Code source identities that map to the same runtime
|
||||
// path. Runtime comparison is handled separately below.
|
||||
return `path:${String(sourcePath || '')}`;
|
||||
}
|
||||
|
||||
breakpointRemoteKey(remotePath) {
|
||||
let normalized = this.normalizePath(remotePath);
|
||||
if (process.platform === 'win32' || /^[A-Za-z]:\//.test(normalized) || normalized.startsWith('//')) {
|
||||
normalized = normalized.toLowerCase();
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
breakpointSignature(breakpoint) {
|
||||
return JSON.stringify([
|
||||
Number(breakpoint.line),
|
||||
breakpoint.condition || '',
|
||||
breakpoint.hitCondition || '',
|
||||
breakpoint.logMessage || '',
|
||||
]);
|
||||
}
|
||||
|
||||
normalizePath(value) {
|
||||
const normalized = String(value || '').replace(/\\/g, '/');
|
||||
if (normalized === '/' || /^[A-Za-z]:\/$/.test(normalized)) return normalized;
|
||||
return normalized.replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
samePathPrefix(candidate, prefix) {
|
||||
let normalizedCandidate = this.normalizePath(candidate);
|
||||
let normalizedPrefix = this.normalizePath(prefix);
|
||||
if (process.platform === 'win32') {
|
||||
normalizedCandidate = normalizedCandidate.toLowerCase();
|
||||
normalizedPrefix = normalizedPrefix.toLowerCase();
|
||||
}
|
||||
if (normalizedCandidate === normalizedPrefix) return true;
|
||||
const boundaryPrefix = normalizedPrefix.endsWith('/') ? normalizedPrefix : `${normalizedPrefix}/`;
|
||||
return normalizedCandidate.startsWith(boundaryPrefix);
|
||||
}
|
||||
|
||||
toRemotePath(localPath) {
|
||||
const normalized = this.normalizePath(path.resolve(localPath || '.'));
|
||||
const localRoot = this.normalizePath(this.localRoot);
|
||||
if (localRoot && this.remoteRoot && this.samePathPrefix(normalized, localRoot)) {
|
||||
return this.remoteRoot + normalized.slice(localRoot.length);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
toLocalPath(remotePath) {
|
||||
const normalized = this.normalizePath(remotePath);
|
||||
if (this.remoteRoot && this.localRoot && this.samePathPrefix(normalized, this.remoteRoot)) {
|
||||
const suffix = normalized.slice(this.remoteRoot.length).replace(/^\//, '');
|
||||
return path.join(this.localRoot, ...suffix.split('/'));
|
||||
}
|
||||
return process.platform === 'win32' ? normalized.replace(/\//g, '\\') : normalized;
|
||||
}
|
||||
|
||||
sendTerminated() {
|
||||
if (this.terminated) return;
|
||||
this.terminated = true;
|
||||
this.sendEvent('terminated');
|
||||
}
|
||||
|
||||
dispose() {
|
||||
if (this.runtime) this.runtime.close();
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
new NxDebugAdapter();
|
||||
}
|
||||
|
||||
module.exports = { NxDebugAdapter, THREAD_ID };
|
||||
@@ -0,0 +1,68 @@
|
||||
'use strict';
|
||||
|
||||
const { PassThrough } = require('node:stream');
|
||||
const { NxDebugAdapter } = require('./debugAdapter');
|
||||
|
||||
function frameMessage(message) {
|
||||
const json = JSON.stringify(message);
|
||||
return `Content-Length: ${Buffer.byteLength(json, 'utf8')}\r\n\r\n${json}`;
|
||||
}
|
||||
|
||||
class NxInlineDebugAdapter {
|
||||
constructor() {
|
||||
this.input = new PassThrough();
|
||||
this.output = new PassThrough();
|
||||
this.outputBuffer = Buffer.alloc(0);
|
||||
this.listeners = new Set();
|
||||
this.disposed = false;
|
||||
|
||||
this.onDidSendMessage = (listener, thisArgs, disposables) => {
|
||||
const registration = { listener, thisArgs };
|
||||
this.listeners.add(registration);
|
||||
const disposable = {
|
||||
dispose: () => this.listeners.delete(registration),
|
||||
};
|
||||
if (Array.isArray(disposables)) disposables.push(disposable);
|
||||
return disposable;
|
||||
};
|
||||
|
||||
this.output.on('data', (chunk) => this.onOutput(chunk));
|
||||
this.adapter = new NxDebugAdapter(this.input, this.output);
|
||||
}
|
||||
|
||||
handleMessage(message) {
|
||||
if (!this.disposed) this.input.write(frameMessage(message));
|
||||
}
|
||||
|
||||
onOutput(chunk) {
|
||||
this.outputBuffer = Buffer.concat([this.outputBuffer, chunk]);
|
||||
while (true) {
|
||||
const headerEnd = this.outputBuffer.indexOf('\r\n\r\n');
|
||||
if (headerEnd < 0) return;
|
||||
const header = this.outputBuffer.subarray(0, headerEnd).toString('ascii');
|
||||
const match = /Content-Length:\s*(\d+)/i.exec(header);
|
||||
if (!match) throw new Error('missing DAP Content-Length header from NX adapter');
|
||||
const contentLength = Number(match[1]);
|
||||
const messageEnd = headerEnd + 4 + contentLength;
|
||||
if (this.outputBuffer.length < messageEnd) return;
|
||||
|
||||
const payload = this.outputBuffer.subarray(headerEnd + 4, messageEnd).toString('utf8');
|
||||
this.outputBuffer = this.outputBuffer.subarray(messageEnd);
|
||||
const message = JSON.parse(payload);
|
||||
for (const { listener, thisArgs } of [...this.listeners]) {
|
||||
listener.call(thisArgs, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
if (this.disposed) return;
|
||||
this.disposed = true;
|
||||
this.adapter.dispose();
|
||||
this.input.end();
|
||||
this.output.destroy();
|
||||
this.listeners.clear();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { NxInlineDebugAdapter, frameMessage };
|
||||
@@ -0,0 +1,116 @@
|
||||
'use strict';
|
||||
|
||||
const net = require('net');
|
||||
const { EventEmitter } = require('events');
|
||||
|
||||
function encodeHex(value) {
|
||||
return Buffer.from(String(value ?? ''), 'utf8').toString('hex') || '-';
|
||||
}
|
||||
|
||||
function decodeHex(value) {
|
||||
if (!value || value === '-') return '';
|
||||
return Buffer.from(value, 'hex').toString('utf8');
|
||||
}
|
||||
|
||||
class RuntimeConnection extends EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
this.socket = null;
|
||||
this.buffer = '';
|
||||
this.nextRequestId = 1;
|
||||
this.pending = new Map();
|
||||
this.closed = false;
|
||||
}
|
||||
|
||||
connect(host, port) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const socket = net.createConnection({ host, port });
|
||||
const onError = (error) => {
|
||||
socket.destroy();
|
||||
reject(error);
|
||||
};
|
||||
socket.once('error', onError);
|
||||
socket.once('connect', () => {
|
||||
socket.removeListener('error', onError);
|
||||
this.socket = socket;
|
||||
this.closed = false;
|
||||
socket.setEncoding('utf8');
|
||||
socket.on('data', (chunk) => this.onData(chunk));
|
||||
socket.on('error', (error) => this.emit('runtimeError', error));
|
||||
socket.on('close', () => this.onClose());
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
request(command, fields = [], timeoutMs = 30000) {
|
||||
if (!this.socket || this.closed) {
|
||||
return Promise.reject(new Error('NX runtime is not connected'));
|
||||
}
|
||||
const id = this.nextRequestId++;
|
||||
const normalized = fields.map((field) => String(field ?? ''));
|
||||
if (normalized.some((field) => field.includes('\t') || field.includes('\n'))) {
|
||||
return Promise.reject(new Error('protocol fields must be hex-encoded before transmission'));
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
this.pending.delete(id);
|
||||
reject(new Error(`NX ${command} request timed out`));
|
||||
}, timeoutMs);
|
||||
this.pending.set(id, { resolve, reject, timer, command });
|
||||
this.socket.write(['REQ', id, command, ...normalized].join('\t') + '\n');
|
||||
});
|
||||
}
|
||||
|
||||
close() {
|
||||
this.closed = true;
|
||||
if (this.socket) this.socket.destroy();
|
||||
this.socket = null;
|
||||
}
|
||||
|
||||
onData(chunk) {
|
||||
this.buffer += chunk;
|
||||
let newline;
|
||||
while ((newline = this.buffer.indexOf('\n')) >= 0) {
|
||||
const line = this.buffer.slice(0, newline).replace(/\r$/, '');
|
||||
this.buffer = this.buffer.slice(newline + 1);
|
||||
if (line) this.onLine(line);
|
||||
}
|
||||
}
|
||||
|
||||
onLine(line) {
|
||||
const fields = line.split('\t');
|
||||
if (fields[0] === 'RES') {
|
||||
const id = Number(fields[1]);
|
||||
const pending = this.pending.get(id);
|
||||
if (!pending) return;
|
||||
this.pending.delete(id);
|
||||
clearTimeout(pending.timer);
|
||||
if (fields[2] === 'OK') {
|
||||
pending.resolve({ command: fields[3], fields: fields.slice(4) });
|
||||
} else {
|
||||
pending.reject(new Error(decodeHex(fields[4]) || `NX ${pending.command} failed`));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (fields[0] === 'EVENT') {
|
||||
this.emit('event', fields[1], fields.slice(2));
|
||||
return;
|
||||
}
|
||||
this.emit('runtimeError', new Error(`unrecognized NX message: ${line}`));
|
||||
}
|
||||
|
||||
onClose() {
|
||||
if (this.closed) return;
|
||||
this.closed = true;
|
||||
for (const pending of this.pending.values()) {
|
||||
clearTimeout(pending.timer);
|
||||
pending.reject(new Error('NX runtime disconnected'));
|
||||
}
|
||||
this.pending.clear();
|
||||
this.emit('close');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { RuntimeConnection, encodeHex, decodeHex };
|
||||
@@ -0,0 +1,42 @@
|
||||
import * as vscode from "vscode"
|
||||
|
||||
// The adapter is shared with the standalone NX Tcl Remote Debugger. It is
|
||||
// bundled by esbuild into this extension, so no child process or additional
|
||||
// VS Code extension is required.
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { NxInlineDebugAdapter } = require("./inlineAdapter")
|
||||
|
||||
class NxDebugAdapterFactory implements vscode.DebugAdapterDescriptorFactory {
|
||||
createDebugAdapterDescriptor(): vscode.ProviderResult<vscode.DebugAdapterDescriptor> {
|
||||
return new vscode.DebugAdapterInlineImplementation(new NxInlineDebugAdapter())
|
||||
}
|
||||
}
|
||||
|
||||
function resolveDebugConfiguration(
|
||||
folder: vscode.WorkspaceFolder | undefined,
|
||||
config: vscode.DebugConfiguration
|
||||
): vscode.DebugConfiguration {
|
||||
const resolved = { ...config }
|
||||
if (!resolved.type) resolved.type = "nx-tcl"
|
||||
if (!resolved.request) resolved.request = "attach"
|
||||
if (!resolved.name) resolved.name = "Attach to NX Post Tcl"
|
||||
if (!resolved.host) resolved.host = "127.0.0.1"
|
||||
if (!resolved.port) resolved.port = 4711
|
||||
if (!resolved.connectTimeout) resolved.connectTimeout = 120000
|
||||
if (resolved.stopOnEntry === undefined) resolved.stopOnEntry = false
|
||||
if (resolved.breakOnError === undefined) resolved.breakOnError = true
|
||||
if (!resolved.localRoot && folder) resolved.localRoot = folder.uri.fsPath
|
||||
return resolved
|
||||
}
|
||||
|
||||
export function registerNxTclDebugger(context: vscode.ExtensionContext): void {
|
||||
const factory = new NxDebugAdapterFactory()
|
||||
context.subscriptions.push(
|
||||
vscode.debug.registerDebugAdapterDescriptorFactory("nx-tcl", factory),
|
||||
vscode.debug.registerDebugConfigurationProvider("nx-tcl", {
|
||||
resolveDebugConfiguration
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
export { resolveDebugConfiguration }
|
||||
@@ -29,10 +29,13 @@ import { checkIfConfigurationChanged, getInterpreterFromSetting } from "./common
|
||||
import { loadServerDefaults } from "./common/setup"
|
||||
import { getLSClientTraceLevel } from "./common/utilities"
|
||||
import { createOutputChannel, onDidChangeConfiguration, registerCommand } from "./common/vscodeapi"
|
||||
import { registerNxTclDebugger } from "./debugger/register"
|
||||
|
||||
let client: LanguageClient | undefined
|
||||
|
||||
export async function activate(context: vscode.ExtensionContext) {
|
||||
registerNxTclDebugger(context)
|
||||
|
||||
// This is required to get server name and module. This should be
|
||||
// the first thing that we do in this extension.
|
||||
const serverInfo = loadServerDefaults()
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 NX Tcl Remote Debugger contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "nx-post-support",
|
||||
"version": "2025.9.200",
|
||||
"version": "2026.8.201",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "nx-post-support",
|
||||
"version": "2025.9.200",
|
||||
"version": "2026.8.201",
|
||||
"devDependencies": {
|
||||
"@types/vscode": "^1.96.0",
|
||||
"@vscode/vsce": "^3.2.1",
|
||||
|
||||
+106
-5
@@ -1,10 +1,16 @@
|
||||
{
|
||||
"name": "nx-post-support",
|
||||
"displayName": "NX Postprocessor Support",
|
||||
"description": "VS Code extension for NX CAM postprocessor development with syntax highlighting, formatting, linting, and auto-completion for CDL, TCL, and DEF files",
|
||||
"version": "2026.8.100",
|
||||
"description": "VS Code extension for NX CAM postprocessor development with language support and remote Tcl debugging for CDL, TCL, and DEF files",
|
||||
"version": "2026.8.201",
|
||||
"publisher": "Christoph",
|
||||
"icon": "images/nx-1.png",
|
||||
"activationEvents": [
|
||||
"onLanguage:tcl",
|
||||
"onLanguage:cdl",
|
||||
"onLanguage:def",
|
||||
"onDebug"
|
||||
],
|
||||
"extensionDependencies": [
|
||||
"ms-python.python"
|
||||
],
|
||||
@@ -23,15 +29,109 @@
|
||||
"UDE",
|
||||
"Postprocessor",
|
||||
"NX CAM",
|
||||
"Siemens NX"
|
||||
"Siemens NX",
|
||||
"debugger",
|
||||
"remote debugging"
|
||||
],
|
||||
"engines": {
|
||||
"vscode": "^1.96.0"
|
||||
},
|
||||
"categories": [
|
||||
"Programming Languages"
|
||||
"Programming Languages",
|
||||
"Debuggers"
|
||||
],
|
||||
"contributes": {
|
||||
"breakpoints": [
|
||||
{
|
||||
"language": "tcl"
|
||||
},
|
||||
{
|
||||
"language": "def"
|
||||
}
|
||||
],
|
||||
"debuggers": [
|
||||
{
|
||||
"type": "nx-tcl",
|
||||
"label": "NX Tcl Remote Debugger",
|
||||
"languages": [
|
||||
"tcl",
|
||||
"def"
|
||||
],
|
||||
"configurationAttributes": {
|
||||
"attach": {
|
||||
"required": [
|
||||
"host",
|
||||
"port"
|
||||
],
|
||||
"properties": {
|
||||
"host": {
|
||||
"type": "string",
|
||||
"default": "127.0.0.1",
|
||||
"description": "Host on which the NX Tcl runtime is listening."
|
||||
},
|
||||
"port": {
|
||||
"type": "number",
|
||||
"default": 4711,
|
||||
"description": "TCP port of the NX Tcl runtime."
|
||||
},
|
||||
"connectTimeout": {
|
||||
"type": "number",
|
||||
"default": 120000,
|
||||
"description": "How long VS Code retries until NX starts the runtime, in milliseconds."
|
||||
},
|
||||
"stopOnEntry": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Stop on the first traced Tcl command."
|
||||
},
|
||||
"breakOnError": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Stop when a traced Tcl command returns TCL_ERROR."
|
||||
},
|
||||
"localRoot": {
|
||||
"type": "string",
|
||||
"description": "Local source root opened in VS Code."
|
||||
},
|
||||
"remoteRoot": {
|
||||
"type": "string",
|
||||
"description": "Corresponding source root on the NX host when it differs from localRoot."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"initialConfigurations": [
|
||||
{
|
||||
"type": "nx-tcl",
|
||||
"request": "attach",
|
||||
"name": "Attach to NX Post Tcl",
|
||||
"host": "127.0.0.1",
|
||||
"port": 4711,
|
||||
"connectTimeout": 120000,
|
||||
"stopOnEntry": false,
|
||||
"breakOnError": true,
|
||||
"localRoot": "${workspaceFolder}"
|
||||
}
|
||||
],
|
||||
"configurationSnippets": [
|
||||
{
|
||||
"label": "NX Tcl: Attach to NX Post",
|
||||
"description": "Attach VS Code to an NX runtime embedded in NX Post.",
|
||||
"body": {
|
||||
"type": "nx-tcl",
|
||||
"request": "attach",
|
||||
"name": "Attach to NX Post Tcl",
|
||||
"host": "127.0.0.1",
|
||||
"port": 4711,
|
||||
"connectTimeout": 120000,
|
||||
"stopOnEntry": false,
|
||||
"breakOnError": true,
|
||||
"localRoot": "${workspaceFolder}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"languages": [
|
||||
{
|
||||
"id": "tcl",
|
||||
@@ -151,7 +251,8 @@
|
||||
"compile": "node esbuild.js --production",
|
||||
"compile:debug": "node esbuild.js",
|
||||
"watch": "node esbuild.js --watch",
|
||||
"package": "node esbuild.js --production"
|
||||
"package": "node esbuild.js --production",
|
||||
"test:debugger": "node --test test/debugger.test.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/vscode": "^1.96.0",
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
"use strict"
|
||||
|
||||
const test = require("node:test")
|
||||
const assert = require("node:assert/strict")
|
||||
const { PassThrough } = require("node:stream")
|
||||
const { NxDebugAdapter } = require("../client/src/debugger/debugAdapter")
|
||||
const manifest = require("../package.json")
|
||||
|
||||
function dapMessage(message) {
|
||||
const json = JSON.stringify(message)
|
||||
return `Content-Length: ${Buffer.byteLength(json, "utf8")}\r\n\r\n${json}`
|
||||
}
|
||||
|
||||
function readDapMessage(stream) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let buffer = Buffer.alloc(0)
|
||||
const timer = setTimeout(() => reject(new Error("DAP response timed out")), 2000)
|
||||
const onData = (chunk) => {
|
||||
buffer = Buffer.concat([buffer, chunk])
|
||||
const headerEnd = buffer.indexOf("\r\n\r\n")
|
||||
if (headerEnd < 0) return
|
||||
const header = buffer.subarray(0, headerEnd).toString("ascii")
|
||||
const match = /Content-Length:\s*(\d+)/i.exec(header)
|
||||
if (!match) return
|
||||
const length = Number(match[1])
|
||||
if (buffer.length < headerEnd + 4 + length) return
|
||||
clearTimeout(timer)
|
||||
stream.removeListener("data", onData)
|
||||
resolve(JSON.parse(buffer.subarray(headerEnd + 4, headerEnd + 4 + length)))
|
||||
}
|
||||
stream.on("data", onData)
|
||||
})
|
||||
}
|
||||
|
||||
test("integrated manifest contributes the NX Tcl debugger", () => {
|
||||
const debuggerContribution = manifest.contributes.debuggers.find(
|
||||
(entry) => entry.type === "nx-tcl"
|
||||
)
|
||||
assert.ok(debuggerContribution)
|
||||
assert.deepEqual(debuggerContribution.languages, ["tcl", "def"])
|
||||
assert.deepEqual(
|
||||
manifest.contributes.breakpoints.map((entry) => entry.language),
|
||||
["tcl", "def"]
|
||||
)
|
||||
})
|
||||
|
||||
test("integrated adapter answers a DAP initialize request", async () => {
|
||||
const input = new PassThrough()
|
||||
const output = new PassThrough()
|
||||
const adapter = new NxDebugAdapter(input, output)
|
||||
const responsePromise = readDapMessage(output)
|
||||
|
||||
input.write(
|
||||
dapMessage({ seq: 1, type: "request", command: "initialize", arguments: {} })
|
||||
)
|
||||
const response = await responsePromise
|
||||
adapter.dispose()
|
||||
|
||||
assert.equal(response.success, true)
|
||||
assert.equal(response.command, "initialize")
|
||||
assert.equal(response.body.supportsConfigurationDoneRequest, true)
|
||||
assert.equal(response.body.supportsSetVariable, true)
|
||||
})
|
||||
Reference in New Issue
Block a user