1// Copyright 2020 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package lsp
6
7import (
8	"context"
9	"fmt"
10	"io"
11	"path"
12	"strings"
13
14	"golang.org/x/tools/internal/event"
15	"golang.org/x/tools/internal/lsp/debug/tag"
16	"golang.org/x/tools/internal/lsp/protocol"
17	"golang.org/x/tools/internal/lsp/source"
18	"golang.org/x/tools/internal/span"
19	"golang.org/x/tools/internal/xcontext"
20	errors "golang.org/x/xerrors"
21)
22
23func (s *Server) executeCommand(ctx context.Context, params *protocol.ExecuteCommandParams) (interface{}, error) {
24	var command *source.Command
25	for _, c := range source.Commands {
26		if c.Name == params.Command {
27			command = c
28			break
29		}
30	}
31	if command == nil {
32		return nil, fmt.Errorf("no known command")
33	}
34	var match bool
35	for _, name := range s.session.Options().SupportedCommands {
36		if command.Name == name {
37			match = true
38			break
39		}
40	}
41	if !match {
42		return nil, fmt.Errorf("%s is not a supported command", command.Name)
43	}
44	// Some commands require that all files are saved to disk. If we detect
45	// unsaved files, warn the user instead of running the commands.
46	unsaved := false
47	for _, overlay := range s.session.Overlays() {
48		if !overlay.Saved() {
49			unsaved = true
50			break
51		}
52	}
53	if unsaved {
54		switch params.Command {
55		case source.CommandTest.Name, source.CommandGenerate.Name, source.CommandToggleDetails.Name:
56			// TODO(PJW): for Toggle, not an error if it is being disabled
57			return nil, s.client.ShowMessage(ctx, &protocol.ShowMessageParams{
58				Type:    protocol.Error,
59				Message: fmt.Sprintf("cannot run command %s: unsaved files in the view", params.Command),
60			})
61		}
62	}
63	// If the command has a suggested fix function available, use it and apply
64	// the edits to the workspace.
65	if command.IsSuggestedFix() {
66		var uri protocol.DocumentURI
67		var rng protocol.Range
68		if err := source.UnmarshalArgs(params.Arguments, &uri, &rng); err != nil {
69			return nil, err
70		}
71		snapshot, fh, ok, release, err := s.beginFileRequest(ctx, uri, source.Go)
72		defer release()
73		if !ok {
74			return nil, err
75		}
76		edits, err := command.SuggestedFix(ctx, snapshot, fh, rng)
77		if err != nil {
78			return nil, err
79		}
80		r, err := s.client.ApplyEdit(ctx, &protocol.ApplyWorkspaceEditParams{
81			Edit: protocol.WorkspaceEdit{
82				DocumentChanges: edits,
83			},
84		})
85		if err != nil {
86			return nil, err
87		}
88		if !r.Applied {
89			return nil, s.client.ShowMessage(ctx, &protocol.ShowMessageParams{
90				Type:    protocol.Error,
91				Message: fmt.Sprintf("%s failed: %v", params.Command, r.FailureReason),
92			})
93		}
94		return nil, nil
95	}
96	// Default commands that don't have suggested fix functions.
97	switch command {
98	case source.CommandTest:
99		var uri protocol.DocumentURI
100		var flag string
101		var funcName string
102		if err := source.UnmarshalArgs(params.Arguments, &uri, &flag, &funcName); err != nil {
103			return nil, err
104		}
105		snapshot, _, ok, release, err := s.beginFileRequest(ctx, uri, source.UnknownKind)
106		defer release()
107		if !ok {
108			return nil, err
109		}
110		go s.runTest(ctx, snapshot, []string{flag, funcName})
111	case source.CommandGenerate:
112		var uri protocol.DocumentURI
113		var recursive bool
114		if err := source.UnmarshalArgs(params.Arguments, &uri, &recursive); err != nil {
115			return nil, err
116		}
117		snapshot, _, ok, release, err := s.beginFileRequest(ctx, uri, source.UnknownKind)
118		defer release()
119		if !ok {
120			return nil, err
121		}
122		go s.runGoGenerate(xcontext.Detach(ctx), snapshot, uri.SpanURI(), recursive)
123	case source.CommandRegenerateCgo:
124		var uri protocol.DocumentURI
125		if err := source.UnmarshalArgs(params.Arguments, &uri); err != nil {
126			return nil, err
127		}
128		mod := source.FileModification{
129			URI:    uri.SpanURI(),
130			Action: source.InvalidateMetadata,
131		}
132		err := s.didModifyFiles(ctx, []source.FileModification{mod}, FromRegenerateCgo)
133		return nil, err
134	case source.CommandTidy, source.CommandVendor:
135		var uri protocol.DocumentURI
136		if err := source.UnmarshalArgs(params.Arguments, &uri); err != nil {
137			return nil, err
138		}
139		// The flow for `go mod tidy` and `go mod vendor` is almost identical,
140		// so we combine them into one case for convenience.
141		a := "tidy"
142		if command == source.CommandVendor {
143			a = "vendor"
144		}
145		err := s.directGoModCommand(ctx, uri, "mod", []string{a}...)
146		return nil, err
147	case source.CommandUpgradeDependency:
148		var uri protocol.DocumentURI
149		var goCmdArgs []string
150		if err := source.UnmarshalArgs(params.Arguments, &uri, &goCmdArgs); err != nil {
151			return nil, err
152		}
153		err := s.directGoModCommand(ctx, uri, "get", goCmdArgs...)
154		return nil, err
155	case source.CommandToggleDetails:
156		var fileURI span.URI
157		if err := source.UnmarshalArgs(params.Arguments, &fileURI); err != nil {
158			return nil, err
159		}
160		pkgDir := span.URIFromPath(path.Dir(fileURI.Filename()))
161		s.gcOptimizationDetailsMu.Lock()
162		if _, ok := s.gcOptimizatonDetails[pkgDir]; ok {
163			delete(s.gcOptimizatonDetails, pkgDir)
164		} else {
165			s.gcOptimizatonDetails[pkgDir] = struct{}{}
166		}
167		s.gcOptimizationDetailsMu.Unlock()
168		event.Log(ctx, fmt.Sprintf("gc_details %s now %v %v", pkgDir, s.gcOptimizatonDetails[pkgDir],
169			s.gcOptimizatonDetails))
170		// need to recompute diagnostics.
171		// so find the snapshot
172		sv, err := s.session.ViewOf(fileURI)
173		if err != nil {
174			return nil, err
175		}
176		snapshot, release := sv.Snapshot()
177		defer release()
178		s.diagnoseSnapshot(snapshot)
179		return nil, nil
180	default:
181		return nil, fmt.Errorf("unknown command: %s", params.Command)
182	}
183	return nil, nil
184}
185
186func (s *Server) directGoModCommand(ctx context.Context, uri protocol.DocumentURI, verb string, args ...string) error {
187	view, err := s.session.ViewOf(uri.SpanURI())
188	if err != nil {
189		return err
190	}
191	snapshot, release := view.Snapshot()
192	defer release()
193	return snapshot.RunGoCommandDirect(ctx, verb, args)
194}
195
196func (s *Server) runTest(ctx context.Context, snapshot source.Snapshot, args []string) error {
197	ctx, cancel := context.WithCancel(ctx)
198	defer cancel()
199
200	ew := &eventWriter{ctx: ctx, operation: "test"}
201	msg := fmt.Sprintf("running `go test %s`", strings.Join(args, " "))
202	wc := s.newProgressWriter(ctx, "test", msg, msg, cancel)
203	defer wc.Close()
204
205	messageType := protocol.Info
206	message := "test passed"
207	stderr := io.MultiWriter(ew, wc)
208
209	if err := snapshot.RunGoCommandPiped(ctx, "test", args, ew, stderr); err != nil {
210		if errors.Is(err, context.Canceled) {
211			return err
212		}
213		messageType = protocol.Error
214		message = "test failed"
215	}
216	return s.client.ShowMessage(ctx, &protocol.ShowMessageParams{
217		Type:    messageType,
218		Message: message,
219	})
220}
221
222// GenerateWorkDoneTitle is the title used in progress reporting for go
223// generate commands. It is exported for testing purposes.
224const GenerateWorkDoneTitle = "generate"
225
226func (s *Server) runGoGenerate(ctx context.Context, snapshot source.Snapshot, uri span.URI, recursive bool) error {
227	ctx, cancel := context.WithCancel(ctx)
228	defer cancel()
229
230	er := &eventWriter{ctx: ctx, operation: "generate"}
231	wc := s.newProgressWriter(ctx, GenerateWorkDoneTitle, "running go generate", "started go generate, check logs for progress", cancel)
232	defer wc.Close()
233	args := []string{"-x"}
234	if recursive {
235		args = append(args, "./...")
236	}
237
238	stderr := io.MultiWriter(er, wc)
239
240	if err := snapshot.RunGoCommandPiped(ctx, "generate", args, er, stderr); err != nil {
241		if errors.Is(err, context.Canceled) {
242			return nil
243		}
244		event.Error(ctx, "generate: command error", err, tag.Directory.Of(uri.Filename()))
245		return s.client.ShowMessage(ctx, &protocol.ShowMessageParams{
246			Type:    protocol.Error,
247			Message: "go generate exited with an error, check gopls logs",
248		})
249	}
250	return nil
251}
252