diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 4eb17d5..62dfb92 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -5,13 +5,13 @@ "build": { "dockerfile": "Dockerfile", // Update 'VARIANT' to pick a Node version: 10, 12, 14 - "args": { + "args": { "VARIANT": "14" } }, // Set *default* container specific settings.json values on container create. - "settings": { + "settings": { "terminal.integrated.shell.linux": "/bin/bash" }, @@ -29,4 +29,4 @@ // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. "remoteUser": "node" -} \ No newline at end of file +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 30bf8c2..9b1ad57 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,5 +7,7 @@ "out": true // set this to false to include "out" folder in search results }, // Turn off tsc task auto detection since we have the necessary tasks as npm scripts - "typescript.tsc.autoDetect": "off" -} \ No newline at end of file + "typescript.tsc.autoDetect": "off", + "files.trimTrailingWhitespace": true, + "files.insertFinalNewline": true +} diff --git a/.yarnrc b/.yarnrc index f757a6a..4f14322 100644 --- a/.yarnrc +++ b/.yarnrc @@ -1 +1 @@ ---ignore-engines true \ No newline at end of file +--ignore-engines true diff --git a/package.json b/package.json index a84e354..135f4c2 100644 --- a/package.json +++ b/package.json @@ -43,4 +43,4 @@ "vscode-test": "^1.5.0", "vsce": "^1.87.1" } -} \ No newline at end of file +} diff --git a/src/extension.ts b/src/extension.ts index 24c9522..d7b4892 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -5,4 +5,4 @@ export function activate(context: vscode.ExtensionContext) { context.subscriptions.push(vscode.languages.registerInlineValuesProvider('powershell', new PowerShellVariableInlineValuesProvider())); } -export function deactivate() {} +export function deactivate() { } diff --git a/src/powerShellVariableInlineValuesProvider.ts b/src/powerShellVariableInlineValuesProvider.ts index f0f5578..7602623 100644 --- a/src/powerShellVariableInlineValuesProvider.ts +++ b/src/powerShellVariableInlineValuesProvider.ts @@ -2,11 +2,24 @@ import * as vscode from 'vscode'; export class PowerShellVariableInlineValuesProvider implements vscode.InlineValuesProvider { - provideInlineValues(document: vscode.TextDocument, viewport: vscode.Range, context: vscode.InlineValueContext) : vscode.ProviderResult { + // Known constants + private readonly knownConstants = /^\$(?:true|false|null)$/i; + + // 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; + + // 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 variableRegex = new RegExp([ + '(?:\\$\\{(?.*?)(? { const allValues: vscode.InlineValue[] = []; - const ignoredVariables = /^\$(?:true|false|null)$/i; - for (let l = 0; l <= context.stoppedLocation.end.line; l++) { const line = document.lineAt(l); @@ -15,26 +28,26 @@ export class PowerShellVariableInlineValuesProvider implements vscode.InlineValu continue; } - const variableMatches = /(?:\${(.*)})|(?:\$\S+:\S+)|(?:\$\S+)/gi; - for (let match = variableMatches.exec(line.text); match; match = variableMatches.exec(line.text)) { - // If we're looking at an "anything goes" variable, that has a capture group so use that instead + for (let match = this.variableRegex.exec(line.text); match; match = this.variableRegex.exec(line.text)) { + // If we're looking at special characters variable, use the extracted variable name in capture group let varName = match[0][1] === '{' - ? '$' + match[1] + ? '$' + match.groups?.specialName?.replace(/`(.)/g, '$1') // Remove backticks used as escape char for curly braces, unicode etc. : match[0]; // If there's a scope, we need to remove it const colon = varName.indexOf(':'); if (colon !== -1) { - varName = '$' + varName.substring(colon + 1); - } + // If invalid scope, ignore + const scope = varName.substring(1, colon); + if (!this.supportedScopes.test(scope)) { + continue; + } - // These characters need to be trimmed off - if ([';', ',', '-', '+', '/', '*'].includes(varName[varName.length - 1])) { - varName = varName.substring(0, varName.length - 1); + varName = '$' + varName.substring(colon + 1); } // If known PowerShell constant, ignore - if (ignoredVariables.test(varName)) { + if (this.knownConstants.test(varName)) { continue; } diff --git a/src/test/suite/extension.test.ts b/src/test/suite/extension.test.ts index 3bea556..3eb485d 100644 --- a/src/test/suite/extension.test.ts +++ b/src/test/suite/extension.test.ts @@ -3,22 +3,20 @@ import * as assert from 'assert'; import * as vscode from 'vscode'; import { PowerShellVariableInlineValuesProvider } from '../../powerShellVariableInlineValuesProvider'; -suite('Extension Test Suite', async () => { +suite('Variable detection', async () => { vscode.window.showInformationMessage('Start all tests.'); test('Misc variable tests', async () => { const doc = await vscode.workspace.openTextDocument({ language: 'powershell', - content: `$normal = Get-Process + content: ` +$normal = Get-Process $script:scoped = 5 -\${braces} = "asdf" $numb3rInside = 'asdf' $33333 = 'numbers' -\${ } = 'spaces' $normal, \${braces}, $script:scoped 4 -$true - `, +`, }); const provider = new PowerShellVariableInlineValuesProvider(); @@ -28,13 +26,13 @@ $true frameId: 0 }); - assert.strictEqual(result?.length, 9); + assert.strictEqual(result?.length, 7); for (let i = 0; i < result.length; i++) { const variable = result![i] as vscode.InlineValueVariableLookup; let name: string = ''; let startChar: number = 0; - let line: number = i; + let line: number = i + 1; switch (i) { case 0: name = '$normal'; @@ -43,32 +41,428 @@ $true name = '$scoped'; break; case 2: - name = '$braces'; + name = '$numb3rInside'; break; case 3: - name = '$numb3rInside'; + name = '$33333'; break; case 4: + name = '$normal'; + break; + case 5: + name = '$braces'; + startChar = 9; + line = 5; + break; + case 6: + name = '$scoped'; + startChar = 20; + line = 5; + break; + } + + assert.strictEqual(variable.caseSensitiveLookup, false); + assert.strictEqual(variable.range.start.line, line); + assert.strictEqual(variable.range.end.line, line); + assert.strictEqual(variable.range.start.character, startChar); + assert.strictEqual(variable.variableName, name); + assert.strictEqual(variable.range.end.character, name.length + startChar); + } + }); + + test('Known constants ignored', async () => { + const doc = await vscode.workspace.openTextDocument({ + language: 'powershell', + content: '$false $true $null $123', + }); + + const provider = new PowerShellVariableInlineValuesProvider(); + + const result = await provider.provideInlineValues(doc, new vscode.Range(0, 0, 0, 0), { + stoppedLocation: new vscode.Range(doc.lineCount - 1, 0, doc.lineCount - 1, 0), + frameId: 0 + }); + + assert.strictEqual(result?.length, 1); + + // Making sure the test actually ran by including a real variable + const variable = result![0] as vscode.InlineValueVariableLookup; + assert.strictEqual(variable.caseSensitiveLookup, false); + assert.strictEqual(variable.range.start.line, 0); + assert.strictEqual(variable.range.end.line, 0); + assert.strictEqual(variable.range.start.character, 19); + assert.strictEqual(variable.variableName, '$123'); + assert.strictEqual(variable.range.end.character, (19 + 4)); + }); + + test('Alphanumerical variables', async () => { + const doc = await vscode.workspace.openTextDocument({ + language: 'powershell', + content: ` +$normal = Get-Process +$numb3rInside = 'asdf' +$33333 = 'numbers' +$something_wrong? = 123 +4 +`, + }); + + const provider = new PowerShellVariableInlineValuesProvider(); + + const result = await provider.provideInlineValues(doc, new vscode.Range(0, 0, 0, 0), { + stoppedLocation: new vscode.Range(doc.lineCount - 1, 0, doc.lineCount - 1, 0), + frameId: 0 + }); + + assert.strictEqual(result?.length, 4); + for (let i = 0; i < result.length; i++) { + const variable = result![i] as vscode.InlineValueVariableLookup; + + let name: string = ''; + let startChar: number = 0; + let line: number = i + 1; + switch (i) { + case 0: + name = '$normal'; + break; + case 1: + name = '$numb3rInside'; + break; + case 2: name = '$33333'; break; + case 3: + name = '$something_wrong?'; + break; + } + + assert.strictEqual(variable.caseSensitiveLookup, false); + assert.strictEqual(variable.range.start.line, line); + assert.strictEqual(variable.range.end.line, line); + assert.strictEqual(variable.range.start.character, startChar); + assert.strictEqual(variable.variableName, name); + assert.strictEqual(variable.range.end.character, name.length + startChar); + } + }); + + test('Scoped variables', async () => { + const doc = await vscode.workspace.openTextDocument({ + language: 'powershell', + content: ` +$script:scoped = 5 +$global:scoped = 5 +$local:scoped = 5 +$using:scoped = 5 +$private:scoped = 5 +$variable:scoped = 5 +\${Script:special scoped} +$invalidscope:notdetected = 123 +4 +`, + }); + + const provider = new PowerShellVariableInlineValuesProvider(); + + const result = await provider.provideInlineValues(doc, new vscode.Range(0, 0, 0, 0), { + stoppedLocation: new vscode.Range(doc.lineCount - 1, 0, doc.lineCount - 1, 0), + frameId: 0 + }); + + assert.strictEqual(result?.length, 7); + for (let i = 0; i < result.length; i++) { + const variable = result![i] as vscode.InlineValueVariableLookup; + + let name: string = ''; + let startChar: number = 0; + let line: number = i + 1; + switch (i) { + case 0: + name = '$scoped'; + break; + case 1: + name = '$scoped'; + break; + case 2: + name = '$scoped'; + break; + case 3: + name = '$scoped'; + break; + case 4: + name = '$scoped'; + break; case 5: - name = '$ '; + name = '$scoped'; break; case 6: + name = '$special scoped'; + break; + } + + assert.strictEqual(variable.caseSensitiveLookup, false); + assert.strictEqual(variable.range.start.line, line); + assert.strictEqual(variable.range.end.line, line); + assert.strictEqual(variable.range.start.character, startChar); + assert.strictEqual(variable.variableName, name); + assert.strictEqual(variable.range.end.character, name.length + startChar); + } + }); + + test('Special character variables', async () => { + const doc = await vscode.workspace.openTextDocument({ + language: 'powershell', + content: ` +\${hello\`\`b} +\${braces} = "asdf" +\${ } = 'spaces' +\${Script:omg\`b} +\${bra%!c\\e\`} { + const doc = await vscode.workspace.openTextDocument({ + language: 'powershell', + content: `$sb = {\${hello \`{ \`} world}}`, + }); + + const provider = new PowerShellVariableInlineValuesProvider(); + + const result = await provider.provideInlineValues(doc, new vscode.Range(0, 0, 0, 0), { + stoppedLocation: new vscode.Range(doc.lineCount - 1, 0, doc.lineCount - 1, 0), + frameId: 0 + }); + + assert.strictEqual(result?.length, 2); + for (let i = 0; i < result.length; i++) { + const variable = result![i] as vscode.InlineValueVariableLookup; + + let name: string = ''; + let startChar: number = 0; + let line: number = i; + switch (i) { + case 0: + name = '$sb'; + break; + case 1: + name = '$hello { } world'; + startChar = 7; + line = 0; + break; + } + + assert.strictEqual(variable.caseSensitiveLookup, false); + assert.strictEqual(variable.range.start.line, line); + assert.strictEqual(variable.range.end.line, line); + assert.strictEqual(variable.range.start.character, startChar); + assert.strictEqual(variable.variableName, name); + assert.strictEqual(variable.range.end.character, name.length + startChar); + } + }); + + test('Variables used with collections', async () => { + const doc = await vscode.workspace.openTextDocument({ + language: 'powershell', + content: ` +$dict[$key] +@($element,$element2, $element3) +$normal, \${braces}, $script:scoped +@{key = $var} +@{key = \${special var}} +`, + }); + + const provider = new PowerShellVariableInlineValuesProvider(); + + const result = await provider.provideInlineValues(doc, new vscode.Range(0, 0, 0, 0), { + stoppedLocation: new vscode.Range(doc.lineCount - 1, 0, doc.lineCount - 1, 0), + frameId: 0 + }); + + assert.strictEqual(result?.length, 10); + for (let i = 0; i < result.length; i++) { + const variable = result![i] as vscode.InlineValueVariableLookup; + + let name: string = ''; + let startChar: number = 0; + let line: number = i + 1; + switch (i) { + case 0: + name = '$dict'; + break; + case 1: + name = '$key'; + startChar = 6; + line = 1; + break; + case 2: + name = '$element'; + startChar = 2; + line = 2; + break; + case 3: + name = '$element2'; + startChar = 11; + line = 2; + break; + case 4: + name = '$element3'; + startChar = 22; + line = 2; + break; + case 5: name = '$normal'; + line = 3; break; - case 7: + case 6: name = '$braces'; startChar = 9; - line = 6; + line = 3; break; - case 8: + case 7: name = '$scoped'; startChar = 20; + line = 3; + break; + case 8: + name = '$var'; + startChar = 8; + line = 4; + break; + case 9: + name = '$special var'; + startChar = 8; + line = 5; + break; + } + + assert.strictEqual(variable.caseSensitiveLookup, false); + assert.strictEqual(variable.range.start.line, line); + assert.strictEqual(variable.range.end.line, line); + assert.strictEqual(variable.range.start.character, startChar); + assert.strictEqual(variable.variableName, name); + assert.strictEqual(variable.range.end.character, name.length + startChar); + } + }); + + test('Variables with modifiers', async () => { + const doc = await vscode.workspace.openTextDocument({ + language: 'powershell', + content: ` +$a; +$a,$b +$a-$b +$a+$b +$a/$b +$a*$b +`, + }); + + const provider = new PowerShellVariableInlineValuesProvider(); + + const result = await provider.provideInlineValues(doc, new vscode.Range(0, 0, 0, 0), { + stoppedLocation: new vscode.Range(doc.lineCount - 1, 0, doc.lineCount - 1, 0), + frameId: 0 + }); + + assert.strictEqual(result?.length, 11); + for (let i = 0; i < result.length; i++) { + const variable = result![i] as vscode.InlineValueVariableLookup; + + let name: string = ''; + let startChar: number = 0; + let line: number = i + 1; + switch (i) { + case 0: + name = '$a'; + break; + case 1: + name = '$a'; + break; + case 2: + name = '$b'; + startChar = 3; + line = 2; + break; + case 3: + name = '$a'; + line = 3; + break; + case 4: + name = '$b'; + startChar = 3; + line = 3; + break; + case 5: + name = '$a'; + line = 4; + break; + case 6: + name = '$b'; + startChar = 3; + line = 4; + break; + case 7: + name = '$a'; + line = 5; + break; + case 8: + name = '$b'; + startChar = 3; + line = 5; + break; + case 9: + name = '$a'; + line = 6; + break; + case 10: + name = '$b'; + startChar = 3; line = 6; break; } - + assert.strictEqual(variable.caseSensitiveLookup, false); assert.strictEqual(variable.range.start.line, line); assert.strictEqual(variable.range.end.line, line);