Skip to content

Commit 8090a2c

Browse files
committed
Add support for completionRequest
1 parent 786a6fe commit 8090a2c

File tree

4 files changed

+111
-2
lines changed

4 files changed

+111
-2
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ Features
6666
- Stack traces, scope variables, superglobals, user defined constants
6767
- Arrays & objects (including classname, private and static properties)
6868
- Debug console
69+
- Autocompletion in debug console for variables, array indexes, object properties (even nested)
6970
- Watches
7071
- Run as CLI
7172
- Run without debugging

src/phpDebug.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ class PhpDebugSession extends vscode.DebugSession {
143143
supportsEvaluateForHovers: false,
144144
supportsConditionalBreakpoints: true,
145145
supportsFunctionBreakpoints: true,
146+
supportsCompletionsRequest: true,
146147
exceptionBreakpointFilters: [
147148
{
148149
filter: 'Notice',
@@ -706,6 +707,105 @@ class PhpDebugSession extends vscode.DebugSession {
706707
this.sendResponse(response);
707708
}
708709

710+
protected async completionsRequest(response: VSCodeDebugProtocol.CompletionsResponse, args: VSCodeDebugProtocol.CompletionsArguments) {
711+
try {
712+
if (!args.frameId) {
713+
throw new Error('No stack frame given');
714+
}
715+
const lineIndex: number = args.line ? args.line - 1 : 0;
716+
const lines: string[] = args.text.split('\n');
717+
/** The text before the cursor */
718+
const typed: string = [...lines.slice(0, Math.max(lineIndex - 1, 0)), lines[lineIndex].substring(0, args.column)].join('\n');
719+
let i = typed.length;
720+
let containerName: string;
721+
let operator: string | undefined;
722+
let query: string;
723+
while (true) {
724+
const substr = typed.substring(0, i);
725+
if (/\[$/.test(substr)) {
726+
// Numeric array index
727+
operator = '[';
728+
} else if (/\['$/.test(substr)) {
729+
// String array index
730+
operator = `['`;
731+
} else if (/->$/.test(substr)) {
732+
operator = '->';
733+
} else if (i > 0) {
734+
i--;
735+
continue;
736+
}
737+
query = typed.substr(i).toLowerCase();
738+
containerName = typed.substring(0, operator ? i - operator.length : i);
739+
break;
740+
}
741+
const frame = this._stackFrames.get(args.frameId);
742+
const contexts = await frame.getContexts();
743+
const targets: VSCodeDebugProtocol.CompletionItem[] = [];
744+
if (!containerName || !operator) {
745+
const responses = await Promise.all(contexts.map(context => context.getProperties()));
746+
for (const properties of responses) {
747+
for (const property of properties) {
748+
if (property.name.toLowerCase().startsWith(query)) {
749+
const text = property.name[0] === '$' ? property.name.substr(1) : property.name;
750+
targets.push({label: property.name, text, type: 'variable', start: i, length: property.name.length});
751+
}
752+
}
753+
}
754+
} else {
755+
// Search all contexts
756+
for (const context of contexts) {
757+
let response: xdebug.PropertyGetResponse | undefined;
758+
try {
759+
response = await frame.connection.sendPropertyGetCommand({context, fullName: containerName});
760+
} catch (err) {
761+
// ignore
762+
}
763+
if (response) {
764+
for (const property of response.children) {
765+
if (property.name.toLowerCase().startsWith(query)) {
766+
let type: VSCodeDebugProtocol.CompletionItemType | undefined;
767+
let text: string = property.name;
768+
if (operator === '->') {
769+
// Object
770+
type = 'property';
771+
} else if (operator[0] === '[') {
772+
// Array
773+
if (parseInt(property.name) + '' === property.name) {
774+
// Numeric index
775+
if (operator[1] === `'`) {
776+
continue;
777+
}
778+
type = 'value';
779+
text += ']';
780+
} else {
781+
// String index
782+
if (operator[1] !== `'`) {
783+
if (query) {
784+
continue;
785+
} else {
786+
text = `'` + text;
787+
}
788+
}
789+
type = 'text';
790+
text += `']`;
791+
}
792+
}
793+
targets.push({label: property.name, text, type, start: i, length: property.name.length });
794+
}
795+
}
796+
// If we found the variable in one context (typically Locals), abort
797+
break;
798+
}
799+
}
800+
}
801+
response.body = {targets};
802+
} catch (err) {
803+
this.sendErrorResponse(response, err);
804+
return;
805+
}
806+
this.sendResponse(response);
807+
}
808+
709809
protected async continueRequest(response: VSCodeDebugProtocol.ContinueResponse, args: VSCodeDebugProtocol.ContinueArguments) {
710810
let xdebugResponse: xdebug.StatusResponse | undefined;
711811
try {

src/test/adapter.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,14 @@ describe('PHP Debug Adapter', () => {
496496
it('should return variable references for structured results');
497497
});
498498

499+
describe('completion', () => {
500+
it('should provide completion for local variables');
501+
it('should provide completion for superglobals');
502+
it('should provide completion for object properties');
503+
it('should provide completion for numeric array indexes');
504+
it('should provide completion for string array indexes');
505+
});
506+
499507
describe.skip('output events', () => {
500508

501509
const program = path.join(TEST_PROJECT, 'output.php');

src/xdebugConnection.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -476,7 +476,7 @@ export class PropertyGetResponse extends Response {
476476
* @param {XMLDocument} document
477477
* @param {Property} property
478478
*/
479-
constructor(document: XMLDocument, property: Property) {
479+
constructor(document: XMLDocument, property: {context: Context}) {
480480
super(document, property.context.stackFrame.connection);
481481
this.children = Array.from(document.documentElement.firstChild.childNodes).map((propertyNode: Element) => new Property(propertyNode, property.context));
482482
}
@@ -782,7 +782,7 @@ export class Connection extends DbgpConnection {
782782
}
783783

784784
/** Sends a property_get command */
785-
public async sendPropertyGetCommand(property: Property): Promise<PropertyGetResponse> {
785+
public async sendPropertyGetCommand(property: {context: Context, fullName: string}): Promise<PropertyGetResponse> {
786786
const escapedFullName = '"' + property.fullName.replace(/("|\\)/g, '\\$1') + '"';
787787
return new PropertyGetResponse(await this._enqueueCommand('property_get', `-d ${property.context.stackFrame.level} -c ${property.context.id} -n ${escapedFullName}`), property);
788788
}

0 commit comments

Comments
 (0)