Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions internal/lsp/cmd/test/refactor_rewrite.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package cmdtest

import (
"testing"

"golang.org/x/tools/internal/span"
)

func (r *runner) RefactorRewrite(t *testing.T, spn span.Span, title string) {}
5 changes: 5 additions & 0 deletions internal/lsp/code_action.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,11 @@ func (s *Server) codeAction(ctx context.Context, params *protocol.CodeActionPara
})
}
}
fillActions, err := source.FillStruct(ctx, snapshot, fh, params.Range)
if err != nil {
return nil, err
}
codeActions = append(codeActions, fillActions...)
default:
// Unsupported file kind for a code action.
return nil, nil
Expand Down
43 changes: 43 additions & 0 deletions internal/lsp/lsp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,49 @@ func (r *runner) Import(t *testing.T, spn span.Span) {
}
}

func (r *runner) RefactorRewrite(t *testing.T, spn span.Span, title string) {
uri := spn.URI()
m, err := r.data.Mapper(uri)
if err != nil {
t.Fatal(err)
}
rng, err := m.Range(spn)
if err != nil {
t.Fatal(err)
}
actions, err := r.server.CodeAction(r.ctx, &protocol.CodeActionParams{
TextDocument: protocol.TextDocumentIdentifier{
URI: protocol.URIFromSpanURI(uri),
},
Range: rng,
})

if len(actions) == 0 {
return
}
for _, action := range actions {
// There may be more code actions available at spn (Span),
// we only need the one specified in the title
if action.Kind != protocol.RefactorRewrite || action.Title != title {
continue
}
res, err := applyWorkspaceEdits(r, action.Edit)
if err != nil {
t.Fatal(err)
}
for u, got := range res {
fixed := string(r.data.Golden(tests.SpanName(spn), u.Filename(), func() ([]byte, error) {
return []byte(got), nil
}))
if fixed != got {
t.Errorf("%s failed for %s, expected:\n%#v\ngot:\n%#v", title, u.Filename(), fixed, got)
}
}
return
}
t.Fatalf("expected code action: %v but none", title)
}

func (r *runner) SuggestedFix(t *testing.T, spn span.Span, actionKinds []string) {
uri := spn.URI()
view, err := r.server.session.ViewOf(uri)
Expand Down
144 changes: 144 additions & 0 deletions internal/lsp/source/fill_struct.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
// 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 source

import (
"context"
"fmt"
"go/format"
"go/types"
"strings"

"golang.org/x/tools/go/ast/astutil"
"golang.org/x/tools/internal/lsp/protocol"
)

// FillStruct completes all of targeted struct's fields with their default values.
func FillStruct(ctx context.Context, snapshot Snapshot, fh FileHandle, protoRng protocol.Range) ([]protocol.CodeAction, error) {

pkg, pgh, err := getParsedFile(ctx, snapshot, fh, NarrowestPackageHandle)
if err != nil {
return nil, fmt.Errorf("getting file for struct fill code action: %v", err)
}
file, src, m, _, err := pgh.Cached()
if err != nil {
return nil, err
}
spn, err := m.PointSpan(protoRng.Start)
if err != nil {
return nil, err
}
spanRng, err := spn.Range(m.Converter)
if err != nil {
return nil, err
}
path, _ := astutil.PathEnclosingInterval(file, spanRng.Start, spanRng.End)
if path == nil {
return nil, nil
}

ecl := enclosingCompositeLiteral(path, spanRng.Start, pkg.GetTypesInfo())
if ecl == nil || !ecl.isStruct() {
return nil, nil
}

// If in F{ Bar<> : V} or anywhere in F{Bar : V, ...}
// we should not fill the struct.
if ecl.inKey || len(ecl.cl.Elts) != 0 {
return nil, nil
}

var codeActions []protocol.CodeAction
qfFunc := qualifier(file, pkg.GetTypes(), pkg.GetTypesInfo())
switch obj := ecl.clType.(type) {
case *types.Struct:
fieldCount := obj.NumFields()
if fieldCount == 0 {
return nil, nil
}
var fieldSourceCode strings.Builder
for i := 0; i < fieldCount; i++ {
field := obj.Field(i)
// Ignore fields that are not accessible in the current package.
if field.Pkg() != nil && field.Pkg() != pkg.GetTypes() && !field.Exported() {
continue
}

label := field.Name()
value := formatZeroValue(field.Type(), qfFunc)
fieldSourceCode.WriteString("\n")
fieldSourceCode.WriteString(label)
fieldSourceCode.WriteString(" : ")
fieldSourceCode.WriteString(value)
fieldSourceCode.WriteString(",")
}

if fieldSourceCode.Len() == 0 {
return nil, nil
}

fieldSourceCode.WriteString("\n")

// the range of all text between '<>', inclusive. E.g. {<> ... <}>
mappedRange := newMappedRange(snapshot.View().Session().Cache().FileSet(), m, ecl.cl.Lbrace, ecl.cl.Rbrace+1)
protoRange, err := mappedRange.Range()
if err != nil {
return nil, err
}
// consider formatting from the first character of the line the lbrace is on.
// ToOffset is 1-based
beginOffset, err := m.Converter.ToOffset(int(protoRange.Start.Line)+1, 1)
if err != nil {
return nil, err
}

endOffset, err := m.Converter.ToOffset(int(protoRange.Start.Line)+1, int(protoRange.Start.Character)+1)
if err != nil {
return nil, err
}

// An increment to make sure the lbrace is included in the slice.
endOffset++
// Append the edits. Then append the closing brace.
var newSourceCode strings.Builder
newSourceCode.Grow(endOffset - beginOffset + fieldSourceCode.Len() + 1)
newSourceCode.WriteString(string(src[beginOffset:endOffset]))
newSourceCode.WriteString(fieldSourceCode.String())
newSourceCode.WriteString("}")

buf, err := format.Source([]byte(newSourceCode.String()))
if err != nil {
return nil, err
}

// it is guaranteed that a left brace exists.
var edit = string(buf[strings.IndexByte(string(buf), '{'):])

codeActions = append(codeActions, protocol.CodeAction{
Title: "Fill struct",
Kind: protocol.RefactorRewrite,
Edit: protocol.WorkspaceEdit{
DocumentChanges: []protocol.TextDocumentEdit{
{
TextDocument: protocol.VersionedTextDocumentIdentifier{
Version: fh.Identity().Version,
TextDocumentIdentifier: protocol.TextDocumentIdentifier{
URI: protocol.URIFromSpanURI(fh.Identity().URI),
},
},
Edits: []protocol.TextEdit{
{
Range: protoRange,
NewText: edit,
},
},
},
},
},
})
}

return codeActions, nil
}
1 change: 1 addition & 0 deletions internal/lsp/source/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ func DefaultOptions() Options {
protocol.SourceFixAll: true,
protocol.SourceOrganizeImports: true,
protocol.QuickFix: true,
protocol.RefactorRewrite: true,
},
Mod: {
protocol.SourceOrganizeImports: true,
Expand Down
1 change: 1 addition & 0 deletions internal/lsp/source/source_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,7 @@ func (r *runner) Import(t *testing.T, spn span.Span) {
}

func (r *runner) SuggestedFix(t *testing.T, spn span.Span, actionKinds []string) {}
func (r *runner) RefactorRewrite(t *testing.T, spn span.Span, title string) {}

func (r *runner) Definition(t *testing.T, spn span.Span, d tests.Definition) {
_, srcRng, err := spanToRange(r.data, d.Src)
Expand Down
1 change: 1 addition & 0 deletions internal/lsp/testdata/indirect/summary.txt.golden
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,5 @@ CaseSensitiveWorkspaceSymbolsCount = 0
SignaturesCount = 0
LinksCount = 0
ImplementationsCount = 0
RefactorRewriteCount = 0

6 changes: 6 additions & 0 deletions internal/lsp/testdata/lsp/primarymod/fillstruct/data/a.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package data

type A struct {
ExportedInt int
unexportedInt int
}
23 changes: 23 additions & 0 deletions internal/lsp/testdata/lsp/primarymod/fillstruct/fill_struct.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package fillstruct

type StructA struct {
unexportedIntField int
ExportedIntField int
MapA map[int]string
Array []int
StructB
}

type StructA2 struct {
B *StructB
}

type StructA3 struct {
B StructB
}

func fill() {
a := StructA{} //@refactorrewrite("}", "Fill struct")
b := StructA2{} //@refactorrewrite("}", "Fill struct")
c := StructA3{} //@refactorrewrite("}", "Fill struct")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
-- fill_struct_20_15 --
package fillstruct

type StructA struct {
unexportedIntField int
ExportedIntField int
MapA map[int]string
Array []int
StructB
}

type StructA2 struct {
B *StructB
}

type StructA3 struct {
B StructB
}

func fill() {
a := StructA{
unexportedIntField: 0,
ExportedIntField: 0,
MapA: nil,
Array: nil,
StructB: StructB{},
} //@refactorrewrite("}", "Fill struct")
b := StructA2{} //@refactorrewrite("}", "Fill struct")
c := StructA3{} //@refactorrewrite("}", "Fill struct")
}

-- fill_struct_21_16 --
package fillstruct

type StructA struct {
unexportedIntField int
ExportedIntField int
MapA map[int]string
Array []int
StructB
}

type StructA2 struct {
B *StructB
}

type StructA3 struct {
B StructB
}

func fill() {
a := StructA{} //@refactorrewrite("}", "Fill struct")
b := StructA2{
B: nil,
} //@refactorrewrite("}", "Fill struct")
c := StructA3{} //@refactorrewrite("}", "Fill struct")
}

-- fill_struct_22_16 --
package fillstruct

type StructA struct {
unexportedIntField int
ExportedIntField int
MapA map[int]string
Array []int
StructB
}

type StructA2 struct {
B *StructB
}

type StructA3 struct {
B StructB
}

func fill() {
a := StructA{} //@refactorrewrite("}", "Fill struct")
b := StructA2{} //@refactorrewrite("}", "Fill struct")
c := StructA3{
B: StructB{},
} //@refactorrewrite("}", "Fill struct")
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package fillstruct

type StructB struct {
StructC
}

type StructC struct {
unexportedInt int
}

func nested() {
c := StructB{
StructC: StructC{}, //@refactorrewrite("}", "Fill struct")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
-- fill_struct_nested_13_20 --
package fillstruct

type StructB struct {
StructC
}

type StructC struct {
unexportedInt int
}

func nested() {
c := StructB{
StructC: StructC{
unexportedInt: 0,
}, //@refactorrewrite("}", "Fill struct")
}
}

Loading