Skip to content

Commit 573a6aa

Browse files
committed
feat: Implement additional evaluate contexts and extend variables and watch context menus
1 parent bcb2ba1 commit 573a6aa

File tree

4 files changed

+176
-6
lines changed

4 files changed

+176
-6
lines changed

package.json

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,30 @@
568568
"command": "extension.php-debug.runEditorContents",
569569
"when": "resourceLangId == php && !inDiffEditor && resourceScheme == file"
570570
}
571+
],
572+
"debug/variables/context": [
573+
{
574+
"command": "extension.php-debug.copyVarExport",
575+
"group": "5_cutcopypaste@10",
576+
"when": "debugType == php"
577+
},
578+
{
579+
"command": "extension.php-debug.copyJson",
580+
"group": "5_cutcopypaste@11",
581+
"when": "debugType == php"
582+
}
583+
],
584+
"debug/watch/context": [
585+
{
586+
"command": "extension.php-debug.copyVarExport",
587+
"group": "3_modification@51",
588+
"when": "debugType == php"
589+
},
590+
{
591+
"command": "extension.php-debug.copyJson",
592+
"group": "3_modification@52",
593+
"when": "debugType == php"
594+
}
571595
]
572596
},
573597
"commands": [
@@ -589,6 +613,14 @@
589613
"category": "PHP Debug",
590614
"enablement": "!inDebugMode",
591615
"icon": "$(play)"
616+
},
617+
{
618+
"command": "extension.php-debug.copyVarExport",
619+
"title": "Copy Value as var_export"
620+
},
621+
{
622+
"command": "extension.php-debug.copyJson",
623+
"title": "Copy Value as json_encode"
592624
}
593625
],
594626
"keybindings": [

src/extension.ts

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import * as vscode from 'vscode'
22
import { WorkspaceFolder, DebugConfiguration, CancellationToken } from 'vscode'
3-
import { LaunchRequestArguments } from './phpDebug'
3+
import { EvaluateExtendedArguments, LaunchRequestArguments } from './phpDebug'
44
import * as which from 'which'
55
import * as path from 'path'
6+
import { DebugProtocol } from '@vscode/debugprotocol'
67

78
export function activate(context: vscode.ExtensionContext) {
89
context.subscriptions.push(
@@ -143,4 +144,49 @@ export function activate(context: vscode.ExtensionContext) {
143144
})
144145
})
145146
)
147+
148+
/* This is coppied from vscode/src/vs/workbench/contrib/debug/browser/variablesView.ts */
149+
interface IVariablesContext {
150+
sessionId: string | undefined
151+
container: DebugProtocol.Variable | DebugProtocol.Scope | DebugProtocol.EvaluateArguments
152+
variable: DebugProtocol.Variable
153+
}
154+
155+
/* This is coppied from @vscode/debugprotocol/lib/debugProtocol.d.ts because customRequest returns the body of the response and not the response itself */
156+
interface EvaluateResponse {
157+
/** The result of the evaluate request. */
158+
result: string
159+
}
160+
161+
const copyVar = async (arg: IVariablesContext, context: string) => {
162+
const aci = vscode.debug.activeStackItem
163+
if (aci && aci instanceof vscode.DebugStackFrame) {
164+
const ret = (await vscode.debug.activeDebugSession?.customRequest('evaluate', <EvaluateExtendedArguments>{
165+
context,
166+
expression: arg.variable.evaluateName,
167+
frameId: aci.frameId,
168+
variablesReference: arg.variable.variablesReference,
169+
})) as EvaluateResponse
170+
await vscode.env.clipboard.writeText(ret.result)
171+
} else {
172+
await vscode.window.showErrorMessage('Cannot derermine active debug session')
173+
}
174+
}
175+
176+
context.subscriptions.push(
177+
vscode.commands.registerCommand(
178+
'extension.php-debug.copyVarExport',
179+
async (arg: IVariablesContext, p2: any, p3: any) => {
180+
await copyVar(arg, 'clipboard-var_export')
181+
}
182+
)
183+
)
184+
context.subscriptions.push(
185+
vscode.commands.registerCommand(
186+
'extension.php-debug.copyJson',
187+
async (arg: IVariablesContext, p2: any, p3: any) => {
188+
await copyVar(arg, 'clipboard-json')
189+
}
190+
)
191+
)
146192
}

src/phpDebug.ts

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { XdebugCloudConnection } from './cloud'
2020
import { shouldIgnoreException } from './ignore'
2121
import { varExportProperty } from './varExport'
2222
import { supportedEngine } from './xdebugUtils'
23+
import { varJsonProperty } from './varJson'
2324

2425
if (process.env['VSCODE_NLS_CONFIG']) {
2526
try {
@@ -56,6 +57,11 @@ function formatPropertyValue(property: xdebug.BaseProperty): string {
5657
return displayValue
5758
}
5859

60+
export interface EvaluateExtendedArguments extends VSCodeDebugProtocol.EvaluateArguments {
61+
/** The variable for which to retrieve its children. The `variablesReference` must have been obtained in the current suspended state. See 'Lifetime of Object References' in the Overview section for details. */
62+
variablesReference?: number
63+
}
64+
5965
/**
6066
* This interface should always match the schema found in the mock-debug extension manifest.
6167
*/
@@ -1496,9 +1502,18 @@ class PhpDebugSession extends vscode.DebugSession {
14961502
this.shutdown()
14971503
}
14981504

1505+
private getPropertyFromReference(variablesReference?: number): xdebug.Property | undefined {
1506+
if (variablesReference && this._properties.has(variablesReference)) {
1507+
return this._properties.get(variablesReference)!
1508+
} /*else if (variablesReference && this._evalResultProperties.has(variablesReference)) {
1509+
return this._evalResultProperties.get(variablesReference)!
1510+
}*/
1511+
return
1512+
}
1513+
14991514
protected async evaluateRequest(
15001515
response: VSCodeDebugProtocol.EvaluateResponse,
1501-
args: VSCodeDebugProtocol.EvaluateArguments
1516+
args: EvaluateExtendedArguments
15021517
): Promise<void> {
15031518
try {
15041519
if (!args.frameId) {
@@ -1525,10 +1540,26 @@ class PhpDebugSession extends vscode.DebugSession {
15251540
if (res.property) {
15261541
result = res.property
15271542
}
1528-
} else if (args.context === 'clipboard') {
1529-
const ctx = await stackFrame.getContexts() // TODO CACHE THIS
1530-
const res = await connection.sendPropertyGetNameCommand(args.expression, ctx[0])
1531-
response.body = { result: await varExportProperty(res.property), variablesReference: 0 }
1543+
} else if (args.context === 'clipboard-var_export') {
1544+
const property =
1545+
this.getPropertyFromReference(args.variablesReference) ??
1546+
(await (async () => {
1547+
const ctx = await stackFrame.getContexts() // TODO CACHE THIS
1548+
const res = await connection.sendPropertyGetNameCommand(args.expression, ctx[0])
1549+
return res.property
1550+
})())
1551+
response.body = { result: await varExportProperty(property), variablesReference: 0 }
1552+
this.sendResponse(response)
1553+
return
1554+
} else if (args.context === 'clipboard-json') {
1555+
const property =
1556+
this.getPropertyFromReference(args.variablesReference) ??
1557+
(await (async () => {
1558+
const ctx = await stackFrame.getContexts() // TODO CACHE THIS
1559+
const res = await connection.sendPropertyGetNameCommand(args.expression, ctx[0])
1560+
return res.property
1561+
})())
1562+
response.body = { result: await varJsonProperty(property), variablesReference: 0 }
15321563
this.sendResponse(response)
15331564
return
15341565
} else if (args.context === 'watch') {

src/varJson.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { randomUUID } from 'crypto'
2+
import * as xdebug from './xdebugConnection'
3+
4+
const recursionMagic = `---MAGIC---${randomUUID()}---MAGIC---`
5+
6+
/**
7+
* Generate a JSON object and pretty print it
8+
*/
9+
export async function varJsonProperty(property: xdebug.Property): Promise<string> {
10+
const obj = await _varJsonProperty(property)
11+
const json = JSON.stringify(obj, null, ' ')
12+
return json.replace(new RegExp(`"${recursionMagic}"`, 'g'), '...')
13+
}
14+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
15+
async function _varJsonProperty(property: xdebug.Property, depth: number = 0): Promise<any> {
16+
if (depth >= 20) {
17+
// prevent infinite recursion
18+
return recursionMagic
19+
}
20+
21+
let displayValue: string
22+
if (property.hasChildren || property.type === 'array' || property.type === 'object') {
23+
if (!property.children || property.children.length === 0) {
24+
// TODO: also take into account the number of children for pagination
25+
property.children = await property.getChildren()
26+
}
27+
28+
const obj = await Promise.all(
29+
property.children.map(async property => {
30+
return [property.name, <object>await _varJsonProperty(property, depth + 1)]
31+
})
32+
)
33+
34+
// TODO: handle only numeric, sequential arrays?
35+
36+
return <object>Object.fromEntries(obj)
37+
} else {
38+
// for null, uninitialized, resource, etc. show the type
39+
displayValue = property.value || property.type === 'string' ? property.value : property.type
40+
if (property.type === 'string') {
41+
// escaping ?
42+
if (property.size > property.value.length) {
43+
// get value
44+
const p2 = await property.context.stackFrame.connection.sendPropertyValueNameCommand(
45+
property.fullName,
46+
property.context
47+
)
48+
displayValue = p2.value
49+
}
50+
return displayValue
51+
} else if (property.type === 'bool') {
52+
return Boolean(parseInt(displayValue, 10))
53+
} else if (property.type === 'int') {
54+
return parseInt(displayValue, 10)
55+
} else if (property.type === 'float' || property.type === 'double') {
56+
return parseFloat(displayValue)
57+
} else {
58+
return property.value
59+
}
60+
}
61+
}

0 commit comments

Comments
 (0)