From 98bd7823160e7fa39b92b2c72fa8ba503ffafbeb Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Thu, 13 Oct 2016 11:38:50 +0200 Subject: [PATCH 1/2] Add support for completionRequest --- README.md | 1 + src/phpDebug.ts | 100 ++++++++++++++++++++++++++++++++++++++++++++ src/test/adapter.ts | 8 ++++ 3 files changed, 109 insertions(+) diff --git a/README.md b/README.md index 4ee056c7..05bfee35 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,7 @@ Options specific to CLI debugging: - Stack traces, scope variables, superglobals, user defined constants - Arrays & objects (including classname, private and static properties) - Debug console + - Autocompletion in debug console for variables, array indexes, object properties (even nested) - Watches - Run as CLI - Run without debugging diff --git a/src/phpDebug.ts b/src/phpDebug.ts index ef4894f3..9a3ea104 100644 --- a/src/phpDebug.ts +++ b/src/phpDebug.ts @@ -993,6 +993,106 @@ class PhpDebugSession extends vscode.DebugSession { this.sendResponse(response) } + protected async completionsRequest(response: VSCodeDebugProtocol.CompletionsResponse, args: VSCodeDebugProtocol.CompletionsArguments) { + try { + if (!args.frameId) { + throw new Error('No stack frame given'); + } + const lineIndex: number = args.line ? args.line - 1 : 0; + const lines: string[] = args.text.split('\n'); + /** The text before the cursor */ + const typed: string = [...lines.slice(0, Math.max(lineIndex - 1, 0)), lines[lineIndex].substring(0, args.column)].join('\n'); + let i = typed.length; + let containerName: string; + let operator: string | undefined; + let query: string; + while (true) { + const substr = typed.substring(0, i); + if (/\[$/.test(substr)) { + // Numeric array index + operator = '['; + } else if (/\['$/.test(substr)) { + // String array index + operator = `['`; + } else if (/->$/.test(substr)) { + operator = '->'; + } else if (i > 0) { + i--; + continue; + } + query = typed.substr(i).toLowerCase(); + containerName = typed.substring(0, operator ? i - operator.length : i); + break; + } + const frame = this._stackFrames.get(args.frameId); + const contexts = await frame.getContexts(); + const targets: VSCodeDebugProtocol.CompletionItem[] = []; + if (!containerName || !operator) { + const responses = await Promise.all(contexts.map(context => context.getProperties())); + for (const properties of responses) { + for (const property of properties) { + if (property.name.toLowerCase().startsWith(query)) { + const text = property.name[0] === '$' ? property.name.substr(1) : property.name; + targets.push({label: property.name, text, type: 'variable', start: i, length: property.name.length}); + } + } + } + } else { + // Search all contexts + for (const context of contexts) { + let response: xdebug.PropertyGetResponse | undefined; + try { + response = await frame.connection.sendPropertyGetCommand({context, fullName: containerName}); + } catch (err) { + // ignore + } + if (response) { + for (const property of response.children) { + if (property.name.toLowerCase().startsWith(query)) { + let type: VSCodeDebugProtocol.CompletionItemType | undefined; + let text: string = property.name; + if (operator === '->') { + // Object + type = 'property'; + } else if (operator[0] === '[') { + // Array + if (parseInt(property.name) + '' === property.name) { + // Numeric index + if (operator[1] === `'`) { + continue; + } + type = 'value'; + text += ']'; + } else { + // String index + if (operator[1] !== `'`) { + if (query) { + continue; + } else { + text = `'` + text; + } + } + type = 'text'; + text += `']`; + } + } + targets.push({label: property.name, text, type, start: i, length: property.name.length }); + } + } + // If we found the variable in one context (typically Locals), abort + break; + } + } + } + response.body = {targets}; + } catch (err) { + this.sendErrorResponse(response, err); + return; + } + this.sendResponse(response); + } + + protected async continueRequest( response: VSCodeDebugProtocol.ContinueResponse, args: VSCodeDebugProtocol.ContinueArguments diff --git a/src/test/adapter.ts b/src/test/adapter.ts index ad05147f..1df7c226 100644 --- a/src/test/adapter.ts +++ b/src/test/adapter.ts @@ -678,6 +678,14 @@ describe('PHP Debug Adapter', () => { it('should return variable references for structured results') }) + describe('completion', () => { + it('should provide completion for local variables'); + it('should provide completion for superglobals'); + it('should provide completion for object properties'); + it('should provide completion for numeric array indexes'); + it('should provide completion for string array indexes'); + }); + describe.skip('output events', () => { const program = path.join(TEST_PROJECT, 'output.php') From 95e07645a97107a6a4d2557bb740ef6b677a55ce Mon Sep 17 00:00:00 2001 From: Damjan Cvetko Date: Fri, 21 Jan 2022 14:33:59 +0100 Subject: [PATCH 2/2] Rebase and fix code changes. --- README.md | 2 +- src/phpDebug.ts | 105 ++++++++++++++++++++++++---------------- src/test/adapter.ts | 12 ++--- src/xdebugConnection.ts | 12 ++--- 4 files changed, 75 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index 05bfee35..3746892c 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ Options specific to CLI debugging: - Stack traces, scope variables, superglobals, user defined constants - Arrays & objects (including classname, private and static properties) - Debug console - - Autocompletion in debug console for variables, array indexes, object properties (even nested) +- Autocompletion in debug console for variables, array indexes, object properties (even nested) - Watches - Run as CLI - Run without debugging diff --git a/src/phpDebug.ts b/src/phpDebug.ts index 9a3ea104..247d3d5a 100644 --- a/src/phpDebug.ts +++ b/src/phpDebug.ts @@ -195,6 +195,7 @@ class PhpDebugSession extends vscode.DebugSession { supportsFunctionBreakpoints: true, supportsLogPoints: true, supportsHitConditionalBreakpoints: true, + supportsCompletionsRequest: true, exceptionBreakpointFilters: [ { filter: 'Notice', @@ -993,106 +994,124 @@ class PhpDebugSession extends vscode.DebugSession { this.sendResponse(response) } - protected async completionsRequest(response: VSCodeDebugProtocol.CompletionsResponse, args: VSCodeDebugProtocol.CompletionsArguments) { + protected async completionsRequest( + response: VSCodeDebugProtocol.CompletionsResponse, + args: VSCodeDebugProtocol.CompletionsArguments + ) { try { if (!args.frameId) { - throw new Error('No stack frame given'); + throw new Error('No stack frame given') } - const lineIndex: number = args.line ? args.line - 1 : 0; - const lines: string[] = args.text.split('\n'); + const lineIndex: number = args.line ? args.line - 1 : 0 + const lines: string[] = args.text.split('\n') /** The text before the cursor */ - const typed: string = [...lines.slice(0, Math.max(lineIndex - 1, 0)), lines[lineIndex].substring(0, args.column)].join('\n'); - let i = typed.length; - let containerName: string; - let operator: string | undefined; - let query: string; + const typed: string = [ + ...lines.slice(0, Math.max(lineIndex - 1, 0)), + lines[lineIndex].substring(0, args.column), + ].join('\n') + let i = typed.length + let containerName: string + let operator: string | undefined + let query: string while (true) { - const substr = typed.substring(0, i); + const substr = typed.substring(0, i) if (/\[$/.test(substr)) { // Numeric array index - operator = '['; + operator = '[' } else if (/\['$/.test(substr)) { // String array index - operator = `['`; + operator = `['` } else if (/->$/.test(substr)) { - operator = '->'; + operator = '->' } else if (i > 0) { - i--; - continue; + i-- + continue } - query = typed.substr(i).toLowerCase(); - containerName = typed.substring(0, operator ? i - operator.length : i); - break; + query = typed.substr(i).toLowerCase() + containerName = typed.substring(0, operator ? i - operator.length : i) + break } - const frame = this._stackFrames.get(args.frameId); - const contexts = await frame.getContexts(); - const targets: VSCodeDebugProtocol.CompletionItem[] = []; + const frame = this._stackFrames.get(args.frameId)! + const contexts = await frame.getContexts() + const targets: VSCodeDebugProtocol.CompletionItem[] = [] if (!containerName || !operator) { - const responses = await Promise.all(contexts.map(context => context.getProperties())); + const responses = await Promise.all(contexts.map(context => context.getProperties())) for (const properties of responses) { for (const property of properties) { if (property.name.toLowerCase().startsWith(query)) { - const text = property.name[0] === '$' ? property.name.substr(1) : property.name; - targets.push({label: property.name, text, type: 'variable', start: i, length: property.name.length}); + const text = property.name[0] === '$' ? property.name.substr(1) : property.name + targets.push({ + label: property.name, + text, + type: 'variable', + //start: i, + length: property.name.length, + }) } } } } else { // Search all contexts for (const context of contexts) { - let response: xdebug.PropertyGetResponse | undefined; + let response: xdebug.PropertyGetResponse | undefined try { - response = await frame.connection.sendPropertyGetCommand({context, fullName: containerName}); + response = await frame.connection.sendPropertyGetCommand({ context, fullName: containerName }) } catch (err) { // ignore } if (response) { for (const property of response.children) { if (property.name.toLowerCase().startsWith(query)) { - let type: VSCodeDebugProtocol.CompletionItemType | undefined; - let text: string = property.name; + let type: VSCodeDebugProtocol.CompletionItemType | undefined + let text: string = property.name if (operator === '->') { // Object - type = 'property'; + type = 'property' } else if (operator[0] === '[') { // Array if (parseInt(property.name) + '' === property.name) { // Numeric index if (operator[1] === `'`) { - continue; + continue } - type = 'value'; - text += ']'; + type = 'value' + text += ']' } else { // String index if (operator[1] !== `'`) { if (query) { - continue; + continue } else { - text = `'` + text; + text = `'` + text } } - type = 'text'; - text += `']`; + type = 'text' + text += `']` } } - targets.push({label: property.name, text, type, start: i, length: property.name.length }); + targets.push({ + label: property.name, + text, + type, + //start: i, + length: property.name.length, + }) } } // If we found the variable in one context (typically Locals), abort - break; + break } } } - response.body = {targets}; + console.log(`completionsRequest ${args.text} (${args.column}:${args.line}) ${JSON.stringify(targets)}`) + response.body = { targets } } catch (err) { - this.sendErrorResponse(response, err); - return; + this.sendErrorResponse(response, err) + return } - this.sendResponse(response); + this.sendResponse(response) } - protected async continueRequest( response: VSCodeDebugProtocol.ContinueResponse, args: VSCodeDebugProtocol.ContinueArguments diff --git a/src/test/adapter.ts b/src/test/adapter.ts index 1df7c226..2d0001f0 100644 --- a/src/test/adapter.ts +++ b/src/test/adapter.ts @@ -679,12 +679,12 @@ describe('PHP Debug Adapter', () => { }) describe('completion', () => { - it('should provide completion for local variables'); - it('should provide completion for superglobals'); - it('should provide completion for object properties'); - it('should provide completion for numeric array indexes'); - it('should provide completion for string array indexes'); - }); + it('should provide completion for local variables') + it('should provide completion for superglobals') + it('should provide completion for object properties') + it('should provide completion for numeric array indexes') + it('should provide completion for string array indexes') + }) describe.skip('output events', () => { const program = path.join(TEST_PROJECT, 'output.php') diff --git a/src/xdebugConnection.ts b/src/xdebugConnection.ts index cd9b5a62..1e7e2b6e 100644 --- a/src/xdebugConnection.ts +++ b/src/xdebugConnection.ts @@ -632,12 +632,12 @@ export class PropertyGetResponse extends Response { children: Property[] /** * @param {XMLDocument} document - * @param {Property} property + * @param {Context} context */ - constructor(document: XMLDocument, property: Property) { - super(document, property.context.stackFrame.connection) + constructor(document: XMLDocument, context: Context) { + super(document, context.stackFrame.connection) this.children = Array.from(document.documentElement.firstChild!.childNodes).map( - (propertyNode: Element) => new Property(propertyNode, property.context) + (propertyNode: Element) => new Property(propertyNode, context) ) } } @@ -1022,14 +1022,14 @@ export class Connection extends DbgpConnection { } /** Sends a property_get command */ - public async sendPropertyGetCommand(property: Property): Promise { + public async sendPropertyGetCommand(property: {context: Context, fullName: string}): Promise { const escapedFullName = '"' + property.fullName.replace(/("|\\)/g, '\\$1') + '"' return new PropertyGetResponse( await this._enqueueCommand( 'property_get', `-d ${property.context.stackFrame.level} -c ${property.context.id} -n ${escapedFullName}` ), - property + property.context ) }