internal/lsp/cache: allow fixing multiple syntax errors
Allow fixSrc to run multiple times on a file. For example:
func main() {
var s S
if s.<>
}
type S struct {
i int
}
To properly complete at <>, we need to perform two fixes. We must
insert a phantom underscore after "s." to deal with the dangling
selector, and then we must insert phantom "{}" for the "if" statement
to deal with the incomplete "if" block. Previously we stopped at one
fix, but now we allow for up to 10. I added a limit because I am
afraid there are cases where we could get stuck trying to apply the
same fix again and again.
Also, drive-by set isIncomplete=true when we return early in
lsp/completion.go. I have found my editor incorrectly caches this zero
result in certain cases because it thinks results are "complete".
Fixes golang/go#43471.
Change-Id: Idd34cc164d579fa12a27920dc3afb372799abf26
Reviewed-on: https://go-review.googlesource.com/c/tools/+/289271
Run-TryBot: Muir Manders <muir@mnd.rs>
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Trust: Hyang-Ah Hana Kim <hyangah@gmail.com>
Trust: Rebecca Stambler <rstambler@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
diff --git a/gopls/internal/regtest/completion/completion_test.go b/gopls/internal/regtest/completion/completion_test.go
index 9afa6ce..6e89c4f 100644
--- a/gopls/internal/regtest/completion/completion_test.go
+++ b/gopls/internal/regtest/completion/completion_test.go
@@ -342,5 +342,35 @@
t.Errorf("expected import of mod.com/mainmod in %q", content)
}
})
+}
+// Test that we can doctor the source code enough so the file is
+// parseable and completion works as expected.
+func TestSourceFixup(t *testing.T) {
+ const files = `
+-- go.mod --
+module mod.com
+
+go 1.12
+-- foo.go --
+package foo
+
+func _() {
+ var s S
+ if s.
+}
+
+type S struct {
+ i int
+}
+`
+
+ Run(t, files, func(t *testing.T, env *Env) {
+ env.OpenFile("foo.go")
+ completions := env.Completion("foo.go", env.RegexpSearch("foo.go", `if s\.()`))
+ diff := compareCompletionResults([]string{"i"}, completions.Items)
+ if diff != "" {
+ t.Fatal(diff)
+ }
+ })
}
diff --git a/internal/lsp/cache/parse.go b/internal/lsp/cache/parse.go
index 0a2c371..edbd519 100644
--- a/internal/lsp/cache/parse.go
+++ b/internal/lsp/cache/parse.go
@@ -17,6 +17,8 @@
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/lsp/debug/tag"
+ "golang.org/x/tools/internal/lsp/diff"
+ "golang.org/x/tools/internal/lsp/diff/myers"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/memoize"
@@ -281,9 +283,26 @@
// Fix any badly parsed parts of the AST.
fixed = fixAST(ctx, file, tok, buf)
- // Fix certain syntax errors that render the file unparseable.
- newSrc := fixSrc(file, tok, buf)
- if newSrc != nil {
+ for i := 0; i < 10; i++ {
+ // Fix certain syntax errors that render the file unparseable.
+ newSrc := fixSrc(file, tok, buf)
+ if newSrc == nil {
+ break
+ }
+
+ // If we thought there was something to fix 10 times in a row,
+ // it is likely we got stuck in a loop somehow. Log out a diff
+ // of the last changes we made to aid in debugging.
+ if i == 9 {
+ edits, err := myers.ComputeEdits(fh.URI(), string(buf), string(newSrc))
+ if err != nil {
+ event.Error(ctx, "error generating fixSrc diff", err, tag.File.Of(tok.Name()))
+ } else {
+ unified := diff.ToUnified("before", "after", string(buf), edits)
+ event.Log(ctx, fmt.Sprintf("fixSrc loop - last diff:\n%v", unified), tag.File.Of(tok.Name()))
+ }
+ }
+
newFile, _ := parser.ParseFile(fset, fh.URI().Filename(), newSrc, parserMode)
if newFile != nil {
// Maintain the original parseError so we don't try formatting the doctored file.
@@ -293,12 +312,14 @@
fixed = fixAST(ctx, file, tok, buf)
}
+
}
if mode == source.ParseExported {
trimAST(file)
}
}
+
// A missing file is always an error; use the parse error or make one up if there isn't one.
if file == nil {
if parseError == nil {
diff --git a/internal/lsp/completion.go b/internal/lsp/completion.go
index 9b55f2f..3762b7a 100644
--- a/internal/lsp/completion.go
+++ b/internal/lsp/completion.go
@@ -37,7 +37,8 @@
}
if candidates == nil {
return &protocol.CompletionList{
- Items: []protocol.CompletionItem{},
+ IsIncomplete: true,
+ Items: []protocol.CompletionItem{},
}, nil
}
// We might need to adjust the position to account for the prefix.