From cb091b7aff42d41f68808edc36758758b54500c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Augusto=20C=C3=A9sar=20Dias?= Date: Thu, 29 Jul 2021 20:50:54 +0200 Subject: [PATCH] feat: Add support for Logpoints (#343) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: initial dev of logpoint. (#258) feat: adding types for string-replace-async library feat: linting and prettifying feat: adding tests for logpoint class feat: fixing formatting feat: fixing doc for attribute * Implemented logpoint support. Co-authored-by: Augusto César Dias Co-authored-by: Damjan Cvetko --- CHANGELOG.md | 6 +++ package-lock.json | 5 +++ package.json | 1 + src/logpoint.ts | 53 ++++++++++++++++++++++++ src/paths.ts | 2 +- src/phpDebug.ts | 32 +++++++++++++++ src/test/logpoint.ts | 98 ++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 196 insertions(+), 1 deletion(-) create mode 100644 src/logpoint.ts create mode 100644 src/test/logpoint.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e55623e..5cc54c1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [1.17.0] + +## Added + +- Added logpoint support. + ## [1.16.3] ## Fixed diff --git a/package-lock.json b/package-lock.json index d21c216a..b3293213 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7212,6 +7212,11 @@ "readable-stream": "^2.0.2" } }, + "string-replace-async": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/string-replace-async/-/string-replace-async-2.0.0.tgz", + "integrity": "sha512-AHMupZscUiDh07F1QziX7PLoB1DQ/pzu19vc8Xa8LwZcgnOXaw7yCgBuSYrxVEfaM2d8scc3Gtp+i+QJZV+spw==" + }, "string-width": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", diff --git a/package.json b/package.json index cf000ffc..c7bacfbb 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "minimatch": "^3.0.4", "moment": "^2.29.1", "relateurl": "^0.2.7", + "string-replace-async": "^2.0.0", "url-relative": "^1.0.0", "urlencode": "^1.1.0", "vscode-debugadapter": "^1.47.0", diff --git a/src/logpoint.ts b/src/logpoint.ts new file mode 100644 index 00000000..d78a00ed --- /dev/null +++ b/src/logpoint.ts @@ -0,0 +1,53 @@ +import stringReplaceAsync = require('string-replace-async') +import { isWindowsUri } from './paths' + +export class LogPointManager { + private _logpoints = new Map>() + + public addLogPoint(fileUri: string, lineNumber: number, logMessage: string) { + if (isWindowsUri(fileUri)) { + fileUri = fileUri.toLowerCase() + } + if (!this._logpoints.has(fileUri)) { + this._logpoints.set(fileUri, new Map()) + } + this._logpoints.get(fileUri)!.set(lineNumber, logMessage) + } + + public clearFromFile(fileUri: string) { + if (isWindowsUri(fileUri)) { + fileUri = fileUri.toLowerCase() + } + if (this._logpoints.has(fileUri)) { + this._logpoints.get(fileUri)!.clear() + } + } + + public hasLogPoint(fileUri: string, lineNumber: number): boolean { + if (isWindowsUri(fileUri)) { + fileUri = fileUri.toLowerCase() + } + return this._logpoints.has(fileUri) && this._logpoints.get(fileUri)!.has(lineNumber) + } + + public async resolveExpressions( + fileUri: string, + lineNumber: number, + callback: (expr: string) => Promise + ): Promise { + if (isWindowsUri(fileUri)) { + fileUri = fileUri.toLowerCase() + } + if (!this.hasLogPoint(fileUri, lineNumber)) { + return Promise.reject('Logpoint not found') + } + const expressionRegex = /\{(.*?)\}/gm + return await stringReplaceAsync( + this._logpoints.get(fileUri)!.get(lineNumber)!, + expressionRegex, + function (_: string, group: string) { + return group.length === 0 ? Promise.resolve('') : callback(group) + } + ) + } +} diff --git a/src/paths.ts b/src/paths.ts index 6fa00011..7024f2db 100644 --- a/src/paths.ts +++ b/src/paths.ts @@ -141,7 +141,7 @@ export function convertClientPathToDebugger(localPath: string, pathMapping?: { [ return serverFileUri } -function isWindowsUri(path: string): boolean { +export function isWindowsUri(path: string): boolean { return /^file:\/\/\/[a-zA-Z]:\//.test(path) } diff --git a/src/phpDebug.ts b/src/phpDebug.ts index e38e0ec1..53b21f19 100644 --- a/src/phpDebug.ts +++ b/src/phpDebug.ts @@ -13,6 +13,7 @@ import { convertClientPathToDebugger, convertDebuggerPathToClient } from './path import minimatch = require('minimatch') import { BreakpointManager, BreakpointAdapter } from './breakpoints' import * as semver from 'semver' +import { LogPointManager } from './logpoint' if (process.env['VSCODE_NLS_CONFIG']) { try { @@ -148,6 +149,13 @@ class PhpDebugSession extends vscode.DebugSession { /** Breakpoint Adapters */ private _breakpointAdapters = new Map() + /** + * The manager for logpoints. Since xdebug does not support anything like logpoints, + * it has to be managed by the extension/debug server. It does that by a Map referencing + * the log messages per file. Xdebug sees it as a regular breakpoint. + */ + private _logPointManager = new LogPointManager() + /** the promise that gets resolved once we receive the done request */ private _donePromise: Promise @@ -170,6 +178,7 @@ class PhpDebugSession extends vscode.DebugSession { supportsEvaluateForHovers: false, supportsConditionalBreakpoints: true, supportsFunctionBreakpoints: true, + supportsLogPoints: true, exceptionBreakpointFilters: [ { filter: 'Notice', @@ -459,6 +468,24 @@ class PhpDebugSession extends vscode.DebugSession { } else { stoppedEventReason = 'breakpoint' } + // Check for log points + if (this._logPointManager.hasLogPoint(response.fileUri, response.line)) { + const logMessage = await this._logPointManager.resolveExpressions( + response.fileUri, + response.line, + async (expr: string): Promise => { + const evaluated = await connection.sendEvalCommand(expr) + return formatPropertyValue(evaluated.result) + } + ) + + this.sendEvent(new vscode.OutputEvent(logMessage + '\n', 'console')) + if (stoppedEventReason === 'breakpoint') { + const responseCommand = await connection.sendRunCommand() + await this._checkStatus(responseCommand) + return + } + } const event: VSCodeDebugProtocol.StoppedEvent = new vscode.StoppedEvent( stoppedEventReason, connection.id, @@ -533,6 +560,11 @@ class PhpDebugSession extends vscode.DebugSession { const fileUri = convertClientPathToDebugger(args.source.path!, this._args.pathMappings) const vscodeBreakpoints = this._breakpointManager.setBreakPoints(args.source, fileUri, args.breakpoints!) response.body = { breakpoints: vscodeBreakpoints } + // Process logpoints + this._logPointManager.clearFromFile(fileUri) + args.breakpoints!.filter(breakpoint => breakpoint.logMessage).forEach(breakpoint => { + this._logPointManager.addLogPoint(fileUri, breakpoint.line, breakpoint.logMessage!) + }) } catch (error) { this.sendErrorResponse(response, error) return diff --git a/src/test/logpoint.ts b/src/test/logpoint.ts new file mode 100644 index 00000000..7be746b4 --- /dev/null +++ b/src/test/logpoint.ts @@ -0,0 +1,98 @@ +import { LogPointManager } from '../logpoint' +import * as assert from 'assert' + +describe('logpoint', () => { + const FILE_URI1 = 'file://my/file1' + const FILE_URI2 = 'file://my/file2' + const FILE_URI3 = 'file://my/file3' + + const LOG_MESSAGE_VAR = '{$variable1}' + const LOG_MESSAGE_MULTIPLE = '{$variable1} {$variable3} {$variable2}' + const LOG_MESSAGE_TEXT_AND_VAR = 'This is my {$variable1}' + const LOG_MESSAGE_TEXT_AND_MULTIVAR = 'Those variables: {$variable1} ${$variable2} should be replaced' + const LOG_MESSAGE_REPEATED_VAR = 'This {$variable1} and {$variable1} should be equal' + const LOG_MESSAGE_BADLY_FORMATED_VAR = 'Only {$variable1} should be resolved and not }$variable1 and $variable1{}' + + const REPLACE_FUNCTION = (str: string): Promise => { + return Promise.resolve(`${str}_value`) + } + + let logPointManager: LogPointManager + + beforeEach('create new instance', () => (logPointManager = new LogPointManager())) + + describe('basic map management', () => { + it('should contain added logpoints', () => { + logPointManager.addLogPoint(FILE_URI1, 10, LOG_MESSAGE_VAR) + logPointManager.addLogPoint(FILE_URI1, 11, LOG_MESSAGE_VAR) + logPointManager.addLogPoint(FILE_URI2, 12, LOG_MESSAGE_VAR) + logPointManager.addLogPoint(FILE_URI3, 13, LOG_MESSAGE_VAR) + + assert.equal(logPointManager.hasLogPoint(FILE_URI1, 10), true) + assert.equal(logPointManager.hasLogPoint(FILE_URI1, 11), true) + assert.equal(logPointManager.hasLogPoint(FILE_URI2, 12), true) + assert.equal(logPointManager.hasLogPoint(FILE_URI3, 13), true) + + assert.equal(logPointManager.hasLogPoint(FILE_URI1, 12), false) + assert.equal(logPointManager.hasLogPoint(FILE_URI2, 13), false) + assert.equal(logPointManager.hasLogPoint(FILE_URI3, 10), false) + }) + + it('should add and clear entries', () => { + logPointManager.addLogPoint(FILE_URI1, 10, LOG_MESSAGE_VAR) + logPointManager.addLogPoint(FILE_URI1, 11, LOG_MESSAGE_VAR) + logPointManager.addLogPoint(FILE_URI2, 12, LOG_MESSAGE_VAR) + logPointManager.addLogPoint(FILE_URI3, 13, LOG_MESSAGE_VAR) + + assert.equal(logPointManager.hasLogPoint(FILE_URI1, 10), true) + assert.equal(logPointManager.hasLogPoint(FILE_URI1, 11), true) + assert.equal(logPointManager.hasLogPoint(FILE_URI2, 12), true) + assert.equal(logPointManager.hasLogPoint(FILE_URI3, 13), true) + + logPointManager.clearFromFile(FILE_URI1) + + assert.equal(logPointManager.hasLogPoint(FILE_URI1, 10), false) + assert.equal(logPointManager.hasLogPoint(FILE_URI1, 11), false) + assert.equal(logPointManager.hasLogPoint(FILE_URI2, 12), true) + assert.equal(logPointManager.hasLogPoint(FILE_URI3, 13), true) + }) + }) + + describe('variable resolution', () => { + it('should resolve variables', async () => { + logPointManager.addLogPoint(FILE_URI1, 10, LOG_MESSAGE_VAR) + const result = await logPointManager.resolveExpressions(FILE_URI1, 10, REPLACE_FUNCTION) + assert.equal(result, '$variable1_value') + }) + + it('should resolve multiple variables', async () => { + logPointManager.addLogPoint(FILE_URI1, 10, LOG_MESSAGE_MULTIPLE) + const result = await logPointManager.resolveExpressions(FILE_URI1, 10, REPLACE_FUNCTION) + assert.equal(result, '$variable1_value $variable3_value $variable2_value') + }) + + it('should resolve variables with text', async () => { + logPointManager.addLogPoint(FILE_URI1, 10, LOG_MESSAGE_TEXT_AND_VAR) + const result = await logPointManager.resolveExpressions(FILE_URI1, 10, REPLACE_FUNCTION) + assert.equal(result, 'This is my $variable1_value') + }) + + it('should resolve multiple variables with text', async () => { + logPointManager.addLogPoint(FILE_URI1, 10, LOG_MESSAGE_TEXT_AND_MULTIVAR) + const result = await logPointManager.resolveExpressions(FILE_URI1, 10, REPLACE_FUNCTION) + assert.equal(result, 'Those variables: $variable1_value $$variable2_value should be replaced') + }) + + it('should resolve repeated variables', async () => { + logPointManager.addLogPoint(FILE_URI1, 10, LOG_MESSAGE_REPEATED_VAR) + const result = await logPointManager.resolveExpressions(FILE_URI1, 10, REPLACE_FUNCTION) + assert.equal(result, 'This $variable1_value and $variable1_value should be equal') + }) + + it('should resolve repeated bad formated messages correctly', async () => { + logPointManager.addLogPoint(FILE_URI1, 10, LOG_MESSAGE_BADLY_FORMATED_VAR) + const result = await logPointManager.resolveExpressions(FILE_URI1, 10, REPLACE_FUNCTION) + assert.equal(result, 'Only $variable1_value should be resolved and not }$variable1 and $variable1') + }) + }) +})