diff --git a/README.md b/README.md index 4ee056c7..3746892c 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..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,6 +994,124 @@ 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 + } + } + } + console.log(`completionsRequest ${args.text} (${args.column}:${args.line}) ${JSON.stringify(targets)}`) + 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..2d0001f0 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') 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 ) }