roku-debug 0.23.13 → 0.23.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/dist/LaunchConfiguration.d.ts +18 -5
- package/dist/PerfettoManager.d.ts +26 -1
- package/dist/PerfettoManager.js +64 -20
- package/dist/PerfettoManager.js.map +1 -1
- package/dist/RendezvousTracker.d.ts +1 -1
- package/dist/RendezvousTracker.js +2 -2
- package/dist/RendezvousTracker.js.map +1 -1
- package/dist/adapters/TelnetAdapter.d.ts +1 -1
- package/dist/bsc/BscProjectThreaded.d.ts +1 -1
- package/dist/bsc/threading/WorkerPool.d.ts +1 -1
- package/dist/debugProtocol/client/DebugProtocolClient.d.ts +1 -1
- package/dist/debugProtocol/client/DebugProtocolClient.js +5 -2
- package/dist/debugProtocol/client/DebugProtocolClient.js.map +1 -1
- package/dist/debugSession/BrightScriptDebugSession.d.ts +75 -1
- package/dist/debugSession/BrightScriptDebugSession.js +498 -190
- package/dist/debugSession/BrightScriptDebugSession.js.map +1 -1
- package/dist/logging.d.ts +1 -2
- package/dist/logging.js +2 -4
- package/dist/logging.js.map +1 -1
- package/dist/managers/ProjectManager.d.ts +14 -1
- package/dist/managers/ProjectManager.js +39 -13
- package/dist/managers/ProjectManager.js.map +1 -1
- package/package.json +4 -4
|
@@ -32,9 +32,21 @@ const ecpRegistryUtils_1 = require("./ecpRegistryUtils");
|
|
|
32
32
|
const RokuECP_1 = require("../RokuECP");
|
|
33
33
|
const Exceptions_1 = require("../Exceptions");
|
|
34
34
|
const diagnosticSource = 'roku-debug';
|
|
35
|
+
/**
|
|
36
|
+
* Sort tiers for debug-console completions. Lower values sort first, so a variable's own members rank
|
|
37
|
+
* above interface methods, then the file's scope functions, and finally the (large) set of globals.
|
|
38
|
+
*/
|
|
39
|
+
var CompletionSortTier;
|
|
40
|
+
(function (CompletionSortTier) {
|
|
41
|
+
CompletionSortTier["Member"] = "1";
|
|
42
|
+
CompletionSortTier["Method"] = "2";
|
|
43
|
+
CompletionSortTier["ScopeFunction"] = "3";
|
|
44
|
+
CompletionSortTier["Global"] = "4";
|
|
45
|
+
})(CompletionSortTier || (CompletionSortTier = {}));
|
|
35
46
|
class BrightScriptDebugSession extends debugadapter_1.LoggingDebugSession {
|
|
36
47
|
constructor() {
|
|
37
48
|
super();
|
|
49
|
+
this.handlingProcessError = false;
|
|
38
50
|
this.logger = logging_1.logger.createLogger(`[session]`);
|
|
39
51
|
this.isWindowsPlatform = process.platform.startsWith('win');
|
|
40
52
|
/**
|
|
@@ -43,6 +55,10 @@ class BrightScriptDebugSession extends debugadapter_1.LoggingDebugSession {
|
|
|
43
55
|
this.idCounter = 1;
|
|
44
56
|
this.processErrorHandlersRegistered = false;
|
|
45
57
|
this.isCrashed = false;
|
|
58
|
+
/** Set once the client (e.g. VS Code) disconnects, so we stop writing to a now-dead stream */
|
|
59
|
+
this.clientDisconnected = false;
|
|
60
|
+
/** How long to wait for a graceful shutdown before forcibly exiting the process */
|
|
61
|
+
this.shutdownForceExitTimeout = 10000;
|
|
46
62
|
//set imports as class properties so they can be spied upon during testing
|
|
47
63
|
this.rokuDeploy = roku_deploy_1.rokuDeploy;
|
|
48
64
|
this.componentLibraryServer = new ComponentLibraryServer_1.ComponentLibraryServer();
|
|
@@ -58,6 +74,12 @@ class BrightScriptDebugSession extends debugadapter_1.LoggingDebugSession {
|
|
|
58
74
|
this.evaluateRefIdLookup = {};
|
|
59
75
|
this.evaluateRefIdCounter = 1;
|
|
60
76
|
this.variables = {};
|
|
77
|
+
/**
|
|
78
|
+
* Caches the device lookups performed while resolving completion requests. Variables don't change
|
|
79
|
+
* while the debugger is paused, so this avoids repeated round-trips for the same path. Cleared by
|
|
80
|
+
* `clearState` whenever the debugger resumes or steps.
|
|
81
|
+
*/
|
|
82
|
+
this.completionParentVariableCache = new Map();
|
|
61
83
|
this.tempVarPrefix = '__rokudebug__';
|
|
62
84
|
/**
|
|
63
85
|
* A magic number to represent a fake thread that will be used for showing compile errors in the UI as if they were runtime crashes
|
|
@@ -95,7 +117,17 @@ class BrightScriptDebugSession extends debugadapter_1.LoggingDebugSession {
|
|
|
95
117
|
this.fileLoggingManager = new logging_1.FileLoggingManager();
|
|
96
118
|
}
|
|
97
119
|
start(inStream, outStream) {
|
|
120
|
+
var _a, _b, _c;
|
|
98
121
|
super.start(inStream, outStream);
|
|
122
|
+
//When the client's pipe goes away (e.g. VS Code closed), stop forwarding output. Otherwise the
|
|
123
|
+
//still-running Roku app keeps streaming output, every write to the dead pipe fails, and each
|
|
124
|
+
//failure re-triggers shutdown() in a tight loop that pegs the CPU and orphans this process.
|
|
125
|
+
const markClientGone = () => {
|
|
126
|
+
this.clientDisconnected = true;
|
|
127
|
+
};
|
|
128
|
+
(_a = inStream === null || inStream === void 0 ? void 0 : inStream.on) === null || _a === void 0 ? void 0 : _a.call(inStream, 'close', markClientGone);
|
|
129
|
+
(_b = inStream === null || inStream === void 0 ? void 0 : inStream.on) === null || _b === void 0 ? void 0 : _b.call(inStream, 'end', markClientGone);
|
|
130
|
+
(_c = outStream === null || outStream === void 0 ? void 0 : outStream.on) === null || _c === void 0 ? void 0 : _c.call(outStream, 'error', markClientGone);
|
|
99
131
|
// Set up DAP protocol logging as early as possible — immediately after start() so we capture
|
|
100
132
|
// the initialize request and all early DAP traffic before launchRequest config is available.
|
|
101
133
|
// The log file path is injected as ROKU_DAP_LOG_FILE by the extension's DebugAdapterDescriptorFactory,
|
|
@@ -111,106 +143,147 @@ class BrightScriptDebugSession extends debugadapter_1.LoggingDebugSession {
|
|
|
111
143
|
debugadapter_1.logger.setup(debugadapter_1.Logger.LogLevel.Error, dapLogFile);
|
|
112
144
|
}
|
|
113
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* Once the client has disconnected, drop outgoing events instead of writing to the dead stream.
|
|
148
|
+
* Writing to a broken pipe re-triggers the base 'error' -> shutdown() handler in a tight loop.
|
|
149
|
+
*/
|
|
150
|
+
sendEvent(event) {
|
|
151
|
+
if (this.clientDisconnected) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
super.sendEvent(event);
|
|
155
|
+
}
|
|
114
156
|
setupProcessErrorHandlers() {
|
|
115
157
|
if (this.processErrorHandlersRegistered) {
|
|
116
158
|
return;
|
|
117
159
|
}
|
|
118
160
|
this.processErrorHandlersRegistered = true;
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const logger = this.logger.createLogger(`${type}`);
|
|
122
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
123
|
-
const stack = error instanceof Error ? error.stack : undefined;
|
|
124
|
-
logger.error(message, stack);
|
|
125
|
-
let output;
|
|
126
|
-
let debuggerVersion;
|
|
127
|
-
let additionalInfo;
|
|
128
|
-
try {
|
|
129
|
-
debuggerVersion = fsExtra.readJsonSync(path.resolve(__dirname, '../../package.json')).version;
|
|
130
|
-
const clientName = (_b = (_a = this.initRequestArgs) === null || _a === void 0 ? void 0 : _a.clientName) !== null && _b !== void 0 ? _b : 'unknown';
|
|
131
|
-
additionalInfo = {
|
|
132
|
-
clientName: clientName,
|
|
133
|
-
rokuDebugVersion: debuggerVersion,
|
|
134
|
-
ecpMode: (_c = this.deviceInfo) === null || _c === void 0 ? void 0 : _c.ecpSettingMode,
|
|
135
|
-
developerMode: (_d = this.deviceInfo) === null || _d === void 0 ? void 0 : _d.developerEnabled,
|
|
136
|
-
firmware: this.deviceInfo ? `${(_e = this.deviceInfo) === null || _e === void 0 ? void 0 : _e.softwareVersion}.${(_f = this.deviceInfo) === null || _f === void 0 ? void 0 : _f.softwareBuild}` : undefined,
|
|
137
|
-
protocolVersion: (_g = this.deviceInfo) === null || _g === void 0 ? void 0 : _g.brightscriptDebuggerVersion,
|
|
138
|
-
protocolEnabled: this.enableDebugProtocol
|
|
139
|
-
};
|
|
140
|
-
const lines = Object.entries(additionalInfo).map(([key, value]) => {
|
|
141
|
-
// Insert a space before all uppercase letters preceded by a lowercase letter, then uppercase the first char
|
|
142
|
-
const spacedString = key.replace(/([a-z])([A-Z])/g, '$1 $2');
|
|
143
|
-
const formattedKey = spacedString.charAt(0).toUpperCase() + spacedString.slice(1);
|
|
144
|
-
return `**${formattedKey}:** ${JSON.stringify(value)}`;
|
|
145
|
-
});
|
|
146
|
-
const issueBodyPrefix = [
|
|
147
|
-
`**Error type:** ${type}`,
|
|
148
|
-
`**Message:** ${message}`,
|
|
149
|
-
...lines,
|
|
150
|
-
'',
|
|
151
|
-
`**Steps to reproduce:**`,
|
|
152
|
-
`<!-- Please describe what you were doing when this crash occurred -->`,
|
|
153
|
-
'',
|
|
154
|
-
'**Stack trace:**',
|
|
155
|
-
'```',
|
|
156
|
-
''
|
|
157
|
-
].join('\n');
|
|
158
|
-
const issueBodySuffix = '\n```';
|
|
159
|
-
const issueTitle = encodeURIComponent(`[crash] ${type}: ${message}`);
|
|
160
|
-
const baseUrl = 'https://github.com/RokuCommunity/roku-debug/issues/new';
|
|
161
|
-
const maxUrlLength = 2000;
|
|
162
|
-
const urlOverhead = `${baseUrl}?title=${issueTitle}&body=`.length;
|
|
163
|
-
const bodyBudget = maxUrlLength - urlOverhead;
|
|
164
|
-
const encodedPrefix = encodeURIComponent(issueBodyPrefix);
|
|
165
|
-
const encodedSuffix = encodeURIComponent(issueBodySuffix);
|
|
166
|
-
const stackBudget = bodyBudget - encodedPrefix.length - encodedSuffix.length;
|
|
167
|
-
let truncatedStack;
|
|
168
|
-
if (!stack) {
|
|
169
|
-
truncatedStack = '(no stack trace)';
|
|
170
|
-
}
|
|
171
|
-
else if (encodeURIComponent(stack).length <= stackBudget) {
|
|
172
|
-
truncatedStack = stack;
|
|
173
|
-
}
|
|
174
|
-
else {
|
|
175
|
-
truncatedStack = decodeURIComponent(encodeURIComponent(stack).slice(0, stackBudget)) + '\n...(truncated)';
|
|
176
|
-
}
|
|
177
|
-
const issueUrl = `${baseUrl}?title=${issueTitle}&body=${encodedPrefix}${encodeURIComponent(truncatedStack)}${encodedSuffix}`;
|
|
178
|
-
output = [
|
|
179
|
-
'',
|
|
180
|
-
'================================================================',
|
|
181
|
-
'\tBRIGHTSCRIPT DEBUGGER INTERNAL ERROR',
|
|
182
|
-
'\tThis is a crash in the debug adapter, not in your application.',
|
|
183
|
-
'================================================================',
|
|
184
|
-
`\tError type: ${type}`,
|
|
185
|
-
`\tMessage: ${message}`,
|
|
186
|
-
...lines.map(l => `\t${l}`),
|
|
187
|
-
'',
|
|
188
|
-
'\tStack trace:',
|
|
189
|
-
...(stack !== null && stack !== void 0 ? stack : '(no stack trace)').split('\n').map(l => `\t${l}`),
|
|
190
|
-
'',
|
|
191
|
-
'\tPlease report this at:',
|
|
192
|
-
`\t${issueUrl}`,
|
|
193
|
-
'================================================================',
|
|
194
|
-
''
|
|
195
|
-
].join('\n');
|
|
196
|
-
}
|
|
197
|
-
catch (e) {
|
|
198
|
-
output = JSON.stringify({
|
|
199
|
-
name: e.name,
|
|
200
|
-
message: e.message,
|
|
201
|
-
stack: e.stack
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
void this.sendLogOutput(output).catch(() => { });
|
|
205
|
-
this.isCrashed = true;
|
|
206
|
-
this.sendEvent(new Events_1.ProcessCrashEvent({ type, message, stack, additionalInfo: additionalInfo !== null && additionalInfo !== void 0 ? additionalInfo : {} }));
|
|
207
|
-
setTimeout(() => void this.shutdown(), 5000);
|
|
208
|
-
};
|
|
209
|
-
this._uncaughtExceptionHandler = (error) => handleError('uncaughtException', error);
|
|
210
|
-
this._unhandledRejectionHandler = (reason) => handleError('unhandledRejection', reason);
|
|
161
|
+
this._uncaughtExceptionHandler = (error) => this.handleProcessError('uncaughtException', error);
|
|
162
|
+
this._unhandledRejectionHandler = (reason) => this.handleProcessError('unhandledRejection', reason);
|
|
211
163
|
process.on('uncaughtException', this._uncaughtExceptionHandler);
|
|
212
164
|
process.on('unhandledRejection', this._unhandledRejectionHandler);
|
|
213
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* True when an error indicates the client (e.g. VS Code) has gone away, such as a broken stdout pipe
|
|
168
|
+
* (EPIPE). Writing to a dead pipe just produces more EPIPEs, so we must not try to report over it.
|
|
169
|
+
*/
|
|
170
|
+
isClientGoneError(error) {
|
|
171
|
+
const code = error === null || error === void 0 ? void 0 : error.code;
|
|
172
|
+
const message = error instanceof Error ? error.message : String(error !== null && error !== void 0 ? error : '');
|
|
173
|
+
return code === 'EPIPE' || /\bEPIPE\b|write after end/i.test(message);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Tear down the process error handlers and forcibly exit, so we never leave an orphaned adapter
|
|
177
|
+
* spinning in the background after the client is gone or a graceful shutdown has hung.
|
|
178
|
+
*/
|
|
179
|
+
forceExit(code = 0) {
|
|
180
|
+
this.teardownProcessErrorHandlers();
|
|
181
|
+
process.exit(code);
|
|
182
|
+
}
|
|
183
|
+
handleProcessError(type, error) {
|
|
184
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
185
|
+
//a broken client pipe (EPIPE) means the client (e.g. VS Code) is gone. Trying to report it over
|
|
186
|
+
//the now-dead stream just produces more EPIPEs, which re-enter this handler in a tight loop and
|
|
187
|
+
//peg the CPU. Exit instead.
|
|
188
|
+
if (this.isClientGoneError(error)) {
|
|
189
|
+
this.clientDisconnected = true;
|
|
190
|
+
this.forceExit();
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
//only handle the first error; re-entering here (e.g. from a failed write while reporting) would
|
|
194
|
+
//spin the CPU and flood the logs
|
|
195
|
+
if (this.handlingProcessError) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
this.handlingProcessError = true;
|
|
199
|
+
const logger = this.logger.createLogger(`${type}`);
|
|
200
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
201
|
+
const stack = error instanceof Error ? error.stack : undefined;
|
|
202
|
+
logger.error(message, stack);
|
|
203
|
+
let output;
|
|
204
|
+
let debuggerVersion;
|
|
205
|
+
let additionalInfo;
|
|
206
|
+
try {
|
|
207
|
+
debuggerVersion = fsExtra.readJsonSync(path.resolve(__dirname, '../../package.json')).version;
|
|
208
|
+
const clientName = (_b = (_a = this.initRequestArgs) === null || _a === void 0 ? void 0 : _a.clientName) !== null && _b !== void 0 ? _b : 'unknown';
|
|
209
|
+
additionalInfo = {
|
|
210
|
+
clientName: clientName,
|
|
211
|
+
rokuDebugVersion: debuggerVersion,
|
|
212
|
+
ecpMode: (_c = this.deviceInfo) === null || _c === void 0 ? void 0 : _c.ecpSettingMode,
|
|
213
|
+
developerMode: (_d = this.deviceInfo) === null || _d === void 0 ? void 0 : _d.developerEnabled,
|
|
214
|
+
firmware: this.deviceInfo ? `${(_e = this.deviceInfo) === null || _e === void 0 ? void 0 : _e.softwareVersion}.${(_f = this.deviceInfo) === null || _f === void 0 ? void 0 : _f.softwareBuild}` : undefined,
|
|
215
|
+
protocolVersion: (_g = this.deviceInfo) === null || _g === void 0 ? void 0 : _g.brightscriptDebuggerVersion,
|
|
216
|
+
protocolEnabled: this.enableDebugProtocol
|
|
217
|
+
};
|
|
218
|
+
const lines = Object.entries(additionalInfo).map(([key, value]) => {
|
|
219
|
+
// Insert a space before all uppercase letters preceded by a lowercase letter, then uppercase the first char
|
|
220
|
+
const spacedString = key.replace(/([a-z])([A-Z])/g, '$1 $2');
|
|
221
|
+
const formattedKey = spacedString.charAt(0).toUpperCase() + spacedString.slice(1);
|
|
222
|
+
return `**${formattedKey}:** ${JSON.stringify(value)}`;
|
|
223
|
+
});
|
|
224
|
+
const issueBodyPrefix = [
|
|
225
|
+
`**Error type:** ${type}`,
|
|
226
|
+
`**Message:** ${message}`,
|
|
227
|
+
...lines,
|
|
228
|
+
'',
|
|
229
|
+
`**Steps to reproduce:**`,
|
|
230
|
+
`<!-- Please describe what you were doing when this crash occurred -->`,
|
|
231
|
+
'',
|
|
232
|
+
'**Stack trace:**',
|
|
233
|
+
'```',
|
|
234
|
+
''
|
|
235
|
+
].join('\n');
|
|
236
|
+
const issueBodySuffix = '\n```';
|
|
237
|
+
const issueTitle = encodeURIComponent(`[crash] ${type}: ${message}`);
|
|
238
|
+
const baseUrl = 'https://github.com/RokuCommunity/roku-debug/issues/new';
|
|
239
|
+
const maxUrlLength = 2000;
|
|
240
|
+
const urlOverhead = `${baseUrl}?title=${issueTitle}&body=`.length;
|
|
241
|
+
const bodyBudget = maxUrlLength - urlOverhead;
|
|
242
|
+
const encodedPrefix = encodeURIComponent(issueBodyPrefix);
|
|
243
|
+
const encodedSuffix = encodeURIComponent(issueBodySuffix);
|
|
244
|
+
const stackBudget = bodyBudget - encodedPrefix.length - encodedSuffix.length;
|
|
245
|
+
let truncatedStack;
|
|
246
|
+
if (!stack) {
|
|
247
|
+
truncatedStack = '(no stack trace)';
|
|
248
|
+
}
|
|
249
|
+
else if (encodeURIComponent(stack).length <= stackBudget) {
|
|
250
|
+
truncatedStack = stack;
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
truncatedStack = decodeURIComponent(encodeURIComponent(stack).slice(0, stackBudget)) + '\n...(truncated)';
|
|
254
|
+
}
|
|
255
|
+
const issueUrl = `${baseUrl}?title=${issueTitle}&body=${encodedPrefix}${encodeURIComponent(truncatedStack)}${encodedSuffix}`;
|
|
256
|
+
output = [
|
|
257
|
+
'',
|
|
258
|
+
'================================================================',
|
|
259
|
+
'\tBRIGHTSCRIPT DEBUGGER INTERNAL ERROR',
|
|
260
|
+
'\tThis is a crash in the debug adapter, not in your application.',
|
|
261
|
+
'================================================================',
|
|
262
|
+
`\tError type: ${type}`,
|
|
263
|
+
`\tMessage: ${message}`,
|
|
264
|
+
...lines.map(l => `\t${l}`),
|
|
265
|
+
'',
|
|
266
|
+
'\tStack trace:',
|
|
267
|
+
...(stack !== null && stack !== void 0 ? stack : '(no stack trace)').split('\n').map(l => `\t${l}`),
|
|
268
|
+
'',
|
|
269
|
+
'\tPlease report this at:',
|
|
270
|
+
`\t${issueUrl}`,
|
|
271
|
+
'================================================================',
|
|
272
|
+
''
|
|
273
|
+
].join('\n');
|
|
274
|
+
}
|
|
275
|
+
catch (e) {
|
|
276
|
+
output = JSON.stringify({
|
|
277
|
+
name: e.name,
|
|
278
|
+
message: e.message,
|
|
279
|
+
stack: e.stack
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
void this.sendLogOutput(output).catch(() => { });
|
|
283
|
+
this.isCrashed = true;
|
|
284
|
+
this.sendEvent(new Events_1.ProcessCrashEvent({ type, message, stack, additionalInfo: additionalInfo !== null && additionalInfo !== void 0 ? additionalInfo : {} }));
|
|
285
|
+
setTimeout(() => void this.shutdown(), 5000).unref();
|
|
286
|
+
}
|
|
214
287
|
teardownProcessErrorHandlers() {
|
|
215
288
|
if (this._uncaughtExceptionHandler) {
|
|
216
289
|
process.removeListener('uncaughtException', this._uncaughtExceptionHandler);
|
|
@@ -452,9 +525,14 @@ class BrightScriptDebugSession extends debugadapter_1.LoggingDebugSession {
|
|
|
452
525
|
catch (e) {
|
|
453
526
|
return this.shutdown(`Could not resolve ip address for host '${this.launchConfiguration.host}'`);
|
|
454
527
|
}
|
|
455
|
-
//
|
|
528
|
+
// fetch device info if not supplied via launch config
|
|
456
529
|
try {
|
|
457
|
-
|
|
530
|
+
if (this.launchConfiguration.deviceInfo) {
|
|
531
|
+
this.deviceInfo = roku_deploy_1.rokuDeploy.enhanceDeviceInfo(this.launchConfiguration.deviceInfo);
|
|
532
|
+
}
|
|
533
|
+
else {
|
|
534
|
+
this.deviceInfo = await roku_deploy_1.rokuDeploy.getDeviceInfo({ host: this.launchConfiguration.host, remotePort: this.launchConfiguration.remotePort, enhance: true, timeout: 4000 });
|
|
535
|
+
}
|
|
458
536
|
if (this.deviceInfo.ecpSettingMode === 'limited') {
|
|
459
537
|
return await this.shutdown(`ECP access is limited on this Roku. Please change it to 'permissive' or 'enabled' and try again. (device: ${this.launchConfiguration.host})`);
|
|
460
538
|
}
|
|
@@ -1202,6 +1280,7 @@ class BrightScriptDebugSession extends debugadapter_1.LoggingDebugSession {
|
|
|
1202
1280
|
sourceDirs: componentLibrary.sourceDirs,
|
|
1203
1281
|
bsConst: componentLibrary.bsConst,
|
|
1204
1282
|
install: componentLibrary.install,
|
|
1283
|
+
enablePostfix: componentLibrary.enablePostfix,
|
|
1205
1284
|
injectRaleTrackerTask: componentLibrary.injectRaleTrackerTask,
|
|
1206
1285
|
raleTrackerTaskFileLocation: componentLibrary.raleTrackerTaskFileLocation,
|
|
1207
1286
|
libraryIndex: libraryIndex,
|
|
@@ -1453,24 +1532,8 @@ class BrightScriptDebugSession extends debugadapter_1.LoggingDebugSession {
|
|
|
1453
1532
|
logger.info('begin', { args });
|
|
1454
1533
|
try {
|
|
1455
1534
|
const scopes = new Array();
|
|
1456
|
-
let v;
|
|
1457
1535
|
// create the locals scope
|
|
1458
|
-
let
|
|
1459
|
-
if (this.variables[localsRefId]) {
|
|
1460
|
-
v = this.variables[localsRefId];
|
|
1461
|
-
}
|
|
1462
|
-
else {
|
|
1463
|
-
v = {
|
|
1464
|
-
variablesReference: localsRefId,
|
|
1465
|
-
name: 'Locals',
|
|
1466
|
-
value: '',
|
|
1467
|
-
type: '$$Locals',
|
|
1468
|
-
frameId: args.frameId,
|
|
1469
|
-
isScope: true,
|
|
1470
|
-
childVariables: []
|
|
1471
|
-
};
|
|
1472
|
-
this.variables[localsRefId] = v;
|
|
1473
|
-
}
|
|
1536
|
+
let v = this.getOrCreateLocalsScope(args.frameId);
|
|
1474
1537
|
let localScope = {
|
|
1475
1538
|
name: 'Local',
|
|
1476
1539
|
variablesReference: v.variablesReference,
|
|
@@ -1515,6 +1578,25 @@ class BrightScriptDebugSession extends debugadapter_1.LoggingDebugSession {
|
|
|
1515
1578
|
logger.error('Error getting scopes', { error, args });
|
|
1516
1579
|
}
|
|
1517
1580
|
}
|
|
1581
|
+
/**
|
|
1582
|
+
* Get the locals scope container for a frame, creating an (unpopulated) one if it doesn't exist yet.
|
|
1583
|
+
* The child variables are filled in lazily by `populateScopeVariables`.
|
|
1584
|
+
*/
|
|
1585
|
+
getOrCreateLocalsScope(frameId) {
|
|
1586
|
+
const refId = this.getEvaluateRefId('$$locals', frameId);
|
|
1587
|
+
if (!this.variables[refId]) {
|
|
1588
|
+
this.variables[refId] = {
|
|
1589
|
+
variablesReference: refId,
|
|
1590
|
+
name: 'Locals',
|
|
1591
|
+
value: '',
|
|
1592
|
+
type: '$$Locals',
|
|
1593
|
+
frameId: frameId,
|
|
1594
|
+
isScope: true,
|
|
1595
|
+
childVariables: []
|
|
1596
|
+
};
|
|
1597
|
+
}
|
|
1598
|
+
return this.variables[refId];
|
|
1599
|
+
}
|
|
1518
1600
|
async continueRequest(response, args) {
|
|
1519
1601
|
//if we have a compile error, we should shut down
|
|
1520
1602
|
if (this.compileError) {
|
|
@@ -2009,7 +2091,7 @@ class BrightScriptDebugSession extends debugadapter_1.LoggingDebugSession {
|
|
|
2009
2091
|
return results;
|
|
2010
2092
|
}
|
|
2011
2093
|
async completionsRequest(response, args, request) {
|
|
2012
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
2094
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
|
|
2013
2095
|
this.logger.log('completionsRequest', args, request);
|
|
2014
2096
|
// this.sendEvent(new LogOutputEvent(`completionsRequest: ${args.text}`));
|
|
2015
2097
|
// this.sendEvent(new OutputEvent(`completionsRequest: ${args.text}\n`, 'stderr'));
|
|
@@ -2025,30 +2107,41 @@ class BrightScriptDebugSession extends debugadapter_1.LoggingDebugSession {
|
|
|
2025
2107
|
}
|
|
2026
2108
|
let completions = new Map();
|
|
2027
2109
|
let parentVariablePath = closestCompletionDetails.parentVariablePath;
|
|
2110
|
+
// When set, the user is typing a string key (ex: `m["fo`) and completions should insert the
|
|
2111
|
+
// key wrapped to close the access (ex: `firstName"]`) rather than appending a bare label. The
|
|
2112
|
+
// value is the closing text to append (empty when a closing bracket is already present).
|
|
2113
|
+
const stringKeyClosing = closestCompletionDetails.stringKeyClosing;
|
|
2114
|
+
// The span of input each completion replaces. The client requests completions once (at the first
|
|
2115
|
+
// character) and then filters the list as the user keeps typing, so without an explicit range that
|
|
2116
|
+
// incremental filtering is anchored incorrectly.
|
|
2117
|
+
const replaceRange = this.getCompletionReplaceRange(args);
|
|
2118
|
+
// Whether the character immediately before the replaced span is a `.` (ie. the user is doing dot
|
|
2119
|
+
// member access). A key that can't be dot-accessed (ex: `my key`) is rewritten as bracket access,
|
|
2120
|
+
// which has to consume that `.` so `m.` becomes `m["my key"]` rather than `m.["my key"]`.
|
|
2121
|
+
const lines = args.text.split('\n');
|
|
2122
|
+
const targetLine = (_a = lines[this.toDebuggerLine(args.line, 0)]) !== null && _a !== void 0 ? _a : '';
|
|
2123
|
+
const precededByDot = targetLine[replaceRange.start - 1] === '.';
|
|
2028
2124
|
// Get the completions if the variable path was valid
|
|
2029
2125
|
if (parentVariablePath) {
|
|
2030
2126
|
// If the parent variable path is an empty string, then we are looking up the local scope variables and global functions
|
|
2031
2127
|
if (parentVariablePath.length === 1 && parentVariablePath[0] === '') {
|
|
2032
2128
|
supplyLocalScopeCompletions = true;
|
|
2033
2129
|
}
|
|
2034
|
-
// Look up the parent variable
|
|
2035
|
-
let parentVariable = this.
|
|
2036
|
-
if (!parentVariable || parentVariable.childVariables.length === 0) {
|
|
2037
|
-
// We did not find the parent variable, so try to look it up from the device
|
|
2038
|
-
try {
|
|
2039
|
-
let { evalArgs } = await this.evaluateExpressionToTempVar({ expression: parentVariablePath.join('.'), frameId: args.frameId }, parentVariablePath);
|
|
2040
|
-
let result = await this.rokuAdapter.getVariable(evalArgs.expression, args.frameId);
|
|
2041
|
-
parentVariable = await this.getVariableFromResult(result, args.frameId);
|
|
2042
|
-
}
|
|
2043
|
-
catch (error) {
|
|
2044
|
-
this.logger.error('Error looking up parent completions', error, { parentVariablePath });
|
|
2045
|
-
}
|
|
2046
|
-
}
|
|
2130
|
+
// Look up the parent variable (in-memory first, then the device), scoped to the current frame.
|
|
2131
|
+
let parentVariable = await this.resolveCompletionParentVariable(parentVariablePath, args.frameId);
|
|
2047
2132
|
// provide completions for the parent variable if one was found
|
|
2048
2133
|
if (parentVariable) {
|
|
2049
|
-
|
|
2050
|
-
//
|
|
2051
|
-
|
|
2134
|
+
// arrays and lists are integer-indexed; their `[N]` elements aren't valid `.` or `["..."]`
|
|
2135
|
+
// completions (you can't write `arr.[0]` or `arr["0"]`), so don't offer them as members.
|
|
2136
|
+
// Only the interface methods below (Count, Push, ...) apply to these containers.
|
|
2137
|
+
const isIntegerIndexed = parentVariable.type === VariablesResponse_1.VariableType.Array ||
|
|
2138
|
+
parentVariable.type === VariablesResponse_1.VariableType.List ||
|
|
2139
|
+
parentVariable.type === 'roXMLList' ||
|
|
2140
|
+
parentVariable.type === 'roByteArray';
|
|
2141
|
+
const possibleFieldsAndMethods = isIntegerIndexed
|
|
2142
|
+
? []
|
|
2143
|
+
// Filter out virtual variables and the empty-named placeholder used for empty scopes
|
|
2144
|
+
: parentVariable.childVariables.filter((child) => { var _a; return child.name && ((_a = child.presentationHint) === null || _a === void 0 ? void 0 : _a.kind) !== 'virtual'; });
|
|
2052
2145
|
for (let v of possibleFieldsAndMethods) {
|
|
2053
2146
|
// Default completion type should be variable
|
|
2054
2147
|
let completionType = 'variable';
|
|
@@ -2066,37 +2159,49 @@ class BrightScriptDebugSession extends debugadapter_1.LoggingDebugSession {
|
|
|
2066
2159
|
break;
|
|
2067
2160
|
}
|
|
2068
2161
|
}
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
parentVariable.type === VariablesResponse_1.VariableType.List ||
|
|
2072
|
-
parentVariable.type === 'roXMLList' ||
|
|
2073
|
-
parentVariable.type === 'roByteArray') {
|
|
2074
|
-
label = `[${v.name}]`;
|
|
2075
|
-
}
|
|
2076
|
-
completions.set(`${completionType}-${v.name}`, {
|
|
2077
|
-
label: label,
|
|
2162
|
+
const completionItem = {
|
|
2163
|
+
label: v.name,
|
|
2078
2164
|
type: completionType,
|
|
2079
|
-
|
|
2080
|
-
|
|
2165
|
+
//rank a variable's own members/locals above everything else
|
|
2166
|
+
sortText: `${CompletionSortTier.Member}${v.name}`
|
|
2167
|
+
};
|
|
2168
|
+
if (stringKeyClosing !== undefined) {
|
|
2169
|
+
// Insert the key and close the access, ex: `firstName"]` (the replacement range is applied
|
|
2170
|
+
// below). A `"` inside the key is escaped as `""` so the inserted string literal stays valid
|
|
2171
|
+
// (ex: a key of `a"b` is inserted as `a""b`).
|
|
2172
|
+
completionItem.text = `${v.name.replace(/"/g, '""')}${stringKeyClosing}`;
|
|
2173
|
+
}
|
|
2174
|
+
else if (!supplyLocalScopeCompletions && precededByDot && !/^[a-z_][a-z0-9_]*$/i.test(v.name)) {
|
|
2175
|
+
// The key can't be dot-accessed (ex: it has a space or a quote), so rewrite the access as
|
|
2176
|
+
// bracket notation and consume the `.` before the cursor: `m.` -> `m["my key"]`. A `"` in
|
|
2177
|
+
// the key is escaped as `""` so the inserted string literal stays valid.
|
|
2178
|
+
completionItem.text = `["${v.name.replace(/"/g, '""')}"]`;
|
|
2179
|
+
completionItem.start = replaceRange.start - 1;
|
|
2180
|
+
completionItem.length = replaceRange.length + 1;
|
|
2181
|
+
}
|
|
2182
|
+
completions.set(`${completionType}-${v.name}`, completionItem);
|
|
2081
2183
|
}
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
//
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
completions
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2184
|
+
// Interface methods aren't valid string keys, so skip them when completing a string key
|
|
2185
|
+
if (stringKeyClosing === undefined) {
|
|
2186
|
+
let parentComponentType = this.debuggerVarTypeToRoType(parentVariable.type).toLowerCase();
|
|
2187
|
+
//assemble a list of all methods on the parent component
|
|
2188
|
+
const methods = [
|
|
2189
|
+
//if the parent variable is an actual interface (if applicable) Ex: `ifString` or `ifArray`
|
|
2190
|
+
...(_c = (_b = roku_types_1.interfaces[parentComponentType]) === null || _b === void 0 ? void 0 : _b.methods) !== null && _c !== void 0 ? _c : [],
|
|
2191
|
+
//interfaces from component of this name (if applicable) Ex: `roSGNode` or `roDateTime`
|
|
2192
|
+
...(_e = (_d = roku_types_1.components[parentComponentType]) === null || _d === void 0 ? void 0 : _d.interfaces.map((i) => { var _a; return (_a = roku_types_1.interfaces[i.name.toLowerCase()]) === null || _a === void 0 ? void 0 : _a.methods; })) !== null && _e !== void 0 ? _e : [],
|
|
2193
|
+
// Add parent event function completions (if applicable) Ex: `roSGNodeEvent` or `roDeviceInfoEvent`
|
|
2194
|
+
...(_g = (_f = roku_types_1.events[parentComponentType]) === null || _f === void 0 ? void 0 : _f.methods) !== null && _g !== void 0 ? _g : []
|
|
2195
|
+
].flat();
|
|
2196
|
+
// Based on the results of interface, component, and event looks up, add all the methods to the completions
|
|
2197
|
+
for (const method of methods) {
|
|
2198
|
+
completions.set(`method-${method.name}`, {
|
|
2199
|
+
label: method.name,
|
|
2200
|
+
type: 'method',
|
|
2201
|
+
detail: (_h = method.description) !== null && _h !== void 0 ? _h : '',
|
|
2202
|
+
sortText: `${CompletionSortTier.Method}${method.name}`
|
|
2203
|
+
});
|
|
2204
|
+
}
|
|
2100
2205
|
}
|
|
2101
2206
|
// Add the global functions to the completions results
|
|
2102
2207
|
if (supplyLocalScopeCompletions) {
|
|
@@ -2104,8 +2209,8 @@ class BrightScriptDebugSession extends debugadapter_1.LoggingDebugSession {
|
|
|
2104
2209
|
completions.set(`function-${globalCallable.name.toLocaleLowerCase()}`, {
|
|
2105
2210
|
label: globalCallable.name,
|
|
2106
2211
|
type: 'function',
|
|
2107
|
-
detail: (
|
|
2108
|
-
sortText:
|
|
2212
|
+
detail: (_k = (_j = globalCallable.shortDescription) !== null && _j !== void 0 ? _j : globalCallable.documentation) !== null && _k !== void 0 ? _k : '',
|
|
2213
|
+
sortText: `${CompletionSortTier.Global}${globalCallable.name}`
|
|
2109
2214
|
});
|
|
2110
2215
|
}
|
|
2111
2216
|
const frame = this.rokuAdapter.getStackFrameById(args.frameId);
|
|
@@ -2116,7 +2221,7 @@ class BrightScriptDebugSession extends debugadapter_1.LoggingDebugSession {
|
|
|
2116
2221
|
completions.set(`${scopeFunction.completionItemKind}-${scopeFunction.name.toLocaleLowerCase()}`, {
|
|
2117
2222
|
label: scopeFunction.name,
|
|
2118
2223
|
type: scopeFunction.completionItemKind,
|
|
2119
|
-
sortText:
|
|
2224
|
+
sortText: `${CompletionSortTier.ScopeFunction}${scopeFunction.name}`
|
|
2120
2225
|
});
|
|
2121
2226
|
}
|
|
2122
2227
|
}
|
|
@@ -2127,8 +2232,14 @@ class BrightScriptDebugSession extends debugadapter_1.LoggingDebugSession {
|
|
|
2127
2232
|
}
|
|
2128
2233
|
}
|
|
2129
2234
|
}
|
|
2130
|
-
//
|
|
2131
|
-
//
|
|
2235
|
+
// Apply the default replacement span to every completion that didn't already set its own (bracket
|
|
2236
|
+
// rewrites above use an extended range that also consumes the preceding `.`).
|
|
2237
|
+
for (const target of completions.values()) {
|
|
2238
|
+
if (target.start === undefined) {
|
|
2239
|
+
target.start = replaceRange.start;
|
|
2240
|
+
target.length = replaceRange.length;
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2132
2243
|
response.body = {
|
|
2133
2244
|
targets: [...completions.values()]
|
|
2134
2245
|
};
|
|
@@ -2144,32 +2255,76 @@ class BrightScriptDebugSession extends debugadapter_1.LoggingDebugSession {
|
|
|
2144
2255
|
* Gets the closest completion details the incoming completion request.
|
|
2145
2256
|
*/
|
|
2146
2257
|
getClosestCompletionDetails(args) {
|
|
2258
|
+
var _a;
|
|
2147
2259
|
const incomingText = args.text;
|
|
2148
2260
|
const lines = incomingText.split('\n');
|
|
2149
2261
|
let lineNumber = this.toDebuggerLine(args.line, 0);
|
|
2150
2262
|
let column = this.toDebuggerColumn(args.column);
|
|
2151
|
-
const targetLine = lines[lineNumber];
|
|
2152
|
-
|
|
2153
|
-
let i = column - 1;
|
|
2263
|
+
const targetLine = (_a = lines[lineNumber]) !== null && _a !== void 0 ? _a : '';
|
|
2264
|
+
const cursorIndex = column - 1;
|
|
2154
2265
|
const variableChars = /[a-z0-9_\.]/i;
|
|
2155
|
-
// If the character
|
|
2156
|
-
|
|
2266
|
+
// If the character immediately to the right of the cursor is a variable character, then we are
|
|
2267
|
+
// in the middle of a token and should not supply completions yet.
|
|
2268
|
+
if (cursorIndex + 1 < targetLine.length && variableChars.test(targetLine[cursorIndex + 1])) {
|
|
2157
2269
|
return undefined;
|
|
2158
2270
|
}
|
|
2159
|
-
//
|
|
2160
|
-
|
|
2161
|
-
|
|
2271
|
+
// Determine where the expression we want to complete ends, and whether we are completing the
|
|
2272
|
+
// members of that expression. A trailing `.` or being inside an unclosed string-key bracket
|
|
2273
|
+
// (ex: `m["fo`) are both treated as member access on the parent expression.
|
|
2274
|
+
let endColumn = column;
|
|
2275
|
+
let isMemberAccess = false;
|
|
2276
|
+
//when set (including ''), the user is completing a string key; the value is the text to append to
|
|
2277
|
+
//close the access (ex: `"]`), empty when a closing bracket is already present
|
|
2278
|
+
let stringKeyClosing;
|
|
2279
|
+
const openBracket = this.findUnclosedOpener(targetLine, column);
|
|
2280
|
+
if ((openBracket === null || openBracket === void 0 ? void 0 : openBracket.char) === '[') {
|
|
2281
|
+
// find the opening quote (skipping any whitespace after the `[`)
|
|
2282
|
+
let quoteIndex = openBracket.index + 1;
|
|
2283
|
+
while (targetLine[quoteIndex] === ' ' || targetLine[quoteIndex] === '\t') {
|
|
2284
|
+
quoteIndex++;
|
|
2285
|
+
}
|
|
2286
|
+
const quote = targetLine[quoteIndex];
|
|
2287
|
+
if (quote === '"' || quote === `'`) {
|
|
2288
|
+
// The user is typing a string key, so complete the keys of the expression before the `[`
|
|
2289
|
+
endColumn = openBracket.index;
|
|
2290
|
+
isMemberAccess = true;
|
|
2291
|
+
// close the string and bracket only when there is nothing meaningful after the cursor
|
|
2292
|
+
stringKeyClosing = targetLine.slice(column).trim() === '' ? `${quote}]` : '';
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
// Walk backwards from `endColumn` to find the start of the variable path, stepping over balanced
|
|
2296
|
+
// `[...]` index access so paths like `arr[0].name` are captured as a whole.
|
|
2297
|
+
let startIndex = endColumn - 1;
|
|
2298
|
+
let bracketDepth = 0;
|
|
2299
|
+
while (startIndex >= 0) {
|
|
2300
|
+
const char = targetLine[startIndex];
|
|
2301
|
+
if (char === ']') {
|
|
2302
|
+
bracketDepth++;
|
|
2303
|
+
}
|
|
2304
|
+
else if (char === '[') {
|
|
2305
|
+
if (bracketDepth === 0) {
|
|
2306
|
+
// An unbalanced `[` means we hit the start of an index/key being typed; stop here.
|
|
2307
|
+
break;
|
|
2308
|
+
}
|
|
2309
|
+
bracketDepth--;
|
|
2310
|
+
}
|
|
2311
|
+
else if (bracketDepth === 0 && (char === undefined || !variableChars.test(char))) {
|
|
2312
|
+
break;
|
|
2313
|
+
}
|
|
2314
|
+
startIndex--;
|
|
2162
2315
|
}
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
//
|
|
2166
|
-
//
|
|
2167
|
-
// and results in `.name` being the variable path string
|
|
2316
|
+
const variablePathString = targetLine.slice(startIndex + 1, endColumn);
|
|
2317
|
+
// Attempted dot access on something unexpected.
|
|
2318
|
+
// Example: `getPerson().name` where `getPerson()` is not a valid variable path,
|
|
2319
|
+
// which leaves `.name` as the variable path string.
|
|
2168
2320
|
if (variablePathString.startsWith('.')) {
|
|
2169
2321
|
return undefined;
|
|
2170
2322
|
}
|
|
2323
|
+
if (variablePathString.endsWith('.')) {
|
|
2324
|
+
isMemberAccess = true;
|
|
2325
|
+
}
|
|
2171
2326
|
// Get the variable path from the text
|
|
2172
|
-
let variablePath
|
|
2327
|
+
let variablePath;
|
|
2173
2328
|
if (!variablePathString.trim()) {
|
|
2174
2329
|
// The text was empty so assume via '' that we are looking up the local scope variables and global functions
|
|
2175
2330
|
variablePath = [''];
|
|
@@ -2185,28 +2340,170 @@ class BrightScriptDebugSession extends debugadapter_1.LoggingDebugSession {
|
|
|
2185
2340
|
if (!variablePath) {
|
|
2186
2341
|
return undefined;
|
|
2187
2342
|
}
|
|
2188
|
-
|
|
2189
|
-
//
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
}
|
|
2193
|
-
else {
|
|
2194
|
-
// Otherwise, pull completions for the parent variable
|
|
2195
|
-
parentVariablePath = variablePath.slice(0, variablePath.length - 1);
|
|
2196
|
-
}
|
|
2197
|
-
// If the parent variable path is empty or an empty string, then we are looking up the local scope variables and global functions
|
|
2343
|
+
// For member access we complete the members of the full expression. Otherwise we complete the
|
|
2344
|
+
// siblings of the final (partial) segment, so drop it to get the parent.
|
|
2345
|
+
let parentVariablePath = isMemberAccess ? variablePath : variablePath.slice(0, variablePath.length - 1);
|
|
2346
|
+
// An empty parent path means we are looking up the local scope variables and global functions
|
|
2198
2347
|
if (parentVariablePath.length === 0) {
|
|
2199
2348
|
parentVariablePath = [''];
|
|
2200
2349
|
}
|
|
2201
|
-
|
|
2350
|
+
const result = { parentVariablePath: parentVariablePath };
|
|
2351
|
+
// Only attach the string-key context when we actually resolved a parent object to complete keys on
|
|
2352
|
+
if (stringKeyClosing !== undefined && !(parentVariablePath.length === 1 && parentVariablePath[0] === '')) {
|
|
2353
|
+
result.stringKeyClosing = stringKeyClosing;
|
|
2354
|
+
}
|
|
2355
|
+
return result;
|
|
2356
|
+
}
|
|
2357
|
+
/**
|
|
2358
|
+
* Compute the span of input text that a completion replaces: the run of identifier characters
|
|
2359
|
+
* immediately before the cursor. This lets the client filter the list correctly as the user keeps
|
|
2360
|
+
* typing past the first character.
|
|
2361
|
+
*
|
|
2362
|
+
* `start` is a 0-based offset into the line, NOT a client column. Per the Debug Adapter Protocol,
|
|
2363
|
+
* `CompletionItem.start` is measured in UTF-16 code units and the client maps it to a position
|
|
2364
|
+
* itself, so it must not be run through `toClientColumn` (unlike stack-frame, breakpoint, and scope
|
|
2365
|
+
* positions). Our debugger column base is already 0-based, so the internal offset is sent as-is.
|
|
2366
|
+
*/
|
|
2367
|
+
getCompletionReplaceRange(args) {
|
|
2368
|
+
var _a;
|
|
2369
|
+
const lines = args.text.split('\n');
|
|
2370
|
+
const lineNumber = this.toDebuggerLine(args.line, 0);
|
|
2371
|
+
const cursorOffset = this.toDebuggerColumn(args.column);
|
|
2372
|
+
const targetLine = (_a = lines[lineNumber]) !== null && _a !== void 0 ? _a : '';
|
|
2373
|
+
const identifierChars = /[a-z0-9_]/i;
|
|
2374
|
+
let wordStart = cursorOffset;
|
|
2375
|
+
while (wordStart > 0 && identifierChars.test(targetLine[wordStart - 1])) {
|
|
2376
|
+
wordStart--;
|
|
2377
|
+
}
|
|
2378
|
+
return {
|
|
2379
|
+
start: wordStart,
|
|
2380
|
+
length: cursorOffset - wordStart
|
|
2381
|
+
};
|
|
2382
|
+
}
|
|
2383
|
+
/**
|
|
2384
|
+
* Scan backwards from `column` to find the nearest opening bracket (`(`, `[`, or `{`) that has not
|
|
2385
|
+
* been closed before the cursor. Returns the opener's index and character, or undefined if none.
|
|
2386
|
+
*/
|
|
2387
|
+
findUnclosedOpener(line, column) {
|
|
2388
|
+
let depth = 0;
|
|
2389
|
+
for (let i = column - 1; i >= 0; i--) {
|
|
2390
|
+
const char = line[i];
|
|
2391
|
+
if (char === ')' || char === ']' || char === '}') {
|
|
2392
|
+
depth++;
|
|
2393
|
+
}
|
|
2394
|
+
else if (char === '(' || char === '[' || char === '{') {
|
|
2395
|
+
if (depth === 0) {
|
|
2396
|
+
return { index: i, char: char };
|
|
2397
|
+
}
|
|
2398
|
+
depth--;
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
return undefined;
|
|
2402
|
+
}
|
|
2403
|
+
/**
|
|
2404
|
+
* Resolve the parent variable for a completion request. Prefers the in-memory locals for the frame,
|
|
2405
|
+
* then falls back to a device lookup. Device lookups are cached for the duration of the paused state
|
|
2406
|
+
* (cleared by `clearState`) so repeated completion requests on the same path don't hammer the device.
|
|
2407
|
+
*/
|
|
2408
|
+
async resolveCompletionParentVariable(parentVariablePath, frameId) {
|
|
2409
|
+
// For local-scope completions, make sure the frame's locals are fetched on demand. Otherwise they
|
|
2410
|
+
// would only appear once the user expands the Variables panel (which is what triggers the fetch).
|
|
2411
|
+
const isLocalScope = parentVariablePath.length === 1 && parentVariablePath[0] === '';
|
|
2412
|
+
if (isLocalScope) {
|
|
2413
|
+
const localsScope = this.getOrCreateLocalsScope(frameId);
|
|
2414
|
+
if (!localsScope.isResolved) {
|
|
2415
|
+
try {
|
|
2416
|
+
await this.populateScopeVariables(localsScope, { variablesReference: localsScope.variablesReference });
|
|
2417
|
+
}
|
|
2418
|
+
catch (error) {
|
|
2419
|
+
this.logger.debug('Could not populate locals for completions', error, { frameId });
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
const inMemory = this.findFrameVariableByPath(parentVariablePath, frameId);
|
|
2424
|
+
if (inMemory && inMemory.childVariables.length > 0) {
|
|
2425
|
+
return inMemory;
|
|
2426
|
+
}
|
|
2427
|
+
// Rebuild a valid accessor expression for the device lookup. Joining with `.` is wrong for indexed
|
|
2428
|
+
// segments (ex: `m.services[0]` would become the invalid `m.services.0` and the index gets dropped).
|
|
2429
|
+
const expression = this.buildVariableExpression(parentVariablePath);
|
|
2430
|
+
const cacheKey = `${frameId}:${expression}`;
|
|
2431
|
+
if (this.completionParentVariableCache.has(cacheKey)) {
|
|
2432
|
+
return this.completionParentVariableCache.get(cacheKey);
|
|
2433
|
+
}
|
|
2434
|
+
let parentVariable;
|
|
2435
|
+
try {
|
|
2436
|
+
let { evalArgs } = await this.evaluateExpressionToTempVar({ expression: expression, frameId: frameId }, parentVariablePath);
|
|
2437
|
+
let result = await this.rokuAdapter.getVariable(evalArgs.expression, frameId);
|
|
2438
|
+
parentVariable = await this.getVariableFromResult(result, frameId);
|
|
2439
|
+
}
|
|
2440
|
+
catch (error) {
|
|
2441
|
+
// A failed lookup is expected while the user is still typing an incomplete expression, so keep it quiet.
|
|
2442
|
+
this.logger.debug('Could not resolve parent variable for completions', error, { parentVariablePath });
|
|
2443
|
+
parentVariable = undefined;
|
|
2444
|
+
}
|
|
2445
|
+
this.completionParentVariableCache.set(cacheKey, parentVariable);
|
|
2446
|
+
return parentVariable;
|
|
2447
|
+
}
|
|
2448
|
+
/**
|
|
2449
|
+
* Rebuild a valid BrightScript accessor expression from a resolved variable path. String-literal keys
|
|
2450
|
+
* arrive already quoted from `getVariablePath` and are emitted as `["key"]` so they stay case-sensitive
|
|
2451
|
+
* on the device (Roku AAs can be set case-sensitive); numeric segments use `[index]`, and identifiers
|
|
2452
|
+
* use dot access. This keeps array indices and string keys correct through the device lookup.
|
|
2453
|
+
*/
|
|
2454
|
+
buildVariableExpression(segments) {
|
|
2455
|
+
return segments.reduce((expression, segment, index) => {
|
|
2456
|
+
if (index === 0) {
|
|
2457
|
+
return segment;
|
|
2458
|
+
}
|
|
2459
|
+
//already-quoted string key (preserve the quotes so the device matches it case-sensitively).
|
|
2460
|
+
//A lone `"` is not a quoted literal (the shortest is `""`), so require at least 2 chars.
|
|
2461
|
+
if (segment.length >= 2 && segment.startsWith('"') && segment.endsWith('"')) {
|
|
2462
|
+
return `${expression}[${segment}]`;
|
|
2463
|
+
}
|
|
2464
|
+
if (/^[0-9]+$/.test(segment)) {
|
|
2465
|
+
return `${expression}[${segment}]`;
|
|
2466
|
+
}
|
|
2467
|
+
if (/^[a-z_][a-z0-9_]*$/i.test(segment)) {
|
|
2468
|
+
return `${expression}.${segment}`;
|
|
2469
|
+
}
|
|
2470
|
+
return `${expression}["${segment.replace(/"/g, '""')}"]`;
|
|
2471
|
+
}, '');
|
|
2472
|
+
}
|
|
2473
|
+
/**
|
|
2474
|
+
* Normalize a variable path segment or variable name for matching: drop surrounding string-key quotes
|
|
2475
|
+
* and lower-case it. BrightScript variables and dotted access are case-insensitive, and the device
|
|
2476
|
+
* reports names lower-cased, so this lets the in-memory lookup find the parent regardless of the casing
|
|
2477
|
+
* the user typed (ex: `topRef` matching the cached `topref`).
|
|
2478
|
+
*/
|
|
2479
|
+
normalizeVariableName(name) {
|
|
2480
|
+
let value = name !== null && name !== void 0 ? name : '';
|
|
2481
|
+
if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
|
|
2482
|
+
value = value.slice(1, -1).replace(/""/g, '"');
|
|
2483
|
+
}
|
|
2484
|
+
return value.toLowerCase();
|
|
2485
|
+
}
|
|
2486
|
+
/**
|
|
2487
|
+
* Resolve a variable path against the current frame's local scope. The first path segment is matched
|
|
2488
|
+
* against the frame's locals (not the global pool of every materialized variable), then we walk down
|
|
2489
|
+
* the child variables. The empty path (`['']`) resolves to the locals scope container itself.
|
|
2490
|
+
*/
|
|
2491
|
+
findFrameVariableByPath(path, frameId) {
|
|
2492
|
+
var _a;
|
|
2493
|
+
const localsContainer = this.variables[this.getEvaluateRefId('$$locals', frameId)];
|
|
2494
|
+
if (path.length === 1 && path[0] === '') {
|
|
2495
|
+
return localsContainer;
|
|
2496
|
+
}
|
|
2497
|
+
return this.findVariableByPath((_a = localsContainer === null || localsContainer === void 0 ? void 0 : localsContainer.childVariables) !== null && _a !== void 0 ? _a : [], path, frameId);
|
|
2202
2498
|
}
|
|
2203
2499
|
findVariableByPath(variables, path, frameId) {
|
|
2204
2500
|
var _a;
|
|
2205
2501
|
let current = null;
|
|
2206
2502
|
for (const name of path) {
|
|
2207
|
-
|
|
2503
|
+
const normalizedName = this.normalizeVariableName(name);
|
|
2504
|
+
// Find the object matching the current name in the data (case-insensitive, per BrightScript)
|
|
2208
2505
|
current = (_a = (Array.isArray(variables) ? variables : current === null || current === void 0 ? void 0 : current.childVariables)) === null || _a === void 0 ? void 0 : _a.find(obj => {
|
|
2209
|
-
return obj.name ===
|
|
2506
|
+
return this.normalizeVariableName(obj.name) === normalizedName && obj.frameId === frameId;
|
|
2210
2507
|
});
|
|
2211
2508
|
// If no match is found, return null
|
|
2212
2509
|
if (!current) {
|
|
@@ -2597,6 +2894,7 @@ class BrightScriptDebugSession extends debugadapter_1.LoggingDebugSession {
|
|
|
2597
2894
|
clearState() {
|
|
2598
2895
|
//erase all cached variables
|
|
2599
2896
|
this.variables = {};
|
|
2897
|
+
this.completionParentVariableCache.clear();
|
|
2600
2898
|
}
|
|
2601
2899
|
/**
|
|
2602
2900
|
* Sends a launch progress event to the client if the client supports progress reporting.
|
|
@@ -2700,9 +2998,19 @@ class BrightScriptDebugSession extends debugadapter_1.LoggingDebugSession {
|
|
|
2700
2998
|
* the same promise on subsequent calls
|
|
2701
2999
|
*/
|
|
2702
3000
|
async shutdown(errorMessage, modal = false) {
|
|
3001
|
+
var _a;
|
|
2703
3002
|
if (this.shutdownPromise === undefined) {
|
|
2704
3003
|
this.logger.log('[shutdown] Beginning shutdown sequence', errorMessage);
|
|
2705
|
-
|
|
3004
|
+
//Backstop: if the graceful shutdown hangs (e.g. pressHomeButton against an unreachable
|
|
3005
|
+
//device), force-exit anyway so we never leave an orphaned adapter running forever
|
|
3006
|
+
const forceExitTimer = setTimeout(() => {
|
|
3007
|
+
this.logger.error('[shutdown] graceful shutdown timed out; forcing exit');
|
|
3008
|
+
this.forceExit();
|
|
3009
|
+
}, this.shutdownForceExitTimeout);
|
|
3010
|
+
(_a = forceExitTimer.unref) === null || _a === void 0 ? void 0 : _a.call(forceExitTimer);
|
|
3011
|
+
this.shutdownPromise = this._shutdown(errorMessage, modal).finally(() => {
|
|
3012
|
+
clearTimeout(forceExitTimer);
|
|
3013
|
+
});
|
|
2706
3014
|
}
|
|
2707
3015
|
else {
|
|
2708
3016
|
this.logger.log('[shutdown] Tried to call `.shutdown()` again. Returning the same promise');
|