diff --git a/src/extension/common/vscodeapi.ts b/src/extension/common/vscodeapi.ts index 3425be77..7ba92c93 100644 --- a/src/extension/common/vscodeapi.ts +++ b/src/extension/common/vscodeapi.ts @@ -128,3 +128,7 @@ export function startDebugging( ) { debug.startDebugging(folder, nameOrConfiguration, parentSession); } + +export function customRequest(command: string, args?: any): any { + return debug.activeDebugSession?.customRequest(command, args); +} diff --git a/src/extension/debugger/inlineValue/pythonInlineValueProvider.ts b/src/extension/debugger/inlineValue/pythonInlineValueProvider.ts new file mode 100644 index 00000000..f71a37d6 --- /dev/null +++ b/src/extension/debugger/inlineValue/pythonInlineValueProvider.ts @@ -0,0 +1,127 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +import { + InlineValue, + InlineValueContext, + InlineValuesProvider, + Range, + TextDocument, + InlineValueVariableLookup, + InlineValueEvaluatableExpression, +} from 'vscode'; +import { customRequest } from '../../common/vscodeapi'; + +export class PythonInlineValueProvider implements InlineValuesProvider { + public async provideInlineValues( + document: TextDocument, + viewPort: Range, + context: InlineValueContext, + ): Promise { + let scopesRequest = await customRequest('scopes', { frameId: context.frameId }); + let variablesRequest = await customRequest('variables', { + variablesReference: scopesRequest.scopes[0].variablesReference, + }); + + //https://docs.python.org/3/reference/lexical_analysis.html#keywords + const pythonKeywords = [ + 'False', + 'await', + 'else', + 'import ', + 'pass', + 'None', + 'break', + 'except', + 'in', + 'raise', + 'True', + 'class', + 'finally', + 'is', + 'return', + 'and', + 'continue', + 'for', + 'lambda', + 'try', + 'as', + 'def', + 'from', + 'nonlocal', + 'while', + 'assert', + 'del', + 'global', + 'not', + 'with', + 'async', + 'elif', + 'if', + 'or', + 'yield', + 'self', + ]; + + const pythonVariables: any[] = variablesRequest.variables + .filter((variable: any) => variable.type) + .map((variable: any) => variable.name); + + let variableRegex = new RegExp( + '(?:self.)?' + //match self. if present + '[a-zA-Z_][a-zA-Z0-9_]*', //math variable name + 'g', + ); + + const allValues: InlineValue[] = []; + for (let l = viewPort.start.line; l <= viewPort.end.line; l++) { + const line = document.lineAt(l); + // Skip comments + if (line.text.trimStart().startsWith('#')) { + continue; + } + + let code = removeCharsOutsideBraces(line.text); + + for (let match = variableRegex.exec(code); match; match = variableRegex.exec(code)) { + let varName = match[0]; + // Skip python keywords + if (pythonKeywords.includes(varName)) { + continue; + } + if (pythonVariables.includes(varName.split('.')[0])) { + if (varName.includes('self')) { + const rng = new Range(l, match.index, l, match.index + varName.length); + allValues.push(new InlineValueEvaluatableExpression(rng, varName)); + } else { + const rng = new Range(l, match.index, l, match.index + varName.length); + allValues.push(new InlineValueVariableLookup(rng, varName, false)); + } + } + } + } + return allValues; + } +} + +function removeCharsOutsideBraces(code: string): string { + // Regular expression to find Python strings + const stringRegex = /(["'])(?:(?=(\\?))\2.)*?\1/g; + + //Regular expression to match values inside {} + const insideBracesRegex = /{[^{}]*}/g; + + return code.replace(stringRegex, (match) => { + const content = match.slice(1, -1); + + let result = ''; + let tempMatch; + + while ((tempMatch = insideBracesRegex.exec(content)) !== null) { + result += tempMatch[0]; + } + const processedContent = result || content; + + return match[0] + processedContent + match[0]; + }); +} diff --git a/src/extension/extensionInit.ts b/src/extension/extensionInit.ts index 760644cb..6f5a75b4 100644 --- a/src/extension/extensionInit.ts +++ b/src/extension/extensionInit.ts @@ -49,6 +49,7 @@ import { openReportIssue } from './common/application/commands/reportIssueComman import { buildApi } from './api'; import { IExtensionApi } from './apiTypes'; import { registerHexDebugVisualizationTreeProvider } from './debugger/visualizers/inlineHexDecoder'; +import { PythonInlineValueProvider } from './debugger/inlineValue/pythonInlineValueProvider'; export async function registerDebugger(context: IExtensionContext): Promise { const childProcessAttachService = new ChildProcessAttachService(); @@ -136,6 +137,7 @@ export async function registerDebugger(context: IExtensionContext): Promise { const shouldTerminalFocusOnStart = getConfiguration('python', debugSession.workspaceFolder?.uri)?.terminal @@ -195,6 +197,10 @@ export async function registerDebugger(context: IExtensionContext): Promise('inlineHexDecoder', registerHexDebugVisualizationTreeProvider()), ); + context.subscriptions.push( + languages.registerInlineValuesProvider({ language: 'python' }, new PythonInlineValueProvider()), + ); + context.subscriptions.push( debug.registerDebugVisualizationProvider('inlineHexDecoder', { provideDebugVisualization(_context, _token) { diff --git a/src/test/pythonFiles/testClassVarType.py b/src/test/pythonFiles/testClassVarType.py new file mode 100644 index 00000000..5b91c3b6 --- /dev/null +++ b/src/test/pythonFiles/testClassVarType.py @@ -0,0 +1,10 @@ +class Person: + def __init__(self, name, age): + self.name = name + self.age = age + + def greet(self): + return f"Hello, my name is {self.name} and I a {self.age} years old." + +person1 = Person("John Doe", 30) +person1.greet() diff --git a/src/test/pythonFiles/testVarTypes.py b/src/test/pythonFiles/testVarTypes.py new file mode 100644 index 00000000..2e660e7b --- /dev/null +++ b/src/test/pythonFiles/testVarTypes.py @@ -0,0 +1,6 @@ +var1 = 5 +var2 = 7 +var3 = "hola" +var4 = {"a": 1, "b": 2} +var5 = [1, 2, 3] +var6 =var1 + var2 diff --git a/src/test/unittest/inlineValue/pythonInlineValueProvider.unit.test.ts b/src/test/unittest/inlineValue/pythonInlineValueProvider.unit.test.ts new file mode 100644 index 00000000..accdf38d --- /dev/null +++ b/src/test/unittest/inlineValue/pythonInlineValueProvider.unit.test.ts @@ -0,0 +1,268 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as chaiAsPromised from 'chai-as-promised'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import { use, expect } from 'chai'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../constants'; +import { PythonInlineValueProvider } from '../../../extension/debugger/inlineValue/pythonInlineValueProvider'; +import { workspace, Range, InlineValueContext } from 'vscode'; +import * as vscodeapi from '../../../extension/common/vscodeapi'; + +use(chaiAsPromised); + +const WS_ROOT = path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'test'); + +suite('Debugging - pythonInlineProvider', () => { + let customRequestStub: sinon.SinonStub; + + setup(() => { + customRequestStub = sinon.stub(vscodeapi, 'customRequest'); + customRequestStub.withArgs('scopes', sinon.match.any).resolves({ scopes: [{ variablesReference: 0 }] }); + }); + + teardown(async () => { + sinon.restore(); + }); + + test('ProvideInlineValues function should return all the vars in the python file', async () => { + customRequestStub.withArgs('variables', sinon.match.any).resolves({ + variables: [ + { + name: 'special variables', + value: '', + type: '', + evaluateName: 'special variables', + variablesReference: 10, + }, + { + name: 'var1', + value: '5', + type: 'int', + evaluateName: 'var1', + variablesReference: 0, + }, + { + name: 'var2', + value: '7', + type: 'int', + evaluateName: 'var2', + variablesReference: 0, + }, + { + name: 'var3', + value: "'hola'", + type: 'str', + evaluateName: 'var3', + variablesReference: 0, + presentationHint: { + attributes: ['rawString'], + }, + }, + { + name: 'var4', + value: "{'a': 1, 'b': 2}", + type: 'dict', + evaluateName: 'var4', + variablesReference: 8, + }, + { + name: 'var5', + value: '[1, 2, 3]', + type: 'list', + evaluateName: 'var5', + variablesReference: 9, + }, + ], + }); + const file = path.join(WS_ROOT, 'pythonFiles', 'testVarTypes.py'); + let document = await workspace.openTextDocument(file); + const inlineValueProvider = new PythonInlineValueProvider(); + + const viewPort = new Range(0, 0, 5, 17); + const context = { frameId: 0, stoppedLocation: new Range(5, 1, 5, 1) } as InlineValueContext; + + const result = await inlineValueProvider.provideInlineValues(document, viewPort, context); + const expected = [ + { + range: { + c: { + c: 0, + e: 0, + }, + e: { + c: 0, + e: 4, + }, + }, + variableName: 'var1', + caseSensitiveLookup: false, + }, + { + range: { + c: { + c: 1, + e: 0, + }, + e: { + c: 1, + e: 4, + }, + }, + variableName: 'var2', + caseSensitiveLookup: false, + }, + { + range: { + c: { + c: 2, + e: 0, + }, + e: { + c: 2, + e: 4, + }, + }, + variableName: 'var3', + caseSensitiveLookup: false, + }, + { + range: { + c: { + c: 3, + e: 0, + }, + e: { + c: 3, + e: 4, + }, + }, + variableName: 'var4', + caseSensitiveLookup: false, + }, + { + range: { + c: { + c: 4, + e: 0, + }, + e: { + c: 4, + e: 4, + }, + }, + variableName: 'var5', + caseSensitiveLookup: false, + }, + { + range: { + c: { + c: 5, + e: 6, + }, + e: { + c: 5, + e: 10, + }, + }, + variableName: 'var1', + caseSensitiveLookup: false, + }, + { + range: { + c: { + c: 5, + e: 13, + }, + e: { + c: 5, + e: 17, + }, + }, + variableName: 'var2', + caseSensitiveLookup: false, + }, + ]; + expect(result).to.deep.equal(expected); + }); + + test('ProvideInlineValues function should return all the vars in the python file with class variables', async () => { + customRequestStub.withArgs('variables', sinon.match.any).resolves({ + variables: [ + { + name: 'self', + value: '<__main__.Person object at 0x10b223310>', + type: 'Person', + evaluateName: 'self', + variablesReference: 5, + }, + ], + }); + const file = path.join(WS_ROOT, 'pythonFiles', 'testClassVarType.py'); + let document = await workspace.openTextDocument(file); + const inlineValueProvider = new PythonInlineValueProvider(); + + const viewPort = new Range(0, 0, 10, 0); + const context = { frameId: 0, stoppedLocation: new Range(6, 1, 6, 1) } as InlineValueContext; + + const result = await inlineValueProvider.provideInlineValues(document, viewPort, context); + const expected = [ + { + range: { + c: { + c: 2, + e: 8, + }, + e: { + c: 2, + e: 17, + }, + }, + expression: 'self.name', + }, + { + range: { + c: { + c: 3, + e: 8, + }, + e: { + c: 3, + e: 16, + }, + }, + expression: 'self.age', + }, + { + range: { + c: { + c: 6, + e: 18, + }, + e: { + c: 6, + e: 27, + }, + }, + expression: 'self.name', + }, + { + range: { + c: { + c: 6, + e: 29, + }, + e: { + c: 6, + e: 37, + }, + }, + expression: 'self.age', + }, + ]; + expect(result).to.deep.equal(expected); + }); +});