roku-debug 0.23.12 → 0.23.14
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 +22 -0
- package/dist/LaunchConfiguration.d.ts +24 -0
- 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 +54 -1
- package/dist/debugSession/BrightScriptDebugSession.js +341 -98
- package/dist/debugSession/BrightScriptDebugSession.js.map +1 -1
- package/dist/debugSession/Events.d.ts +4 -0
- package/dist/debugSession/Events.js +5 -1
- package/dist/debugSession/Events.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 +34 -1
- package/dist/managers/ProjectManager.js +50 -13
- package/dist/managers/ProjectManager.js.map +1 -1
- package/package.json +3 -3
|
@@ -32,6 +32,17 @@ 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();
|
|
@@ -58,6 +69,12 @@ class BrightScriptDebugSession extends debugadapter_1.LoggingDebugSession {
|
|
|
58
69
|
this.evaluateRefIdLookup = {};
|
|
59
70
|
this.evaluateRefIdCounter = 1;
|
|
60
71
|
this.variables = {};
|
|
72
|
+
/**
|
|
73
|
+
* Caches the device lookups performed while resolving completion requests. Variables don't change
|
|
74
|
+
* while the debugger is paused, so this avoids repeated round-trips for the same path. Cleared by
|
|
75
|
+
* `clearState` whenever the debugger resumes or steps.
|
|
76
|
+
*/
|
|
77
|
+
this.completionParentVariableCache = new Map();
|
|
61
78
|
this.tempVarPrefix = '__rokudebug__';
|
|
62
79
|
/**
|
|
63
80
|
* 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
|
|
@@ -432,7 +449,7 @@ class BrightScriptDebugSession extends debugadapter_1.LoggingDebugSession {
|
|
|
432
449
|
return config;
|
|
433
450
|
}
|
|
434
451
|
async launchRequest(response, config) {
|
|
435
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
452
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
|
|
436
453
|
const logEnd = this.logger.timeStart('log', '[launchRequest] launch');
|
|
437
454
|
try {
|
|
438
455
|
this.resetSessionState();
|
|
@@ -485,6 +502,12 @@ class BrightScriptDebugSession extends debugadapter_1.LoggingDebugSession {
|
|
|
485
502
|
]);
|
|
486
503
|
//all of the projects have been successfully staged.
|
|
487
504
|
this.stagingDefered.tryResolve();
|
|
505
|
+
//if the client supports it, let it process (inspect/modify) each project's staging dir before we package them
|
|
506
|
+
if ((_h = this.launchConfiguration.clientCapabilities) === null || _h === void 0 ? void 0 : _h.supportsProcessStagingDir) {
|
|
507
|
+
await this.sendCustomRequest('processStagingDir', {
|
|
508
|
+
projects: this.projectManager.getProjectStagingInfo()
|
|
509
|
+
});
|
|
510
|
+
}
|
|
488
511
|
packageEnd();
|
|
489
512
|
if (this.enableDebugProtocol) {
|
|
490
513
|
util_1.util.log(`Connecting to Roku via the BrightScript debug protocol at ${this.launchConfiguration.host}:${this.launchConfiguration.controlPort}`);
|
|
@@ -531,8 +554,8 @@ class BrightScriptDebugSession extends debugadapter_1.LoggingDebugSession {
|
|
|
531
554
|
util_1.util.log('Encountered an issue during the launch process');
|
|
532
555
|
util_1.util.log(e === null || e === void 0 ? void 0 : e.stack);
|
|
533
556
|
//send any compile errors to the client
|
|
534
|
-
await ((
|
|
535
|
-
const message = (e instanceof Exceptions_1.SocketConnectionInUseError) ? e.message : ((
|
|
557
|
+
await ((_j = this.rokuAdapter) === null || _j === void 0 ? void 0 : _j.sendErrors());
|
|
558
|
+
const message = (e instanceof Exceptions_1.SocketConnectionInUseError) ? e.message : ((_k = e === null || e === void 0 ? void 0 : e.stack) !== null && _k !== void 0 ? _k : e);
|
|
536
559
|
await this.shutdown(message, true);
|
|
537
560
|
}
|
|
538
561
|
else {
|
|
@@ -1196,6 +1219,7 @@ class BrightScriptDebugSession extends debugadapter_1.LoggingDebugSession {
|
|
|
1196
1219
|
sourceDirs: componentLibrary.sourceDirs,
|
|
1197
1220
|
bsConst: componentLibrary.bsConst,
|
|
1198
1221
|
install: componentLibrary.install,
|
|
1222
|
+
enablePostfix: componentLibrary.enablePostfix,
|
|
1199
1223
|
injectRaleTrackerTask: componentLibrary.injectRaleTrackerTask,
|
|
1200
1224
|
raleTrackerTaskFileLocation: componentLibrary.raleTrackerTaskFileLocation,
|
|
1201
1225
|
libraryIndex: libraryIndex,
|
|
@@ -1447,24 +1471,8 @@ class BrightScriptDebugSession extends debugadapter_1.LoggingDebugSession {
|
|
|
1447
1471
|
logger.info('begin', { args });
|
|
1448
1472
|
try {
|
|
1449
1473
|
const scopes = new Array();
|
|
1450
|
-
let v;
|
|
1451
1474
|
// create the locals scope
|
|
1452
|
-
let
|
|
1453
|
-
if (this.variables[localsRefId]) {
|
|
1454
|
-
v = this.variables[localsRefId];
|
|
1455
|
-
}
|
|
1456
|
-
else {
|
|
1457
|
-
v = {
|
|
1458
|
-
variablesReference: localsRefId,
|
|
1459
|
-
name: 'Locals',
|
|
1460
|
-
value: '',
|
|
1461
|
-
type: '$$Locals',
|
|
1462
|
-
frameId: args.frameId,
|
|
1463
|
-
isScope: true,
|
|
1464
|
-
childVariables: []
|
|
1465
|
-
};
|
|
1466
|
-
this.variables[localsRefId] = v;
|
|
1467
|
-
}
|
|
1475
|
+
let v = this.getOrCreateLocalsScope(args.frameId);
|
|
1468
1476
|
let localScope = {
|
|
1469
1477
|
name: 'Local',
|
|
1470
1478
|
variablesReference: v.variablesReference,
|
|
@@ -1509,6 +1517,25 @@ class BrightScriptDebugSession extends debugadapter_1.LoggingDebugSession {
|
|
|
1509
1517
|
logger.error('Error getting scopes', { error, args });
|
|
1510
1518
|
}
|
|
1511
1519
|
}
|
|
1520
|
+
/**
|
|
1521
|
+
* Get the locals scope container for a frame, creating an (unpopulated) one if it doesn't exist yet.
|
|
1522
|
+
* The child variables are filled in lazily by `populateScopeVariables`.
|
|
1523
|
+
*/
|
|
1524
|
+
getOrCreateLocalsScope(frameId) {
|
|
1525
|
+
const refId = this.getEvaluateRefId('$$locals', frameId);
|
|
1526
|
+
if (!this.variables[refId]) {
|
|
1527
|
+
this.variables[refId] = {
|
|
1528
|
+
variablesReference: refId,
|
|
1529
|
+
name: 'Locals',
|
|
1530
|
+
value: '',
|
|
1531
|
+
type: '$$Locals',
|
|
1532
|
+
frameId: frameId,
|
|
1533
|
+
isScope: true,
|
|
1534
|
+
childVariables: []
|
|
1535
|
+
};
|
|
1536
|
+
}
|
|
1537
|
+
return this.variables[refId];
|
|
1538
|
+
}
|
|
1512
1539
|
async continueRequest(response, args) {
|
|
1513
1540
|
//if we have a compile error, we should shut down
|
|
1514
1541
|
if (this.compileError) {
|
|
@@ -2003,7 +2030,7 @@ class BrightScriptDebugSession extends debugadapter_1.LoggingDebugSession {
|
|
|
2003
2030
|
return results;
|
|
2004
2031
|
}
|
|
2005
2032
|
async completionsRequest(response, args, request) {
|
|
2006
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
2033
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
|
|
2007
2034
|
this.logger.log('completionsRequest', args, request);
|
|
2008
2035
|
// this.sendEvent(new LogOutputEvent(`completionsRequest: ${args.text}`));
|
|
2009
2036
|
// this.sendEvent(new OutputEvent(`completionsRequest: ${args.text}\n`, 'stderr'));
|
|
@@ -2019,30 +2046,41 @@ class BrightScriptDebugSession extends debugadapter_1.LoggingDebugSession {
|
|
|
2019
2046
|
}
|
|
2020
2047
|
let completions = new Map();
|
|
2021
2048
|
let parentVariablePath = closestCompletionDetails.parentVariablePath;
|
|
2049
|
+
// When set, the user is typing a string key (ex: `m["fo`) and completions should insert the
|
|
2050
|
+
// key wrapped to close the access (ex: `firstName"]`) rather than appending a bare label. The
|
|
2051
|
+
// value is the closing text to append (empty when a closing bracket is already present).
|
|
2052
|
+
const stringKeyClosing = closestCompletionDetails.stringKeyClosing;
|
|
2053
|
+
// The span of input each completion replaces. The client requests completions once (at the first
|
|
2054
|
+
// character) and then filters the list as the user keeps typing, so without an explicit range that
|
|
2055
|
+
// incremental filtering is anchored incorrectly.
|
|
2056
|
+
const replaceRange = this.getCompletionReplaceRange(args);
|
|
2057
|
+
// Whether the character immediately before the replaced span is a `.` (ie. the user is doing dot
|
|
2058
|
+
// member access). A key that can't be dot-accessed (ex: `my key`) is rewritten as bracket access,
|
|
2059
|
+
// which has to consume that `.` so `m.` becomes `m["my key"]` rather than `m.["my key"]`.
|
|
2060
|
+
const lines = args.text.split('\n');
|
|
2061
|
+
const targetLine = (_a = lines[this.toDebuggerLine(args.line, 0)]) !== null && _a !== void 0 ? _a : '';
|
|
2062
|
+
const precededByDot = targetLine[replaceRange.start - 1] === '.';
|
|
2022
2063
|
// Get the completions if the variable path was valid
|
|
2023
2064
|
if (parentVariablePath) {
|
|
2024
2065
|
// If the parent variable path is an empty string, then we are looking up the local scope variables and global functions
|
|
2025
2066
|
if (parentVariablePath.length === 1 && parentVariablePath[0] === '') {
|
|
2026
2067
|
supplyLocalScopeCompletions = true;
|
|
2027
2068
|
}
|
|
2028
|
-
// Look up the parent variable
|
|
2029
|
-
let parentVariable = this.
|
|
2030
|
-
if (!parentVariable || parentVariable.childVariables.length === 0) {
|
|
2031
|
-
// We did not find the parent variable, so try to look it up from the device
|
|
2032
|
-
try {
|
|
2033
|
-
let { evalArgs } = await this.evaluateExpressionToTempVar({ expression: parentVariablePath.join('.'), frameId: args.frameId }, parentVariablePath);
|
|
2034
|
-
let result = await this.rokuAdapter.getVariable(evalArgs.expression, args.frameId);
|
|
2035
|
-
parentVariable = await this.getVariableFromResult(result, args.frameId);
|
|
2036
|
-
}
|
|
2037
|
-
catch (error) {
|
|
2038
|
-
this.logger.error('Error looking up parent completions', error, { parentVariablePath });
|
|
2039
|
-
}
|
|
2040
|
-
}
|
|
2069
|
+
// Look up the parent variable (in-memory first, then the device), scoped to the current frame.
|
|
2070
|
+
let parentVariable = await this.resolveCompletionParentVariable(parentVariablePath, args.frameId);
|
|
2041
2071
|
// provide completions for the parent variable if one was found
|
|
2042
2072
|
if (parentVariable) {
|
|
2043
|
-
|
|
2044
|
-
//
|
|
2045
|
-
|
|
2073
|
+
// arrays and lists are integer-indexed; their `[N]` elements aren't valid `.` or `["..."]`
|
|
2074
|
+
// completions (you can't write `arr.[0]` or `arr["0"]`), so don't offer them as members.
|
|
2075
|
+
// Only the interface methods below (Count, Push, ...) apply to these containers.
|
|
2076
|
+
const isIntegerIndexed = parentVariable.type === VariablesResponse_1.VariableType.Array ||
|
|
2077
|
+
parentVariable.type === VariablesResponse_1.VariableType.List ||
|
|
2078
|
+
parentVariable.type === 'roXMLList' ||
|
|
2079
|
+
parentVariable.type === 'roByteArray';
|
|
2080
|
+
const possibleFieldsAndMethods = isIntegerIndexed
|
|
2081
|
+
? []
|
|
2082
|
+
// Filter out virtual variables and the empty-named placeholder used for empty scopes
|
|
2083
|
+
: parentVariable.childVariables.filter((child) => { var _a; return child.name && ((_a = child.presentationHint) === null || _a === void 0 ? void 0 : _a.kind) !== 'virtual'; });
|
|
2046
2084
|
for (let v of possibleFieldsAndMethods) {
|
|
2047
2085
|
// Default completion type should be variable
|
|
2048
2086
|
let completionType = 'variable';
|
|
@@ -2060,37 +2098,49 @@ class BrightScriptDebugSession extends debugadapter_1.LoggingDebugSession {
|
|
|
2060
2098
|
break;
|
|
2061
2099
|
}
|
|
2062
2100
|
}
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
parentVariable.type === VariablesResponse_1.VariableType.List ||
|
|
2066
|
-
parentVariable.type === 'roXMLList' ||
|
|
2067
|
-
parentVariable.type === 'roByteArray') {
|
|
2068
|
-
label = `[${v.name}]`;
|
|
2069
|
-
}
|
|
2070
|
-
completions.set(`${completionType}-${v.name}`, {
|
|
2071
|
-
label: label,
|
|
2101
|
+
const completionItem = {
|
|
2102
|
+
label: v.name,
|
|
2072
2103
|
type: completionType,
|
|
2073
|
-
|
|
2074
|
-
|
|
2104
|
+
//rank a variable's own members/locals above everything else
|
|
2105
|
+
sortText: `${CompletionSortTier.Member}${v.name}`
|
|
2106
|
+
};
|
|
2107
|
+
if (stringKeyClosing !== undefined) {
|
|
2108
|
+
// Insert the key and close the access, ex: `firstName"]` (the replacement range is applied
|
|
2109
|
+
// below). A `"` inside the key is escaped as `""` so the inserted string literal stays valid
|
|
2110
|
+
// (ex: a key of `a"b` is inserted as `a""b`).
|
|
2111
|
+
completionItem.text = `${v.name.replace(/"/g, '""')}${stringKeyClosing}`;
|
|
2112
|
+
}
|
|
2113
|
+
else if (!supplyLocalScopeCompletions && precededByDot && !/^[a-z_][a-z0-9_]*$/i.test(v.name)) {
|
|
2114
|
+
// The key can't be dot-accessed (ex: it has a space or a quote), so rewrite the access as
|
|
2115
|
+
// bracket notation and consume the `.` before the cursor: `m.` -> `m["my key"]`. A `"` in
|
|
2116
|
+
// the key is escaped as `""` so the inserted string literal stays valid.
|
|
2117
|
+
completionItem.text = `["${v.name.replace(/"/g, '""')}"]`;
|
|
2118
|
+
completionItem.start = replaceRange.start - 1;
|
|
2119
|
+
completionItem.length = replaceRange.length + 1;
|
|
2120
|
+
}
|
|
2121
|
+
completions.set(`${completionType}-${v.name}`, completionItem);
|
|
2075
2122
|
}
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
//
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
completions
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2123
|
+
// Interface methods aren't valid string keys, so skip them when completing a string key
|
|
2124
|
+
if (stringKeyClosing === undefined) {
|
|
2125
|
+
let parentComponentType = this.debuggerVarTypeToRoType(parentVariable.type).toLowerCase();
|
|
2126
|
+
//assemble a list of all methods on the parent component
|
|
2127
|
+
const methods = [
|
|
2128
|
+
//if the parent variable is an actual interface (if applicable) Ex: `ifString` or `ifArray`
|
|
2129
|
+
...(_c = (_b = roku_types_1.interfaces[parentComponentType]) === null || _b === void 0 ? void 0 : _b.methods) !== null && _c !== void 0 ? _c : [],
|
|
2130
|
+
//interfaces from component of this name (if applicable) Ex: `roSGNode` or `roDateTime`
|
|
2131
|
+
...(_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 : [],
|
|
2132
|
+
// Add parent event function completions (if applicable) Ex: `roSGNodeEvent` or `roDeviceInfoEvent`
|
|
2133
|
+
...(_g = (_f = roku_types_1.events[parentComponentType]) === null || _f === void 0 ? void 0 : _f.methods) !== null && _g !== void 0 ? _g : []
|
|
2134
|
+
].flat();
|
|
2135
|
+
// Based on the results of interface, component, and event looks up, add all the methods to the completions
|
|
2136
|
+
for (const method of methods) {
|
|
2137
|
+
completions.set(`method-${method.name}`, {
|
|
2138
|
+
label: method.name,
|
|
2139
|
+
type: 'method',
|
|
2140
|
+
detail: (_h = method.description) !== null && _h !== void 0 ? _h : '',
|
|
2141
|
+
sortText: `${CompletionSortTier.Method}${method.name}`
|
|
2142
|
+
});
|
|
2143
|
+
}
|
|
2094
2144
|
}
|
|
2095
2145
|
// Add the global functions to the completions results
|
|
2096
2146
|
if (supplyLocalScopeCompletions) {
|
|
@@ -2098,8 +2148,8 @@ class BrightScriptDebugSession extends debugadapter_1.LoggingDebugSession {
|
|
|
2098
2148
|
completions.set(`function-${globalCallable.name.toLocaleLowerCase()}`, {
|
|
2099
2149
|
label: globalCallable.name,
|
|
2100
2150
|
type: 'function',
|
|
2101
|
-
detail: (
|
|
2102
|
-
sortText:
|
|
2151
|
+
detail: (_k = (_j = globalCallable.shortDescription) !== null && _j !== void 0 ? _j : globalCallable.documentation) !== null && _k !== void 0 ? _k : '',
|
|
2152
|
+
sortText: `${CompletionSortTier.Global}${globalCallable.name}`
|
|
2103
2153
|
});
|
|
2104
2154
|
}
|
|
2105
2155
|
const frame = this.rokuAdapter.getStackFrameById(args.frameId);
|
|
@@ -2110,7 +2160,7 @@ class BrightScriptDebugSession extends debugadapter_1.LoggingDebugSession {
|
|
|
2110
2160
|
completions.set(`${scopeFunction.completionItemKind}-${scopeFunction.name.toLocaleLowerCase()}`, {
|
|
2111
2161
|
label: scopeFunction.name,
|
|
2112
2162
|
type: scopeFunction.completionItemKind,
|
|
2113
|
-
sortText:
|
|
2163
|
+
sortText: `${CompletionSortTier.ScopeFunction}${scopeFunction.name}`
|
|
2114
2164
|
});
|
|
2115
2165
|
}
|
|
2116
2166
|
}
|
|
@@ -2121,8 +2171,14 @@ class BrightScriptDebugSession extends debugadapter_1.LoggingDebugSession {
|
|
|
2121
2171
|
}
|
|
2122
2172
|
}
|
|
2123
2173
|
}
|
|
2124
|
-
//
|
|
2125
|
-
//
|
|
2174
|
+
// Apply the default replacement span to every completion that didn't already set its own (bracket
|
|
2175
|
+
// rewrites above use an extended range that also consumes the preceding `.`).
|
|
2176
|
+
for (const target of completions.values()) {
|
|
2177
|
+
if (target.start === undefined) {
|
|
2178
|
+
target.start = replaceRange.start;
|
|
2179
|
+
target.length = replaceRange.length;
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2126
2182
|
response.body = {
|
|
2127
2183
|
targets: [...completions.values()]
|
|
2128
2184
|
};
|
|
@@ -2138,32 +2194,76 @@ class BrightScriptDebugSession extends debugadapter_1.LoggingDebugSession {
|
|
|
2138
2194
|
* Gets the closest completion details the incoming completion request.
|
|
2139
2195
|
*/
|
|
2140
2196
|
getClosestCompletionDetails(args) {
|
|
2197
|
+
var _a;
|
|
2141
2198
|
const incomingText = args.text;
|
|
2142
2199
|
const lines = incomingText.split('\n');
|
|
2143
2200
|
let lineNumber = this.toDebuggerLine(args.line, 0);
|
|
2144
2201
|
let column = this.toDebuggerColumn(args.column);
|
|
2145
|
-
const targetLine = lines[lineNumber];
|
|
2146
|
-
|
|
2147
|
-
let i = column - 1;
|
|
2202
|
+
const targetLine = (_a = lines[lineNumber]) !== null && _a !== void 0 ? _a : '';
|
|
2203
|
+
const cursorIndex = column - 1;
|
|
2148
2204
|
const variableChars = /[a-z0-9_\.]/i;
|
|
2149
|
-
// If the character
|
|
2150
|
-
|
|
2205
|
+
// If the character immediately to the right of the cursor is a variable character, then we are
|
|
2206
|
+
// in the middle of a token and should not supply completions yet.
|
|
2207
|
+
if (cursorIndex + 1 < targetLine.length && variableChars.test(targetLine[cursorIndex + 1])) {
|
|
2151
2208
|
return undefined;
|
|
2152
2209
|
}
|
|
2153
|
-
//
|
|
2154
|
-
|
|
2155
|
-
|
|
2210
|
+
// Determine where the expression we want to complete ends, and whether we are completing the
|
|
2211
|
+
// members of that expression. A trailing `.` or being inside an unclosed string-key bracket
|
|
2212
|
+
// (ex: `m["fo`) are both treated as member access on the parent expression.
|
|
2213
|
+
let endColumn = column;
|
|
2214
|
+
let isMemberAccess = false;
|
|
2215
|
+
//when set (including ''), the user is completing a string key; the value is the text to append to
|
|
2216
|
+
//close the access (ex: `"]`), empty when a closing bracket is already present
|
|
2217
|
+
let stringKeyClosing;
|
|
2218
|
+
const openBracket = this.findUnclosedOpener(targetLine, column);
|
|
2219
|
+
if ((openBracket === null || openBracket === void 0 ? void 0 : openBracket.char) === '[') {
|
|
2220
|
+
// find the opening quote (skipping any whitespace after the `[`)
|
|
2221
|
+
let quoteIndex = openBracket.index + 1;
|
|
2222
|
+
while (targetLine[quoteIndex] === ' ' || targetLine[quoteIndex] === '\t') {
|
|
2223
|
+
quoteIndex++;
|
|
2224
|
+
}
|
|
2225
|
+
const quote = targetLine[quoteIndex];
|
|
2226
|
+
if (quote === '"' || quote === `'`) {
|
|
2227
|
+
// The user is typing a string key, so complete the keys of the expression before the `[`
|
|
2228
|
+
endColumn = openBracket.index;
|
|
2229
|
+
isMemberAccess = true;
|
|
2230
|
+
// close the string and bracket only when there is nothing meaningful after the cursor
|
|
2231
|
+
stringKeyClosing = targetLine.slice(column).trim() === '' ? `${quote}]` : '';
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
// Walk backwards from `endColumn` to find the start of the variable path, stepping over balanced
|
|
2235
|
+
// `[...]` index access so paths like `arr[0].name` are captured as a whole.
|
|
2236
|
+
let startIndex = endColumn - 1;
|
|
2237
|
+
let bracketDepth = 0;
|
|
2238
|
+
while (startIndex >= 0) {
|
|
2239
|
+
const char = targetLine[startIndex];
|
|
2240
|
+
if (char === ']') {
|
|
2241
|
+
bracketDepth++;
|
|
2242
|
+
}
|
|
2243
|
+
else if (char === '[') {
|
|
2244
|
+
if (bracketDepth === 0) {
|
|
2245
|
+
// An unbalanced `[` means we hit the start of an index/key being typed; stop here.
|
|
2246
|
+
break;
|
|
2247
|
+
}
|
|
2248
|
+
bracketDepth--;
|
|
2249
|
+
}
|
|
2250
|
+
else if (bracketDepth === 0 && (char === undefined || !variableChars.test(char))) {
|
|
2251
|
+
break;
|
|
2252
|
+
}
|
|
2253
|
+
startIndex--;
|
|
2156
2254
|
}
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
//
|
|
2160
|
-
//
|
|
2161
|
-
// and results in `.name` being the variable path string
|
|
2255
|
+
const variablePathString = targetLine.slice(startIndex + 1, endColumn);
|
|
2256
|
+
// Attempted dot access on something unexpected.
|
|
2257
|
+
// Example: `getPerson().name` where `getPerson()` is not a valid variable path,
|
|
2258
|
+
// which leaves `.name` as the variable path string.
|
|
2162
2259
|
if (variablePathString.startsWith('.')) {
|
|
2163
2260
|
return undefined;
|
|
2164
2261
|
}
|
|
2262
|
+
if (variablePathString.endsWith('.')) {
|
|
2263
|
+
isMemberAccess = true;
|
|
2264
|
+
}
|
|
2165
2265
|
// Get the variable path from the text
|
|
2166
|
-
let variablePath
|
|
2266
|
+
let variablePath;
|
|
2167
2267
|
if (!variablePathString.trim()) {
|
|
2168
2268
|
// The text was empty so assume via '' that we are looking up the local scope variables and global functions
|
|
2169
2269
|
variablePath = [''];
|
|
@@ -2179,28 +2279,170 @@ class BrightScriptDebugSession extends debugadapter_1.LoggingDebugSession {
|
|
|
2179
2279
|
if (!variablePath) {
|
|
2180
2280
|
return undefined;
|
|
2181
2281
|
}
|
|
2182
|
-
|
|
2183
|
-
//
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
}
|
|
2187
|
-
else {
|
|
2188
|
-
// Otherwise, pull completions for the parent variable
|
|
2189
|
-
parentVariablePath = variablePath.slice(0, variablePath.length - 1);
|
|
2190
|
-
}
|
|
2191
|
-
// If the parent variable path is empty or an empty string, then we are looking up the local scope variables and global functions
|
|
2282
|
+
// For member access we complete the members of the full expression. Otherwise we complete the
|
|
2283
|
+
// siblings of the final (partial) segment, so drop it to get the parent.
|
|
2284
|
+
let parentVariablePath = isMemberAccess ? variablePath : variablePath.slice(0, variablePath.length - 1);
|
|
2285
|
+
// An empty parent path means we are looking up the local scope variables and global functions
|
|
2192
2286
|
if (parentVariablePath.length === 0) {
|
|
2193
2287
|
parentVariablePath = [''];
|
|
2194
2288
|
}
|
|
2195
|
-
|
|
2289
|
+
const result = { parentVariablePath: parentVariablePath };
|
|
2290
|
+
// Only attach the string-key context when we actually resolved a parent object to complete keys on
|
|
2291
|
+
if (stringKeyClosing !== undefined && !(parentVariablePath.length === 1 && parentVariablePath[0] === '')) {
|
|
2292
|
+
result.stringKeyClosing = stringKeyClosing;
|
|
2293
|
+
}
|
|
2294
|
+
return result;
|
|
2295
|
+
}
|
|
2296
|
+
/**
|
|
2297
|
+
* Compute the span of input text that a completion replaces: the run of identifier characters
|
|
2298
|
+
* immediately before the cursor. This lets the client filter the list correctly as the user keeps
|
|
2299
|
+
* typing past the first character.
|
|
2300
|
+
*
|
|
2301
|
+
* `start` is a 0-based offset into the line, NOT a client column. Per the Debug Adapter Protocol,
|
|
2302
|
+
* `CompletionItem.start` is measured in UTF-16 code units and the client maps it to a position
|
|
2303
|
+
* itself, so it must not be run through `toClientColumn` (unlike stack-frame, breakpoint, and scope
|
|
2304
|
+
* positions). Our debugger column base is already 0-based, so the internal offset is sent as-is.
|
|
2305
|
+
*/
|
|
2306
|
+
getCompletionReplaceRange(args) {
|
|
2307
|
+
var _a;
|
|
2308
|
+
const lines = args.text.split('\n');
|
|
2309
|
+
const lineNumber = this.toDebuggerLine(args.line, 0);
|
|
2310
|
+
const cursorOffset = this.toDebuggerColumn(args.column);
|
|
2311
|
+
const targetLine = (_a = lines[lineNumber]) !== null && _a !== void 0 ? _a : '';
|
|
2312
|
+
const identifierChars = /[a-z0-9_]/i;
|
|
2313
|
+
let wordStart = cursorOffset;
|
|
2314
|
+
while (wordStart > 0 && identifierChars.test(targetLine[wordStart - 1])) {
|
|
2315
|
+
wordStart--;
|
|
2316
|
+
}
|
|
2317
|
+
return {
|
|
2318
|
+
start: wordStart,
|
|
2319
|
+
length: cursorOffset - wordStart
|
|
2320
|
+
};
|
|
2321
|
+
}
|
|
2322
|
+
/**
|
|
2323
|
+
* Scan backwards from `column` to find the nearest opening bracket (`(`, `[`, or `{`) that has not
|
|
2324
|
+
* been closed before the cursor. Returns the opener's index and character, or undefined if none.
|
|
2325
|
+
*/
|
|
2326
|
+
findUnclosedOpener(line, column) {
|
|
2327
|
+
let depth = 0;
|
|
2328
|
+
for (let i = column - 1; i >= 0; i--) {
|
|
2329
|
+
const char = line[i];
|
|
2330
|
+
if (char === ')' || char === ']' || char === '}') {
|
|
2331
|
+
depth++;
|
|
2332
|
+
}
|
|
2333
|
+
else if (char === '(' || char === '[' || char === '{') {
|
|
2334
|
+
if (depth === 0) {
|
|
2335
|
+
return { index: i, char: char };
|
|
2336
|
+
}
|
|
2337
|
+
depth--;
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
return undefined;
|
|
2341
|
+
}
|
|
2342
|
+
/**
|
|
2343
|
+
* Resolve the parent variable for a completion request. Prefers the in-memory locals for the frame,
|
|
2344
|
+
* then falls back to a device lookup. Device lookups are cached for the duration of the paused state
|
|
2345
|
+
* (cleared by `clearState`) so repeated completion requests on the same path don't hammer the device.
|
|
2346
|
+
*/
|
|
2347
|
+
async resolveCompletionParentVariable(parentVariablePath, frameId) {
|
|
2348
|
+
// For local-scope completions, make sure the frame's locals are fetched on demand. Otherwise they
|
|
2349
|
+
// would only appear once the user expands the Variables panel (which is what triggers the fetch).
|
|
2350
|
+
const isLocalScope = parentVariablePath.length === 1 && parentVariablePath[0] === '';
|
|
2351
|
+
if (isLocalScope) {
|
|
2352
|
+
const localsScope = this.getOrCreateLocalsScope(frameId);
|
|
2353
|
+
if (!localsScope.isResolved) {
|
|
2354
|
+
try {
|
|
2355
|
+
await this.populateScopeVariables(localsScope, { variablesReference: localsScope.variablesReference });
|
|
2356
|
+
}
|
|
2357
|
+
catch (error) {
|
|
2358
|
+
this.logger.debug('Could not populate locals for completions', error, { frameId });
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
const inMemory = this.findFrameVariableByPath(parentVariablePath, frameId);
|
|
2363
|
+
if (inMemory && inMemory.childVariables.length > 0) {
|
|
2364
|
+
return inMemory;
|
|
2365
|
+
}
|
|
2366
|
+
// Rebuild a valid accessor expression for the device lookup. Joining with `.` is wrong for indexed
|
|
2367
|
+
// segments (ex: `m.services[0]` would become the invalid `m.services.0` and the index gets dropped).
|
|
2368
|
+
const expression = this.buildVariableExpression(parentVariablePath);
|
|
2369
|
+
const cacheKey = `${frameId}:${expression}`;
|
|
2370
|
+
if (this.completionParentVariableCache.has(cacheKey)) {
|
|
2371
|
+
return this.completionParentVariableCache.get(cacheKey);
|
|
2372
|
+
}
|
|
2373
|
+
let parentVariable;
|
|
2374
|
+
try {
|
|
2375
|
+
let { evalArgs } = await this.evaluateExpressionToTempVar({ expression: expression, frameId: frameId }, parentVariablePath);
|
|
2376
|
+
let result = await this.rokuAdapter.getVariable(evalArgs.expression, frameId);
|
|
2377
|
+
parentVariable = await this.getVariableFromResult(result, frameId);
|
|
2378
|
+
}
|
|
2379
|
+
catch (error) {
|
|
2380
|
+
// A failed lookup is expected while the user is still typing an incomplete expression, so keep it quiet.
|
|
2381
|
+
this.logger.debug('Could not resolve parent variable for completions', error, { parentVariablePath });
|
|
2382
|
+
parentVariable = undefined;
|
|
2383
|
+
}
|
|
2384
|
+
this.completionParentVariableCache.set(cacheKey, parentVariable);
|
|
2385
|
+
return parentVariable;
|
|
2386
|
+
}
|
|
2387
|
+
/**
|
|
2388
|
+
* Rebuild a valid BrightScript accessor expression from a resolved variable path. String-literal keys
|
|
2389
|
+
* arrive already quoted from `getVariablePath` and are emitted as `["key"]` so they stay case-sensitive
|
|
2390
|
+
* on the device (Roku AAs can be set case-sensitive); numeric segments use `[index]`, and identifiers
|
|
2391
|
+
* use dot access. This keeps array indices and string keys correct through the device lookup.
|
|
2392
|
+
*/
|
|
2393
|
+
buildVariableExpression(segments) {
|
|
2394
|
+
return segments.reduce((expression, segment, index) => {
|
|
2395
|
+
if (index === 0) {
|
|
2396
|
+
return segment;
|
|
2397
|
+
}
|
|
2398
|
+
//already-quoted string key (preserve the quotes so the device matches it case-sensitively).
|
|
2399
|
+
//A lone `"` is not a quoted literal (the shortest is `""`), so require at least 2 chars.
|
|
2400
|
+
if (segment.length >= 2 && segment.startsWith('"') && segment.endsWith('"')) {
|
|
2401
|
+
return `${expression}[${segment}]`;
|
|
2402
|
+
}
|
|
2403
|
+
if (/^[0-9]+$/.test(segment)) {
|
|
2404
|
+
return `${expression}[${segment}]`;
|
|
2405
|
+
}
|
|
2406
|
+
if (/^[a-z_][a-z0-9_]*$/i.test(segment)) {
|
|
2407
|
+
return `${expression}.${segment}`;
|
|
2408
|
+
}
|
|
2409
|
+
return `${expression}["${segment.replace(/"/g, '""')}"]`;
|
|
2410
|
+
}, '');
|
|
2411
|
+
}
|
|
2412
|
+
/**
|
|
2413
|
+
* Normalize a variable path segment or variable name for matching: drop surrounding string-key quotes
|
|
2414
|
+
* and lower-case it. BrightScript variables and dotted access are case-insensitive, and the device
|
|
2415
|
+
* reports names lower-cased, so this lets the in-memory lookup find the parent regardless of the casing
|
|
2416
|
+
* the user typed (ex: `topRef` matching the cached `topref`).
|
|
2417
|
+
*/
|
|
2418
|
+
normalizeVariableName(name) {
|
|
2419
|
+
let value = name !== null && name !== void 0 ? name : '';
|
|
2420
|
+
if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
|
|
2421
|
+
value = value.slice(1, -1).replace(/""/g, '"');
|
|
2422
|
+
}
|
|
2423
|
+
return value.toLowerCase();
|
|
2424
|
+
}
|
|
2425
|
+
/**
|
|
2426
|
+
* Resolve a variable path against the current frame's local scope. The first path segment is matched
|
|
2427
|
+
* against the frame's locals (not the global pool of every materialized variable), then we walk down
|
|
2428
|
+
* the child variables. The empty path (`['']`) resolves to the locals scope container itself.
|
|
2429
|
+
*/
|
|
2430
|
+
findFrameVariableByPath(path, frameId) {
|
|
2431
|
+
var _a;
|
|
2432
|
+
const localsContainer = this.variables[this.getEvaluateRefId('$$locals', frameId)];
|
|
2433
|
+
if (path.length === 1 && path[0] === '') {
|
|
2434
|
+
return localsContainer;
|
|
2435
|
+
}
|
|
2436
|
+
return this.findVariableByPath((_a = localsContainer === null || localsContainer === void 0 ? void 0 : localsContainer.childVariables) !== null && _a !== void 0 ? _a : [], path, frameId);
|
|
2196
2437
|
}
|
|
2197
2438
|
findVariableByPath(variables, path, frameId) {
|
|
2198
2439
|
var _a;
|
|
2199
2440
|
let current = null;
|
|
2200
2441
|
for (const name of path) {
|
|
2201
|
-
|
|
2442
|
+
const normalizedName = this.normalizeVariableName(name);
|
|
2443
|
+
// Find the object matching the current name in the data (case-insensitive, per BrightScript)
|
|
2202
2444
|
current = (_a = (Array.isArray(variables) ? variables : current === null || current === void 0 ? void 0 : current.childVariables)) === null || _a === void 0 ? void 0 : _a.find(obj => {
|
|
2203
|
-
return obj.name ===
|
|
2445
|
+
return this.normalizeVariableName(obj.name) === normalizedName && obj.frameId === frameId;
|
|
2204
2446
|
});
|
|
2205
2447
|
// If no match is found, return null
|
|
2206
2448
|
if (!current) {
|
|
@@ -2591,6 +2833,7 @@ class BrightScriptDebugSession extends debugadapter_1.LoggingDebugSession {
|
|
|
2591
2833
|
clearState() {
|
|
2592
2834
|
//erase all cached variables
|
|
2593
2835
|
this.variables = {};
|
|
2836
|
+
this.completionParentVariableCache.clear();
|
|
2594
2837
|
}
|
|
2595
2838
|
/**
|
|
2596
2839
|
* Sends a launch progress event to the client if the client supports progress reporting.
|