// Copyright 2020 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 (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log"
	"maps"
	"os"
	"path/filepath"
	"regexp"
	"runtime"
	"runtime/pprof"
	"slices"
	"sort"
	"strings"
	"sync"

	"github.com/fatih/gomodifytags/modifytags"
	"golang.org/x/mod/modfile"
	"golang.org/x/telemetry/counter"
	"golang.org/x/tools/go/ast/astutil"
	"golang.org/x/tools/gopls/internal/cache"
	"golang.org/x/tools/gopls/internal/cache/metadata"
	"golang.org/x/tools/gopls/internal/cache/parsego"
	"golang.org/x/tools/gopls/internal/debug"
	"golang.org/x/tools/gopls/internal/file"
	"golang.org/x/tools/gopls/internal/golang"
	"golang.org/x/tools/gopls/internal/progress"
	"golang.org/x/tools/gopls/internal/protocol"
	"golang.org/x/tools/gopls/internal/protocol/command"
	"golang.org/x/tools/gopls/internal/settings"
	"golang.org/x/tools/gopls/internal/util/bug"
	"golang.org/x/tools/gopls/internal/vulncheck"
	"golang.org/x/tools/gopls/internal/vulncheck/scan"
	"golang.org/x/tools/internal/diff"
	"golang.org/x/tools/internal/event"
	"golang.org/x/tools/internal/gocommand"
	"golang.org/x/tools/internal/jsonrpc2"
	"golang.org/x/tools/internal/tokeninternal"
	"golang.org/x/tools/internal/xcontext"
)

func (s *server) ExecuteCommand(ctx context.Context, params *protocol.ExecuteCommandParams) (any, error) {
	ctx, done := event.Start(ctx, "server.ExecuteCommand")
	defer done()

	// For test synchronization, always create a progress notification.
	//
	// This may be in addition to user-facing progress notifications created in
	// the course of command execution.
	if s.Options().VerboseWorkDoneProgress {
		work := s.progress.Start(ctx, params.Command, "Verbose: running command...", nil, nil)
		defer work.End(ctx, "Done.")
	}

	if !slices.Contains(s.Options().SupportedCommands, params.Command) {
		return nil, fmt.Errorf("%s is not a supported command", params.Command)
	}

	handler := &commandHandler{
		s:      s,
		params: params,
	}
	return command.Dispatch(ctx, params, handler)
}

type commandHandler struct {
	s      *server
	params *protocol.ExecuteCommandParams
}

func (h *commandHandler) Modules(ctx context.Context, args command.ModulesArgs) (command.ModulesResult, error) {
	// keepModule filters modules based on the command args
	keepModule := func(goMod protocol.DocumentURI) bool {
		// Does the directory enclose the view's go.mod file?
		if !args.Dir.Encloses(goMod) {
			return false
		}

		// Calculate the relative path
		rel, err := filepath.Rel(args.Dir.Path(), goMod.Path())
		if err != nil {
			return false // "can't happen" (see prior Encloses check)
		}

		assert(goMod.Base() == "go.mod", fmt.Sprintf("invalid go.mod path: want go.mod, got %q", goMod.Path()))

		// Invariant: rel is a relative path without "../" segments and the last
		// segment is "go.mod"
		nparts := strings.Count(rel, string(filepath.Separator))
		return args.MaxDepth < 0 || nparts <= args.MaxDepth
	}

	// Views may include:
	//   - go.work views containing one or more modules each;
	//   - go.mod views containing a single module each;
	//   - GOPATH and/or ad hoc views containing no modules.
	//
	// Retrieving a view via the request path would only work for a
	// non-recursive query for a go.mod view, and even in that case
	// [Session.SnapshotOf] doesn't work on directories. Thus we check every
	// view.
	var result command.ModulesResult
	seen := map[protocol.DocumentURI]bool{}
	for _, v := range h.s.session.Views() {
		s, release, err := v.Snapshot()
		if err != nil {
			return command.ModulesResult{}, err
		}
		defer release()

		for _, modFile := range v.ModFiles() {
			if !keepModule(modFile) {
				continue
			}

			// Deduplicate
			if seen[modFile] {
				continue
			}
			seen[modFile] = true

			fh, err := s.ReadFile(ctx, modFile)
			if err != nil {
				return command.ModulesResult{}, err
			}
			mod, err := s.ParseMod(ctx, fh)
			if err != nil {
				return command.ModulesResult{}, err
			}
			if mod.File.Module == nil {
				continue // syntax contains errors
			}
			result.Modules = append(result.Modules, command.Module{
				Path:    mod.File.Module.Mod.Path,
				Version: mod.File.Module.Mod.Version,
				GoMod:   mod.URI,
			})
		}
	}
	return result, nil
}

func (h *commandHandler) Packages(ctx context.Context, args command.PackagesArgs) (command.PackagesResult, error) {
	// Convert file arguments into directories
	dirs := make([]protocol.DocumentURI, len(args.Files))
	for i, file := range args.Files {
		if filepath.Ext(file.Path()) == ".go" {
			dirs[i] = file.Dir()
		} else {
			dirs[i] = file
		}
	}

	keepPackage := func(pkg *metadata.Package) bool {
		for _, file := range pkg.GoFiles {
			for _, dir := range dirs {
				if file.Dir() == dir || args.Recursive && dir.Encloses(file) {
					return true
				}
			}
		}
		return false
	}

	result := command.PackagesResult{
		Module: make(map[string]command.Module),
	}

	err := h.run(ctx, commandConfig{
		progress: "Packages",
	}, func(ctx context.Context, _ commandDeps) error {
		for _, view := range h.s.session.Views() {
			snapshot, release, err := view.Snapshot()
			if err != nil {
				return err
			}
			defer release()

			metas, err := snapshot.WorkspaceMetadata(ctx)
			if err != nil {
				return err
			}

			// Filter out unwanted packages
			metas = slices.DeleteFunc(metas, func(meta *metadata.Package) bool {
				return meta.IsIntermediateTestVariant() ||
					!keepPackage(meta)
			})

			start := len(result.Packages)
			for _, meta := range metas {
				var mod command.Module
				if meta.Module != nil {
					mod = command.Module{
						Path:    meta.Module.Path,
						Version: meta.Module.Version,
						GoMod:   protocol.URIFromPath(meta.Module.GoMod),
					}
					result.Module[mod.Path] = mod // Overwriting is ok
				}

				result.Packages = append(result.Packages, command.Package{
					Path:       string(meta.PkgPath),
					ForTest:    string(meta.ForTest),
					ModulePath: mod.Path,
				})
			}

			if args.Mode&command.NeedTests == 0 {
				continue
			}

			// Make a single request to the index (per snapshot) to minimize the
			// performance hit
			var ids []cache.PackageID
			for _, meta := range metas {
				ids = append(ids, meta.ID)
			}

			allTests, err := snapshot.Tests(ctx, ids...)
			if err != nil {
				return err
			}

			for i, tests := range allTests {
				pkg := &result.Packages[start+i]
				fileByPath := map[protocol.DocumentURI]*command.TestFile{}
				for _, test := range tests.All() {
					test := command.TestCase{
						Name: test.Name,
						Loc:  test.Location,
					}

					file, ok := fileByPath[test.Loc.URI]
					if !ok {
						f := command.TestFile{
							URI: test.Loc.URI,
						}
						i := len(pkg.TestFiles)
						pkg.TestFiles = append(pkg.TestFiles, f)
						file = &pkg.TestFiles[i]
						fileByPath[test.Loc.URI] = file
					}
					file.Tests = append(file.Tests, test)
				}
			}
		}

		return nil
	})
	return result, err
}

func (h *commandHandler) MaybePromptForTelemetry(ctx context.Context) error {
	// if the server's TelemetryPrompt is true, it's likely the server already
	// handled prompting for it. Don't try to prompt again.
	if !h.s.options.TelemetryPrompt {
		go h.s.maybePromptForTelemetry(ctx, true)
	}
	return nil
}

func (*commandHandler) AddTelemetryCounters(_ context.Context, args command.AddTelemetryCountersArgs) error {
	if len(args.Names) != len(args.Values) {
		return fmt.Errorf("Names and Values must have the same length")
	}
	// invalid counter update requests will be silently dropped. (no audience)
	for i, n := range args.Names {
		v := args.Values[i]
		if n == "" || v < 0 {
			continue
		}
		counter.Add("fwd/"+n, v)
	}
	return nil
}

func (c *commandHandler) AddTest(ctx context.Context, loc protocol.Location) (*protocol.WorkspaceEdit, error) {
	var result *protocol.WorkspaceEdit
	err := c.run(ctx, commandConfig{
		forURI: loc.URI,
	}, func(ctx context.Context, deps commandDeps) error {
		if deps.snapshot.FileKind(deps.fh) != file.Go {
			return fmt.Errorf("can't add test for non-Go file")
		}
		docedits, err := golang.AddTestForFunc(ctx, deps.snapshot, loc)
		if err != nil {
			return err
		}
		return applyChanges(ctx, c.s.client, docedits)
	})
	// TODO(hxjiang): move the cursor to the new test once edits applied.
	return result, err
}

// commandConfig configures common command set-up and execution.
type commandConfig struct {
	requireSave   bool                           // whether all files must be saved for the command to work
	progress      string                         // title to use for progress reporting. If empty, no progress will be reported.
	progressStyle settings.WorkDoneProgressStyle // style information for client-side progress display.
	forView       string                         // view to resolve to a snapshot; incompatible with forURI
	forURI        protocol.DocumentURI           // URI to resolve to a snapshot. If unset, snapshot will be nil.
}

// commandDeps is evaluated from a commandConfig. Note that not all fields may
// be populated, depending on which configuration is set. See comments in-line
// for details.
type commandDeps struct {
	snapshot *cache.Snapshot    // present if cfg.forURI or forView was set
	fh       file.Handle        // present if cfg.forURI was set
	work     *progress.WorkDone // present if cfg.progress was set
}

type commandFunc func(context.Context, commandDeps) error

// These strings are reported as the final WorkDoneProgressEnd message
// for each workspace/executeCommand request.
const (
	CommandCanceled  = "canceled"
	CommandFailed    = "failed"
	CommandCompleted = "completed"
)

// run performs command setup for command execution, and invokes the given run
// function. If cfg.async is set, run executes the given func in a separate
// goroutine, and returns as soon as setup is complete and the goroutine is
// scheduled.
//
// Invariant: if the resulting error is non-nil, the given run func will
// (eventually) be executed exactly once.
func (c *commandHandler) run(ctx context.Context, cfg commandConfig, run commandFunc) (err error) {
	if cfg.requireSave {
		var unsaved []string
		for _, overlay := range c.s.session.Overlays() {
			if !overlay.SameContentsOnDisk() {
				unsaved = append(unsaved, overlay.URI().Path())
			}
		}
		if len(unsaved) > 0 {
			return fmt.Errorf("All files must be saved first (unsaved: %v).", unsaved)
		}
	}
	var deps commandDeps
	var release func()
	if cfg.forURI != "" && cfg.forView != "" {
		return bug.Errorf("internal error: forURI=%q, forView=%q", cfg.forURI, cfg.forView)
	}
	if cfg.forURI != "" {
		deps.fh, deps.snapshot, release, err = c.s.session.FileOf(ctx, cfg.forURI)
		if err != nil {
			return err
		}

	} else if cfg.forView != "" {
		view, err := c.s.session.View(cfg.forView)
		if err != nil {
			return err
		}
		deps.snapshot, release, err = view.Snapshot()
		if err != nil {
			return err
		}

	} else {
		release = func() {}
	}
	// Inv: release() must be called exactly once after this point.
	// In the async case, runcmd may outlive run().

	ctx, cancel := context.WithCancel(xcontext.Detach(ctx))
	if cfg.progress != "" {
		header := ""
		if _, ok := c.s.options.SupportedWorkDoneProgressFormats[cfg.progressStyle]; ok && cfg.progressStyle != "" {
			header = fmt.Sprintf("style: %s\n\n", cfg.progressStyle)
		}
		deps.work = c.s.progress.Start(ctx, cfg.progress, header+"Running...", c.params.WorkDoneToken, cancel)
	}
	runcmd := func() error {
		defer release()
		defer cancel()
		err := run(ctx, deps)
		if deps.work != nil {
			switch {
			case errors.Is(err, context.Canceled):
				deps.work.End(ctx, CommandCanceled)
			case err != nil:
				event.Error(ctx, "command error", err)
				deps.work.End(ctx, CommandFailed)
			default:
				deps.work.End(ctx, CommandCompleted)
			}
		}
		return err
	}

	// For legacy reasons, gopls.run_govulncheck must run asynchronously.
	// TODO(golang/vscode-go#3572): remove this (along with the
	// gopls.run_govulncheck command entirely) once VS Code only uses the new
	// gopls.vulncheck command.
	if c.params.Command == "gopls.run_govulncheck" {
		if cfg.progress == "" {
			log.Fatalf("asynchronous command gopls.run_govulncheck does not enable progress reporting")
		}
		go func() {
			if err := runcmd(); err != nil {
				showMessage(ctx, c.s.client, protocol.Error, err.Error())
			}
		}()
		return nil
	}

	return runcmd()
}

func (c *commandHandler) ApplyFix(ctx context.Context, args command.ApplyFixArgs) (*protocol.WorkspaceEdit, error) {
	var result *protocol.WorkspaceEdit
	err := c.run(ctx, commandConfig{
		// Note: no progress here. Applying fixes should be quick.
		forURI: args.Location.URI,
	}, func(ctx context.Context, deps commandDeps) error {
		changes, err := golang.ApplyFix(ctx, args.Fix, deps.snapshot, deps.fh, args.Location.Range)
		if err != nil {
			return err
		}
		wsedit := protocol.NewWorkspaceEdit(changes...)
		if args.ResolveEdits {
			result = wsedit
			return nil
		}
		return applyChanges(ctx, c.s.client, changes)
	})
	return result, err
}

func (c *commandHandler) RegenerateCgo(ctx context.Context, args command.URIArg) error {
	return c.run(ctx, commandConfig{
		progress: "Regenerating Cgo",
	}, func(ctx context.Context, _ commandDeps) error {
		return c.modifyState(ctx, FromRegenerateCgo, func() (*cache.Snapshot, func(), error) {
			// Resetting the view causes cgo to be regenerated via `go list`.
			v, err := c.s.session.ResetView(ctx, args.URI)
			if err != nil {
				return nil, nil, err
			}
			return v.Snapshot()
		})
	})
}

// modifyState performs an operation that modifies the snapshot state.
//
// It causes a snapshot diagnosis for the provided ModificationSource.
func (c *commandHandler) modifyState(ctx context.Context, source ModificationSource, work func() (*cache.Snapshot, func(), error)) error {
	var wg sync.WaitGroup // tracks work done on behalf of this function, incl. diagnostics
	wg.Add(1)
	defer wg.Done()

	// Track progress on this operation for testing.
	if c.s.Options().VerboseWorkDoneProgress {
		work := c.s.progress.Start(ctx, DiagnosticWorkTitle(source), "Calculating file diagnostics...", nil, nil)
		go func() {
			wg.Wait()
			work.End(ctx, "Done.")
		}()
	}
	snapshot, release, err := work()
	if err != nil {
		return err
	}
	wg.Add(1)
	go func() {
		// Diagnosing with the background context ensures new snapshots are fully
		// diagnosed.
		c.s.diagnoseSnapshot(snapshot.BackgroundContext(), snapshot, nil, 0)
		release()
		wg.Done()
	}()
	return nil
}

func (c *commandHandler) CheckUpgrades(ctx context.Context, args command.CheckUpgradesArgs) error {
	return c.run(ctx, commandConfig{
		forURI:   args.URI,
		progress: "Checking for upgrades",
	}, func(ctx context.Context, deps commandDeps) error {
		return c.modifyState(ctx, FromCheckUpgrades, func() (*cache.Snapshot, func(), error) {
			upgrades, err := c.s.getUpgrades(ctx, deps.snapshot, args.URI, args.Modules)
			if err != nil {
				return nil, nil, err
			}
			return c.s.session.InvalidateView(ctx, deps.snapshot.View(), cache.StateChange{
				ModuleUpgrades: map[protocol.DocumentURI]map[string]string{args.URI: upgrades},
			})
		})
	})
}

func (c *commandHandler) AddDependency(ctx context.Context, args command.DependencyArgs) error {
	return c.GoGetModule(ctx, args)
}

func (c *commandHandler) UpgradeDependency(ctx context.Context, args command.DependencyArgs) error {
	return c.GoGetModule(ctx, args)
}

func (c *commandHandler) ResetGoModDiagnostics(ctx context.Context, args command.ResetGoModDiagnosticsArgs) error {
	return c.run(ctx, commandConfig{
		forURI: args.URI,
	}, func(ctx context.Context, deps commandDeps) error {
		return c.modifyState(ctx, FromResetGoModDiagnostics, func() (*cache.Snapshot, func(), error) {
			return c.s.session.InvalidateView(ctx, deps.snapshot.View(), cache.StateChange{
				ModuleUpgrades: map[protocol.DocumentURI]map[string]string{
					deps.fh.URI(): nil,
				},
				Vulns: map[protocol.DocumentURI]*vulncheck.Result{
					deps.fh.URI(): nil,
				},
			})
		})
	})
}

func (c *commandHandler) GoGetModule(ctx context.Context, args command.DependencyArgs) error {
	return c.run(ctx, commandConfig{
		progress: "Running go get",
		forURI:   args.URI,
	}, func(ctx context.Context, deps commandDeps) error {
		return c.s.runGoModUpdateCommands(ctx, deps.snapshot, args.URI, func(invoke func(...string) (*bytes.Buffer, error)) error {
			return runGoGetModule(invoke, args.AddRequire, args.GoCmdArgs)
		})
	})
}

// TODO(rFindley): UpdateGoSum, Tidy, and Vendor could probably all be one command.
func (c *commandHandler) UpdateGoSum(ctx context.Context, args command.URIArgs) error {
	return c.run(ctx, commandConfig{
		progress: "Updating go.sum",
	}, func(ctx context.Context, _ commandDeps) error {
		for _, uri := range args.URIs {
			fh, snapshot, release, err := c.s.session.FileOf(ctx, uri)
			if err != nil {
				return err
			}
			defer release()
			if err := c.s.runGoModUpdateCommands(ctx, snapshot, fh.URI(), func(invoke func(...string) (*bytes.Buffer, error)) error {
				_, err := invoke("list", "all")
				return err
			}); err != nil {
				return err
			}
		}
		return nil
	})
}

func (c *commandHandler) Tidy(ctx context.Context, args command.URIArgs) error {
	return c.run(ctx, commandConfig{
		progress: "Running go mod tidy",
	}, func(ctx context.Context, _ commandDeps) error {
		for _, uri := range args.URIs {
			fh, snapshot, release, err := c.s.session.FileOf(ctx, uri)
			if err != nil {
				return err
			}
			defer release()
			if err := c.s.runGoModUpdateCommands(ctx, snapshot, fh.URI(), func(invoke func(...string) (*bytes.Buffer, error)) error {
				_, err := invoke("mod", "tidy")
				return err
			}); err != nil {
				return err
			}
		}
		return nil
	})
}

func (c *commandHandler) Vendor(ctx context.Context, args command.URIArg) error {
	return c.run(ctx, commandConfig{
		requireSave: true, // TODO(adonovan): probably not needed; but needs a test.
		progress:    "Running go mod vendor",
		forURI:      args.URI,
	}, func(ctx context.Context, deps commandDeps) error {
		// Use RunGoCommandPiped here so that we don't compete with any other go
		// command invocations. go mod vendor deletes modules.txt before recreating
		// it, and therefore can run into file locking issues on Windows if that
		// file is in use by another process, such as go list.
		//
		// If golang/go#44119 is resolved, go mod vendor will instead modify
		// modules.txt in-place. In that case we could theoretically allow this
		// command to run concurrently.
		stderr := new(bytes.Buffer)
		inv, cleanupInvocation, err := deps.snapshot.GoCommandInvocation(cache.NetworkOK, args.URI.DirPath(), "mod", []string{"vendor"})
		if err != nil {
			return err
		}
		defer cleanupInvocation()
		err = deps.snapshot.View().GoCommandRunner().RunPiped(ctx, *inv, &bytes.Buffer{}, stderr)
		if err != nil {
			return fmt.Errorf("running go mod vendor failed: %v\nstderr:\n%s", err, stderr.String())
		}
		return nil
	})
}

func (c *commandHandler) EditGoDirective(ctx context.Context, args command.EditGoDirectiveArgs) error {
	return c.run(ctx, commandConfig{
		requireSave: true, // if go.mod isn't saved it could cause a problem
		forURI:      args.URI,
	}, func(ctx context.Context, _ commandDeps) error {
		fh, snapshot, release, err := c.s.session.FileOf(ctx, args.URI)
		if err != nil {
			return err
		}
		defer release()
		if err := c.s.runGoModUpdateCommands(ctx, snapshot, fh.URI(), func(invoke func(...string) (*bytes.Buffer, error)) error {
			_, err := invoke("mod", "edit", "-go", args.Version)
			return err
		}); err != nil {
			return err
		}
		return nil
	})
}

func (c *commandHandler) RemoveDependency(ctx context.Context, args command.RemoveDependencyArgs) error {
	return c.run(ctx, commandConfig{
		progress: "Removing dependency",
		forURI:   args.URI,
	}, func(ctx context.Context, deps commandDeps) error {
		// See the documentation for OnlyDiagnostic.
		//
		// TODO(rfindley): In Go 1.17+, we will be able to use the go command
		// without checking if the module is tidy.
		if args.OnlyDiagnostic {
			return c.s.runGoModUpdateCommands(ctx, deps.snapshot, args.URI, func(invoke func(...string) (*bytes.Buffer, error)) error {
				if err := runGoGetModule(invoke, false, []string{args.ModulePath + "@none"}); err != nil {
					return err
				}
				_, err := invoke("mod", "tidy")
				return err
			})
		}
		pm, err := deps.snapshot.ParseMod(ctx, deps.fh)
		if err != nil {
			return err
		}
		edits, err := dropDependency(pm, args.ModulePath)
		if err != nil {
			return err
		}
		return applyChanges(ctx, c.s.client, []protocol.DocumentChange{protocol.DocumentChangeEdit(deps.fh, edits)})
	})
}

// dropDependency returns the edits to remove the given require from the go.mod
// file.
func dropDependency(pm *cache.ParsedModule, modulePath string) ([]protocol.TextEdit, error) {
	// We need a private copy of the parsed go.mod file, since we're going to
	// modify it.
	copied, err := modfile.Parse("", pm.Mapper.Content, nil)
	if err != nil {
		return nil, err
	}
	if err := copied.DropRequire(modulePath); err != nil {
		return nil, err
	}
	copied.Cleanup()
	newContent, err := copied.Format()
	if err != nil {
		return nil, err
	}
	// Calculate the edits to be made due to the change.
	diff := diff.Bytes(pm.Mapper.Content, newContent)
	return protocol.EditsFromDiffEdits(pm.Mapper, diff)
}

func (c *commandHandler) Doc(ctx context.Context, args command.DocArgs) (protocol.URI, error) {
	if args.Location.URI == "" {
		return "", errors.New("missing location URI")
	}

	var result protocol.URI
	err := c.run(ctx, commandConfig{
		progress: "", // the operation should be fast
		forURI:   args.Location.URI,
	}, func(ctx context.Context, deps commandDeps) error {
		pkg, pgf, err := golang.NarrowestPackageForFile(ctx, deps.snapshot, args.Location.URI)
		if err != nil {
			return err
		}
		start, end, err := pgf.RangePos(args.Location.Range)
		if err != nil {
			return err
		}

		// Start web server.
		web, err := c.s.getWeb()
		if err != nil {
			return err
		}

		// Compute package path and optional symbol fragment
		// (e.g. "#Buffer.Len") from the the selection.
		pkgpath, fragment, _ := golang.DocFragment(pkg, pgf, start, end)

		// Direct the client to open the /pkg page.
		result = web.PkgURL(deps.snapshot.View().ID(), pkgpath, fragment)
		if args.ShowDocument {
			openClientBrowser(ctx, c.s.client, "Doc", result, c.s.Options())
		}

		return nil
	})
	return result, err
}

func (c *commandHandler) RunTests(ctx context.Context, args command.RunTestsArgs) error {
	return c.run(ctx, commandConfig{
		progress:    "Running go test", // (asynchronous)
		requireSave: true,              // go test honors overlays, but tests themselves cannot
		forURI:      args.URI,
	}, func(ctx context.Context, deps commandDeps) error {
		jsonrpc2.Async(ctx) // don't block RPCs behind this command, since it can take a while
		return c.runTests(ctx, deps.snapshot, deps.work, args.URI, args.Tests, args.Benchmarks)
	})
}

func (c *commandHandler) runTests(ctx context.Context, snapshot *cache.Snapshot, work *progress.WorkDone, uri protocol.DocumentURI, tests, benchmarks []string) error {
	// TODO: fix the error reporting when this runs async.
	meta, err := snapshot.NarrowestMetadataForFile(ctx, uri)
	if err != nil {
		return err
	}
	pkgPath := string(meta.ForTest)

	// create output
	buf := &bytes.Buffer{}
	ew := progress.NewEventWriter(ctx, "test")
	out := io.MultiWriter(ew, progress.NewWorkDoneWriter(ctx, work), buf)

	// Run `go test -run Func` on each test.
	var failedTests int
	for _, funcName := range tests {
		args := []string{pkgPath, "-v", "-count=1", fmt.Sprintf("-run=^%s$", regexp.QuoteMeta(funcName))}
		inv, cleanupInvocation, err := snapshot.GoCommandInvocation(cache.NoNetwork, uri.DirPath(), "test", args)
		if err != nil {
			return err
		}
		defer cleanupInvocation()
		if err := snapshot.View().GoCommandRunner().RunPiped(ctx, *inv, out, out); err != nil {
			if errors.Is(err, context.Canceled) {
				return err
			}
			failedTests++
		}
	}

	// Run `go test -run=^$ -bench Func` on each test.
	var failedBenchmarks int
	for _, funcName := range benchmarks {
		inv, cleanupInvocation, err := snapshot.GoCommandInvocation(cache.NoNetwork, uri.DirPath(), "test", []string{
			pkgPath, "-v", "-run=^$", fmt.Sprintf("-bench=^%s$", regexp.QuoteMeta(funcName)),
		})
		if err != nil {
			return err
		}
		defer cleanupInvocation()
		if err := snapshot.View().GoCommandRunner().RunPiped(ctx, *inv, out, out); err != nil {
			if errors.Is(err, context.Canceled) {
				return err
			}
			failedBenchmarks++
		}
	}

	var title string
	if len(tests) > 0 && len(benchmarks) > 0 {
		title = "tests and benchmarks"
	} else if len(tests) > 0 {
		title = "tests"
	} else if len(benchmarks) > 0 {
		title = "benchmarks"
	} else {
		return errors.New("No functions were provided")
	}
	message := fmt.Sprintf("all %s passed", title)
	if failedTests > 0 && failedBenchmarks > 0 {
		message = fmt.Sprintf("%d / %d tests failed and %d / %d benchmarks failed", failedTests, len(tests), failedBenchmarks, len(benchmarks))
	} else if failedTests > 0 {
		message = fmt.Sprintf("%d / %d tests failed", failedTests, len(tests))
	} else if failedBenchmarks > 0 {
		message = fmt.Sprintf("%d / %d benchmarks failed", failedBenchmarks, len(benchmarks))
	}
	if failedTests > 0 || failedBenchmarks > 0 {
		message += "\n" + buf.String()
	}

	showMessage(ctx, c.s.client, protocol.Info, message)

	if failedTests > 0 || failedBenchmarks > 0 {
		return errors.New("gopls.test command failed")
	}
	return nil
}

func (c *commandHandler) Generate(ctx context.Context, args command.GenerateArgs) error {
	title := "Running go generate ."
	if args.Recursive {
		title = "Running go generate ./..."
	}
	return c.run(ctx, commandConfig{
		requireSave: true, // commands executed by go generate cannot honor overlays
		progress:    title,
		forURI:      args.Dir,
	}, func(ctx context.Context, deps commandDeps) error {
		er := progress.NewEventWriter(ctx, "generate")

		pattern := "."
		if args.Recursive {
			pattern = "./..."
		}
		inv, cleanupInvocation, err := deps.snapshot.GoCommandInvocation(cache.NetworkOK, args.Dir.Path(), "generate", []string{"-x", pattern})
		if err != nil {
			return err
		}
		defer cleanupInvocation()
		stderr := io.MultiWriter(er, progress.NewWorkDoneWriter(ctx, deps.work))
		if err := deps.snapshot.View().GoCommandRunner().RunPiped(ctx, *inv, er, stderr); err != nil {
			return err
		}
		return nil
	})
}

func (c *commandHandler) GoGetPackage(ctx context.Context, args command.GoGetPackageArgs) error {
	return c.run(ctx, commandConfig{
		forURI:   args.URI,
		progress: "Running go get",
	}, func(ctx context.Context, deps commandDeps) error {
		snapshot := deps.snapshot
		modURI := snapshot.GoModForFile(args.URI)
		if modURI == "" {
			return fmt.Errorf("no go.mod file found for %s", args.URI)
		}
		tempDir, cleanupModDir, err := cache.TempModDir(ctx, snapshot, modURI)
		if err != nil {
			return fmt.Errorf("creating a temp go.mod: %v", err)
		}
		defer cleanupModDir()

		inv, cleanupInvocation, err := snapshot.GoCommandInvocation(cache.NetworkOK, modURI.DirPath(), "list",
			[]string{"-f", "{{.Module.Path}}@{{.Module.Version}}", "-mod=mod", "-modfile=" + filepath.Join(tempDir, "go.mod"), args.Pkg},
			"GOWORK=off",
		)
		if err != nil {
			return err
		}
		defer cleanupInvocation()
		stdout, err := snapshot.View().GoCommandRunner().Run(ctx, *inv)
		if err != nil {
			return err
		}
		ver := strings.TrimSpace(stdout.String())
		return c.s.runGoModUpdateCommands(ctx, snapshot, args.URI, func(invoke func(...string) (*bytes.Buffer, error)) error {
			if args.AddRequire {
				if err := addModuleRequire(invoke, []string{ver}); err != nil {
					return err
				}
			}
			_, err := invoke(append([]string{"get", "-d"}, args.Pkg)...)
			return err
		})
	})
}

func (s *server) runGoModUpdateCommands(ctx context.Context, snapshot *cache.Snapshot, uri protocol.DocumentURI, run func(invoke func(...string) (*bytes.Buffer, error)) error) error {
	// TODO(rfindley): can/should this use findRootPattern?
	modURI := snapshot.GoModForFile(uri)
	if modURI == "" {
		return fmt.Errorf("no go.mod file found for %s", uri.Path())
	}
	newModBytes, newSumBytes, err := snapshot.RunGoModUpdateCommands(ctx, modURI, run)
	if err != nil {
		return err
	}
	sumURI := protocol.URIFromPath(strings.TrimSuffix(modURI.Path(), ".mod") + ".sum")

	modChange, err := computeEditChange(ctx, snapshot, modURI, newModBytes)
	if err != nil {
		return err
	}
	sumChange, err := computeEditChange(ctx, snapshot, sumURI, newSumBytes)
	if err != nil {
		return err
	}

	var changes []protocol.DocumentChange
	if modChange.Valid() {
		changes = append(changes, modChange)
	}
	if sumChange.Valid() {
		changes = append(changes, sumChange)
	}
	return applyChanges(ctx, s.client, changes)
}

// computeEditChange computes the edit change required to transform the
// snapshot file specified by uri to the provided new content.
// Beware: returns a DocumentChange that is !Valid() if none were necessary.
//
// If the file is not open, computeEditChange simply writes the new content to
// disk.
//
// TODO(rfindley): fix this API asymmetry. It should be up to the caller to
// write the file or apply the edits.
func computeEditChange(ctx context.Context, snapshot *cache.Snapshot, uri protocol.DocumentURI, newContent []byte) (protocol.DocumentChange, error) {
	fh, err := snapshot.ReadFile(ctx, uri)
	if err != nil {
		return protocol.DocumentChange{}, err
	}
	oldContent, err := fh.Content()
	if err != nil && !os.IsNotExist(err) {
		return protocol.DocumentChange{}, err
	}

	if bytes.Equal(oldContent, newContent) {
		return protocol.DocumentChange{}, nil // note: result is !Valid()
	}

	// Sending a workspace edit to a closed file causes VS Code to open the
	// file and leave it unsaved. We would rather apply the changes directly,
	// especially to go.sum, which should be mostly invisible to the user.
	if !snapshot.IsOpen(uri) {
		err := os.WriteFile(uri.Path(), newContent, 0666)
		return protocol.DocumentChange{}, err
	}

	m := protocol.NewMapper(fh.URI(), oldContent)
	diff := diff.Bytes(oldContent, newContent)
	textedits, err := protocol.EditsFromDiffEdits(m, diff)
	if err != nil {
		return protocol.DocumentChange{}, err
	}
	return protocol.DocumentChangeEdit(fh, textedits), nil
}

func applyChanges(ctx context.Context, cli protocol.Client, changes []protocol.DocumentChange) error {
	if len(changes) == 0 {
		return nil
	}
	response, err := cli.ApplyEdit(ctx, &protocol.ApplyWorkspaceEditParams{
		Edit: *protocol.NewWorkspaceEdit(changes...),
	})
	if err != nil {
		return err
	}
	if !response.Applied {
		return fmt.Errorf("edits not applied because of %s", response.FailureReason)
	}
	return nil
}

func runGoGetModule(invoke func(...string) (*bytes.Buffer, error), addRequire bool, args []string) error {
	if addRequire {
		if err := addModuleRequire(invoke, args); err != nil {
			return err
		}
	}
	_, err := invoke(append([]string{"get", "-d"}, args...)...)
	return err
}

func addModuleRequire(invoke func(...string) (*bytes.Buffer, error), args []string) error {
	// Using go get to create a new dependency results in an
	// `// indirect` comment we may not want. The only way to avoid it
	// is to add the require as direct first. Then we can use go get to
	// update go.sum and tidy up.
	_, err := invoke(append([]string{"mod", "edit", "-require"}, args...)...)
	return err
}

// TODO(rfindley): inline.
func (s *server) getUpgrades(ctx context.Context, snapshot *cache.Snapshot, uri protocol.DocumentURI, modules []string) (map[string]string, error) {
	args := append([]string{"-mod=readonly", "-m", "-u", "-json"}, modules...)
	inv, cleanup, err := snapshot.GoCommandInvocation(cache.NetworkOK, uri.DirPath(), "list", args)
	if err != nil {
		return nil, err
	}
	defer cleanup()
	stdout, err := snapshot.View().GoCommandRunner().Run(ctx, *inv)
	if err != nil {
		return nil, err
	}

	upgrades := map[string]string{}
	for dec := json.NewDecoder(stdout); dec.More(); {
		mod := &gocommand.ModuleJSON{}
		if err := dec.Decode(mod); err != nil {
			return nil, err
		}
		if mod.Update == nil {
			continue
		}
		upgrades[mod.Path] = mod.Update.Version
	}
	return upgrades, nil
}

func (c *commandHandler) GCDetails(ctx context.Context, uri protocol.DocumentURI) error {
	return c.run(ctx, commandConfig{
		forURI: uri,
	}, func(ctx context.Context, deps commandDeps) error {
		return c.modifyState(ctx, FromToggleCompilerOptDetails, func() (*cache.Snapshot, func(), error) {
			// Don't blindly use "dir := deps.fh.URI().Dir()"; validate.
			meta, err := deps.snapshot.NarrowestMetadataForFile(ctx, deps.fh.URI())
			if err != nil {
				return nil, nil, err
			}
			if len(meta.CompiledGoFiles) == 0 {
				return nil, nil, fmt.Errorf("package %q does not compile file %q", meta.ID, deps.fh.URI())
			}
			dir := meta.CompiledGoFiles[0].Dir()

			want := !deps.snapshot.WantCompilerOptDetails(dir) // toggle per-directory flag
			return c.s.session.InvalidateView(ctx, deps.snapshot.View(), cache.StateChange{
				CompilerOptDetails: map[protocol.DocumentURI]bool{dir: want},
			})
		})
	})
}

func (c *commandHandler) ListKnownPackages(ctx context.Context, args command.URIArg) (command.ListKnownPackagesResult, error) {
	var result command.ListKnownPackagesResult
	err := c.run(ctx, commandConfig{
		progress: "Listing packages",
		forURI:   args.URI,
	}, func(ctx context.Context, deps commandDeps) error {
		pkgs, err := golang.KnownPackagePaths(ctx, deps.snapshot, deps.fh)
		for _, pkg := range pkgs {
			result.Packages = append(result.Packages, string(pkg))
		}
		return err
	})
	return result, err
}

func (c *commandHandler) ListImports(ctx context.Context, args command.URIArg) (command.ListImportsResult, error) {
	var result command.ListImportsResult
	err := c.run(ctx, commandConfig{
		forURI: args.URI,
	}, func(ctx context.Context, deps commandDeps) error {
		fh, err := deps.snapshot.ReadFile(ctx, args.URI)
		if err != nil {
			return err
		}
		pgf, err := deps.snapshot.ParseGo(ctx, fh, parsego.Header)
		if err != nil {
			return err
		}
		fset := tokeninternal.FileSetFor(pgf.Tok)
		for _, group := range astutil.Imports(fset, pgf.File) {
			for _, imp := range group {
				if imp.Path == nil {
					continue
				}
				var name string
				if imp.Name != nil {
					name = imp.Name.Name
				}
				result.Imports = append(result.Imports, command.FileImport{
					Path: string(metadata.UnquoteImportPath(imp)),
					Name: name,
				})
			}
		}
		meta, err := deps.snapshot.NarrowestMetadataForFile(ctx, args.URI)
		if err != nil {
			return err // e.g. cancelled
		}
		for pkgPath := range meta.DepsByPkgPath {
			result.PackageImports = append(result.PackageImports,
				command.PackageImport{Path: string(pkgPath)})
		}
		sort.Slice(result.PackageImports, func(i, j int) bool {
			return result.PackageImports[i].Path < result.PackageImports[j].Path
		})
		return nil
	})
	return result, err
}

func (c *commandHandler) AddImport(ctx context.Context, args command.AddImportArgs) error {
	return c.run(ctx, commandConfig{
		progress: "Adding import",
		forURI:   args.URI,
	}, func(ctx context.Context, deps commandDeps) error {
		edits, err := golang.AddImport(ctx, deps.snapshot, deps.fh, args.ImportPath)
		if err != nil {
			return fmt.Errorf("could not add import: %v", err)
		}
		return applyChanges(ctx, c.s.client, []protocol.DocumentChange{protocol.DocumentChangeEdit(deps.fh, edits)})
	})
}

func (c *commandHandler) ExtractToNewFile(ctx context.Context, args protocol.Location) error {
	return c.run(ctx, commandConfig{
		progress: "Extract to a new file",
		forURI:   args.URI,
	}, func(ctx context.Context, deps commandDeps) error {
		changes, err := golang.ExtractToNewFile(ctx, deps.snapshot, deps.fh, args.Range)
		if err != nil {
			return err
		}
		return applyChanges(ctx, c.s.client, changes)
	})
}

func (c *commandHandler) StartDebugging(ctx context.Context, args command.DebuggingArgs) (result command.DebuggingResult, _ error) {
	addr := args.Addr
	if addr == "" {
		addr = "localhost:0"
	}
	di := debug.GetInstance(ctx)
	if di == nil {
		return result, errors.New("internal error: server has no debugging instance")
	}
	listenedAddr, err := di.Serve(ctx, addr)
	if err != nil {
		return result, fmt.Errorf("starting debug server: %w", err)
	}
	result.URLs = []string{"http://" + listenedAddr}
	openClientBrowser(ctx, c.s.client, "Debug", result.URLs[0], c.s.Options())
	return result, nil
}

func (c *commandHandler) StartProfile(ctx context.Context, args command.StartProfileArgs) (result command.StartProfileResult, _ error) {
	file, err := os.CreateTemp("", "gopls-profile-*")
	if err != nil {
		return result, fmt.Errorf("creating temp profile file: %v", err)
	}

	c.s.ongoingProfileMu.Lock()
	defer c.s.ongoingProfileMu.Unlock()

	if c.s.ongoingProfile != nil {
		file.Close() // ignore error
		return result, fmt.Errorf("profile already started (for %q)", c.s.ongoingProfile.Name())
	}

	if err := pprof.StartCPUProfile(file); err != nil {
		file.Close() // ignore error
		return result, fmt.Errorf("starting profile: %v", err)
	}

	c.s.ongoingProfile = file
	return result, nil
}

func (c *commandHandler) StopProfile(ctx context.Context, args command.StopProfileArgs) (result command.StopProfileResult, _ error) {
	c.s.ongoingProfileMu.Lock()
	defer c.s.ongoingProfileMu.Unlock()

	prof := c.s.ongoingProfile
	c.s.ongoingProfile = nil

	if prof == nil {
		return result, fmt.Errorf("no ongoing profile")
	}

	pprof.StopCPUProfile()
	if err := prof.Close(); err != nil {
		return result, fmt.Errorf("closing profile file: %v", err)
	}
	result.File = prof.Name()
	return result, nil
}

func (c *commandHandler) FetchVulncheckResult(ctx context.Context, arg command.URIArg) (map[protocol.DocumentURI]*vulncheck.Result, error) {
	ret := map[protocol.DocumentURI]*vulncheck.Result{}
	err := c.run(ctx, commandConfig{forURI: arg.URI}, func(ctx context.Context, deps commandDeps) error {
		if deps.snapshot.Options().Vulncheck == settings.ModeVulncheckImports {
			for _, modfile := range deps.snapshot.View().ModFiles() {
				res, err := deps.snapshot.ModVuln(ctx, modfile)
				if err != nil {
					return err
				}
				ret[modfile] = res
			}
		}
		// Overwrite if there is any govulncheck-based result.
		maps.Copy(ret, deps.snapshot.Vulnerabilities())
		return nil
	})
	return ret, err
}

const GoVulncheckCommandTitle = "govulncheck"

func (c *commandHandler) Vulncheck(ctx context.Context, args command.VulncheckArgs) (command.VulncheckResult, error) {
	if args.URI == "" {
		return command.VulncheckResult{}, errors.New("VulncheckArgs is missing URI field")
	}

	var commandResult command.VulncheckResult
	err := c.run(ctx, commandConfig{
		progress:      GoVulncheckCommandTitle,
		progressStyle: settings.WorkDoneProgressStyleLog,
		requireSave:   true, // govulncheck cannot honor overlays
		forURI:        args.URI,
	}, func(ctx context.Context, deps commandDeps) error {
		jsonrpc2.Async(ctx) // run this in parallel with other requests: vulncheck can be slow.

		workDoneWriter := progress.NewWorkDoneWriter(ctx, deps.work)
		dir := args.URI.DirPath()
		pattern := args.Pattern

		result, err := scan.RunGovulncheck(ctx, pattern, deps.snapshot, dir, workDoneWriter)
		if err != nil {
			return err
		}
		commandResult.Result = result
		commandResult.Token = deps.work.Token()

		snapshot, release, err := c.s.session.InvalidateView(ctx, deps.snapshot.View(), cache.StateChange{
			Vulns: map[protocol.DocumentURI]*vulncheck.Result{args.URI: result},
		})
		if err != nil {
			return err
		}
		defer release()

		// Diagnosing with the background context ensures new snapshots are fully
		// diagnosed.
		c.s.diagnoseSnapshot(snapshot.BackgroundContext(), snapshot, nil, 0)

		affecting := make(map[string]bool, len(result.Entries))
		for _, finding := range result.Findings {
			if len(finding.Trace) > 1 { // at least 2 frames if callstack exists (vulnerability, entry)
				affecting[finding.OSV] = true
			}
		}
		if len(affecting) == 0 {
			showMessage(ctx, c.s.client, protocol.Info, "No vulnerabilities found")
			return nil
		}
		affectingOSVs := make([]string, 0, len(affecting))
		for id := range affecting {
			affectingOSVs = append(affectingOSVs, id)
		}
		sort.Strings(affectingOSVs)

		showMessage(ctx, c.s.client, protocol.Warning, fmt.Sprintf("Found %v", strings.Join(affectingOSVs, ", ")))

		return nil
	})
	if err != nil {
		return command.VulncheckResult{}, err
	}
	return commandResult, nil
}

// RunGovulncheck is like Vulncheck (in fact, a copy), but is tweaked slightly
// to run asynchronously rather than return a result.
//
// This logic was copied, rather than factored out, as this implementation is
// slated for deletion.
//
// TODO(golang/vscode-go#3572)
// TODO(hxjiang): deprecate gopls.run_govulncheck.
func (c *commandHandler) RunGovulncheck(ctx context.Context, args command.VulncheckArgs) (command.RunVulncheckResult, error) {
	if args.URI == "" {
		return command.RunVulncheckResult{}, errors.New("VulncheckArgs is missing URI field")
	}

	// Return the workdone token so that clients can identify when this
	// vulncheck invocation is complete.
	//
	// Since the run function executes asynchronously, we use a channel to
	// synchronize the start of the run and return the token.
	tokenChan := make(chan protocol.ProgressToken, 1)
	err := c.run(ctx, commandConfig{
		progress:    GoVulncheckCommandTitle,
		requireSave: true, // govulncheck cannot honor overlays
		forURI:      args.URI,
	}, func(ctx context.Context, deps commandDeps) error {
		tokenChan <- deps.work.Token()

		workDoneWriter := progress.NewWorkDoneWriter(ctx, deps.work)
		dir := filepath.Dir(args.URI.Path())
		pattern := args.Pattern

		result, err := scan.RunGovulncheck(ctx, pattern, deps.snapshot, dir, workDoneWriter)
		if err != nil {
			return err
		}

		snapshot, release, err := c.s.session.InvalidateView(ctx, deps.snapshot.View(), cache.StateChange{
			Vulns: map[protocol.DocumentURI]*vulncheck.Result{args.URI: result},
		})
		if err != nil {
			return err
		}
		defer release()

		// Diagnosing with the background context ensures new snapshots are fully
		// diagnosed.
		c.s.diagnoseSnapshot(snapshot.BackgroundContext(), snapshot, nil, 0)

		affecting := make(map[string]bool, len(result.Entries))
		for _, finding := range result.Findings {
			if len(finding.Trace) > 1 { // at least 2 frames if callstack exists (vulnerability, entry)
				affecting[finding.OSV] = true
			}
		}
		if len(affecting) == 0 {
			showMessage(ctx, c.s.client, protocol.Info, "No vulnerabilities found")
			return nil
		}
		affectingOSVs := make([]string, 0, len(affecting))
		for id := range affecting {
			affectingOSVs = append(affectingOSVs, id)
		}
		sort.Strings(affectingOSVs)

		showMessage(ctx, c.s.client, protocol.Warning, fmt.Sprintf("Found %v", strings.Join(affectingOSVs, ", ")))

		return nil
	})
	if err != nil {
		return command.RunVulncheckResult{}, err
	}
	select {
	case <-ctx.Done():
		return command.RunVulncheckResult{}, ctx.Err()
	case token := <-tokenChan:
		return command.RunVulncheckResult{Token: token}, nil
	}
}

// MemStats implements the MemStats command. It returns an error as a
// future-proof API, but the resulting error is currently always nil.
func (c *commandHandler) MemStats(ctx context.Context) (command.MemStatsResult, error) {
	// GC a few times for stable results.
	runtime.GC()
	runtime.GC()
	runtime.GC()
	var m runtime.MemStats
	runtime.ReadMemStats(&m)
	return command.MemStatsResult{
		HeapAlloc:  m.HeapAlloc,
		HeapInUse:  m.HeapInuse,
		TotalAlloc: m.TotalAlloc,
	}, nil
}

// WorkspaceStats implements the WorkspaceStats command, reporting information
// about the current state of the loaded workspace for the current session.
func (c *commandHandler) WorkspaceStats(ctx context.Context) (command.WorkspaceStatsResult, error) {
	var res command.WorkspaceStatsResult
	res.Files = c.s.session.Cache().FileStats()

	for _, view := range c.s.session.Views() {
		vs, err := collectViewStats(ctx, view)
		if err != nil {
			return res, err
		}
		res.Views = append(res.Views, vs)
	}
	return res, nil
}

func collectViewStats(ctx context.Context, view *cache.View) (command.ViewStats, error) {
	s, release, err := view.Snapshot()
	if err != nil {
		return command.ViewStats{}, err
	}
	defer release()

	allMD, err := s.AllMetadata(ctx)
	if err != nil {
		return command.ViewStats{}, err
	}
	allPackages := collectPackageStats(allMD)

	wsMD, err := s.WorkspaceMetadata(ctx)
	if err != nil {
		return command.ViewStats{}, err
	}
	workspacePackages := collectPackageStats(wsMD)

	var ids []golang.PackageID
	for _, mp := range wsMD {
		ids = append(ids, mp.ID)
	}

	diags, err := s.PackageDiagnostics(ctx, ids...)
	if err != nil {
		return command.ViewStats{}, err
	}

	ndiags := 0
	for _, d := range diags {
		ndiags += len(d)
	}

	return command.ViewStats{
		GoCommandVersion:  view.GoVersionString(),
		AllPackages:       allPackages,
		WorkspacePackages: workspacePackages,
		Diagnostics:       ndiags,
	}, nil
}

func collectPackageStats(mps []*metadata.Package) command.PackageStats {
	var stats command.PackageStats
	stats.Packages = len(mps)
	modules := make(map[string]bool)

	for _, mp := range mps {
		n := len(mp.CompiledGoFiles)
		stats.CompiledGoFiles += n
		if n > stats.LargestPackage {
			stats.LargestPackage = n
		}
		if mp.Module != nil {
			modules[mp.Module.Path] = true
		}
	}
	stats.Modules = len(modules)

	return stats
}

// RunGoWorkCommand invokes `go work <args>` with the provided arguments.
//
// args.InitFirst controls whether to first run `go work init`. This allows a
// single command to both create and recursively populate a go.work file -- as
// of writing there is no `go work init -r`.
//
// Some thought went into implementing this command. Unlike the go.mod commands
// above, this command simply invokes the go command and relies on the client
// to notify gopls of file changes via didChangeWatchedFile notifications.
// We could instead run these commands with GOWORK set to a temp file, but that
// poses the following problems:
//   - directory locations in the resulting temp go.work file will be computed
//     relative to the directory containing that go.work. If the go.work is in a
//     tempdir, the directories will need to be translated to/from that dir.
//   - it would be simpler to use a temp go.work file in the workspace
//     directory, or whichever directory contains the real go.work file, but
//     that sets a bad precedent of writing to a user-owned directory. We
//     shouldn't start doing that.
//   - Sending workspace edits to create a go.work file would require using
//     the CreateFile resource operation, which would need to be tested in every
//     client as we haven't used it before. We don't have time for that right
//     now.
//
// Therefore, we simply require that the current go.work file is saved (if it
// exists), and delegate to the go command.
func (c *commandHandler) RunGoWorkCommand(ctx context.Context, args command.RunGoWorkArgs) error {
	return c.run(ctx, commandConfig{
		progress: "Running go work command",
		forView:  args.ViewID,
	}, func(ctx context.Context, deps commandDeps) (runErr error) {
		snapshot := deps.snapshot
		view := snapshot.View()
		viewDir := snapshot.Folder().Path()

		if view.Type() != cache.GoWorkView && view.GoWork() != "" {
			// If we are not using an existing go.work file, GOWORK must be explicitly off.
			// TODO(rfindley): what about GO111MODULE=off?
			return fmt.Errorf("cannot modify go.work files when GOWORK=off")
		}

		var gowork string
		// If the user has explicitly set GOWORK=off, we should warn them
		// explicitly and avoid potentially misleading errors below.
		if view.GoWork() != "" {
			gowork = view.GoWork().Path()
			fh, err := snapshot.ReadFile(ctx, view.GoWork())
			if err != nil {
				return err // e.g. canceled
			}
			if !fh.SameContentsOnDisk() {
				return fmt.Errorf("must save workspace file %s before running go work commands", view.GoWork())
			}
		} else {
			if !args.InitFirst {
				// If go.work does not exist, we should have detected that and asked
				// for InitFirst.
				return bug.Errorf("internal error: cannot run go work command: required go.work file not found")
			}
			gowork = filepath.Join(viewDir, "go.work")
			if err := c.invokeGoWork(ctx, viewDir, gowork, []string{"init"}); err != nil {
				return fmt.Errorf("running `go work init`: %v", err)
			}
		}

		return c.invokeGoWork(ctx, viewDir, gowork, args.Args)
	})
}

func (c *commandHandler) invokeGoWork(ctx context.Context, viewDir, gowork string, args []string) error {
	inv := gocommand.Invocation{
		Verb:       "work",
		Args:       args,
		WorkingDir: viewDir,
		Env:        append(os.Environ(), fmt.Sprintf("GOWORK=%s", gowork)),
	}
	if _, err := c.s.session.GoCommandRunner().Run(ctx, inv); err != nil {
		return fmt.Errorf("running go work command: %v", err)
	}
	return nil
}

// showMessage causes the client to show a progress or error message.
//
// It reports whether it succeeded. If it fails, it writes an error to
// the server log, so most callers can safely ignore the result.
func showMessage(ctx context.Context, cli protocol.Client, typ protocol.MessageType, message string) bool {
	err := cli.ShowMessage(ctx, &protocol.ShowMessageParams{
		Type:    typ,
		Message: message,
	})
	if err != nil {
		event.Error(ctx, "client.showMessage: %v", err)
		return false
	}
	return true
}

// openClientBrowser causes the LSP client to open the specified URL
// in an external browser.
//
// If the client does not support window/showDocument, a window/showMessage
// request is instead used, with the format "$title: open your browser to $url".
func openClientBrowser(ctx context.Context, cli protocol.Client, title string, url protocol.URI, opts *settings.Options) {
	if opts.ShowDocumentSupported {
		showDocumentImpl(ctx, cli, url, nil, opts)
	} else {
		params := &protocol.ShowMessageParams{
			Type:    protocol.Info,
			Message: fmt.Sprintf("%s: open your browser to %s", title, url),
		}
		if err := cli.ShowMessage(ctx, params); err != nil {
			event.Error(ctx, "failed to show browser url", err)
		}
	}
}

// openClientEditor causes the LSP client to open the specified document
// and select the indicated range.
//
// Note that VS Code 1.87.2 doesn't currently raise the window; this is
// https://github.com/microsoft/vscode/issues/207634
func openClientEditor(ctx context.Context, cli protocol.Client, loc protocol.Location, opts *settings.Options) {
	if !opts.ShowDocumentSupported {
		return // no op
	}
	showDocumentImpl(ctx, cli, protocol.URI(loc.URI), &loc.Range, opts)
}

func showDocumentImpl(ctx context.Context, cli protocol.Client, url protocol.URI, rangeOpt *protocol.Range, opts *settings.Options) {
	if !opts.ShowDocumentSupported {
		return // no op
	}
	// In principle we shouldn't send a showDocument request to a
	// client that doesn't support it, as reported by
	// ShowDocumentClientCapabilities. But even clients that do
	// support it may defer the real work of opening the document
	// asynchronously, to avoid deadlocks due to rentrancy.
	//
	// For example: client sends request to server; server sends
	// showDocument to client; client opens editor; editor causes
	// new RPC to be sent to server, which is still busy with
	// previous request. (This happens in eglot.)
	//
	// So we can't rely on the success/failure information.
	// That's the reason this function doesn't return an error.

	// "External" means run the system-wide handler (e.g. open(1)
	// on macOS or xdg-open(1) on Linux) for this URL, ignoring
	// TakeFocus and Selection. Note that this may still end up
	// opening the same editor (e.g. VSCode) for a file: URL.
	res, err := cli.ShowDocument(ctx, &protocol.ShowDocumentParams{
		URI:       url,
		External:  rangeOpt == nil,
		TakeFocus: true,
		Selection: rangeOpt, // optional
	})
	if err != nil {
		event.Error(ctx, "client.showDocument: %v", err)
	} else if res != nil && !res.Success {
		event.Log(ctx, fmt.Sprintf("client declined to open document %v", url))
	}
}

func (c *commandHandler) ChangeSignature(ctx context.Context, args command.ChangeSignatureArgs) (*protocol.WorkspaceEdit, error) {
	var result *protocol.WorkspaceEdit
	err := c.run(ctx, commandConfig{
		forURI: args.Location.URI,
	}, func(ctx context.Context, deps commandDeps) error {
		pkg, pgf, err := golang.NarrowestPackageForFile(ctx, deps.snapshot, args.Location.URI)
		if err != nil {
			return err
		}

		// For now, gopls only supports parameter permutation or removal.
		var perm []int
		for _, newParam := range args.NewParams {
			if newParam.NewField != "" {
				return fmt.Errorf("adding new parameters is currently unsupported")
			}
			perm = append(perm, newParam.OldIndex)
		}

		docedits, err := golang.ChangeSignature(ctx, deps.snapshot, pkg, pgf, args.Location.Range, perm)
		if err != nil {
			return err
		}
		wsedit := protocol.NewWorkspaceEdit(docedits...)
		if args.ResolveEdits {
			result = wsedit
			return nil
		}
		return applyChanges(ctx, c.s.client, docedits)
	})
	return result, err
}

func (c *commandHandler) DiagnoseFiles(ctx context.Context, args command.DiagnoseFilesArgs) error {
	return c.run(ctx, commandConfig{
		progress: "Diagnose files",
	}, func(ctx context.Context, _ commandDeps) error {

		// TODO(rfindley): even better would be textDocument/diagnostics (golang/go#60122).
		// Though note that implementing pull diagnostics may cause some servers to
		// request diagnostics in an ad-hoc manner, and break our intentional pacing.

		ctx, done := event.Start(ctx, "commandHandler.DiagnoseFiles")
		defer done()

		snapshots := make(map[*cache.Snapshot]bool)
		for _, uri := range args.Files {
			fh, snapshot, release, err := c.s.session.FileOf(ctx, uri)
			if err != nil {
				return err
			}
			if snapshots[snapshot] || snapshot.FileKind(fh) != file.Go {
				release()
				continue
			}
			defer release()
			snapshots[snapshot] = true
		}

		var wg sync.WaitGroup
		for snapshot := range snapshots {
			wg.Add(1)
			go func() {
				defer wg.Done()

				// Use the operation context for diagnosis, rather than
				// snapshot.BackgroundContext, because this operation does not create
				// new snapshots (so they should also be diagnosed by other means).
				c.s.diagnoseSnapshot(ctx, snapshot, nil, 0)
			}()
		}
		wg.Wait()

		return nil
	})
}

func (c *commandHandler) Views(ctx context.Context) ([]command.View, error) {
	var summaries []command.View
	for _, view := range c.s.session.Views() {
		summaries = append(summaries, command.View{
			ID:         view.ID(),
			Type:       view.Type().String(),
			Root:       view.Root(),
			Folder:     view.Folder().Dir,
			EnvOverlay: view.EnvOverlay(),
		})
	}
	return summaries, nil
}

func (c *commandHandler) FreeSymbols(ctx context.Context, viewID string, loc protocol.Location) error {
	web, err := c.s.getWeb()
	if err != nil {
		return err
	}
	url := web.freesymbolsURL(viewID, loc)
	openClientBrowser(ctx, c.s.client, "Free symbols", url, c.s.Options())
	return nil
}

func (c *commandHandler) Assembly(ctx context.Context, viewID, packageID, symbol string) error {
	web, err := c.s.getWeb()
	if err != nil {
		return err
	}
	url := web.assemblyURL(viewID, packageID, symbol)
	openClientBrowser(ctx, c.s.client, "Assembly", url, c.s.Options())
	return nil
}

func (c *commandHandler) ClientOpenURL(ctx context.Context, url string) error {
	// Fall back to "Gopls: open your browser..." if we must send a showMessage
	// request, since we don't know the context of this command.
	openClientBrowser(ctx, c.s.client, "Gopls", url, c.s.Options())
	return nil
}

func (c *commandHandler) ScanImports(ctx context.Context) error {
	for _, v := range c.s.session.Views() {
		v.ScanImports()
	}
	return nil
}

func (c *commandHandler) PackageSymbols(ctx context.Context, args command.PackageSymbolsArgs) (command.PackageSymbolsResult, error) {
	var result command.PackageSymbolsResult
	err := c.run(ctx, commandConfig{
		forURI: args.URI,
	}, func(ctx context.Context, deps commandDeps) error {
		if deps.snapshot.FileKind(deps.fh) != file.Go {
			// golang/vscode-go#3681: fail silently, to avoid spurious error popups.
			return nil
		}
		res, err := golang.PackageSymbols(ctx, deps.snapshot, args.URI)
		if err != nil {
			return err
		}
		result = res
		return nil
	})

	// sort symbols for determinism
	sort.SliceStable(result.Symbols, func(i, j int) bool {
		iv, jv := result.Symbols[i], result.Symbols[j]
		if iv.Name == jv.Name {
			return iv.Range.Start.Line < jv.Range.Start.Line
		}
		return iv.Name < jv.Name
	})

	return result, err
}

// optionsStringToMap transforms comma-separated options of the form
// "foo=bar,baz=quux" to a go map. Returns nil if any options are malformed.
func optionsStringToMap(options string) (map[string][]string, error) {
	optionsMap := make(map[string][]string)
	for item := range strings.SplitSeq(options, ",") {
		key, option, found := strings.Cut(item, "=")
		if !found {
			return nil, fmt.Errorf("invalid option %q", item)
		}
		optionsMap[key] = append(optionsMap[key], option)
	}
	return optionsMap, nil
}

func (c *commandHandler) ModifyTags(ctx context.Context, args command.ModifyTagsArgs) error {
	return c.run(ctx, commandConfig{
		progress: "Modifying tags",
		forURI:   args.URI,
	}, func(ctx context.Context, deps commandDeps) error {
		m := &modifytags.Modification{
			Clear:        args.Clear,
			ClearOptions: args.ClearOptions,
			ValueFormat:  args.ValueFormat,
			Overwrite:    args.Overwrite,
		}

		transform, err := parseTransform(args.Transform)
		if err != nil {
			return err
		}
		m.Transform = transform

		if args.Add != "" {
			m.Add = strings.Split(args.Add, ",")
		}
		if args.AddOptions != "" {
			if options, err := optionsStringToMap(args.AddOptions); err != nil {
				return err
			} else {
				m.AddOptions = options
			}
		}
		if args.Remove != "" {
			m.Remove = strings.Split(args.Remove, ",")
		}
		if args.RemoveOptions != "" {
			if options, err := optionsStringToMap(args.RemoveOptions); err != nil {
				return err
			} else {
				m.RemoveOptions = options
			}
		}
		fh, err := deps.snapshot.ReadFile(ctx, args.URI)
		if err != nil {
			return err
		}
		changes, err := golang.ModifyTags(ctx, deps.snapshot, fh, args, m)
		if err != nil {
			return err
		}
		return applyChanges(ctx, c.s.client, changes)
	})
}

func parseTransform(input string) (modifytags.Transform, error) {
	switch input {
	case "camelcase":
		return modifytags.CamelCase, nil
	case "lispcase":
		return modifytags.LispCase, nil
	case "pascalcase":
		return modifytags.PascalCase, nil
	case "titlecase":
		return modifytags.TitleCase, nil
	case "keep":
		return modifytags.Keep, nil
	case "":
		fallthrough
	case "snakecase":
		return modifytags.SnakeCase, nil
	default:
		return modifytags.SnakeCase, fmt.Errorf("invalid Transform value")
	}
}
