Skip to content

Commit cb091b7

Browse files
augustocdiasAugusto César Diaszobo
authored
feat: Add support for Logpoints (#343)
* 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 <augusto.dias@olx.com> Co-authored-by: Damjan Cvetko <damjan.cvetko@monotek.net>
1 parent 9a73045 commit cb091b7

File tree

7 files changed

+196
-1
lines changed

7 files changed

+196
-1
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
44

55
The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/).
66

7+
## [1.17.0]
8+
9+
## Added
10+
11+
- Added logpoint support.
12+
713
## [1.16.3]
814

915
## Fixed

package-lock.json

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"minimatch": "^3.0.4",
4646
"moment": "^2.29.1",
4747
"relateurl": "^0.2.7",
48+
"string-replace-async": "^2.0.0",
4849
"url-relative": "^1.0.0",
4950
"urlencode": "^1.1.0",
5051
"vscode-debugadapter": "^1.47.0",

src/logpoint.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import stringReplaceAsync = require('string-replace-async')
2+
import { isWindowsUri } from './paths'
3+
4+
export class LogPointManager {
5+
private _logpoints = new Map<string, Map<number, string>>()
6+
7+
public addLogPoint(fileUri: string, lineNumber: number, logMessage: string) {
8+
if (isWindowsUri(fileUri)) {
9+
fileUri = fileUri.toLowerCase()
10+
}
11+
if (!this._logpoints.has(fileUri)) {
12+
this._logpoints.set(fileUri, new Map<number, string>())
13+
}
14+
this._logpoints.get(fileUri)!.set(lineNumber, logMessage)
15+
}
16+
17+
public clearFromFile(fileUri: string) {
18+
if (isWindowsUri(fileUri)) {
19+
fileUri = fileUri.toLowerCase()
20+
}
21+
if (this._logpoints.has(fileUri)) {
22+
this._logpoints.get(fileUri)!.clear()
23+
}
24+
}
25+
26+
public hasLogPoint(fileUri: string, lineNumber: number): boolean {
27+
if (isWindowsUri(fileUri)) {
28+
fileUri = fileUri.toLowerCase()
29+
}
30+
return this._logpoints.has(fileUri) && this._logpoints.get(fileUri)!.has(lineNumber)
31+
}
32+
33+
public async resolveExpressions(
34+
fileUri: string,
35+
lineNumber: number,
36+
callback: (expr: string) => Promise<string>
37+
): Promise<string> {
38+
if (isWindowsUri(fileUri)) {
39+
fileUri = fileUri.toLowerCase()
40+
}
41+
if (!this.hasLogPoint(fileUri, lineNumber)) {
42+
return Promise.reject('Logpoint not found')
43+
}
44+
const expressionRegex = /\{(.*?)\}/gm
45+
return await stringReplaceAsync(
46+
this._logpoints.get(fileUri)!.get(lineNumber)!,
47+
expressionRegex,
48+
function (_: string, group: string) {
49+
return group.length === 0 ? Promise.resolve('') : callback(group)
50+
}
51+
)
52+
}
53+
}

src/paths.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ export function convertClientPathToDebugger(localPath: string, pathMapping?: { [
141141
return serverFileUri
142142
}
143143

144-
function isWindowsUri(path: string): boolean {
144+
export function isWindowsUri(path: string): boolean {
145145
return /^file:\/\/\/[a-zA-Z]:\//.test(path)
146146
}
147147

src/phpDebug.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { convertClientPathToDebugger, convertDebuggerPathToClient } from './path
1313
import minimatch = require('minimatch')
1414
import { BreakpointManager, BreakpointAdapter } from './breakpoints'
1515
import * as semver from 'semver'
16+
import { LogPointManager } from './logpoint'
1617

1718
if (process.env['VSCODE_NLS_CONFIG']) {
1819
try {
@@ -148,6 +149,13 @@ class PhpDebugSession extends vscode.DebugSession {
148149
/** Breakpoint Adapters */
149150
private _breakpointAdapters = new Map<xdebug.Connection, BreakpointAdapter>()
150151

152+
/**
153+
* The manager for logpoints. Since xdebug does not support anything like logpoints,
154+
* it has to be managed by the extension/debug server. It does that by a Map referencing
155+
* the log messages per file. Xdebug sees it as a regular breakpoint.
156+
*/
157+
private _logPointManager = new LogPointManager()
158+
151159
/** the promise that gets resolved once we receive the done request */
152160
private _donePromise: Promise<void>
153161

@@ -170,6 +178,7 @@ class PhpDebugSession extends vscode.DebugSession {
170178
supportsEvaluateForHovers: false,
171179
supportsConditionalBreakpoints: true,
172180
supportsFunctionBreakpoints: true,
181+
supportsLogPoints: true,
173182
exceptionBreakpointFilters: [
174183
{
175184
filter: 'Notice',
@@ -459,6 +468,24 @@ class PhpDebugSession extends vscode.DebugSession {
459468
} else {
460469
stoppedEventReason = 'breakpoint'
461470
}
471+
// Check for log points
472+
if (this._logPointManager.hasLogPoint(response.fileUri, response.line)) {
473+
const logMessage = await this._logPointManager.resolveExpressions(
474+
response.fileUri,
475+
response.line,
476+
async (expr: string): Promise<string> => {
477+
const evaluated = await connection.sendEvalCommand(expr)
478+
return formatPropertyValue(evaluated.result)
479+
}
480+
)
481+
482+
this.sendEvent(new vscode.OutputEvent(logMessage + '\n', 'console'))
483+
if (stoppedEventReason === 'breakpoint') {
484+
const responseCommand = await connection.sendRunCommand()
485+
await this._checkStatus(responseCommand)
486+
return
487+
}
488+
}
462489
const event: VSCodeDebugProtocol.StoppedEvent = new vscode.StoppedEvent(
463490
stoppedEventReason,
464491
connection.id,
@@ -533,6 +560,11 @@ class PhpDebugSession extends vscode.DebugSession {
533560
const fileUri = convertClientPathToDebugger(args.source.path!, this._args.pathMappings)
534561
const vscodeBreakpoints = this._breakpointManager.setBreakPoints(args.source, fileUri, args.breakpoints!)
535562
response.body = { breakpoints: vscodeBreakpoints }
563+
// Process logpoints
564+
this._logPointManager.clearFromFile(fileUri)
565+
args.breakpoints!.filter(breakpoint => breakpoint.logMessage).forEach(breakpoint => {
566+
this._logPointManager.addLogPoint(fileUri, breakpoint.line, breakpoint.logMessage!)
567+
})
536568
} catch (error) {
537569
this.sendErrorResponse(response, error)
538570
return

src/test/logpoint.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { LogPointManager } from '../logpoint'
2+
import * as assert from 'assert'
3+
4+
describe('logpoint', () => {
5+
const FILE_URI1 = 'file://my/file1'
6+
const FILE_URI2 = 'file://my/file2'
7+
const FILE_URI3 = 'file://my/file3'
8+
9+
const LOG_MESSAGE_VAR = '{$variable1}'
10+
const LOG_MESSAGE_MULTIPLE = '{$variable1} {$variable3} {$variable2}'
11+
const LOG_MESSAGE_TEXT_AND_VAR = 'This is my {$variable1}'
12+
const LOG_MESSAGE_TEXT_AND_MULTIVAR = 'Those variables: {$variable1} ${$variable2} should be replaced'
13+
const LOG_MESSAGE_REPEATED_VAR = 'This {$variable1} and {$variable1} should be equal'
14+
const LOG_MESSAGE_BADLY_FORMATED_VAR = 'Only {$variable1} should be resolved and not }$variable1 and $variable1{}'
15+
16+
const REPLACE_FUNCTION = (str: string): Promise<string> => {
17+
return Promise.resolve(`${str}_value`)
18+
}
19+
20+
let logPointManager: LogPointManager
21+
22+
beforeEach('create new instance', () => (logPointManager = new LogPointManager()))
23+
24+
describe('basic map management', () => {
25+
it('should contain added logpoints', () => {
26+
logPointManager.addLogPoint(FILE_URI1, 10, LOG_MESSAGE_VAR)
27+
logPointManager.addLogPoint(FILE_URI1, 11, LOG_MESSAGE_VAR)
28+
logPointManager.addLogPoint(FILE_URI2, 12, LOG_MESSAGE_VAR)
29+
logPointManager.addLogPoint(FILE_URI3, 13, LOG_MESSAGE_VAR)
30+
31+
assert.equal(logPointManager.hasLogPoint(FILE_URI1, 10), true)
32+
assert.equal(logPointManager.hasLogPoint(FILE_URI1, 11), true)
33+
assert.equal(logPointManager.hasLogPoint(FILE_URI2, 12), true)
34+
assert.equal(logPointManager.hasLogPoint(FILE_URI3, 13), true)
35+
36+
assert.equal(logPointManager.hasLogPoint(FILE_URI1, 12), false)
37+
assert.equal(logPointManager.hasLogPoint(FILE_URI2, 13), false)
38+
assert.equal(logPointManager.hasLogPoint(FILE_URI3, 10), false)
39+
})
40+
41+
it('should add and clear entries', () => {
42+
logPointManager.addLogPoint(FILE_URI1, 10, LOG_MESSAGE_VAR)
43+
logPointManager.addLogPoint(FILE_URI1, 11, LOG_MESSAGE_VAR)
44+
logPointManager.addLogPoint(FILE_URI2, 12, LOG_MESSAGE_VAR)
45+
logPointManager.addLogPoint(FILE_URI3, 13, LOG_MESSAGE_VAR)
46+
47+
assert.equal(logPointManager.hasLogPoint(FILE_URI1, 10), true)
48+
assert.equal(logPointManager.hasLogPoint(FILE_URI1, 11), true)
49+
assert.equal(logPointManager.hasLogPoint(FILE_URI2, 12), true)
50+
assert.equal(logPointManager.hasLogPoint(FILE_URI3, 13), true)
51+
52+
logPointManager.clearFromFile(FILE_URI1)
53+
54+
assert.equal(logPointManager.hasLogPoint(FILE_URI1, 10), false)
55+
assert.equal(logPointManager.hasLogPoint(FILE_URI1, 11), false)
56+
assert.equal(logPointManager.hasLogPoint(FILE_URI2, 12), true)
57+
assert.equal(logPointManager.hasLogPoint(FILE_URI3, 13), true)
58+
})
59+
})
60+
61+
describe('variable resolution', () => {
62+
it('should resolve variables', async () => {
63+
logPointManager.addLogPoint(FILE_URI1, 10, LOG_MESSAGE_VAR)
64+
const result = await logPointManager.resolveExpressions(FILE_URI1, 10, REPLACE_FUNCTION)
65+
assert.equal(result, '$variable1_value')
66+
})
67+
68+
it('should resolve multiple variables', async () => {
69+
logPointManager.addLogPoint(FILE_URI1, 10, LOG_MESSAGE_MULTIPLE)
70+
const result = await logPointManager.resolveExpressions(FILE_URI1, 10, REPLACE_FUNCTION)
71+
assert.equal(result, '$variable1_value $variable3_value $variable2_value')
72+
})
73+
74+
it('should resolve variables with text', async () => {
75+
logPointManager.addLogPoint(FILE_URI1, 10, LOG_MESSAGE_TEXT_AND_VAR)
76+
const result = await logPointManager.resolveExpressions(FILE_URI1, 10, REPLACE_FUNCTION)
77+
assert.equal(result, 'This is my $variable1_value')
78+
})
79+
80+
it('should resolve multiple variables with text', async () => {
81+
logPointManager.addLogPoint(FILE_URI1, 10, LOG_MESSAGE_TEXT_AND_MULTIVAR)
82+
const result = await logPointManager.resolveExpressions(FILE_URI1, 10, REPLACE_FUNCTION)
83+
assert.equal(result, 'Those variables: $variable1_value $$variable2_value should be replaced')
84+
})
85+
86+
it('should resolve repeated variables', async () => {
87+
logPointManager.addLogPoint(FILE_URI1, 10, LOG_MESSAGE_REPEATED_VAR)
88+
const result = await logPointManager.resolveExpressions(FILE_URI1, 10, REPLACE_FUNCTION)
89+
assert.equal(result, 'This $variable1_value and $variable1_value should be equal')
90+
})
91+
92+
it('should resolve repeated bad formated messages correctly', async () => {
93+
logPointManager.addLogPoint(FILE_URI1, 10, LOG_MESSAGE_BADLY_FORMATED_VAR)
94+
const result = await logPointManager.resolveExpressions(FILE_URI1, 10, REPLACE_FUNCTION)
95+
assert.equal(result, 'Only $variable1_value should be resolved and not }$variable1 and $variable1')
96+
})
97+
})
98+
})

0 commit comments

Comments
 (0)