Skip to content

Commit 9982051

Browse files
h9jianggopherbot
authored andcommitted
extension/src/language: create resolve/execute command middleware
Similar to CL 781221, this CL introduces middlewares for interactive execute comman and interactive resolve command requests. The "interactivity" logic is moved from the middleware to the constructor of the interactive language client. The constructor memorize the user provided execute command middleware and replace that with a different middleware allowing the client to resolve command interactively. Once resolved, the resolved command is being handled over to "interactive execute command" or "regular execute command" based on whether any answers is provided by the user. Example & validation: CL 781721 For golang/go#76331 Change-Id: I7a29adb7c5b52cf34d71007d99567727388c32b7 Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/781720 Auto-Submit: Hongxiang Jiang <hxjiang@golang.org> LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Madeline Kalil <mkalil@google.com>
1 parent 783b9ec commit 9982051

2 files changed

Lines changed: 189 additions & 72 deletions

File tree

extension/src/language/form.ts

Lines changed: 172 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import * as vscode from 'vscode';
88
import { InitializeParams } from 'vscode-languageserver-protocol';
99
import {
10+
ExecuteCommandSignature,
1011
LanguageClient,
1112
RequestType,
1213
ServerOptions,
@@ -305,7 +306,53 @@ export interface InteractiveLanguageClientOptions extends LanguageClientOptions
305306
* interactive dialog, including the command resolution phase, the execution of
306307
* the validated command, and dynamic enumeration requests for lazy-loaded options.
307308
*/
308-
export type InteractiveMiddleware = Middleware & InteractiveListEnumMiddleware;
309+
export type InteractiveMiddleware = Middleware &
310+
InteractiveListEnumMiddleware &
311+
InteractiveExecuteCommandMiddleware &
312+
InteractiveResolveCommandMiddleware;
313+
314+
export interface InteractiveResolveCommandSignature {
315+
(this: void, param: InteractiveExecuteCommandParams): vscode.ProviderResult<InteractiveExecuteCommandParams>;
316+
}
317+
318+
export interface InteractiveResolveCommandMiddleware {
319+
interactiveResolveCommand?: (
320+
this: void,
321+
param: InteractiveExecuteCommandParams,
322+
next: InteractiveResolveCommandSignature
323+
) => vscode.ProviderResult<InteractiveExecuteCommandParams>;
324+
}
325+
326+
/**
327+
* Signature for the command execution handler with user form answers.
328+
*
329+
* This signature is distinct from standard `ExecuteCommandSignature` because it
330+
* accepts an additional `formAnswers` argument containing user-provided,
331+
* server-validated inputs.
332+
*/
333+
export interface InteractiveExecuteCommandSignature {
334+
(this: void, command: string, args: any[], formAnswers: any[]): vscode.ProviderResult<any>;
335+
}
336+
337+
/**
338+
* InteractiveExecuteCommandMiddleware allows middleware implementations to
339+
* intercept the execution of commands that require user-provided form answers.
340+
*
341+
* Note: This middleware is defined as a new, separate hook rather than reusing
342+
* the standard `executeCommand` middleware. Reusing the standard middleware
343+
* would require modifying its signature to accept the additional `formAnswers`
344+
* parameter, which would break backward compatibility for existing extension
345+
* middleware configurations.
346+
*/
347+
export interface InteractiveExecuteCommandMiddleware {
348+
interactiveExecuteCommand?: (
349+
this: void,
350+
command: string,
351+
args: any[],
352+
formAnswers: any[],
353+
next: InteractiveExecuteCommandSignature
354+
) => vscode.ProviderResult<any>;
355+
}
309356

310357
export interface InteractiveListEnumSignature {
311358
(this: void, param: InteractiveListEnumParams): vscode.ProviderResult<FormEnumEntry[]>;
@@ -328,6 +375,54 @@ export class InteractiveLanguageClient extends LanguageClient {
328375
forceDebug?: boolean
329376
) {
330377
super(id, name, serverOptions, clientOptions, forceDebug);
378+
379+
const interactiveOptions = this.clientOptions as InteractiveLanguageClientOptions;
380+
const middleware = interactiveOptions.middleware;
381+
382+
// Memorize the language client author defined "executeCommand" middleware.
383+
const original = middleware?.executeCommand;
384+
385+
// Intercept standard command execution to resolve required inputs interactively.
386+
// Once resolved, route the execution to the appropriate middleware:
387+
// - If the command required user answers, execute it using the
388+
// "interactiveExecuteCommand" middleware.
389+
// - If no user answers were collected, execute it using the standard
390+
// "executeCommand" middleware.
391+
const overwrite = async (cmd: string, args: any[], next: ExecuteCommandSignature) => {
392+
const supported = this.initializeResult?.capabilities?.experimental?.interactiveResolveProvider;
393+
394+
// Language server does not support interactive command execution.
395+
if (!Array.isArray(supported) || !supported.includes('command')) {
396+
return original ? original(cmd, args, next) : next(cmd, args);
397+
}
398+
399+
const resolved = await this.resolveCommandInteractively({
400+
command: cmd,
401+
arguments: args
402+
} as InteractiveExecuteCommandParams);
403+
if (!resolved) {
404+
return undefined;
405+
}
406+
407+
cmd = resolved.command;
408+
args = resolved.arguments || [];
409+
const formAnswers = resolved.formAnswers;
410+
411+
if (formAnswers === undefined || formAnswers.length === 0) {
412+
// Execute the vscode language client provided "workspace/executeCommand"
413+
// method if the user does not provide any answers.
414+
return original ? original(cmd, args, next) : next(cmd, args);
415+
} else {
416+
// Execute the interactive language client provided "workspace/executeCommand"
417+
// method if the user does provide answers.
418+
return this.interactiveExecuteCommand(cmd, args, formAnswers);
419+
}
420+
};
421+
422+
interactiveOptions.middleware = {
423+
...middleware,
424+
executeCommand: overwrite
425+
};
331426
}
332427

333428
/**
@@ -358,15 +453,10 @@ export class InteractiveLanguageClient extends LanguageClient {
358453
async resolveCommandInteractively(
359454
param: InteractiveExecuteCommandParams
360455
): Promise<InteractiveExecuteCommandParams | undefined> {
361-
// Avoid resolving for frequently triggered commands for performance.
362-
if (param.command === 'gopls.package_symbols') {
363-
return param;
364-
}
365-
366456
// Invoke "command/resolve" at least once to ensure the command
367457
// is fully specified, as the initial input may lack necessary parameters.
368458
for (let i = 0; i < InteractiveLanguageClient.MAX_RETRY; i++) {
369-
const result = await this.ResolveCommand(param);
459+
const result = await this.interactiveResolveCommand(param);
370460
if (!result) {
371461
return undefined;
372462
}
@@ -403,52 +493,86 @@ export class InteractiveLanguageClient extends LanguageClient {
403493
return param;
404494
}
405495

406-
// ResolveCommand handles the interactive resolution of a command prior to its
407-
// execution.
408-
//
409-
// It processes an [InteractiveExecuteCommandParams] to determine if the command
410-
// requires interactive input, or to validate user-provided answers submitted
411-
// via the embedded [InteractiveParams].
412-
//
413-
// If the command requires user input (e.g., the initial probe) or if the
414-
// provided answers are invalid, it returns a modified [InteractiveExecuteCommandParams]
415-
// populated with FormFields to prompt the user. If the input is valid and
416-
// complete, or if the command requires no interaction at all, it returns an
417-
// [InteractiveExecuteCommandParams] with an empty form, signaling the client to
418-
// proceed with execution.
419-
//
420-
// See [InteractiveParams] for the complete multi-step client-server handshake
421-
// and the architectural reasoning behind dedicated ResolveXXX methods.
422-
async ResolveCommand(param: InteractiveExecuteCommandParams): Promise<InteractiveExecuteCommandParams | undefined> {
423-
const requestType = new RequestType<InteractiveExecuteCommandParams, InteractiveExecuteCommandParams, void>(
424-
'command/resolve'
425-
);
426-
return this.sendRequest<InteractiveExecuteCommandParams>('command/resolve', param).then(undefined, (error) => {
427-
return this.handleFailedRequest(requestType, undefined, error, undefined);
428-
});
429-
}
496+
/**
497+
* Executes a command on the language server with the validated form answers.
498+
*
499+
* It routes the execution through the `interactiveExecuteCommand` middleware
500+
* hook if registered, falling back to sending a `'workspace/executeCommand'`
501+
* LSP request with the user's answered form.
502+
*
503+
* @param command The identifier of the actual command handler to execute.
504+
* @param args Arguments that the command should be invoked with.
505+
* @param formAnswers The finalized, server-validated answers collected from the user.
506+
* @returns A provider result resolving to the command execution result.
507+
*/
508+
private interactiveExecuteCommand = (
509+
command: string,
510+
args: any[],
511+
formAnswers: any[]
512+
): vscode.ProviderResult<any> => {
513+
const _interactiveExecuteCommand: InteractiveExecuteCommandSignature = (command, args, formAnswers) => {
514+
const requestType = new RequestType<InteractiveExecuteCommandParams, any, void>('workspace/executeCommand');
515+
return this.sendRequest<FormEnumEntry[]>('workspace/executeCommand', {
516+
command: command,
517+
arguments: args,
518+
formAnswers: formAnswers
519+
} as InteractiveExecuteCommandParams).then(undefined, (error) => {
520+
return this.handleFailedRequest(requestType, undefined, error, undefined);
521+
});
522+
};
430523

431-
// Executes an LSP command with an extended payload containing interactive form
432-
// answers.
433-
async InteractiveExecuteCommand(command: string, args: any[], formAnswers: any[]): Promise<any> {
434-
const requestType = new RequestType<InteractiveExecuteCommandParams, any, void>('workspace/executeCommand');
435-
return this.sendRequest('workspace/executeCommand', {
436-
command: command,
437-
arguments: args,
438-
formAnswers: formAnswers
439-
} as InteractiveExecuteCommandParams).then(undefined, (error) => {
440-
return this.handleFailedRequest(requestType, undefined, error, undefined);
441-
});
442-
}
524+
const middleware = this.clientOptions.middleware as InteractiveMiddleware | undefined;
525+
return middleware?.interactiveExecuteCommand
526+
? middleware.interactiveExecuteCommand(command, args, formAnswers, _interactiveExecuteCommand)
527+
: _interactiveExecuteCommand(command, args, formAnswers);
528+
};
529+
530+
/**
531+
* Handles the interactive resolution of a command prior to its execution.
532+
*
533+
* It processes an [InteractiveExecuteCommandParams] to determine if the command
534+
* requires interactive input, or to validate user-provided answers submitted
535+
* via the embedded [InteractiveParams].
536+
*
537+
* If the command requires user input (e.g., the initial probe) or if the
538+
* provided answers are invalid, it returns a modified [InteractiveExecuteCommandParams]
539+
* populated with FormFields to prompt the user. If the input is valid and
540+
* complete, or if the command requires no interaction at all, it returns an
541+
* [InteractiveExecuteCommandParams] with an empty form, signaling the client to
542+
* proceed with execution.
543+
*
544+
* See [InteractiveParams] for the complete multi-step client-server handshake
545+
* and the architectural reasoning behind dedicated ResolveXXX methods.
546+
*
547+
* It routes the resolution through the `interactiveResolveCommand`
548+
* middleware hook if registered, falling back to sending a `'command/resolve'`
549+
* LSP request.
550+
*
551+
* @param param The command parameters and previous answers to resolve/validate.
552+
* @returns A provider result resolving to the updated command execution parameters.
553+
*/
554+
private interactiveResolveCommand = (
555+
param: InteractiveExecuteCommandParams
556+
): vscode.ProviderResult<InteractiveExecuteCommandParams> => {
557+
const _interactiveResolveCommand: InteractiveResolveCommandSignature = (param) => {
558+
const requestType = new RequestType<InteractiveExecuteCommandParams, any, void>('command/resolve');
559+
return this.sendRequest<FormEnumEntry[]>('command/resolve', param).then(undefined, (error) => {
560+
return this.handleFailedRequest(requestType, undefined, error, undefined);
561+
});
562+
};
563+
564+
const middleware = this.clientOptions.middleware as InteractiveMiddleware | undefined;
565+
return middleware?.interactiveResolveCommand
566+
? middleware.interactiveResolveCommand(param, _interactiveResolveCommand)
567+
: _interactiveResolveCommand(param);
568+
};
443569

444570
/**
445571
* Queries the language server to dynamically retrieve enumeration entries for
446-
* interactive form fields of type 'lazyEnum'.
572+
* interactive form fields of type `'lazyEnum'`.
447573
*
448-
* This field uses an arrow function to preserve the lexical `this` context of
449-
* the client. It routes the query through the `interactiveListEnum`
450-
* middleware hook if registered, falling back to the default
451-
* `'interactive/listEnum'` LSP request.
574+
* It routes the query through the `interactiveListEnum` middleware hook if
575+
* registered, falling back to sending an `'interactive/listEnum'` LSP request.
452576
*
453577
* @param param The query parameters, including the data source name, static
454578
* config, and filter string.

extension/src/language/goLanguageServer.ts

Lines changed: 17 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,12 @@ import { GoDocumentSelector } from '../goMode';
6969
import { COMMAND as GOPLS_ADD_TEST_COMMAND } from '../goGenerateTests';
7070
import { COMMAND as GOPLS_MODIFY_TAGS_COMMAND } from '../goModifytags';
7171
import { TelemetryKey, telemetryReporter } from '../goTelemetry';
72-
import { InteractiveExecuteCommandParams, InteractiveLanguageClient, InteractiveMiddleware } from './form';
72+
import {
73+
InteractiveExecuteCommandParams,
74+
InteractiveResolveCommandSignature,
75+
InteractiveLanguageClient,
76+
InteractiveMiddleware
77+
} from './form';
7378

7479
export interface LanguageServerConfig {
7580
serverName: string;
@@ -564,24 +569,17 @@ export async function buildLanguageClient(
564569
}
565570
next(token, params);
566571
},
567-
executeCommand: async (command: string, args: any[], next: ExecuteCommandSignature) => {
568-
let formAnswers: any[] | undefined;
569-
const supported = c.initializeResult?.capabilities?.experimental?.interactiveResolveProvider;
570-
if (goCtx.languageClient && Array.isArray(supported) && supported.includes('command')) {
571-
const resolved = await goCtx.languageClient.resolveCommandInteractively({
572-
command: command,
573-
arguments: args
574-
} as InteractiveExecuteCommandParams);
575-
if (!resolved) {
576-
return undefined;
577-
}
578-
579-
// Replace original command and result with resolved command and args.
580-
command = resolved.command;
581-
args = resolved.arguments || [];
582-
formAnswers = resolved.formAnswers;
572+
resolveCommand: async (
573+
param: InteractiveExecuteCommandParams,
574+
next: InteractiveResolveCommandSignature
575+
) => {
576+
// Avoid resolving for frequently triggered commands.
577+
if (param.command === 'gopls.package_symbols') {
578+
return param;
583579
}
584-
580+
return await next(param);
581+
},
582+
executeCommand: async (command: string, args: any[], next: ExecuteCommandSignature) => {
585583
try {
586584
if (command === 'gopls.tidy' || command === 'gopls.vulncheck') {
587585
await vscode.workspace.saveAll(false);
@@ -601,12 +599,7 @@ export async function buildLanguageClient(
601599
govulncheckTerminal.show();
602600
}
603601

604-
let res: any;
605-
if (formAnswers === undefined || formAnswers.length === 0) {
606-
res = await next(command, args);
607-
} else {
608-
res = await goCtx.languageClient!.InteractiveExecuteCommand(command, args, formAnswers);
609-
}
602+
const res: any = await next(command, args);
610603

611604
const progressToken = res?.Token as ProgressToken;
612605
// The progressToken from executeCommand indicates that

0 commit comments

Comments
 (0)