Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for completionRequest #71

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
119 changes: 119 additions & 0 deletions src/phpDebug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ class PhpDebugSession extends vscode.DebugSession {
supportsFunctionBreakpoints: true,
supportsLogPoints: true,
supportsHitConditionalBreakpoints: true,
supportsCompletionsRequest: true,
exceptionBreakpointFilters: [
{
filter: 'Notice',
Expand Down Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions src/test/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand Down
12 changes: 6 additions & 6 deletions src/xdebugConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
)
}
}
Expand Down Expand Up @@ -1022,14 +1022,14 @@ export class Connection extends DbgpConnection {
}

/** Sends a property_get command */
public async sendPropertyGetCommand(property: Property): Promise<PropertyGetResponse> {
public async sendPropertyGetCommand(property: {context: Context, fullName: string}): Promise<PropertyGetResponse> {
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
)
}

Expand Down