Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 18 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"iconv-lite": "^0.4.15",
"minimatch": "^3.0.3",
"moment": "^2.17.1",
"string-replace-async": "^1.2.1",
"url-relative": "^1.0.0",
"urlencode": "^1.1.0",
"vscode-debugadapter": "^1.11.0",
Expand Down
39 changes: 39 additions & 0 deletions src/logpoint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import stringReplaceAsync = require('string-replace-async')

export class LogPointManager {
private _logpoints = new Map<string, Map<number, string>>()

public addLogPoint(fileUri: string, lineNumber: number, logMessage: string) {
if (!this._logpoints.has(fileUri)) {
this._logpoints.set(fileUri, new Map<number, string>())
}
this._logpoints.get(fileUri)!.set(lineNumber, logMessage)
}

public clearFromFile(fileUri: string) {
if (this._logpoints.has(fileUri)) {
this._logpoints.get(fileUri)!.clear()
}
}

public hasLogPoint(fileUri: string, lineNumber: number): boolean {
return this._logpoints.has(fileUri) && this._logpoints.get(fileUri)!.has(lineNumber)
}

public async resolveExpressions(
fileUri: string,
lineNumber: number,
callback: (expr: string) => Promise<string>
): Promise<string> {
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)
})
}
}
28 changes: 28 additions & 0 deletions src/phpDebug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import * as util from 'util'
import * as fs from 'fs'
import { Terminal } from './terminal'
import { isSameUri, convertClientPathToDebugger, convertDebuggerPathToClient } from './paths'
import { LogPointManager } from './logpoint'
import minimatch = require('minimatch')

if (process.env['VSCODE_NLS_CONFIG']) {
Expand Down Expand Up @@ -104,6 +105,13 @@ class PhpDebugSession extends vscode.DebugSession {
*/
private _connections = new Map<number, xdebug.Connection>()

/**
* 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()

/** A set of connections which are not yet running and are waiting for configurationDoneRequest */
private _waitingConnections = new Set<xdebug.Connection>()

Expand Down Expand Up @@ -156,6 +164,7 @@ class PhpDebugSession extends vscode.DebugSession {
supportsEvaluateForHovers: false,
supportsConditionalBreakpoints: true,
supportsFunctionBreakpoints: true,
supportsLogPoints: true,
exceptionBreakpointFilters: [
{
filter: 'Notice',
Expand Down Expand Up @@ -366,6 +375,21 @@ class PhpDebugSession extends vscode.DebugSession {
exceptionText = response.exception.name + ': ' + response.exception.message // this seems to be ignored currently by VS Code
} else if (this._args.stopOnEntry) {
stoppedEventReason = 'entry'
} else if (this._logPointManager.hasLogPoint(response.fileUri, response.line)) {
const logMessage = await this._logPointManager.resolveExpressions(
response.fileUri,
response.line,
async (expr: string): Promise<string> => {
const evaluated = await connection.sendEvalCommand(expr)
return formatPropertyValue(evaluated.result)
}
)

this.sendEvent(new vscode.OutputEvent(logMessage + '\n', 'console'))

const responseCommand = await connection.sendRunCommand()
await this._checkStatus(responseCommand)
return
} else if (response.command.indexOf('step') === 0) {
stoppedEventReason = 'step'
} else {
Expand Down Expand Up @@ -448,6 +472,7 @@ class PhpDebugSession extends vscode.DebugSession {
response.body = { breakpoints: [] }
// this is returned to VS Code
let vscodeBreakpoints: VSCodeDebugProtocol.Breakpoint[]
this._logPointManager.clearFromFile(fileUri)
if (connections.length === 0) {
// if there are no connections yet, we cannot verify any breakpoint
vscodeBreakpoints = args.breakpoints!.map(breakpoint => ({ verified: false, line: breakpoint.line }))
Expand All @@ -458,6 +483,9 @@ class PhpDebugSession extends vscode.DebugSession {
if (breakpoint.condition) {
return new xdebug.ConditionalBreakpoint(breakpoint.condition, fileUri, breakpoint.line)
} else {
if (breakpoint.logMessage !== undefined) {
this._logPointManager.addLogPoint(fileUri, breakpoint.line, breakpoint.logMessage)
}
return new xdebug.LineBreakpoint(fileUri, breakpoint.line)
}
})
Expand Down
98 changes: 98 additions & 0 deletions src/test/logpoint.ts
Original file line number Diff line number Diff line change
@@ -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<string> => {
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')
})
})
})
2 changes: 2 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
{
"exclude": ["node_modules", "out"],
"compilerOptions": {
"baseUrl": ".",
"paths": { "*": ["types/*"] },
"target": "es6",
"module": "commonjs",
"rootDir": "src",
Expand Down
9 changes: 9 additions & 0 deletions types/string-replace-async.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export = index
declare function index(
str: string,
re: RegExp | string,
replacer: (match: string, ...args: any[]) => Promise<string>
): string
declare namespace index {
function seq(str: string, re: RegExp | string, replacer: (match: string, ...args: any[]) => Promise<string>): string
}