diff --git a/gopls/internal/lsp/code_action.go b/gopls/internal/lsp/code_action.go index d1177dd9435..1f823362023 100644 --- a/gopls/internal/lsp/code_action.go +++ b/gopls/internal/lsp/code_action.go @@ -450,6 +450,10 @@ func refactorRewrite(ctx context.Context, snapshot source.Snapshot, pkg source.P }) } + if action, ok := source.ConvertStringLiteral(pgf, fh, rng); ok { + actions = append(actions, action) + } + start, end, err := pgf.RangePos(rng) if err != nil { return nil, err diff --git a/gopls/internal/lsp/source/change_quote.go b/gopls/internal/lsp/source/change_quote.go new file mode 100644 index 00000000000..7639248e041 --- /dev/null +++ b/gopls/internal/lsp/source/change_quote.go @@ -0,0 +1,94 @@ +// Copyright 2023 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 ( + "go/ast" + "go/token" + "strconv" + "strings" + + "golang.org/x/tools/go/ast/astutil" + "golang.org/x/tools/gopls/internal/bug" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/safetoken" + "golang.org/x/tools/internal/diff" +) + +// ConvertStringLiteral reports whether we can convert between raw and interpreted +// string literals in the [start, end), along with a CodeAction containing the edits. +// +// Only the following conditions are true, the action in result is valid +// - [start, end) is enclosed by a string literal +// - if the string is interpreted string, need check whether the convert is allowed +func ConvertStringLiteral(pgf *ParsedGoFile, fh FileHandle, rng protocol.Range) (protocol.CodeAction, bool) { + startPos, endPos, err := pgf.RangePos(rng) + if err != nil { + bug.Reportf("(file=%v).RangePos(%v) failed: %v", pgf.URI, rng, err) + return protocol.CodeAction{}, false + } + path, _ := astutil.PathEnclosingInterval(pgf.File, startPos, endPos) + lit, ok := path[0].(*ast.BasicLit) + if !ok || lit.Kind != token.STRING { + return protocol.CodeAction{}, false + } + + str, err := strconv.Unquote(lit.Value) + if err != nil { + return protocol.CodeAction{}, false + } + + interpreted := lit.Value[0] == '"' + // Not all "..." strings can be represented as `...` strings. + if interpreted && !strconv.CanBackquote(strings.ReplaceAll(str, "\n", "")) { + return protocol.CodeAction{}, false + } + + var ( + title string + newText string + ) + if interpreted { + title = "Convert to raw string literal" + newText = "`" + str + "`" + } else { + title = "Convert to interpreted string literal" + newText = strconv.Quote(str) + } + + start, end, err := safetoken.Offsets(pgf.Tok, lit.Pos(), lit.End()) + if err != nil { + bug.Reportf("failed to get string literal offset by token.Pos:%v", err) + return protocol.CodeAction{}, false + } + edits := []diff.Edit{{ + Start: start, + End: end, + New: newText, + }} + pedits, err := ToProtocolEdits(pgf.Mapper, edits) + if err != nil { + bug.Reportf("failed to convert diff.Edit to protocol.TextEdit:%v", err) + return protocol.CodeAction{}, false + } + + return protocol.CodeAction{ + Title: title, + Kind: protocol.RefactorRewrite, + Edit: &protocol.WorkspaceEdit{ + DocumentChanges: []protocol.DocumentChanges{ + { + TextDocumentEdit: &protocol.TextDocumentEdit{ + TextDocument: protocol.OptionalVersionedTextDocumentIdentifier{ + Version: fh.Version(), + TextDocumentIdentifier: protocol.TextDocumentIdentifier{URI: protocol.URIFromSpanURI(fh.URI())}, + }, + Edits: pedits, + }, + }, + }, + }, + }, true +} diff --git a/gopls/internal/regtest/marker/testdata/codeaction/change_quote.txt b/gopls/internal/regtest/marker/testdata/codeaction/change_quote.txt new file mode 100644 index 00000000000..0fa144c1e56 --- /dev/null +++ b/gopls/internal/regtest/marker/testdata/codeaction/change_quote.txt @@ -0,0 +1,69 @@ +This test checks the behavior of the 'change quote' code action. + +-- flags -- +-ignore_extra_diags + +-- go.mod -- +module golang.org/lsptests/changequote + +go 1.18 + +-- a.go -- +package changequote + +import ( + "fmt" +) + +func foo() { + var s string + s = "hello" //@codeactionedit(`"`, "refactor.rewrite", a1, "Convert to raw string literal") + s = `hello` //@codeactionedit("`", "refactor.rewrite", a2, "Convert to interpreted string literal") + s = "hello\tworld" //@codeactionedit(`"`, "refactor.rewrite", a3, "Convert to raw string literal") + s = `hello world` //@codeactionedit("`", "refactor.rewrite", a4, "Convert to interpreted string literal") + s = "hello\nworld" //@codeactionedit(`"`, "refactor.rewrite", a5, "Convert to raw string literal") + // add a comment to avoid affect diff compute + s = `hello +world` //@codeactionedit("`", "refactor.rewrite", a6, "Convert to interpreted string literal") + s = "hello\"world" //@codeactionedit(`"`, "refactor.rewrite", a7, "Convert to raw string literal") + s = `hello"world` //@codeactionedit("`", "refactor.rewrite", a8, "Convert to interpreted string literal") + s = "hello\x1bworld" //@codeactionerr(`"`, "", "refactor.rewrite", re"found 0 CodeActions") + s = "hello`world" //@codeactionerr(`"`, "", "refactor.rewrite", re"found 0 CodeActions") + s = "hello\x7fworld" //@codeactionerr(`"`, "", "refactor.rewrite", re"found 0 CodeActions") + fmt.Println(s) +} + +-- @a1/a.go -- +@@ -9 +9 @@ +- s = "hello" //@codeactionedit(`"`, "refactor.rewrite", a1, "Convert to raw string literal") ++ s = `hello` //@codeactionedit(`"`, "refactor.rewrite", a1, "Convert to raw string literal") +-- @a2/a.go -- +@@ -10 +10 @@ +- s = `hello` //@codeactionedit("`", "refactor.rewrite", a2, "Convert to interpreted string literal") ++ s = "hello" //@codeactionedit("`", "refactor.rewrite", a2, "Convert to interpreted string literal") +-- @a3/a.go -- +@@ -11 +11 @@ +- s = "hello\tworld" //@codeactionedit(`"`, "refactor.rewrite", a3, "Convert to raw string literal") ++ s = `hello world` //@codeactionedit(`"`, "refactor.rewrite", a3, "Convert to raw string literal") +-- @a4/a.go -- +@@ -12 +12 @@ +- s = `hello world` //@codeactionedit("`", "refactor.rewrite", a4, "Convert to interpreted string literal") ++ s = "hello\tworld" //@codeactionedit("`", "refactor.rewrite", a4, "Convert to interpreted string literal") +-- @a5/a.go -- +@@ -13 +13,2 @@ +- s = "hello\nworld" //@codeactionedit(`"`, "refactor.rewrite", a5, "Convert to raw string literal") ++ s = `hello ++world` //@codeactionedit(`"`, "refactor.rewrite", a5, "Convert to raw string literal") +-- @a6/a.go -- +@@ -15,2 +15 @@ +- s = `hello +-world` //@codeactionedit("`", "refactor.rewrite", a6, "Convert to interpreted string literal") ++ s = "hello\nworld" //@codeactionedit("`", "refactor.rewrite", a6, "Convert to interpreted string literal") +-- @a7/a.go -- +@@ -17 +17 @@ +- s = "hello\"world" //@codeactionedit(`"`, "refactor.rewrite", a7, "Convert to raw string literal") ++ s = `hello"world` //@codeactionedit(`"`, "refactor.rewrite", a7, "Convert to raw string literal") +-- @a8/a.go -- +@@ -18 +18 @@ +- s = `hello"world` //@codeactionedit("`", "refactor.rewrite", a8, "Convert to interpreted string literal") ++ s = "hello\"world" //@codeactionedit("`", "refactor.rewrite", a8, "Convert to interpreted string literal")