Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Limit inline values to current function-scope #14

Merged
merged 15 commits into from
Nov 3, 2022
23 changes: 21 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,26 @@
"onDebugResolve:PowerShell"
],
"main": "./out/extension.js",
"contributes": {},
"contributes": {
"configuration": {
"title": "Inline Values support for PowerShell",
"properties": {
"powershellInlineValues.startLocation": {
"type": "string",
"default": "currentFunction",
"enum": [
"currentFunction",
"document"
],
"enumDescriptions": [
"Start of current function. Defaults to top of document if not stopped inside function.",
"Always from top of document. Default before 0.0.7."
],
"description": "Specifies the start position for inline values while debugging. Inline values will be shown from selected start positiion until stopped location."
}
}
}
},
"scripts": {
"vscode:prepublish": "yarn run compile",
"compile": "tsc -p ./",
Expand Down Expand Up @@ -49,4 +68,4 @@
"publisherId": "2d97a8b2-cb9f-4729-9fca-51d9cea8f5dc",
"isPreReleaseVersion": false
}
}
}
77 changes: 77 additions & 0 deletions src/documentParser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import * as vscode from 'vscode';
import * as utils from './utils';

export class DocumentParser {
// Used to avoid calling symbol provider for the same document on every stopped location
private readonly functionCache: Map<string, vscode.DocumentSymbol[]> = new Map<string, vscode.DocumentSymbol[]>();

// Clear cache between debugsessions to get updated symbols
clearFunctionCache(): void {
this.functionCache.clear();
}

async getFunctionsInScope(document: vscode.TextDocument, stoppedLocation: vscode.Range): Promise<vscode.DocumentSymbol[]> {
const functions = await this.getFunctionsInDocument(document);
const stoppedStart = stoppedLocation.start.line;
const stoppedEnd = stoppedLocation.end.line;
const res: vscode.DocumentSymbol[] = [];

for (var i = 0, length = functions.length; i < length; ++i) {
const func = functions[i];
// Only return functions with stopped location inside range
if (func.range.start.line <= stoppedStart && func.range.end.line >= stoppedEnd && func.range.contains(stoppedLocation)) {
res.push(func);
}
}

return res;
}

async getFunctionsInDocument(document: vscode.TextDocument): Promise<vscode.DocumentSymbol[]> {
const cacheKey = document.uri.toString();
if (this.functionCache.has(cacheKey)) {
return this.functionCache.get(cacheKey)!;
}

const documentSymbols = await vscode.commands.executeCommand<vscode.DocumentSymbol[]>('vscode.executeDocumentSymbolProvider', document.uri);
TylerLeonhardt marked this conversation as resolved.
Show resolved Hide resolved
let functions: vscode.DocumentSymbol[] = [];

if (documentSymbols) {
// Get all functions in a flat array from the symbol-tree
functions = utils.flattenSymbols(documentSymbols).filter(s => s.kind === vscode.SymbolKind.Function);
}

this.functionCache.set(cacheKey, functions);
return functions;
}

async getStartLine(document: vscode.TextDocument, startLocationSetting: string, stoppedLocation: vscode.Range): Promise<number> {
if (startLocationSetting === 'document') {
return 0;
}

// Lookup closest matching function start line or default to document start (0)
const functions = await this.getFunctionsInScope(document, stoppedLocation);
return Math.max(0, ...functions.map(fn => fn.range.start.line));
}

async getExcludedLines(document: vscode.TextDocument, stoppedLocation: vscode.Range, startLine: number): Promise<Set<number>> {
const functions = await this.getFunctionsInDocument(document);
const stoppedEnd = stoppedLocation.end.line;
const excludedLines = [];

for (var i = 0, length = functions.length; i < length; ++i) {
const func = functions[i];
// StartLine (either document start or closest function start) are provided, so functions necessary to exclude
// will always start >= documentStart or same as currentFunction start if nested function.
// Don't bother checking functions before startLine or after stoppedLocation
if (func.range.start.line >= startLine && func.range.start.line <= stoppedEnd && !func.range.contains(stoppedLocation)) {
const functionRange = utils.range(func.range.start.line, func.range.end.line);
excludedLines.push(...functionRange);
}
}

// Ensure we don't exclude our stopped location and make lookup blazing fast
return new Set(excludedLines.filter(line => line < stoppedLocation.start.line || line > stoppedEnd));
}
}
14 changes: 13 additions & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
import * as vscode from 'vscode';
import { PowerShellVariableInlineValuesProvider } from './powerShellVariableInlineValuesProvider';
import { DocumentParser } from './documentParser';

export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(vscode.languages.registerInlineValuesProvider('powershell', new PowerShellVariableInlineValuesProvider()));
const parser = new DocumentParser();

context.subscriptions.push(vscode.languages.registerInlineValuesProvider('powershell', new PowerShellVariableInlineValuesProvider(parser)));

// Clear function symbol cache to ensure we get symbols from any updated files
context.subscriptions.push(
vscode.debug.onDidTerminateDebugSession((e) => {
if (e.type.toLowerCase() === 'powershell') {
parser.clearFunctionCache();
}
})
);
}

export function deactivate() { }
36 changes: 26 additions & 10 deletions src/powerShellVariableInlineValuesProvider.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,43 @@
import * as vscode from 'vscode';
import { DocumentParser } from './documentParser';

export class PowerShellVariableInlineValuesProvider implements vscode.InlineValuesProvider {

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

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

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

provideInlineValues(document: vscode.TextDocument, viewport: vscode.Range, context: vscode.InlineValueContext): vscode.ProviderResult<vscode.InlineValue[]> {
private readonly documentParser: DocumentParser;

constructor(documentParser: DocumentParser) {
this.documentParser = documentParser;
}

async provideInlineValues(document: vscode.TextDocument, viewport: vscode.Range, context: vscode.InlineValueContext): Promise<vscode.InlineValue[]> {
const allValues: vscode.InlineValue[] = [];

for (let l = 0; l <= context.stoppedLocation.end.line; l++) {
const extensionSettings = vscode.workspace.getConfiguration('powershellInlineValues');
const startLocationSetting = extensionSettings.get<string>('startLocation') ?? 'currentFunction';
const startLine = await this.documentParser.getStartLine(document, startLocationSetting, context.stoppedLocation);
const endLine = context.stoppedLocation.end.line;
const excludedLines = await this.documentParser.getExcludedLines(document, context.stoppedLocation, startLine);

for (let l = startLine; l <= endLine; l++) {
// Exclude lines out of scope (other functions)
if (excludedLines.has(l)) {
continue;
}

const line = document.lineAt(l);

// Skip over comments
Expand All @@ -39,23 +56,22 @@ export class PowerShellVariableInlineValuesProvider implements vscode.InlineValu
if (colon !== -1) {
// If invalid scope, ignore
const scope = varName.substring(1, colon);
if (!this.supportedScopes.test(scope)) {
if (!this.supportedScopes.includes(scope.toLowerCase())) {
continue;
}

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

// If known PowerShell constant, ignore
if (this.knownConstants.test(varName)) {
if (this.knownConstants.includes(varName.toLowerCase())) {
continue;
}

const rng = new vscode.Range(l, match.index, l, match.index + varName.length);
allValues.push(new vscode.InlineValueVariableLookup(rng, varName, false));
}
}

return allValues;
}
}
19 changes: 15 additions & 4 deletions src/test/runTest.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as path from 'path';

import { runTests } from 'vscode-test';
import * as cp from 'child_process';
import { runTests, downloadAndUnzipVSCode, resolveCliPathFromVSCodeExecutablePath } from 'vscode-test';

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

// Download VS Code, unzip it and run the integration test
await runTests({ extensionDevelopmentPath, extensionTestsPath, version: 'stable' });
// Download VS Code and unzip it
const vscodeExecutablePath = await downloadAndUnzipVSCode('stable');
const cliPath = resolveCliPathFromVSCodeExecutablePath(vscodeExecutablePath);

// Use cp.spawn / cp.exec for custom setup.
// Need powershell extension for document symbol provider
cp.spawnSync(cliPath, ['--install-extension', 'ms-vscode.powershell'], {
encoding: 'utf-8',
stdio: 'inherit'
});

// Run tests using custom vscode setup
await runTests({ vscodeExecutablePath, extensionDevelopmentPath, extensionTestsPath });
} catch (err) {
console.error(err);
console.error('Failed to run tests');
Expand Down
Loading