Skip to content

Commit 65aafa5

Browse files
authored
Limit inline values to current function-scope (#14)
* add setting to control start of inline values * v2 * fix tests * optimize regex * optimize * cleanup and split util functions to own file * add tests * add tests * fix tests * increase timeout for tests * extract documentParser and add unit tests * fix typo in tests * fix perf excludedLines lookup * fix perf tests * cleanup comments and import
1 parent b8189a2 commit 65aafa5

12 files changed

+1205
-492
lines changed

package.json

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,26 @@
2020
"onDebugResolve:PowerShell"
2121
],
2222
"main": "./out/extension.js",
23-
"contributes": {},
23+
"contributes": {
24+
"configuration": {
25+
"title": "Inline Values support for PowerShell",
26+
"properties": {
27+
"powershellInlineValues.startLocation": {
28+
"type": "string",
29+
"default": "currentFunction",
30+
"enum": [
31+
"currentFunction",
32+
"document"
33+
],
34+
"enumDescriptions": [
35+
"Start of current function. Defaults to top of document if not stopped inside function.",
36+
"Always from top of document. Default before 0.0.7."
37+
],
38+
"description": "Specifies the start position for inline values while debugging. Inline values will be shown from selected start positiion until stopped location."
39+
}
40+
}
41+
}
42+
},
2443
"scripts": {
2544
"vscode:prepublish": "yarn run compile",
2645
"compile": "tsc -p ./",
@@ -49,4 +68,4 @@
4968
"publisherId": "2d97a8b2-cb9f-4729-9fca-51d9cea8f5dc",
5069
"isPreReleaseVersion": false
5170
}
52-
}
71+
}

src/documentParser.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import * as vscode from 'vscode';
2+
import * as utils from './utils';
3+
4+
export class DocumentParser {
5+
// Used to avoid calling symbol provider for the same document on every stopped location
6+
private readonly functionCache: Map<string, vscode.DocumentSymbol[]> = new Map<string, vscode.DocumentSymbol[]>();
7+
8+
// Clear cache between debugsessions to get updated symbols
9+
clearFunctionCache(): void {
10+
this.functionCache.clear();
11+
}
12+
13+
async getFunctionsInScope(document: vscode.TextDocument, stoppedLocation: vscode.Range): Promise<vscode.DocumentSymbol[]> {
14+
const functions = await this.getFunctionsInDocument(document);
15+
const stoppedStart = stoppedLocation.start.line;
16+
const stoppedEnd = stoppedLocation.end.line;
17+
const res: vscode.DocumentSymbol[] = [];
18+
19+
for (var i = 0, length = functions.length; i < length; ++i) {
20+
const func = functions[i];
21+
// Only return functions with stopped location inside range
22+
if (func.range.start.line <= stoppedStart && func.range.end.line >= stoppedEnd && func.range.contains(stoppedLocation)) {
23+
res.push(func);
24+
}
25+
}
26+
27+
return res;
28+
}
29+
30+
async getFunctionsInDocument(document: vscode.TextDocument): Promise<vscode.DocumentSymbol[]> {
31+
const cacheKey = document.uri.toString();
32+
if (this.functionCache.has(cacheKey)) {
33+
return this.functionCache.get(cacheKey)!;
34+
}
35+
36+
const documentSymbols = await vscode.commands.executeCommand<vscode.DocumentSymbol[]>('vscode.executeDocumentSymbolProvider', document.uri);
37+
let functions: vscode.DocumentSymbol[] = [];
38+
39+
if (documentSymbols) {
40+
// Get all functions in a flat array from the symbol-tree
41+
functions = utils.flattenSymbols(documentSymbols).filter(s => s.kind === vscode.SymbolKind.Function);
42+
}
43+
44+
this.functionCache.set(cacheKey, functions);
45+
return functions;
46+
}
47+
48+
async getStartLine(document: vscode.TextDocument, startLocationSetting: string, stoppedLocation: vscode.Range): Promise<number> {
49+
if (startLocationSetting === 'document') {
50+
return 0;
51+
}
52+
53+
// Lookup closest matching function start line or default to document start (0)
54+
const functions = await this.getFunctionsInScope(document, stoppedLocation);
55+
return Math.max(0, ...functions.map(fn => fn.range.start.line));
56+
}
57+
58+
async getExcludedLines(document: vscode.TextDocument, stoppedLocation: vscode.Range, startLine: number): Promise<Set<number>> {
59+
const functions = await this.getFunctionsInDocument(document);
60+
const stoppedEnd = stoppedLocation.end.line;
61+
const excludedLines = [];
62+
63+
for (var i = 0, length = functions.length; i < length; ++i) {
64+
const func = functions[i];
65+
// StartLine (either document start or closest function start) are provided, so functions necessary to exclude
66+
// will always start >= documentStart or same as currentFunction start if nested function.
67+
// Don't bother checking functions before startLine or after stoppedLocation
68+
if (func.range.start.line >= startLine && func.range.start.line <= stoppedEnd && !func.range.contains(stoppedLocation)) {
69+
const functionRange = utils.range(func.range.start.line, func.range.end.line);
70+
excludedLines.push(...functionRange);
71+
}
72+
}
73+
74+
// Ensure we don't exclude our stopped location and make lookup blazing fast
75+
return new Set(excludedLines.filter(line => line < stoppedLocation.start.line || line > stoppedEnd));
76+
}
77+
}

src/extension.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,20 @@
11
import * as vscode from 'vscode';
22
import { PowerShellVariableInlineValuesProvider } from './powerShellVariableInlineValuesProvider';
3+
import { DocumentParser } from './documentParser';
34

45
export function activate(context: vscode.ExtensionContext) {
5-
context.subscriptions.push(vscode.languages.registerInlineValuesProvider('powershell', new PowerShellVariableInlineValuesProvider()));
6+
const parser = new DocumentParser();
7+
8+
context.subscriptions.push(vscode.languages.registerInlineValuesProvider('powershell', new PowerShellVariableInlineValuesProvider(parser)));
9+
10+
// Clear function symbol cache to ensure we get symbols from any updated files
11+
context.subscriptions.push(
12+
vscode.debug.onDidTerminateDebugSession((e) => {
13+
if (e.type.toLowerCase() === 'powershell') {
14+
parser.clearFunctionCache();
15+
}
16+
})
17+
);
618
}
719

820
export function deactivate() { }

src/powerShellVariableInlineValuesProvider.ts

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,43 @@
11
import * as vscode from 'vscode';
2+
import { DocumentParser } from './documentParser';
23

34
export class PowerShellVariableInlineValuesProvider implements vscode.InlineValuesProvider {
45

56
// Known constants
6-
private readonly knownConstants = /^\$(?:true|false|null)$/i;
7+
private readonly knownConstants = ['$true', '$false', '$null'];
78

89
// https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_scopes?view=powershell-5.1#scope-modifiers
9-
private readonly supportedScopes = /^(?:global|local|script|private|using|variable)$/i;
10+
private readonly supportedScopes = ['global', 'local', 'script', 'private', 'using', 'variable'];
1011

1112
// Variable patterns
1213
// https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_variables?view=powershell-5.1#variable-names-that-include-special-characters
13-
private readonly alphanumChars = /(?:\p{Lu}|\p{Ll}|\p{Lt}|\p{Lm}|\p{Lo}|\p{Nd}|[_?])/.source;
14+
private readonly alphanumChars = /(?:\p{Ll}|\p{Lu}|\p{Nd}|[_?]|\p{Lt}|\p{Lm}|\p{Lo})/.source;
1415
private readonly variableRegex = new RegExp([
1516
'(?:\\$\\{(?<specialName>.*?)(?<!`)\\})', // Special characters variables. Lazy match until unescaped }
16-
`(?:\\$\\w+:${this.alphanumChars}+)`, // Scoped variables
17-
`(?:\\$${this.alphanumChars}+)`, // Normal variables
17+
`(?:\\$(?:[a-zA-Z]+:)?${this.alphanumChars}+)`, // Scoped or normal variables
1818
].join('|'), 'giu'); // u flag to support unicode char classes
1919

20-
provideInlineValues(document: vscode.TextDocument, viewport: vscode.Range, context: vscode.InlineValueContext): vscode.ProviderResult<vscode.InlineValue[]> {
20+
private readonly documentParser: DocumentParser;
21+
22+
constructor(documentParser: DocumentParser) {
23+
this.documentParser = documentParser;
24+
}
25+
26+
async provideInlineValues(document: vscode.TextDocument, viewport: vscode.Range, context: vscode.InlineValueContext): Promise<vscode.InlineValue[]> {
2127
const allValues: vscode.InlineValue[] = [];
2228

23-
for (let l = 0; l <= context.stoppedLocation.end.line; l++) {
29+
const extensionSettings = vscode.workspace.getConfiguration('powershellInlineValues');
30+
const startLocationSetting = extensionSettings.get<string>('startLocation') ?? 'currentFunction';
31+
const startLine = await this.documentParser.getStartLine(document, startLocationSetting, context.stoppedLocation);
32+
const endLine = context.stoppedLocation.end.line;
33+
const excludedLines = await this.documentParser.getExcludedLines(document, context.stoppedLocation, startLine);
34+
35+
for (let l = startLine; l <= endLine; l++) {
36+
// Exclude lines out of scope (other functions)
37+
if (excludedLines.has(l)) {
38+
continue;
39+
}
40+
2441
const line = document.lineAt(l);
2542

2643
// Skip over comments
@@ -39,23 +56,22 @@ export class PowerShellVariableInlineValuesProvider implements vscode.InlineValu
3956
if (colon !== -1) {
4057
// If invalid scope, ignore
4158
const scope = varName.substring(1, colon);
42-
if (!this.supportedScopes.test(scope)) {
59+
if (!this.supportedScopes.includes(scope.toLowerCase())) {
4360
continue;
4461
}
4562

4663
varName = '$' + varName.substring(colon + 1);
4764
}
4865

4966
// If known PowerShell constant, ignore
50-
if (this.knownConstants.test(varName)) {
67+
if (this.knownConstants.includes(varName.toLowerCase())) {
5168
continue;
5269
}
5370

5471
const rng = new vscode.Range(l, match.index, l, match.index + varName.length);
5572
allValues.push(new vscode.InlineValueVariableLookup(rng, varName, false));
5673
}
5774
}
58-
5975
return allValues;
6076
}
6177
}

src/test/runTest.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as path from 'path';
2-
3-
import { runTests } from 'vscode-test';
2+
import * as cp from 'child_process';
3+
import { runTests, downloadAndUnzipVSCode, resolveCliPathFromVSCodeExecutablePath } from 'vscode-test';
44

55
async function main() {
66
try {
@@ -12,8 +12,19 @@ async function main() {
1212
// Passed to --extensionTestsPath
1313
const extensionTestsPath = path.resolve(__dirname, './suite/index');
1414

15-
// Download VS Code, unzip it and run the integration test
16-
await runTests({ extensionDevelopmentPath, extensionTestsPath, version: 'stable' });
15+
// Download VS Code and unzip it
16+
const vscodeExecutablePath = await downloadAndUnzipVSCode('stable');
17+
const cliPath = resolveCliPathFromVSCodeExecutablePath(vscodeExecutablePath);
18+
19+
// Use cp.spawn / cp.exec for custom setup.
20+
// Need powershell extension for document symbol provider
21+
cp.spawnSync(cliPath, ['--install-extension', 'ms-vscode.powershell'], {
22+
encoding: 'utf-8',
23+
stdio: 'inherit'
24+
});
25+
26+
// Run tests using custom vscode setup
27+
await runTests({ vscodeExecutablePath, extensionDevelopmentPath, extensionTestsPath });
1728
} catch (err) {
1829
console.error(err);
1930
console.error('Failed to run tests');

0 commit comments

Comments
 (0)