Skip to content

Commit b81d260

Browse files
committed
feat: improve hover support (#689)
* Implement basic regex EvaluatableExpressionProvider * Ignoring codecov.yml in vscodeignore. * Code style. * Change features test. * Evaluate hover by property_get instead of eval. Add Property Get by name. * Additional tests for evaluation (hover). * Changelog.
1 parent f6be72f commit b81d260

File tree

6 files changed

+124
-12
lines changed

6 files changed

+124
-12
lines changed

.vscodeignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,4 @@ tsconfig.json
2525
tslint.json
2626
renovate.json
2727
.github
28+
codecov.yml

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
88

99
- When `env` is specified in launch configuration it will be merged the process environment.
1010
- Set variable support.
11+
- Improved hover support
1112

1213
## [1.22.0]
1314

src/extension.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,26 @@ export function activate(context: vscode.ExtensionContext) {
7070
},
7171
})
7272
)
73+
context.subscriptions.push(
74+
vscode.languages.registerEvaluatableExpressionProvider('php', {
75+
async provideEvaluatableExpression(
76+
document: vscode.TextDocument,
77+
position: vscode.Position,
78+
token: CancellationToken
79+
): Promise<ProviderResult<vscode.EvaluatableExpression>> {
80+
// see https://www.php.net/manual/en/language.variables.basics.php
81+
// const wordRange = document.getWordRangeAtPosition(position, /\$([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)((->(?1))|\[(\d+|'[^']+'|"[^"]+"|(?0))\])*/)
82+
const wordRange = document.getWordRangeAtPosition(
83+
position,
84+
/\$[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*(->[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)*/
85+
)
86+
if (wordRange) {
87+
return new vscode.EvaluatableExpression(wordRange)
88+
}
89+
return undefined // nothing evaluatable found under mouse
90+
},
91+
})
92+
)
7393

7494
context.subscriptions.push(
7595
vscode.commands.registerCommand('php.debug.debugPhpFile', async (uri: vscode.Uri) => {

src/phpDebug.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ class PhpDebugSession extends vscode.DebugSession {
190190
): void {
191191
response.body = {
192192
supportsConfigurationDoneRequest: true,
193-
supportsEvaluateForHovers: false,
193+
supportsEvaluateForHovers: true,
194194
supportsConditionalBreakpoints: true,
195195
supportsFunctionBreakpoints: true,
196196
supportsLogPoints: true,
@@ -1197,15 +1197,34 @@ class PhpDebugSession extends vscode.DebugSession {
11971197
if (!this._stackFrames.has(args.frameId)) {
11981198
throw new Error(`Unknown frameId ${args.frameId}`)
11991199
}
1200-
const connection = this._stackFrames.get(args.frameId)!.connection
1201-
const { result } = await connection.sendEvalCommand(args.expression)
1200+
const stackFrame = this._stackFrames.get(args.frameId)!
1201+
const connection = stackFrame.connection
1202+
let result: xdebug.BaseProperty | null = null
1203+
if (args.context === 'hover') {
1204+
// try to get variable from property_get
1205+
const ctx = await stackFrame.getContexts() // TODO CACHE THIS
1206+
const response = await connection.sendPropertyGetNameCommand(args.expression, ctx[0])
1207+
if (response.property) {
1208+
result = response.property
1209+
}
1210+
} else {
1211+
const response = await connection.sendEvalCommand(args.expression)
1212+
if (response.result) {
1213+
result = response.result
1214+
}
1215+
}
1216+
12021217
if (result) {
12031218
const displayValue = formatPropertyValue(result)
12041219
let variablesReference: number
12051220
// if the property has children, generate a variable ID and save the property (including children) so VS Code can request them
12061221
if (result.hasChildren || result.type === 'array' || result.type === 'object') {
12071222
variablesReference = this._variableIdCounter++
1208-
this._evalResultProperties.set(variablesReference, result)
1223+
if (result instanceof xdebug.Property) {
1224+
this._properties.set(variablesReference, result)
1225+
} else {
1226+
this._evalResultProperties.set(variablesReference, result)
1227+
}
12091228
} else {
12101229
variablesReference = 0
12111230
}

src/test/adapter.ts

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ describe('PHP Debug Adapter', () => {
2525
it('should return supported features', async () => {
2626
const response = await client.initializeRequest()
2727
assert.equal(response.body!.supportsConfigurationDoneRequest, true)
28-
assert.equal(response.body!.supportsEvaluateForHovers, false)
28+
assert.equal(response.body!.supportsEvaluateForHovers, true)
2929
assert.equal(response.body!.supportsConditionalBreakpoints, true)
3030
assert.equal(response.body!.supportsFunctionBreakpoints, true)
3131
})
@@ -740,8 +740,53 @@ describe('PHP Debug Adapter', () => {
740740
})
741741

742742
describe('evaluation', () => {
743-
it('should return the eval result')
744-
it('should return variable references for structured results')
743+
it('should return the eval result', async () => {
744+
const program = path.join(TEST_PROJECT, 'variables.php')
745+
746+
client.launch({
747+
program,
748+
})
749+
await client.waitForEvent('initialized')
750+
await client.setBreakpointsRequest({ source: { path: program }, breakpoints: [{ line: 19 }] })
751+
await client.configurationDoneRequest()
752+
const { frame } = await assertStoppedLocation('breakpoint', program, 19)
753+
754+
const response = (
755+
await client.evaluateRequest({
756+
context: 'hover',
757+
frameId: frame.id,
758+
expression: '$anInt',
759+
})
760+
).body
761+
762+
assert.equal(response.result, '123')
763+
assert.equal(response.variablesReference, 0)
764+
})
765+
it('should return variable references for structured results', async () => {
766+
const program = path.join(TEST_PROJECT, 'variables.php')
767+
768+
client.launch({
769+
program,
770+
})
771+
await client.waitForEvent('initialized')
772+
await client.setBreakpointsRequest({ source: { path: program }, breakpoints: [{ line: 19 }] })
773+
await client.configurationDoneRequest()
774+
const { frame } = await assertStoppedLocation('breakpoint', program, 19)
775+
776+
const response = (
777+
await client.evaluateRequest({
778+
context: 'hover',
779+
frameId: frame.id,
780+
expression: '$anArray',
781+
})
782+
).body
783+
784+
assert.equal(response.result, 'array(3)')
785+
assert.notEqual(response.variablesReference, 0)
786+
const vars = await client.variablesRequest({ variablesReference: response.variablesReference })
787+
assert.deepEqual(vars.body.variables[0].name, '0')
788+
assert.deepEqual(vars.body.variables[0].value, '1')
789+
})
745790
})
746791

747792
describe.skip('output events', () => {

src/xdebugConnection.ts

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -640,16 +640,30 @@ export class PropertyGetResponse extends Response {
640640
children: Property[]
641641
/**
642642
* @param {XMLDocument} document
643-
* @param {Property} property
643+
* @param {Context} context
644644
*/
645-
constructor(document: XMLDocument, property: Property) {
646-
super(document, property.context.stackFrame.connection)
645+
constructor(document: XMLDocument, context: Context) {
646+
super(document, context.stackFrame.connection)
647647
this.children = Array.from(document.documentElement.firstChild!.childNodes).map(
648-
(propertyNode: Element) => new Property(propertyNode, property.context)
648+
(propertyNode: Element) => new Property(propertyNode, context)
649649
)
650650
}
651651
}
652652

653+
/** The response to a property_get by name command */
654+
export class PropertyGetNameResponse extends Response {
655+
/** The property being resolved */
656+
property: Property
657+
/**
658+
* @param {XMLDocument} document
659+
* @param {Context} context
660+
*/
661+
constructor(document: XMLDocument, context: Context) {
662+
super(document, context.stackFrame.connection)
663+
this.property = new Property(<Element>document.documentElement.firstChild!, context)
664+
}
665+
}
666+
653667
/** class for properties returned from eval commands. These don't have a full name or an ID, but have all children already inlined. */
654668
export class EvalResultProperty extends BaseProperty {
655669
children: EvalResultProperty[]
@@ -1045,7 +1059,19 @@ export class Connection extends DbgpConnection {
10451059
'property_get',
10461060
`-d ${property.context.stackFrame.level} -c ${property.context.id} -n ${escape(property.fullName)}`
10471061
),
1048-
property
1062+
property.context
1063+
)
1064+
}
1065+
1066+
/** Sends a property_get by name command */
1067+
public async sendPropertyGetNameCommand(name: String, context: Context): Promise<PropertyGetNameResponse> {
1068+
const escapedFullName = '"' + name.replace(/("|\\)/g, '\\$1') + '"'
1069+
return new PropertyGetNameResponse(
1070+
await this._enqueueCommand(
1071+
'property_get',
1072+
`-d ${context.stackFrame.level} -c ${context.id} -n ${escapedFullName}`
1073+
),
1074+
context
10491075
)
10501076
}
10511077

0 commit comments

Comments
 (0)