Skip to content

Commit 255cfd7

Browse files
xieyuschengopherbot
authored andcommitted
gopls: automatically insert package clause for new go files
This CL introduces a new feature in gopls to handle didCreateFiles requests from the client. When a new Go file is created, gopls will automatically insert the appropriate package clause at the beginning of the file, streamlining the file initialization process. Updates golang/go#72930 Change-Id: I72277294764300bc81f6c8d17ce54b7ed2cc55eb Reviewed-on: https://go-review.googlesource.com/c/tools/+/659595 LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Alan Donovan <[email protected]> Auto-Submit: Alan Donovan <[email protected]> Reviewed-by: Robert Findley <[email protected]>
1 parent ead1fea commit 255cfd7

File tree

13 files changed

+310
-13
lines changed

13 files changed

+310
-13
lines changed

gopls/doc/release/v0.19.0.md

+7
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,10 @@ TODO: implement global.
3939
This code action, available on a dotted import, will offer to replace
4040
the import with a regular one and qualify each use of the package
4141
with its name.
42+
43+
### Auto-complete package clause for new Go files
44+
45+
Gopls now automatically adds the appropriate `package` clause to newly created Go files,
46+
so that you can immediately get started writing the interesting part.
47+
48+
It requires client support for `workspace/didCreateFiles`

gopls/internal/cmd/cmd.go

+9-5
Original file line numberDiff line numberDiff line change
@@ -343,15 +343,16 @@ func (c *connection) initialize(ctx context.Context, options func(*settings.Opti
343343

344344
// Make sure to respect configured options when sending initialize request.
345345
opts := settings.DefaultOptions(options)
346-
// If you add an additional option here, you must update the map key in connect.
346+
// If you add an additional option here,
347+
// you must update the map key of settings.DefaultOptions called in (*Application).connect.
347348
params.Capabilities.TextDocument.Hover = &protocol.HoverClientCapabilities{
348349
ContentFormat: []protocol.MarkupKind{opts.PreferredContentFormat},
349350
}
350351
params.Capabilities.TextDocument.DocumentSymbol.HierarchicalDocumentSymbolSupport = opts.HierarchicalDocumentSymbolSupport
351352
params.Capabilities.TextDocument.SemanticTokens = protocol.SemanticTokensClientCapabilities{}
352353
params.Capabilities.TextDocument.SemanticTokens.Formats = []protocol.TokenFormat{"relative"}
353354
params.Capabilities.TextDocument.SemanticTokens.Requests.Range = &protocol.Or_ClientSemanticTokensRequestOptions_range{Value: true}
354-
//params.Capabilities.TextDocument.SemanticTokens.Requests.Range.Value = true
355+
// params.Capabilities.TextDocument.SemanticTokens.Requests.Range.Value = true
355356
params.Capabilities.TextDocument.SemanticTokens.Requests.Full = &protocol.Or_ClientSemanticTokensRequestOptions_full{Value: true}
356357
params.Capabilities.TextDocument.SemanticTokens.TokenTypes = moreslices.ConvertStrings[string](semtok.TokenTypes)
357358
params.Capabilities.TextDocument.SemanticTokens.TokenModifiers = moreslices.ConvertStrings[string](semtok.TokenModifiers)
@@ -363,6 +364,9 @@ func (c *connection) initialize(ctx context.Context, options func(*settings.Opti
363364
},
364365
}
365366
params.Capabilities.Window.WorkDoneProgress = true
367+
params.Capabilities.Workspace.FileOperations = &protocol.FileOperationClientCapabilities{
368+
DidCreate: true,
369+
}
366370

367371
params.InitializationOptions = map[string]any{
368372
"symbolMatcher": string(opts.SymbolMatcher),
@@ -817,10 +821,10 @@ func (c *connection) diagnoseFiles(ctx context.Context, files []protocol.Documen
817821
}
818822

819823
func (c *connection) terminate(ctx context.Context) {
820-
//TODO: do we need to handle errors on these calls?
824+
// TODO: do we need to handle errors on these calls?
821825
c.Shutdown(ctx)
822-
//TODO: right now calling exit terminates the process, we should rethink that
823-
//server.Exit(ctx)
826+
// TODO: right now calling exit terminates the process, we should rethink that
827+
// server.Exit(ctx)
824828
}
825829

826830
// Implement io.Closer.

gopls/internal/golang/addtest.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,7 @@ func AddTestForFunc(ctx context.Context, snapshot *cache.Snapshot, loc protocol.
319319
// package decl based on the originating file.
320320
// Search for something that looks like a copyright header, to replicate
321321
// in the new file.
322-
if c := copyrightComment(pgf.File); c != nil {
322+
if c := CopyrightComment(pgf.File); c != nil {
323323
start, end, err := pgf.NodeOffsets(c)
324324
if err != nil {
325325
return nil, err
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Copyright 2025 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+
5+
package completion
6+
7+
import (
8+
"bytes"
9+
"context"
10+
"fmt"
11+
12+
"golang.org/x/tools/gopls/internal/cache"
13+
"golang.org/x/tools/gopls/internal/cache/parsego"
14+
"golang.org/x/tools/gopls/internal/file"
15+
"golang.org/x/tools/gopls/internal/golang"
16+
"golang.org/x/tools/gopls/internal/protocol"
17+
)
18+
19+
// NewFile returns a document change to complete an empty go file.
20+
func NewFile(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) (*protocol.DocumentChange, error) {
21+
if bs, err := fh.Content(); err != nil || len(bs) != 0 {
22+
return nil, err
23+
}
24+
meta, err := golang.NarrowestMetadataForFile(ctx, snapshot, fh.URI())
25+
if err != nil {
26+
return nil, err
27+
}
28+
var buf bytes.Buffer
29+
// Copy the copyright header from the first existing file that has one.
30+
for _, fileURI := range meta.GoFiles {
31+
if fileURI == fh.URI() {
32+
continue
33+
}
34+
fh, err := snapshot.ReadFile(ctx, fileURI)
35+
if err != nil {
36+
continue
37+
}
38+
pgf, err := snapshot.ParseGo(ctx, fh, parsego.Header)
39+
if err != nil {
40+
continue
41+
}
42+
if group := golang.CopyrightComment(pgf.File); group != nil {
43+
start, end, err := pgf.NodeOffsets(group)
44+
if err != nil {
45+
continue
46+
}
47+
buf.Write(pgf.Src[start:end])
48+
buf.WriteString("\n\n")
49+
break
50+
}
51+
}
52+
53+
pkgName, err := bestPackage(ctx, snapshot, fh.URI())
54+
if err != nil {
55+
return nil, err
56+
}
57+
58+
fmt.Fprintf(&buf, "package %s\n", pkgName)
59+
change := protocol.DocumentChangeEdit(fh, []protocol.TextEdit{{
60+
Range: protocol.Range{}, // insert at start of file
61+
NewText: buf.String(),
62+
}})
63+
64+
return &change, nil
65+
}

gopls/internal/golang/completion/package.go

+19
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"go/token"
1616
"go/types"
1717
"path/filepath"
18+
"sort"
1819
"strings"
1920
"unicode"
2021

@@ -27,6 +28,24 @@ import (
2728
"golang.org/x/tools/gopls/internal/util/safetoken"
2829
)
2930

31+
// bestPackage offers the best package name for a package declaration when
32+
// one is not present in the given file.
33+
func bestPackage(ctx context.Context, snapshot *cache.Snapshot, uri protocol.DocumentURI) (string, error) {
34+
suggestions, err := packageSuggestions(ctx, snapshot, uri, "")
35+
if err != nil {
36+
return "", err
37+
}
38+
// sort with the same way of sortItems.
39+
sort.SliceStable(suggestions, func(i, j int) bool {
40+
if suggestions[i].score != suggestions[j].score {
41+
return suggestions[i].score > suggestions[j].score
42+
}
43+
return suggestions[i].name < suggestions[j].name
44+
})
45+
46+
return suggestions[0].name, nil
47+
}
48+
3049
// packageClauseCompletions offers completions for a package declaration when
3150
// one is not present in the given file.
3251
func packageClauseCompletions(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, position protocol.Position) ([]CompletionItem, *Selection, error) {

gopls/internal/golang/extracttofile.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ func ExtractToNewFile(ctx context.Context, snapshot *cache.Snapshot, fh file.Han
138138
}
139139

140140
var buf bytes.Buffer
141-
if c := copyrightComment(pgf.File); c != nil {
141+
if c := CopyrightComment(pgf.File); c != nil {
142142
start, end, err := pgf.NodeOffsets(c)
143143
if err != nil {
144144
return nil, err

gopls/internal/golang/util.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -361,9 +361,9 @@ func AbbreviateVarName(s string) string {
361361
return b.String()
362362
}
363363

364-
// copyrightComment returns the copyright comment group from the input file, or
364+
// CopyrightComment returns the copyright comment group from the input file, or
365365
// nil if not found.
366-
func copyrightComment(file *ast.File) *ast.CommentGroup {
366+
func CopyrightComment(file *ast.File) *ast.CommentGroup {
367367
if len(file.Comments) == 0 {
368368
return nil
369369
}

gopls/internal/server/general.go

+9
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,15 @@ func (s *server) Initialize(ctx context.Context, params *protocol.ParamInitializ
189189
Supported: true,
190190
ChangeNotifications: "workspace/didChangeWorkspaceFolders",
191191
},
192+
FileOperations: &protocol.FileOperationOptions{
193+
DidCreate: &protocol.FileOperationRegistrationOptions{
194+
Filters: []protocol.FileOperationFilter{{
195+
Scheme: "file",
196+
// gopls is only interested with files in .go extension.
197+
Pattern: protocol.FileOperationPattern{Glob: "**/*.go"},
198+
}},
199+
},
200+
},
192201
},
193202
},
194203
ServerInfo: &protocol.ServerInfo{

gopls/internal/server/unimplemented.go

-4
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,6 @@ func (s *server) DidCloseNotebookDocument(context.Context, *protocol.DidCloseNot
3434
return notImplemented("DidCloseNotebookDocument")
3535
}
3636

37-
func (s *server) DidCreateFiles(context.Context, *protocol.CreateFilesParams) error {
38-
return notImplemented("DidCreateFiles")
39-
}
40-
4137
func (s *server) DidDeleteFiles(context.Context, *protocol.DeleteFilesParams) error {
4238
return notImplemented("DidDeleteFiles")
4339
}

gopls/internal/server/workspace.go

+30
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import (
1212
"sync"
1313

1414
"golang.org/x/tools/gopls/internal/cache"
15+
"golang.org/x/tools/gopls/internal/file"
16+
"golang.org/x/tools/gopls/internal/golang/completion"
1517
"golang.org/x/tools/gopls/internal/protocol"
1618
"golang.org/x/tools/gopls/internal/settings"
1719
"golang.org/x/tools/internal/event"
@@ -139,3 +141,31 @@ func (s *server) DidChangeConfiguration(ctx context.Context, _ *protocol.DidChan
139141

140142
return nil
141143
}
144+
145+
func (s *server) DidCreateFiles(ctx context.Context, params *protocol.CreateFilesParams) error {
146+
ctx, done := event.Start(ctx, "lsp.Server.didCreateFiles")
147+
defer done()
148+
149+
var allChanges []protocol.DocumentChange
150+
for _, createdFile := range params.Files {
151+
uri := protocol.DocumentURI(createdFile.URI)
152+
fh, snapshot, release, err := s.fileOf(ctx, uri)
153+
if err != nil {
154+
event.Error(ctx, "fail to call fileOf", err)
155+
continue
156+
}
157+
defer release()
158+
159+
switch snapshot.FileKind(fh) {
160+
case file.Go:
161+
change, err := completion.NewFile(ctx, snapshot, fh)
162+
if err != nil {
163+
continue
164+
}
165+
allChanges = append(allChanges, *change)
166+
default:
167+
}
168+
}
169+
170+
return applyChanges(ctx, s.client, allChanges)
171+
}

gopls/internal/test/integration/fake/editor.go

+13
Original file line numberDiff line numberDiff line change
@@ -1309,6 +1309,19 @@ func (e *Editor) Completion(ctx context.Context, loc protocol.Location) (*protoc
13091309
return completions, nil
13101310
}
13111311

1312+
func (e *Editor) DidCreateFiles(ctx context.Context, files ...protocol.DocumentURI) error {
1313+
if e.Server == nil {
1314+
return nil
1315+
}
1316+
params := &protocol.CreateFilesParams{}
1317+
for _, file := range files {
1318+
params.Files = append(params.Files, protocol.FileCreate{
1319+
URI: string(file),
1320+
})
1321+
}
1322+
return e.Server.DidCreateFiles(ctx, params)
1323+
}
1324+
13121325
func (e *Editor) SetSuggestionInsertReplaceMode(_ context.Context, useReplaceMode bool) {
13131326
e.mu.Lock()
13141327
defer e.mu.Unlock()

0 commit comments

Comments
 (0)