blob: 28b8e579053abb37a6b1015c13701a30beebc623 [file]
// Copyright 2026 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package server
import (
"context"
"fmt"
"slices"
"strings"
"golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/protocol/command"
)
// Ths file contains the code to mediate user dialogs in the client
// The logical flow is as follows:
// 1. Client sends 'textDocument/codeAction'
// gopls respond with one or more CodeActions, including one with kind either
// 'refactor.rewrite.addTags' or 'refactor.rewrite.removeTags'
// 2. Client sends 'codeAction/resolve' with the selected CodeActions
// gopls returns the same CodeAction
// 3. Client sends 'workspace/executeCommand' to get the dialog form
// gopls responds with the form to display to the user
// 4. Client sends 'command/resolve' with the filled out form
// gopls responds, with 'workspace/applyEdit' saying what to do
// 5. Client return ApplyWorkspaceEditResult with applied = true
// 6. Client sends a textDocument/didChange notification, with the edits applied (optional)
// In vscode-go, because it is a standard LSP client,
// cannot do step 4 as described. Instead
// it calls "workspace/executeCommand" with command 'gopls.lsp'
// and parameter.Method "command/resolve".
//
// ExecuteCommand() calls command.Dispatch() which calls LSP()
// which calls protocol.ServerDispatchCall("command/resolve")
// which calls ResolveCommand. command.Dispatch() switches on param.Command and LSP() passes param.Method
// to ServerDispatchCall
// The neovim flow is simpler, as 'command/resolve' can be invoked directly.
var AddTagsForm = []protocol.FormField{
{
Description: `comma-separated list of tags to add; e.g.. "json,xml"`,
Type: protocol.FormFieldTypeString{Kind: "string"},
Default: "json",
},
{
Description: `transform rule for added tags, e.g., "camelcase' or 'snakecase"`,
Type: protocol.FormFieldTypeEnum{
Kind: "enum",
Entries: []protocol.FormEnumEntry{
{
// MyField -> myField
Value: "camelcase",
Description: "camelcase",
},
{
// MyField -> my-field
Value: "lispcase",
Description: "lispcase",
},
{
// MyField -> MyField
Value: "pascalcase",
Description: "pascalcase",
},
{
// MyField -> My Field
Value: "titlecase",
Description: "titlecase",
},
{
// MyField -> my_field
Value: "snakecase",
Description: "snakecase",
},
{
// keep the existing field name
Value: "keep",
Description: "keep",
},
},
},
Default: "camelcase",
},
}
var RemoveTagsForm = []protocol.FormField{
{
Description: `comma-separated list of tags to remove; e.g., "json,xml"`,
Type: protocol.FormFieldTypeString{Kind: "string"},
Default: "json", // TODO(?): put the existing tags here?
},
}
// ResolveComamnd is called indirectly from WorkspaceCommand(), explained above.
func (s *server) ResolveCommand(ctx context.Context, param *protocol.ExecuteCommandParams) (*protocol.ExecuteCommandParams, error) {
switch param.Command {
case "gopls.modify_tags":
return resolveModifyTags(param)
}
return nil, notImplemented(fmt.Sprintf("ResolveCommand(%s)", param.Command))
}
func resolveModifyTags(param *protocol.ExecuteCommandParams) (*protocol.ExecuteCommandParams, error) {
var a0 command.ModifyTagsArgs
if err := command.UnmarshalArgs(param.Arguments, &a0); err != nil {
return nil, err
}
switch a0.Modification {
case "add":
switch len(param.FormAnswers) {
case 0: // first call, return the form
param.FormFields = AddTagsForm
return param, nil
case 2: // second call, process the form
var ok bool
if a0.Add, ok = param.FormAnswers[0].(string); !ok {
return nil, fmt.Errorf("invalid type of first value, want string: %v", param.FormAnswers[0])
}
if slices.Contains(strings.Split(a0.Add, ","), "") {
// TODO(pjw): instead filter AddTagsForm to remove empty tags and tags containing spaces
form := slices.Clone(AddTagsForm)
form[0].Error = "input tags should not contain empty tag"
param.FormFields = form
return param, nil
}
if a0.Transform, ok = param.FormAnswers[1].(string); !ok {
return nil, fmt.Errorf("invalid type of second value, want string: %v", param.FormAnswers[1])
}
// PJW: what happens when the user enters a bad value? (i think the client handles it)
raw, err := command.MarshalArgs(a0)
if err != nil {
return nil, err
}
param.FormAnswers = nil
param.FormFields = nil
param.Arguments = raw
return param, nil
default:
return nil, fmt.Errorf("modify tags command expecting 1 value from client, got %v", len(param.FormAnswers))
}
case "remove":
switch len(param.FormAnswers) {
case 0: // first call, return the form
// TODO? show the user the current list of tags?
param.FormFields = RemoveTagsForm
return param, nil
case 1: // second call, process the form
var ok bool
if a0.Remove, ok = param.FormAnswers[0].(string); !ok {
return nil, fmt.Errorf("invalid type of value, want string: %v", param.FormAnswers[0])
}
raw, err := command.MarshalArgs(a0)
if err != nil {
return nil, err
}
param.FormAnswers = nil
param.FormFields = nil
param.Arguments = raw
return param, nil
default:
return nil, fmt.Errorf("modify tags command expecting 1 value from client, got %v", len(param.FormAnswers))
}
default:
return nil, fmt.Errorf("unsupported modify tags operation: %s", a0.Modification)
}
}