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>
This commit is contained in:
Rebecca Stambler 2020-09-04 13:36:45 -04:00
parent 12e1bf57a1
commit 201d438bce
4 changed files with 84 additions and 48 deletions

View File

@ -6,8 +6,10 @@ package regtest
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 @@ package
-- 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)
}

View File

@ -5,6 +5,7 @@
package lsp
import (
"bytes"
"context"
"fmt"
"strings"
@ -52,42 +53,41 @@ func (s *Server) completion(ctx context.Context, params *protocol.CompletionPara
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
}
}

View File

@ -500,8 +500,8 @@ func Completion(ctx context.Context, snapshot source.Snapshot, fh source.FileHan
// 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

View File

@ -73,7 +73,6 @@ func packageCompletionSurrounding(ctx context.Context, fset *token.FileSet, fh s
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))