internal/lsp: add more tests for package completion
This change adds one more test for package completions (and slightly
modifies the way we pass in file content without a terminal newline).
Also, a few tiny modifications to the package completion code.
Change-Id: I3041b5ad648873881b2b8df17df6f78fbd1898e5
Reviewed-on: https://go-review.googlesource.com/c/tools/+/253177
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Danish Dua <danishdua@google.com>
diff --git a/gopls/internal/regtest/completion_test.go b/gopls/internal/regtest/completion_test.go
index e987e10..957a829 100644
--- a/gopls/internal/regtest/completion_test.go
+++ b/gopls/internal/regtest/completion_test.go
@@ -6,8 +6,10 @@
import (
"fmt"
+ "strings"
"testing"
+ "golang.org/x/tools/internal/lsp"
"golang.org/x/tools/internal/lsp/fake"
"golang.org/x/tools/internal/lsp/protocol"
)
@@ -36,56 +38,91 @@
-- fruits/testfile3.go --
pac
-
--- fruits/testfile4.go --`
- for _, testcase := range []struct {
+`
+ var (
+ testfile4 = ""
+ testfile5 = "/*a comment*/ "
+ )
+ for _, tc := range []struct {
name string
filename string
+ content *string
line, col int
want []string
}{
{
- "package completion at valid position",
- "fruits/testfile.go", 1, 0,
- []string{"package apple", "package apple_test", "package fruits", "package fruits_test", "package main"},
+ name: "package completion at valid position",
+ filename: "fruits/testfile.go",
+ line: 1, col: 0,
+ want: []string{"package apple", "package apple_test", "package fruits", "package fruits_test", "package main"},
},
{
- "package completion in a comment",
- "fruits/testfile.go", 0, 5,
- nil,
+ name: "package completion in a comment",
+ filename: "fruits/testfile.go",
+ line: 0, col: 5,
+ want: nil,
},
{
- "package completion at invalid position",
- "fruits/testfile.go", 4, 0,
- nil,
+ name: "package completion at invalid position",
+ filename: "fruits/testfile.go",
+ line: 4, col: 0,
+ want: nil,
},
{
- "package completion works after keyword 'package'",
- "fruits/testfile2.go", 0, 7,
- []string{"package apple", "package apple_test", "package fruits", "package fruits_test", "package main"},
+ name: "package completion after keyword 'package'",
+ filename: "fruits/testfile2.go",
+ line: 0, col: 7,
+ want: []string{"package apple", "package apple_test", "package fruits", "package fruits_test", "package main"},
},
{
- "package completion works with a prefix for keyword 'package'",
- "fruits/testfile3.go", 0, 3,
- []string{"package apple", "package apple_test", "package fruits", "package fruits_test", "package main"},
+ name: "package completion with 'pac' prefix",
+ filename: "fruits/testfile3.go",
+ line: 0, col: 3,
+ want: []string{"package apple", "package apple_test", "package fruits", "package fruits_test", "package main"},
},
{
- "package completion at end of file",
- "fruits/testfile4.go", 0, 0,
- []string{"package apple", "package apple_test", "package fruits", "package fruits_test", "package main"},
+ name: "package completion at end of file",
+ filename: "fruits/testfile4.go",
+ line: 0, col: 0,
+ content: &testfile4,
+ want: []string{"package apple", "package apple_test", "package fruits", "package fruits_test", "package main"},
+ },
+ {
+ name: "package completion without terminal newline",
+ filename: "fruits/testfile5.go",
+ line: 0, col: 14,
+ content: &testfile5,
+ want: []string{"package apple", "package apple_test", "package fruits", "package fruits_test", "package main"},
},
} {
- t.Run(testcase.name, func(t *testing.T) {
+ t.Run(tc.name, func(t *testing.T) {
run(t, files, func(t *testing.T, env *Env) {
- env.OpenFile(testcase.filename)
- completions, err := env.Editor.Completion(env.Ctx, testcase.filename, fake.Pos{
- Line: testcase.line,
- Column: testcase.col,
+ if tc.content != nil {
+ env.WriteWorkspaceFile(tc.filename, *tc.content)
+ env.Await(
+ CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 1),
+ )
+ }
+ env.OpenFile(tc.filename)
+ completions, err := env.Editor.Completion(env.Ctx, tc.filename, fake.Pos{
+ Line: tc.line,
+ Column: tc.col,
})
if err != nil {
t.Fatal(err)
}
- diff := compareCompletionResults(testcase.want, completions.Items)
+ // Check that the completion item suggestions are in the range
+ // of the file.
+ lineCount := len(strings.Split(env.Editor.BufferText(tc.filename), "\n"))
+ for _, item := range completions.Items {
+ if start := int(item.TextEdit.Range.Start.Line); start >= lineCount {
+ t.Fatalf("unexpected text edit range start line number: got %d, want less than %d", start, lineCount)
+ }
+ if end := int(item.TextEdit.Range.End.Line); end >= lineCount {
+ t.Fatalf("unexpected text edit range end line number: got %d, want less than %d", end, lineCount)
+ }
+ }
+ diff := compareCompletionResults(tc.want, completions.Items)
if diff != "" {
t.Error(diff)
}
diff --git a/internal/lsp/completion.go b/internal/lsp/completion.go
index f02f915..9f12051 100644
--- a/internal/lsp/completion.go
+++ b/internal/lsp/completion.go
@@ -5,6 +5,7 @@
package lsp
import (
+ "bytes"
"context"
"fmt"
"strings"
@@ -52,42 +53,41 @@
if err != nil {
return nil, err
}
-
- // Get the actual number of lines in source.
- numLines := len(strings.Split(string(src), "\n"))
-
+ numLines := len(bytes.Split(src, []byte("\n")))
tok := snapshot.FileSet().File(surrounding.Start())
- endOfFile := tok.Pos(tok.Size())
+ eof := tok.Pos(tok.Size())
// For newline-terminated files, the line count reported by go/token should
// be lower than the actual number of lines we see when splitting by \n. If
// they're the same, the file isn't newline-terminated.
- if numLines == tok.LineCount() && tok.Size() != 0 {
- // Get span for character before end of file to bypass span's treatment of end
- // of file. We correct for this later.
- spn, err := span.NewRange(snapshot.FileSet(), endOfFile-1, endOfFile-1).Span()
+ if tok.Size() > 0 && tok.LineCount() == numLines {
+ // Get the span for the last character in the file-1. This is
+ // technically incorrect, but will get span to point to the previous
+ // line.
+ spn, err := span.NewRange(snapshot.FileSet(), eof-1, eof-1).Span()
if err != nil {
return nil, err
}
m := &protocol.ColumnMapper{
URI: fh.URI(),
- Converter: span.NewContentConverter(fh.URI().Filename(), []byte(src)),
- Content: []byte(src),
+ Converter: span.NewContentConverter(fh.URI().Filename(), src),
+ Content: src,
}
eofRng, err := m.Range(spn)
if err != nil {
return nil, err
}
- eofPosition := protocol.Position{
- Line: eofRng.Start.Line,
- // Correct for using endOfFile - 1 earlier.
+ // Instead of using the computed range, correct for our earlier
+ // position adjustment by adding 1 to the column, not the line number.
+ pos := protocol.Position{
+ Line: eofRng.Start.Line,
Character: eofRng.Start.Character + 1,
}
- if surrounding.Start() == endOfFile {
- rng.Start = eofPosition
+ if surrounding.Start() >= eof {
+ rng.Start = pos
}
- if surrounding.End() == endOfFile {
- rng.End = eofPosition
+ if surrounding.End() >= eof {
+ rng.End = pos
}
}
diff --git a/internal/lsp/source/completion/completion.go b/internal/lsp/source/completion/completion.go
index edaf6f0..9ae9e66 100644
--- a/internal/lsp/source/completion/completion.go
+++ b/internal/lsp/source/completion/completion.go
@@ -500,8 +500,8 @@
// present but no package name exists.
items, surrounding, innerErr := packageClauseCompletions(ctx, snapshot, fh, protoPos)
if innerErr != nil {
- // return the error for GetParsedFile since it's more relevant in this situation.
- return nil, nil, errors.Errorf("getting file for Completion: %w", err)
+ // return the error for getParsedFile since it's more relevant in this situation.
+ return nil, nil, errors.Errorf("getting file for Completion: %w (package completions: %v)", err, innerErr)
}
return items, surrounding, nil
diff --git a/internal/lsp/source/completion/completion_package.go b/internal/lsp/source/completion/completion_package.go
index a6ac7af..d18e2f8 100644
--- a/internal/lsp/source/completion/completion_package.go
+++ b/internal/lsp/source/completion/completion_package.go
@@ -73,7 +73,6 @@
if err != nil {
return nil, err
}
-
// If the file lacks a package declaration, the parser will return an empty
// AST. As a work-around, try to parse an expression from the file contents.
expr, _ := parser.ParseExprFrom(fset, fh.URI().Filename(), src, parser.Mode(0))