Skip to content

Commit 8c82f65

Browse files
authored
feat: support clipboard context on variable copy operation (#917)
* feat: Add support for clipboard context using var_export * Use property_value to get clipboard evaluation, tests. * Testing property_value * Test for eval with stack depth. * Implement var_export in TS to format property * Revert eval with context/stackframe as this is not supported by Xdebug.
1 parent a89d294 commit 8c82f65

File tree

5 files changed

+171
-9
lines changed

5 files changed

+171
-9
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ 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.36.0]
8+
9+
- Implement copy to clipboard in var_export format
10+
711
## [1.35.0]
812

913
- Support for DBGp stream command

src/phpDebug.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { randomUUID } from 'crypto'
1919
import { getConfiguredEnvironment } from './envfile'
2020
import { XdebugCloudConnection } from './cloud'
2121
import { shouldIgnoreException } from './ignore'
22+
import { varExportProperty } from './varExport'
2223

2324
if (process.env['VSCODE_NLS_CONFIG']) {
2425
try {
@@ -247,6 +248,7 @@ class PhpDebugSession extends vscode.DebugSession {
247248
],
248249
supportTerminateDebuggee: true,
249250
supportsDelayedStackTraceLoading: false,
251+
supportsClipboardContext: true,
250252
}
251253
this.sendResponse(response)
252254
}
@@ -1469,22 +1471,28 @@ class PhpDebugSession extends vscode.DebugSession {
14691471
if (args.context === 'hover') {
14701472
// try to get variable from property_get
14711473
const ctx = await stackFrame.getContexts() // TODO CACHE THIS
1472-
const response = await connection.sendPropertyGetNameCommand(args.expression, ctx[0])
1473-
if (response.property) {
1474-
result = response.property
1474+
const res = await connection.sendPropertyGetNameCommand(args.expression, ctx[0])
1475+
if (res.property) {
1476+
result = res.property
14751477
}
14761478
} else if (args.context === 'repl') {
14771479
const uuid = randomUUID()
14781480
await connection.sendEvalCommand(`$GLOBALS['eval_cache']['${uuid}']=${args.expression}`)
14791481
const ctx = await stackFrame.getContexts() // TODO CACHE THIS
1480-
const response = await connection.sendPropertyGetNameCommand(`$eval_cache['${uuid}']`, ctx[1])
1481-
if (response.property) {
1482-
result = response.property
1482+
const res = await connection.sendPropertyGetNameCommand(`$eval_cache['${uuid}']`, ctx[1])
1483+
if (res.property) {
1484+
result = res.property
14831485
}
1486+
} else if (args.context === 'clipboard') {
1487+
const ctx = await stackFrame.getContexts() // TODO CACHE THIS
1488+
const res = await connection.sendPropertyGetNameCommand(args.expression, ctx[0])
1489+
response.body = { result: await varExportProperty(res.property), variablesReference: 0 }
1490+
this.sendResponse(response)
1491+
return
14841492
} else {
1485-
const response = await connection.sendEvalCommand(args.expression)
1486-
if (response.result) {
1487-
result = response.result
1493+
const res = await connection.sendEvalCommand(args.expression)
1494+
if (res.result) {
1495+
result = res.result
14881496
}
14891497
}
14901498

src/test/adapter.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -821,6 +821,52 @@ describe('PHP Debug Adapter', () => {
821821
assert.deepEqual(vars.body.variables[0].name, '0')
822822
assert.deepEqual(vars.body.variables[0].value, '1')
823823
})
824+
it('should return the eval result for clipboard', async () => {
825+
const program = path.join(TEST_PROJECT, 'variables.php')
826+
827+
await client.launch({
828+
program,
829+
})
830+
await client.setBreakpointsRequest({ source: { path: program }, breakpoints: [{ line: 19 }] })
831+
await client.configurationDoneRequest()
832+
const { frame } = await assertStoppedLocation('breakpoint', program, 19)
833+
834+
const response = (
835+
await client.evaluateRequest({
836+
context: 'clipboard',
837+
frameId: frame.id,
838+
expression: '$anInt',
839+
})
840+
).body
841+
842+
assert.equal(response.result, '123')
843+
assert.equal(response.variablesReference, 0)
844+
845+
const response2 = (
846+
await client.evaluateRequest({
847+
context: 'clipboard',
848+
frameId: frame.id,
849+
expression: '$aString',
850+
})
851+
).body
852+
853+
assert.equal(response2.result, "'123'")
854+
assert.equal(response2.variablesReference, 0)
855+
856+
const response3 = (
857+
await client.evaluateRequest({
858+
context: 'clipboard',
859+
frameId: frame.id,
860+
expression: '$anArray',
861+
})
862+
).body
863+
864+
assert.equal(
865+
response3.result,
866+
'array (\n 0 => 1,\n test => 2,\n test2 => \n array (\n t => 123,\n ),\n)'
867+
)
868+
assert.equal(response3.variablesReference, 0)
869+
})
824870
})
825871

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

src/varExport.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import * as xdebug from './xdebugConnection'
2+
3+
export async function varExportProperty(property: xdebug.Property, indent: string = ''): Promise<string> {
4+
if (indent.length >= 20) {
5+
// prevent infinite recursion
6+
return `...`
7+
}
8+
9+
let displayValue: string
10+
if (property.hasChildren || property.type === 'array' || property.type === 'object') {
11+
if (!property.children || property.children.length === 0) {
12+
// TODO: also take into account the number of children for pagination
13+
property.children = await property.getChildren()
14+
}
15+
displayValue = (
16+
await Promise.all(
17+
property.children.map(async property => {
18+
const indent2 = indent + ' '
19+
if (property.hasChildren) {
20+
return `${indent2}${property.name} => \n${indent2}${await varExportProperty(
21+
property,
22+
indent2
23+
)},`
24+
} else {
25+
return `${indent2}${property.name} => ${await varExportProperty(property, indent2)},`
26+
}
27+
})
28+
)
29+
).join('\n')
30+
31+
if (property.type === 'array') {
32+
// for arrays, show the length, like a var_dump would do
33+
displayValue = `array (\n${displayValue}\n${indent})`
34+
} else if (property.type === 'object' && property.class) {
35+
// for objects, show the class name as type (if specified)
36+
displayValue = `${property.class}::__set_state(array(\n${displayValue}\n${indent}))`
37+
} else {
38+
// edge case: show the type of the property as the value
39+
displayValue = `?${property.type}?(\n${displayValue})`
40+
}
41+
} else {
42+
// for null, uninitialized, resource, etc. show the type
43+
displayValue = property.value || property.type === 'string' ? property.value : property.type
44+
if (property.type === 'string') {
45+
// escaping ?
46+
if (property.size > property.value.length) {
47+
// get value
48+
const p2 = await property.context.stackFrame.connection.sendPropertyValueNameCommand(
49+
property.fullName,
50+
property.context
51+
)
52+
displayValue = p2.value
53+
}
54+
displayValue = `'${displayValue}'`
55+
} else if (property.type === 'bool') {
56+
displayValue = Boolean(parseInt(displayValue, 10)).toString()
57+
}
58+
}
59+
return displayValue
60+
}

src/xdebugConnection.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -525,6 +525,8 @@ export abstract class BaseProperty {
525525
hasChildren: boolean
526526
/** the number of children this property has, if any. Useful for showing array length. */
527527
numberOfChildren: number
528+
/** the size of the value */
529+
size: number
528530
/** the value of the property for primitive types */
529531
value: string
530532
/** children that were already included in the response */
@@ -550,6 +552,9 @@ export abstract class BaseProperty {
550552
if (propertyNode.hasAttribute('facet')) {
551553
this.facets = propertyNode.getAttribute('facet')!.split(' ')
552554
}
555+
if (propertyNode.hasAttribute('size')) {
556+
this.size = parseInt(propertyNode.getAttribute('size') ?? '0')
557+
}
553558
this.hasChildren = !!parseInt(propertyNode.getAttribute('children')!)
554559
if (this.hasChildren) {
555560
this.numberOfChildren = parseInt(propertyNode.getAttribute('numchildren')!)
@@ -684,6 +689,33 @@ export class PropertyGetNameResponse extends Response {
684689
}
685690
}
686691

692+
/** The response to a property_value command */
693+
export class PropertyValueResponse extends Response {
694+
/** the size of the value */
695+
size: number
696+
/** the data type of the variable. Can be string, int, float, bool, array, object, uninitialized, null or resource */
697+
type: string
698+
/** the value of the property for primitive types */
699+
value: string
700+
constructor(document: XMLDocument, connection: Connection) {
701+
super(document, connection)
702+
if (document.documentElement.hasAttribute('size')) {
703+
this.size = parseInt(document.documentElement.getAttribute('size') ?? '0')
704+
}
705+
this.type = document.documentElement.getAttribute('type') ?? ''
706+
if (document.documentElement.getElementsByTagName('value').length > 0) {
707+
this.value = decodeTag(document.documentElement, 'value')
708+
} else {
709+
const encoding = document.documentElement.getAttribute('encoding')
710+
if (encoding) {
711+
this.value = iconv.encode(document.documentElement.textContent!, encoding).toString()
712+
} else {
713+
this.value = document.documentElement.textContent!
714+
}
715+
}
716+
}
717+
}
718+
687719
/** class for properties returned from eval commands. These don't have a full name or an ID, but have all children already inlined. */
688720
export class EvalResultProperty extends BaseProperty {
689721
children: EvalResultProperty[]
@@ -1119,6 +1151,18 @@ export class Connection extends DbgpConnection {
11191151
)
11201152
}
11211153

1154+
/** Sends a property_value by name command and request full data */
1155+
public async sendPropertyValueNameCommand(name: string, context: Context): Promise<PropertyValueResponse> {
1156+
const escapedFullName = '"' + name.replace(/("|\\)/g, '\\$1') + '"'
1157+
return new PropertyValueResponse(
1158+
await this._enqueueCommand(
1159+
'property_value',
1160+
`-m 0 -d ${context.stackFrame.level} -c ${context.id} -n ${escapedFullName}`
1161+
),
1162+
context.stackFrame.connection
1163+
)
1164+
}
1165+
11221166
/** Sends a property_set command */
11231167
public async sendPropertySetCommand(property: Property, value: string): Promise<Response> {
11241168
return new Response(

0 commit comments

Comments
 (0)