Author(s): Hongxiang Jiang (hxjiang@golang.org)
Thanks to: Alan Donovan, Brian Wilkerson, Danny Tuppeny, Hana Kim, Madeline Kalil, Peter Weinberger, Robert Findley.
Last updated: 16 July 2026
This proposal outlines systematic and scalable approaches for the Language Server Protocol (LSP) to support refactoring operations that require user input. Current LSP refactoring is limited to operations that do not require user input (with the exception of renaming).
The teams working on the Go and Dart programming languages have received a number of feature requests that require user input to implement:
We have researched the prior art mostly from Dart (see below) and propose a few systematic and scalable approaches for the LSP spec.
The primary goal of these proposals is to support interactive workflows in LSP, establishing a backward-compatible mechanism for language servers to collect user input, primarily for code transformations, as well as non-edit server operations.
This design centralizes the workflow logic within the language server while delegating UI rendering entirely to the client. We recommend the Command layer proposal as our primary path, which we have successfully validated in production with releases in gopls v0.23.0 and vscode-go v0.56.0.
We propose a common interface, InteractiveParams, because this data structure is used in multiple places across the proposals below. This interface introduces the core concepts of “questions” and “answers”.
A key component of the design is that the communication mechanism between the language client and the server should be stateless, modeled after stateless HTML/HTTP form submissions. Imagine going to the DMV to submit an application: you fill in a form full of questions. The officer sees a missing answer and asks you to go back, fill that in, and come back, not necessarily to the same officer. When you come back to the counter, you bring the entire form instead of only the missing answer.
As a result, the language server evaluates the form and may:
In a design based on server-to-client requests (reversing the client-to-server flow above), the language server must keep an active RPC open and hold resources while waiting for user input (analogous to a DMV teller standing idle at the counter while the customer fills out a form). By contrast, this stateless approach provides three main benefits:
Depending on UI capabilities, clients can either render the entire form at once or prompt questions sequentially. For sequential clients, we recommend only presenting fields that are missing valid answers (i.e., where the field's ID is not in formAnswers or has an explicit validation error) to avoid repetitive prompts. Since the protocol is stateless, the server can control question dependencies or revoke a previously answered question simply by omitting its answer from formAnswers or marking it with an error in the validation response, signaling the client to ask it again.
export interface InteractiveParams { // FormFields defines the questions and validation errors in previous // answers to the same questions. // // This is a server-to-client field. The language server defines these, and // the client uses them to render the form. // // The interactive phase is considered complete when the server returns a // response where this slice is omitted. formFields?: FormField[]; // FormAnswers contains the answers for the form questions. // // When sent by the language server, this field is optional and contains the // user's previous answers from prior resolution steps to support editing // previously entered values. // // When sent by the language client, this field contains the user's answers. // Answers are linked to their respective questions using the field's unique // `id` rather than their array index. The list must not contain duplicate IDs, // and each answer's ID must correspond to a field ID defined in `formFields`. // // The client must include answers for all required fields (where `required` // is true). Answers for optional fields (where `required` is false) // may be omitted if no answer was provided, or included if an answer is available. formAnswers?: FormAnswer[]; } // FormField describes a single question in a form and its validation state. export interface FormField { // ID is a unique identifier for this field. This key is used as the property // name in FormAnswers to map the user's input back to this specific field. id: string; // Description is the text content of the question (the prompt) presented to the user. description: string; // Type specifies the data type and validation constraints for the answer. type: FormFieldType; // Required specifies whether an answer is absolutely required for this field. required: boolean; // Default specifies an optional initial value for the answer. // If Type is FormFieldTypeEnum, this value must be present in the enum's values array. default?: any; // Error provides a validation message from the language server. // If empty or undefined, the current answer is considered valid. error?: string; } // FormAnswer describes a single answer to a FormField, identified by its unique // ID. export interface FormAnswer { // The ID of the FormField being answered. id: string; // The user's answer value. value: any; }
The typical question types include:
string: A simple text value.bool: A boolean value.file: A valid Document URI with filters on preexistence and file typenumber: A numeric value.enum: A selection from a pre-defined set of options.lazy enum: A selection from a dynamic or large set of options queried on demand and reactive to partial user input.list: A homogenous list of items.// FormFieldKind defines the set of supported input type. export type FormFieldKind = 'string' | 'file' | 'bool' | 'number' | 'enum' | 'lazyEnum' | 'list'; // FormFieldType acts as a Discriminated Union based on the 'kind' property. export type FormFieldType = | FormFieldTypeString | FormFieldTypeFile | FormFieldTypeBool | FormFieldTypeNumber | FormFieldTypeEnum | FormFieldTypeLazyEnum | FormFieldTypeList; // FormFieldTypeString defines a text input. export interface FormFieldTypeString { kind: 'string'; } // FileExistence represents whether the file denoted by a DocumentURI exists. // // It is a bit set allowing combinations of existence states. For // example, New|Existing allows either state. export enum FileExistence { // New indicates that file has not yet been created. New = 1 << 0, // Existing indicates that the file exists already. Existing = 1 << 1 } // FileType represents the expected filesystem resource type. // // It is a bit set allowing combinations of file types. For example, Regular|Directory // allows either types. export enum FileType { // Regular indicates the resource could be a regular file. Regular = 1 << 0, // Directory indicates the resource could be a directory. Directory = 1 << 1 } // FormFieldTypeFile defines an input for a file or directory URI. // // The client determines the best mechanism to collect this information from // the user (e.g., a graphical file picker, a text input with autocomplete, etc). // // The value returned by the client must be a valid "DocumentUri" as defined // in the LSP specification: // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#documentUri export interface FormFieldTypeFile { kind: 'file'; // Existence constraint. // // If omitted, allows both `New` and `Existing` files. existence?: FileExistence; // The expected file type (e.g., regular file or directory). // // If omitted, defaults to `Regular`. type?: FileType; // Filters specifies the allowed file extensions without the leading dot. A file // is valid if it matches any of the extensions (OR logic). e.g. ["png", "jpg"]. // // If omitted or empty, no extension filter is applied. filters?: string[]; } // FormFieldTypeBool defines a boolean input. export interface FormFieldTypeBool { kind: 'bool'; } // FormFieldTypeNumber defines a numeric input. export interface FormFieldTypeNumber { kind: 'number'; } // FormEnumEntry represents a single option in an enumeration. export interface FormEnumEntry { // Value is the unique string identifier for this option. // // This is the value that will be sent back to the server in // 'FormAnswers' if the user selects this option. value: string; // Description is the human-readable label presented to the user. description: string; } // FormFieldTypeEnum defines a selection from a set of values. // // Use this type when: // - The number of options is small (e.g., < 20). // - All options are known at the time the form is created. export interface FormFieldTypeEnum { kind: 'enum'; // Name is an optional identifier for the enum type. name?: string; // Entries is the list of allowable options. entries: FormEnumEntry[]; } // FormFieldTypeLazyEnum defines a selection from a large or dynamic enum entry set. // // Use this type when: // 1. The dataset is too large to send efficiently in a single payload // (e.g., thousands of workspace symbols, file uri or cloud resources). // 2. The available options depend on the user's input (e.g., semantic search). // 3. Generating the list is expensive and should only be done if requested. // // The client is expected to render a search interface (e.g., a combo box with // a text input) and query the server via 'interactive/listEnum' as the user types. export interface FormFieldTypeLazyEnum { kind: 'lazyEnum'; // Source identifies the data source on the server. // // Examples: "workspace/symbol", "database/schema", "git/tags". source: string; // Config contains the static settings for the source. // The client treats this as opaque data and echoes it back in the // 'interactive/listEnum' request. config?: any; } // FormFieldTypeList defines a homogenous list of items. export interface FormFieldTypeList { kind: 'list'; // ElementType specifies the type of the items in the list. // Recursive reference to the union type. elementType: FormFieldType; }
Before discussing the proposals, it is important to understand the context of how code actions work today because most code transformations are done through code actions.
Today, there are primarily two kinds of code actions:
workspaceEdit): The client applies the edits directly to the user's workspace.command): The client instructs the server to execute the command, which may have a possible side effect of making a server-to-client workspace/applyEdit request.(There is a third kind that resolves to both edits and a command, but it is just a combination of the first two.)
If we want to make code transformations interactive, we can introduce the “interactive” behavior (the back and forth communication) at different layers:
The sections below detail our primary recommendation (Command layer), followed by alternative protocol layers (WorkspaceEdit, CodeAction, and Server-to-Client requests) for comparison.
This approach introduces interactivity prior to command execution, parameterizing arguments via a stateless command/resolve loop.
The command/resolve request is sent from the client to the server to check whether a command is ready for execution or requires additional user input. Before executing any command that supports interactive resolution, the client must call command/resolve at least once to determine if the command arguments are comprehensive. As long as the ExecuteCommandParams contains non-empty formFields (questions), the client should not execute the command. Instead, the client should collect the user's answers and call command/resolve with the answers populated in formAnswers.
The server processes the answers and may return a new ExecuteCommandParams with new questions or validation errors, requiring further interaction. This process can repeat for multiple rounds. The interactive phase is considered complete when the server returns a response where formFields is omitted or empty, signaling that the command is ready to be executed via workspace/executeCommand including the answers in formAnswers.
Client Capability:
workspace.interactiveResolveInteractiveResolveClientCapabilitiesexport interface InteractiveResolveClientCapabilities { /** * The input types the client supports for interactive dialogs. * The presence of this field implies support for interactive refactoring. */ inputTypes?: FormFieldKind[]; }
Server Capability:
interactiveResolveProviderexport interface interactiveResolveOptions { /** * The kinds of interactive resolutions that the server supports. * * For example, "command" indicates that the server supports resolving * `ExecuteCommandParams` interactively through "command/resolve". */ kinds?: string[]; }
Request:
command/resolveExecuteCommandParamsexport interface ExecuteCommandParams extends InteractiveParams { // ... original fields ... }
Response:
ExecuteCommandParamsExample: Suppose the server returns a Code Action with a command to modify tags:
CodeAction: command: command: gopls.modify_tags args: [...]
When the user clicks the action, the client calls command/resolve for the first time to ask if the command is comprehensive:
ExecuteCommandParams: command: gopls.modify_tags args: [...]
The server returns questions in formFields because the command arguments are not comprehensive:
ExecuteCommandParams: command: gopls.modify_tags args: [...] formFields: [ {id: "tags", description: "tags to add", kind: string, required: true}, {id: "case", description: "case to use", kind: enum, required: true} ]
The client collects the user input "foo,," and "snake_case" then calls command/resolve again to validate:
ExecuteCommandParams: command: gopls.modify_tags args: [...] formAnswers: [ {id: "tags", value: "foo,,"}, {id: "case", value: "snake_case"} ]
The server return questions with error:
ExecuteCommandParams: command: gopls.modify_tags args: [...] formFields: [ {id: "tags", description: "tags to add", kind: string, required: true, err: "invalid"}, {id: "case", description: "case to use", kind: enum, required: true} ] formAnswers: [ {id: "tags", value: "foo,,"}, {id: "case", value: "snake_case"} ]
The client again collects user input and calls command/resolve with the new answers.
ExecuteCommandParams: command: gopls.modify_tags args: [...] formAnswers: [ {id: "tags", value: "json"}, {id: "case", value: "snake_case"} ]
The server returns a result without formFields, indicating the answer is validated:
ExecuteCommandParams: command: gopls.modify_tags args: [...] formAnswers: [ {id: "tags", value: "json"}, {id: "case", value: "snake_case"} ]
Finally, the client calls workspace/executeCommand:
ExecuteCommandParams: command: gopls.modify_tags args: [...] formAnswers: [ {id: "tags", value: "json"}, {id: "case", value: "snake_case"} ]
Pros:
govulncheck), navigate the cursor, display web-based reports, and so on. Language servers are treated as execution hosts, not just code text manipulators.<method>/resolve pattern that can be extended to other LSP requests. For example, although Rename (unique among LSP requests) already supports a limited form of interactivity (prepareRename) to retrieve a target symbol's placeholder, complex renaming operations may wish to ask additional questions, such as how it should handle certain kinds of conflict. Before executing a rename, the client could call rename/resolve to resolve a RenameParams extending InteractiveParams.Cons:
WorkspaceEdit). To use interactive forms, language servers must express the CodeAction using a Command payload instead.An alternative to parameterizing commands prior to execution is to introduce interactivity at the WorkspaceEdit layer via workspaceEdit/resolve when edits are applied to the workspace.
We do not recommend this approach as it requires server-to-client workspace/applyEdit requests that block indefinitely while the client fills out the form, tying up server resources and incurring client RPC timeouts. Nonetheless, we detail it here because it provides a unified mechanism for text edit resolution across LSP methods and directly supports CodeActions that resolve to edits.
The workspaceEdit/resolve request is sent from the client to the server to resolve the final edits that should be applied to the workspace. As long as the WorkspaceEdit contains non-empty formFields (questions), the client should not apply the edits. Instead, the client should collect the user's answers and call workspaceEdit/resolve with the answers populated in formAnswers.
The server processes the answers and may return a new WorkspaceEdit with new questions or validation errors, requiring further interaction. This process can repeat for multiple rounds. The interactive phase is considered complete when the server returns a WorkspaceEdit where formFields is omitted or empty, signaling that the edits are finalized and ready to be applied to the workspace.
Client Capability:
workspace.workspaceEditWorkspaceEditClientCapabilitiesexport interface WorkspaceEditClientCapabilities { /** * The input types the client supports for interactive dialogs. * The presence of this field implies support for interactive refactoring. */ interactiveResolveInputTypes?: FormFieldKind[]; }
Server Capability:
workspace.workspaceEditexport interface WorkspaceEditServerCapabilities { /** * The server provides support to resolve WorkspaceEdits interactively. */ interactiveResolveProvider?: boolean; }
Request:
workspaceEdit/resolveWorkspaceEditinterface WorkspaceEdit extends InteractiveParams { // ... original fields ... }
Response:
WorkspaceEditExample: Imagine the original edit returned by the server is:
WorkspaceEdit: changes: --- a/foo.go +++ b/foo.go type Foo struct { - FC string - CC string + FC string `$TAG$:"$FIELD$"` + CC string `$TAG$:"$FIELD$"` } formFields: [ {id: "tags", description: "tags to add", kind: string, required: true}, {id: "case", description: "case to use", kind: enum, required: true} ]
The client collects the user input "foo,," and "snake_case" then calls workspaceEdit/resolve with the answers:
WorkspaceEdit: changes: --- a/foo.go +++ b/foo.go type Foo struct { - FC string - CC string + FC string `$TAG$:"$FIELD$"` + CC string `$TAG$:"$FIELD$"` } formAnswers: [ {id: "tags", value: "foo,,"}, {id: "case", value: "snake_case"} ]
The server return questions with error:
WorkspaceEdit: changes: --- a/foo.go +++ b/foo.go type Foo struct { - FC string - CC string + FC string `$TAG$:"$FIELD$"` + CC string `$TAG$:"$FIELD$"` } formFields: [ {id: "tags", description: "tags to add", kind: string, required: true, err: "invalid"}, {id: "case", description: "case to use", kind: enum, required: true} ] formAnswers: [ {id: "tags", value: "foo,,"}, {id: "case", value: "snake_case"} ]
The client again collects user input and calls workspaceEdit/resolve with the new answers.
WorkspaceEdit: changes: --- a/foo.go +++ b/foo.go type Foo struct { - FC string - CC string + FC string `$TAG$:"$FIELD$"` + CC string `$TAG$:"$FIELD$"` } formAnswers: [ {id: "tags", value: "json"}, {id: "case", value: "snake_case"} ]
The server returns the finalized workspace edit:
WorkspaceEdit: changes: --- a/foo.go +++ b/foo.go type Foo struct { - FC string - CC string + FC string `json:"fc"` + CC string `json:"cc"` }
The client notices that formFields is gone, so it applies the edits to the workspace.
Note: The original placeholder edits allow eager language clients to render a tentative preview of the changes (e.g., when a user hovers over a code action). The actual finalized edit is re-computed by the server by reading InteractiveParams.data once the user provides the required input.
Pros:
CodeAction, CodeLens, workspace/applyEdits, etc.).Cons:
workspace/applyEdit: While edit resolution works well for client-initiated edit-based Code Actions, nothing prevents a server from sending an interactive WorkspaceEdit inside a server-to-client workspace/applyEdit request (e.g., during command execution). Doing so forces workspace/applyEdit to become a blocking server-to-client call while the user fills out the form, requiring the server to hold file system snapshots in memory and risking RPC timeouts which for some LSP clients are as short as 10 seconds—far too brief for a user to complete a form.govulncheck), or CLI tool execution.A third alternative is to introduce interactive behavior directly at the Code Action layer by extending the existing codeAction/resolve method. However, this approach was rejected because it conflicts with LSP clients that eagerly resolve code actions (e.g., to generate hover previews), which would unexpectedly trigger interactive prompts before the user explicitly selects an action.
In standard LSP, codeAction/resolve is sent from the client to the server to resolve additional information (usually the edit property) lazily, avoiding expensive computation during the initial textDocument/codeAction request. Typically, this is a single round-trip.
Now, while the focus remains the same—to make the edit or command available—the process becomes iterative. The client must keep resolving the action as long as it contains non-empty formFields (questions). The client collects the user's answers and calls codeAction/resolve again with the answers populated in formAnswers.
The server processes the answers and may return a new CodeAction with new questions or validation errors. This process can repeat for multiple rounds. The interactive phase is considered complete when the server returns a response CodeAction where formFields is omitted. At this point, the edit or command property is usually computed and ready to be applied or executed.
Client Capability:
textDocument.codeActionCodeActionClientCapabilitiesinterface CodeActionClientCapabilities { /** * The input types the client supports for interactive dialogs. * The presence of this field implies support for interactive refactoring. */ interactiveResolveInputTypes?: FormFieldKind[]; }
Server Capability:
codeActionProviderboolean | CodeActionOptions where CodeActionOptions is defined as follows:interface CodeActionOptions extends WorkDoneProgressOptions { // ... existing fields ... /** * The server provides support to resolve code action interactively. */ interactiveResolveProvider?: boolean; }
Request:
codeAction/resolveCodeActionexport interface CodeAction extends InteractiveParams { // ... original fields ... }
Response:
CodeActionExample: Suppose the server returns a Code Action from textDocument/codeAction where the command is empty:
code action: command:
The client calls codeAction/resolve to resolve the code action, and the server returns questions:
code action: command: formFields: [ {id: "tags", description: "tags to add", kind: string, required: true}, {id: "case", description: "case to use", kind: enum, required: true} ]
The client collects the user input "foo,," and "snake_case" then calls codeAction/resolve again:
code action: command: formAnswers: [ {id: "tags", value: "foo,,"}, {id: "case", value: "snake_case"} ]
The server return questions with error:
code action: command: formFields: [ {id: "tags", description: "tags to add", kind: string, required: true, err: "invalid"}, {id: "case", description: "case to use", kind: enum, required: true} ] formAnswers: [ {id: "tags", value: "foo,,"}, {id: "case", value: "snake_case"} ]
The client again collects user input and calls workspaceEdit/resolve with the new answers.
code action: command: formAnswers: [ {id: "tags", value: "json"}, {id: "case", value: "snake_case"} ]
The server returns the finalized code action with command:
code action: command: { command: "gopls.modify_tags" args: [..., "json", "snake_case"] }
The client notices formFields is gone, so it can execute the command and generatte the diffs accordingly.
Note: The example above demonstrates a code action resolving to a command, but it is also possible for a code action to resolve directly to edits following a similar flow.
Pros: Does not introduce any new LSP method.
Cons:
CodeAction flows. It does not support direct client invocation of server commands (e.g., custom editor keybindings calling workspace/executeCommand) that would also benefit from interactivity.Note on Extensibility: This behavior is easily extensible to other operations like codeLens/resolve.
window/collectInput, a new server-to-client request (rejected: not scalable)A fourth alternative is to introduce a new server-to-client request method, window/collectInput. However, this approach was also rejected due to the same blocking RPC concerns discussed in Approach 1.
This works similarly to window/showMessageRequest, allowing the server to call this method at any point during command execution to collect required information.
Client Capability:
window.collectInputCollectInputClientCapabilities defined as followsinterface CollectInputClientCapabilities { /** * The input types the client supports. */ inputTypes?: FormFieldKind[]; }
Request:
window/collectInputFormField[]Response:
FormAnswer[] | null if none of the input is provided.Pros: Information collection is not limited to executeCommand and can be called whenever needed.
Cons: The main problem with this proposal is that it requires embedding a server-to-client request (window/collectInput) within an ongoing client-to-server request (such as workspace/executeCommand). As discussed earlier in Approach 1 (in the context of making workspace/applyEdit a blocking call), embedding a blocking server-to-client request within a client-to-server operation creates serious drawbacks: holding non-trivial file system snapshots in memory during user interaction is inefficient, and standard client RPC timeouts will likely expire while the user is filling out the form.
After exploring these different layers, we want to share our stance and recommendations to help the LSP community decide on the best approach:
command/resolve or generalized <method>/resolve) as the primary path. Beyond text edits, it supports non-edit capabilities such as interactive test runners (profiling/coverage options), analysis tools (govulncheck), webviews, and CLI tools. Because resolution fires only upon explicit user action, it avoids eager hover issues in clients like Visual Studio. It also avoids making server-to-client workspace/applyEdit requests blocking, eliminating RPC timeouts and snapshot memory retention. Its main limitation is that interactive code action must use a command payload rather than resolving purely to edits.workspaceEdit/resolve) detailed as an alternative. It handles text edits uniformly across different LSP requests and works for code actions that resolve directly to edits. However, it cannot support non-edit features, risks turning workspace/applyEdit into a blocking call during command execution, and cannot produce meaningful diff previews for certain complex operations (like “move declaration”) before user input is provided.Taking these trade-offs into account, the ability to extend interactivity beyond edits and the avoidance of blocking workspace/applyEdit calls strongly favor the Command layer. We recommend command/resolve (<method>/resolve) as the primary standard path for LSP interactive refactoring.