From c485164fefd65713381c43378596c23c065dbe9b Mon Sep 17 00:00:00 2001 From: Damjan Cvetko Date: Fri, 21 Jul 2023 23:31:53 +0200 Subject: [PATCH 1/2] feat: Add support for clipboard context using var_export --- src/phpDebug.ts | 12 ++++++++++++ src/test/adapter.ts | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/phpDebug.ts b/src/phpDebug.ts index f9aab57e..f7db36d8 100644 --- a/src/phpDebug.ts +++ b/src/phpDebug.ts @@ -241,6 +241,7 @@ class PhpDebugSession extends vscode.DebugSession { ], supportTerminateDebuggee: true, supportsDelayedStackTraceLoading: false, + supportsClipboardContext: true, } this.sendResponse(response) } @@ -1441,6 +1442,17 @@ class PhpDebugSession extends vscode.DebugSession { if (response.property) { result = response.property } + } else if (args.context === 'clipboard') { + const uuid = randomUUID() + await connection.sendEvalCommand(`$GLOBALS['eval_cache']['${uuid}']=var_export(${args.expression}, true)`) + const ctx = await stackFrame.getContexts() // TODO CACHE THIS + const res = await connection.sendPropertyGetNameCommand(`$eval_cache['${uuid}']`, ctx[1]) + if (res.property) { + // force a string response + response.body = { result: res.property.value, variablesReference: 0 } + this.sendResponse(response) + return + } } else { const response = await connection.sendEvalCommand(args.expression) if (response.result) { diff --git a/src/test/adapter.ts b/src/test/adapter.ts index 15b73ad9..5858a4ff 100644 --- a/src/test/adapter.ts +++ b/src/test/adapter.ts @@ -827,7 +827,7 @@ describe('PHP Debug Adapter', () => { await Promise.all([client.launch({ maxConnections: 1, log: true }), client.configurationSequence()]) const s1 = net.createConnection({ port: 9003 }) - await client.assertOutput('console', 'new connection 1 from ') + await client.assertOutput('console', 'new connection ') net.createConnection({ port: 9003 }) const o = await client.waitForEvent('output') assert.match( From 1773ad8f15d01bae0151884d7dfe1fe6ab7946f9 Mon Sep 17 00:00:00 2001 From: Damjan Cvetko Date: Sat, 22 Jul 2023 12:38:47 +0200 Subject: [PATCH 2/2] Use property_value to get clipboard evaluation, tests. --- src/phpDebug.ts | 16 +++++++------- src/test/adapter.ts | 46 +++++++++++++++++++++++++++++++++++++++++ src/xdebugConnection.ts | 39 ++++++++++++++++++++++++++++++++++ 3 files changed, 93 insertions(+), 8 deletions(-) diff --git a/src/phpDebug.ts b/src/phpDebug.ts index f7db36d8..0fc3e395 100644 --- a/src/phpDebug.ts +++ b/src/phpDebug.ts @@ -1444,15 +1444,15 @@ class PhpDebugSession extends vscode.DebugSession { } } else if (args.context === 'clipboard') { const uuid = randomUUID() - await connection.sendEvalCommand(`$GLOBALS['eval_cache']['${uuid}']=var_export(${args.expression}, true)`) + await connection.sendEvalCommand( + `$GLOBALS['eval_cache']['${uuid}']=var_export(${args.expression}, true)` + ) const ctx = await stackFrame.getContexts() // TODO CACHE THIS - const res = await connection.sendPropertyGetNameCommand(`$eval_cache['${uuid}']`, ctx[1]) - if (res.property) { - // force a string response - response.body = { result: res.property.value, variablesReference: 0 } - this.sendResponse(response) - return - } + const res = await connection.sendPropertyValueNameCommand(`$eval_cache['${uuid}']`, ctx[1]) + // force a string response + response.body = { result: res.value, variablesReference: 0 } + this.sendResponse(response) + return } else { const response = await connection.sendEvalCommand(args.expression) if (response.result) { diff --git a/src/test/adapter.ts b/src/test/adapter.ts index 5858a4ff..b8d1e211 100644 --- a/src/test/adapter.ts +++ b/src/test/adapter.ts @@ -810,6 +810,52 @@ describe('PHP Debug Adapter', () => { assert.deepEqual(vars.body.variables[0].name, '0') assert.deepEqual(vars.body.variables[0].value, '1') }) + it('should return the eval result for clipboard', async () => { + const program = path.join(TEST_PROJECT, 'variables.php') + + await client.launch({ + program, + }) + await client.setBreakpointsRequest({ source: { path: program }, breakpoints: [{ line: 19 }] }) + await client.configurationDoneRequest() + const { frame } = await assertStoppedLocation('breakpoint', program, 19) + + const response = ( + await client.evaluateRequest({ + context: 'clipboard', + frameId: frame.id, + expression: '$anInt', + }) + ).body + + assert.equal(response.result, '123') + assert.equal(response.variablesReference, 0) + + const response2 = ( + await client.evaluateRequest({ + context: 'clipboard', + frameId: frame.id, + expression: '$aString', + }) + ).body + + assert.equal(response2.result, "'123'") + assert.equal(response2.variablesReference, 0) + + const response3 = ( + await client.evaluateRequest({ + context: 'clipboard', + frameId: frame.id, + expression: '$anArray', + }) + ).body + + assert.equal( + response3.result, + "array (\n 0 => 1,\n 'test' => 2,\n 'test2' => \n array (\n 't' => 123,\n ),\n)" + ) + assert.equal(response3.variablesReference, 0) + }) }) describe.skip('output events', () => { diff --git a/src/xdebugConnection.ts b/src/xdebugConnection.ts index 53f648d4..c1f6c727 100644 --- a/src/xdebugConnection.ts +++ b/src/xdebugConnection.ts @@ -666,6 +666,33 @@ export class PropertyGetNameResponse extends Response { } } +/** The response to a property_value command */ +export class PropertyValueResponse extends Response { + /** the size of the value */ + size: number + /** the data type of the variable. Can be string, int, float, bool, array, object, uninitialized, null or resource */ + type: string + /** the value of the property for primitive types */ + value: string + constructor(document: XMLDocument, connection: Connection) { + super(document, connection) + if (document.documentElement.hasAttribute('size')) { + this.size = parseInt(document.documentElement.getAttribute('size') ?? '0') + } + this.type = document.documentElement.getAttribute('type') ?? '' + if (document.documentElement.getElementsByTagName('value').length > 0) { + this.value = decodeTag(document.documentElement, 'value') + } else { + const encoding = document.documentElement.getAttribute('encoding') + if (encoding) { + this.value = iconv.encode(document.documentElement.textContent!, encoding).toString() + } else { + this.value = document.documentElement.textContent! + } + } + } +} + /** class for properties returned from eval commands. These don't have a full name or an ID, but have all children already inlined. */ export class EvalResultProperty extends BaseProperty { children: EvalResultProperty[] @@ -1091,6 +1118,18 @@ export class Connection extends DbgpConnection { ) } + /** Sends a property_value by name command */ + public async sendPropertyValueNameCommand(name: string, context: Context): Promise { + const escapedFullName = '"' + name.replace(/("|\\)/g, '\\$1') + '"' + return new PropertyValueResponse( + await this._enqueueCommand( + 'property_value', + `-d ${context.stackFrame.level} -c ${context.id} -n ${escapedFullName}` + ), + context.stackFrame.connection + ) + } + /** Sends a property_set command */ public async sendPropertySetCommand(property: Property, value: string): Promise { return new Response(