gopls/internal/span: some cleanups
This change eliminates these redundant helper functions
- cache.rangeFromPositions
- source.LineToRange
- source.ByteOffsetsToRange
and makes various other simplifications and documentation
improvements.
Change-Id: Ic820ab560d71b88bde00b005e3a919334a5d1856
Reviewed-on: https://go-review.googlesource.com/c/tools/+/442015
TryBot-Result: Gopher Robot <gobot@golang.org>
gopls-CI: kokoro <noreply+kokoro@google.com>
Run-TryBot: Alan Donovan <adonovan@google.com>
Reviewed-by: Robert Findley <rfindley@google.com>
diff --git a/gopls/internal/lsp/cache/check.go b/gopls/internal/lsp/cache/check.go
index 0b37135..a186a81 100644
--- a/gopls/internal/lsp/cache/check.go
+++ b/gopls/internal/lsp/cache/check.go
@@ -729,7 +729,7 @@
if reference == nil {
continue
}
- rng, err := rangeFromPositions(pm.Mapper, reference.Start, reference.End)
+ rng, err := pm.Mapper.OffsetRange(reference.Start.Byte, reference.End.Byte)
if err != nil {
return nil, err
}
diff --git a/gopls/internal/lsp/cache/load.go b/gopls/internal/lsp/cache/load.go
index 3ef5301..c33561f 100644
--- a/gopls/internal/lsp/cache/load.go
+++ b/gopls/internal/lsp/cache/load.go
@@ -392,8 +392,8 @@
}
case source.Mod:
if pmf, err := s.ParseMod(ctx, fh); err == nil {
- if pmf.File.Module != nil && pmf.File.Module.Syntax != nil {
- rng, _ = rangeFromPositions(pmf.Mapper, pmf.File.Module.Syntax.Start, pmf.File.Module.Syntax.End)
+ if mod := pmf.File.Module; mod != nil && mod.Syntax != nil {
+ rng, _ = pmf.Mapper.OffsetRange(mod.Syntax.Start.Byte, mod.Syntax.End.Byte)
}
}
}
diff --git a/gopls/internal/lsp/cache/mod.go b/gopls/internal/lsp/cache/mod.go
index f8404e7..97bdb2f 100644
--- a/gopls/internal/lsp/cache/mod.go
+++ b/gopls/internal/lsp/cache/mod.go
@@ -79,7 +79,7 @@
return nil, fmt.Errorf("unexpected parse error type %v", parseErr)
}
for _, mfErr := range mfErrList {
- rng, err := rangeFromPositions(m, mfErr.Pos, mfErr.Pos)
+ rng, err := m.OffsetRange(mfErr.Pos.Byte, mfErr.Pos.Byte)
if err != nil {
return nil, err
}
@@ -155,7 +155,7 @@
return nil, fmt.Errorf("unexpected parse error type %v", parseErr)
}
for _, mfErr := range mfErrList {
- rng, err := rangeFromPositions(m, mfErr.Pos, mfErr.Pos)
+ rng, err := m.OffsetRange(mfErr.Pos.Byte, mfErr.Pos.Byte)
if err != nil {
return nil, err
}
diff --git a/gopls/internal/lsp/cache/mod_tidy.go b/gopls/internal/lsp/cache/mod_tidy.go
index 63346dc..8e4e060 100644
--- a/gopls/internal/lsp/cache/mod_tidy.go
+++ b/gopls/internal/lsp/cache/mod_tidy.go
@@ -287,7 +287,7 @@
// unusedDiagnostic returns a source.Diagnostic for an unused require.
func unusedDiagnostic(m *protocol.ColumnMapper, req *modfile.Require, onlyDiagnostic bool) (*source.Diagnostic, error) {
- rng, err := rangeFromPositions(m, req.Syntax.Start, req.Syntax.End)
+ rng, err := m.OffsetRange(req.Syntax.Start.Byte, req.Syntax.End.Byte)
if err != nil {
return nil, err
}
@@ -313,7 +313,7 @@
// directnessDiagnostic extracts errors when a dependency is labeled indirect when
// it should be direct and vice versa.
func directnessDiagnostic(m *protocol.ColumnMapper, req *modfile.Require, computeEdits source.DiffFunction) (*source.Diagnostic, error) {
- rng, err := rangeFromPositions(m, req.Syntax.Start, req.Syntax.End)
+ rng, err := m.OffsetRange(req.Syntax.Start.Byte, req.Syntax.End.Byte)
if err != nil {
return nil, err
}
@@ -325,8 +325,8 @@
if comments := req.Syntax.Comment(); comments != nil && len(comments.Suffix) > 0 {
end := comments.Suffix[0].Start
end.LineRune += len(comments.Suffix[0].Token)
- end.Byte += len([]byte(comments.Suffix[0].Token))
- rng, err = rangeFromPositions(m, comments.Suffix[0].Start, end)
+ end.Byte += len(comments.Suffix[0].Token)
+ rng, err = m.OffsetRange(comments.Suffix[0].Start.Byte, end.Byte)
if err != nil {
return nil, err
}
@@ -359,7 +359,7 @@
if pm.File != nil && pm.File.Module != nil && pm.File.Module.Syntax != nil {
start, end := pm.File.Module.Syntax.Span()
var err error
- rng, err = rangeFromPositions(pm.Mapper, start, end)
+ rng, err = pm.Mapper.OffsetRange(start.Byte, end.Byte)
if err != nil {
return nil, err
}
@@ -429,11 +429,7 @@
if req.Syntax == nil {
return nil, fmt.Errorf("no syntax for %v", req)
}
- spn, err := span.NewRange(file, imp.Path.Pos(), imp.Path.End()).Span()
- if err != nil {
- return nil, err
- }
- rng, err := m.Range(spn)
+ rng, err := m.PosRange(imp.Path.Pos(), imp.Path.End())
if err != nil {
return nil, err
}
@@ -447,14 +443,6 @@
}, nil
}
-func rangeFromPositions(m *protocol.ColumnMapper, s, e modfile.Position) (protocol.Range, error) {
- spn, err := spanFromPositions(m, s, e)
- if err != nil {
- return protocol.Range{}, err
- }
- return m.Range(spn)
-}
-
func spanFromPositions(m *protocol.ColumnMapper, s, e modfile.Position) (span.Span, error) {
toPoint := func(offset int) (span.Point, error) {
l, c, err := span.ToPosition(m.TokFile, offset)
diff --git a/gopls/internal/lsp/diagnostics.go b/gopls/internal/lsp/diagnostics.go
index f6f9f79..5a47626 100644
--- a/gopls/internal/lsp/diagnostics.go
+++ b/gopls/internal/lsp/diagnostics.go
@@ -545,11 +545,7 @@
if !pgf.File.Name.Pos().IsValid() {
return nil
}
- spn, err := span.NewRange(pgf.Tok, pgf.File.Name.Pos(), pgf.File.Name.End()).Span()
- if err != nil {
- return nil
- }
- rng, err := pgf.Mapper.Range(spn)
+ rng, err := pgf.Mapper.PosRange(pgf.File.Name.Pos(), pgf.File.Name.End())
if err != nil {
return nil
}
diff --git a/gopls/internal/lsp/lsp_test.go b/gopls/internal/lsp/lsp_test.go
index 3969e88..d966d94 100644
--- a/gopls/internal/lsp/lsp_test.go
+++ b/gopls/internal/lsp/lsp_test.go
@@ -360,26 +360,18 @@
res := contents
// Apply the edits from the end of the file forward
// to preserve the offsets
+ // TODO(adonovan): factor to use diff.ApplyEdits, which validates the input.
for i := len(ranges) - 1; i >= 0; i-- {
- fRange := ranges[i]
- spn, err := m.RangeSpan(protocol.Range{
- Start: protocol.Position{
- Line: fRange.StartLine,
- Character: fRange.StartCharacter,
- },
- End: protocol.Position{
- Line: fRange.EndLine,
- Character: fRange.EndCharacter,
- },
- })
+ r := ranges[i]
+ start, err := m.Point(protocol.Position{r.StartLine, r.StartCharacter})
if err != nil {
return "", err
}
- start := spn.Start().Offset()
- end := spn.End().Offset()
-
- tmp := res[0:start] + foldedText
- res = tmp + res[end:]
+ end, err := m.Point(protocol.Position{r.EndLine, r.EndCharacter})
+ if err != nil {
+ return "", err
+ }
+ res = res[:start.Offset()] + foldedText + res[end.Offset():]
}
return res, nil
}
diff --git a/gopls/internal/lsp/mod/code_lens.go b/gopls/internal/lsp/mod/code_lens.go
index daafee0..01d75d9 100644
--- a/gopls/internal/lsp/mod/code_lens.go
+++ b/gopls/internal/lsp/mod/code_lens.go
@@ -134,7 +134,7 @@
return protocol.Range{}, fmt.Errorf("no module statement in %s", fh.URI())
}
syntax := pm.File.Module.Syntax
- return source.LineToRange(pm.Mapper, fh.URI(), syntax.Start, syntax.End)
+ return pm.Mapper.OffsetRange(syntax.Start.Byte, syntax.End.Byte)
}
// firstRequireRange returns the range for the first "require" in the given
@@ -155,7 +155,7 @@
if start.Byte == 0 || firstRequire.Start.Byte < start.Byte {
start, end = firstRequire.Start, firstRequire.End
}
- return source.LineToRange(pm.Mapper, fh.URI(), start, end)
+ return pm.Mapper.OffsetRange(start.Byte, end.Byte)
}
func vulncheckLenses(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) ([]protocol.CodeLens, error) {
diff --git a/gopls/internal/lsp/mod/diagnostics.go b/gopls/internal/lsp/mod/diagnostics.go
index d794424..4b4f421 100644
--- a/gopls/internal/lsp/mod/diagnostics.go
+++ b/gopls/internal/lsp/mod/diagnostics.go
@@ -136,7 +136,7 @@
if !ok || req.Mod.Version == ver {
continue
}
- rng, err := source.LineToRange(pm.Mapper, fh.URI(), req.Syntax.Start, req.Syntax.End)
+ rng, err := pm.Mapper.OffsetRange(req.Syntax.Start.Byte, req.Syntax.End.Byte)
if err != nil {
return nil, err
}
@@ -205,7 +205,7 @@
if !ok {
continue
}
- rng, err := source.LineToRange(pm.Mapper, fh.URI(), req.Syntax.Start, req.Syntax.End)
+ rng, err := pm.Mapper.OffsetRange(req.Syntax.Start.Byte, req.Syntax.End.Byte)
if err != nil {
return nil, err
}
diff --git a/gopls/internal/lsp/mod/hover.go b/gopls/internal/lsp/mod/hover.go
index 0f3dfc4..2981274 100644
--- a/gopls/internal/lsp/mod/hover.go
+++ b/gopls/internal/lsp/mod/hover.go
@@ -11,9 +11,9 @@
"strings"
"golang.org/x/mod/modfile"
- "golang.org/x/tools/internal/event"
"golang.org/x/tools/gopls/internal/lsp/protocol"
"golang.org/x/tools/gopls/internal/lsp/source"
+ "golang.org/x/tools/internal/event"
)
func Hover(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, position protocol.Position) (*protocol.Hover, error) {
@@ -78,7 +78,7 @@
}
// Get the range to highlight for the hover.
- rng, err := source.ByteOffsetsToRange(pm.Mapper, fh.URI(), startPos, endPos)
+ rng, err := pm.Mapper.OffsetRange(startPos, endPos)
if err != nil {
return nil, err
}
diff --git a/gopls/internal/lsp/protocol/span.go b/gopls/internal/lsp/protocol/span.go
index bfa2f81..2d87c08 100644
--- a/gopls/internal/lsp/protocol/span.go
+++ b/gopls/internal/lsp/protocol/span.go
@@ -4,6 +4,34 @@
// this file contains protocol<->span converters
+// Here's a handy guide for your tour of the location zoo:
+//
+// Imports: source --> lsppos --> protocol --> span --> token
+//
+// source.MappedRange = (span.Range, protocol.ColumnMapper)
+//
+// lsppos.TokenMapper = (token.File, lsppos.Mapper)
+// lsppos.Mapper = (line offset table, content)
+//
+// protocol.ColumnMapper = (URI, token.File, content)
+// protocol.Location = (URI, protocol.Range)
+// protocol.Range = (start, end Position)
+// protocol.Position = (line, char uint32) 0-based UTF-16
+//
+// span.Point = (line?, col?, offset?) 1-based UTF-8
+// span.Span = (uri URI, start, end span.Point)
+// span.Range = (file token.File, start, end token.Pos)
+//
+// token.Pos
+// token.FileSet
+// offset int
+//
+// TODO(adonovan): simplify this picture. Eliminate the optionality of
+// span.{Span,Point}'s position and offset fields: work internally in
+// terms of offsets (like span.Range), and require a mapper to convert
+// them to protocol (UTF-16) line/col form. Stop honoring //line
+// directives.
+
package protocol
import (
@@ -12,6 +40,7 @@
"go/token"
"unicode/utf8"
+ "golang.org/x/tools/gopls/internal/lsp/safetoken"
"golang.org/x/tools/gopls/internal/span"
)
@@ -67,7 +96,7 @@
if span.CompareURI(m.URI, s.URI()) != 0 {
return Range{}, fmt.Errorf("column mapper is for file %q instead of %q", m.URI, s.URI())
}
- s, err := s.WithAll(m.TokFile)
+ s, err := s.WithOffset(m.TokFile)
if err != nil {
return Range{}, err
}
@@ -97,6 +126,19 @@
return Range{Start: startPosition, End: endPosition}, nil
}
+// PosRange returns a protocol Range for the token.Pos interval Content[start:end].
+func (m *ColumnMapper) PosRange(start, end token.Pos) (Range, error) {
+ startOffset, err := safetoken.Offset(m.TokFile, start)
+ if err != nil {
+ return Range{}, fmt.Errorf("start: %v", err)
+ }
+ endOffset, err := safetoken.Offset(m.TokFile, end)
+ if err != nil {
+ return Range{}, fmt.Errorf("end: %v", err)
+ }
+ return m.OffsetRange(startOffset, endOffset)
+}
+
// Position returns the protocol position for the specified point,
// which must have a byte offset.
func (m *ColumnMapper) Position(p span.Point) (Position, error) {
@@ -163,6 +205,8 @@
return m.RangeSpan(l.Range)
}
+// RangeSpan converts a UTF-16 range to a Span with both the
+// position (line/col) and offset fields populated.
func (m *ColumnMapper) RangeSpan(r Range) (span.Span, error) {
start, err := m.Point(r.Start)
if err != nil {
@@ -183,22 +227,13 @@
return spn.Range(m.TokFile)
}
-// Pos returns the token.Pos of p within the mapped file.
+// Pos returns the token.Pos of protocol position p within the mapped file.
func (m *ColumnMapper) Pos(p Position) (token.Pos, error) {
start, err := m.Point(p)
if err != nil {
return token.NoPos, err
}
- // TODO: refactor the span package to avoid creating this unnecessary end position.
- spn, err := span.New(m.URI, start, start).WithAll(m.TokFile)
- if err != nil {
- return token.NoPos, err
- }
- rng, err := spn.Range(m.TokFile)
- if err != nil {
- return token.NoPos, err
- }
- return rng.Start, nil
+ return safetoken.Pos(m.TokFile, start.Offset())
}
// Offset returns the utf-8 byte offset of p within the mapped file.
@@ -210,10 +245,12 @@
return start.Offset(), nil
}
-// Point returns a span.Point for p within the mapped file. The resulting point
-// always has an Offset.
+// Point returns a span.Point for the protocol position p within the mapped file.
+// The resulting point has a valid Position and Offset.
func (m *ColumnMapper) Point(p Position) (span.Point, error) {
line := int(p.Line) + 1
+
+ // Find byte offset of start of containing line.
offset, err := span.ToOffset(m.TokFile, line, 1)
if err != nil {
return span.Point{}, err
diff --git a/gopls/internal/lsp/source/fix.go b/gopls/internal/lsp/source/fix.go
index 1ce6d75..a4c6676 100644
--- a/gopls/internal/lsp/source/fix.go
+++ b/gopls/internal/lsp/source/fix.go
@@ -93,15 +93,11 @@
if !end.IsValid() {
end = edit.Pos
}
- spn, err := span.NewRange(tokFile, edit.Pos, end).Span()
+ fh, err := snapshot.GetVersionedFile(ctx, span.URIFromPath(tokFile.Name()))
if err != nil {
return nil, err
}
- fh, err := snapshot.GetVersionedFile(ctx, spn.URI())
- if err != nil {
- return nil, err
- }
- te, ok := editsPerFile[spn.URI()]
+ te, ok := editsPerFile[fh.URI()]
if !ok {
te = &protocol.TextDocumentEdit{
TextDocument: protocol.OptionalVersionedTextDocumentIdentifier{
@@ -111,13 +107,13 @@
},
},
}
- editsPerFile[spn.URI()] = te
+ editsPerFile[fh.URI()] = te
}
_, pgf, err := GetParsedFile(ctx, snapshot, fh, NarrowestPackage)
if err != nil {
return nil, err
}
- rng, err := pgf.Mapper.Range(spn)
+ rng, err := pgf.Mapper.PosRange(edit.Pos, end)
if err != nil {
return nil, err
}
diff --git a/gopls/internal/lsp/source/util.go b/gopls/internal/lsp/source/util.go
index b06992e..c933fba 100644
--- a/gopls/internal/lsp/source/util.go
+++ b/gopls/internal/lsp/source/util.go
@@ -17,7 +17,6 @@
"strconv"
"strings"
- "golang.org/x/mod/modfile"
"golang.org/x/tools/gopls/internal/lsp/protocol"
"golang.org/x/tools/gopls/internal/span"
"golang.org/x/tools/internal/bug"
@@ -26,6 +25,9 @@
// MappedRange provides mapped protocol.Range for a span.Range, accounting for
// UTF-16 code points.
+//
+// TOOD(adonovan): stop treating //line directives specially, and
+// eliminate this type. All callers need either m, or a protocol.Range.
type MappedRange struct {
spanRange span.Range // the range in the compiled source (package.CompiledGoFiles)
m *protocol.ColumnMapper // a mapper of the edited source (package.GoFiles)
@@ -555,26 +557,6 @@
return strings.Contains(s, "command-line-arguments")
}
-// LineToRange creates a Range spanning start and end.
-func LineToRange(m *protocol.ColumnMapper, uri span.URI, start, end modfile.Position) (protocol.Range, error) {
- return ByteOffsetsToRange(m, uri, start.Byte, end.Byte)
-}
-
-// ByteOffsetsToRange creates a range spanning start and end.
-func ByteOffsetsToRange(m *protocol.ColumnMapper, uri span.URI, start, end int) (protocol.Range, error) {
- line, col, err := span.ToPosition(m.TokFile, start)
- if err != nil {
- return protocol.Range{}, err
- }
- s := span.NewPoint(line, col, start)
- line, col, err = span.ToPosition(m.TokFile, end)
- if err != nil {
- return protocol.Range{}, err
- }
- e := span.NewPoint(line, col, end)
- return m.Range(span.New(uri, s, e))
-}
-
// RecvIdent returns the type identifier of a method receiver.
// e.g. A for all of A, *A, A[T], *A[T], etc.
func RecvIdent(recv *ast.FieldList) *ast.Ident {
diff --git a/gopls/internal/lsp/text_synchronization.go b/gopls/internal/lsp/text_synchronization.go
index 9687528..63bc0e8 100644
--- a/gopls/internal/lsp/text_synchronization.go
+++ b/gopls/internal/lsp/text_synchronization.go
@@ -356,6 +356,9 @@
return nil, fmt.Errorf("%w: file not found (%v)", jsonrpc2.ErrInternal, err)
}
for _, change := range changes {
+ // TODO(adonovan): refactor to use diff.Apply, which is robust w.r.t.
+ // out-of-order or overlapping changes---and much more efficient.
+
// Make sure to update column mapper along with the content.
m := protocol.NewColumnMapper(uri, content)
if change.Range == nil {
@@ -365,9 +368,6 @@
if err != nil {
return nil, err
}
- if !spn.HasOffset() {
- return nil, fmt.Errorf("%w: invalid range for content change", jsonrpc2.ErrInternal)
- }
start, end := spn.Start().Offset(), spn.End().Offset()
if end < start {
return nil, fmt.Errorf("%w: invalid range for content change", jsonrpc2.ErrInternal)
diff --git a/gopls/internal/lsp/work/diagnostics.go b/gopls/internal/lsp/work/diagnostics.go
index 5d3a328..0d0f4eb 100644
--- a/gopls/internal/lsp/work/diagnostics.go
+++ b/gopls/internal/lsp/work/diagnostics.go
@@ -59,7 +59,7 @@
// Add diagnostic if a directory does not contain a module.
var diagnostics []*source.Diagnostic
for _, use := range pw.File.Use {
- rng, err := source.LineToRange(pw.Mapper, fh.URI(), use.Syntax.Start, use.Syntax.End)
+ rng, err := pw.Mapper.OffsetRange(use.Syntax.Start.Byte, use.Syntax.End.Byte)
if err != nil {
return nil, err
}
diff --git a/gopls/internal/lsp/work/hover.go b/gopls/internal/lsp/work/hover.go
index ea4c1e5..641028b 100644
--- a/gopls/internal/lsp/work/hover.go
+++ b/gopls/internal/lsp/work/hover.go
@@ -11,9 +11,9 @@
"go/token"
"golang.org/x/mod/modfile"
- "golang.org/x/tools/internal/event"
"golang.org/x/tools/gopls/internal/lsp/protocol"
"golang.org/x/tools/gopls/internal/lsp/source"
+ "golang.org/x/tools/internal/event"
)
func Hover(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, position protocol.Position) (*protocol.Hover, error) {
@@ -56,7 +56,7 @@
mod := pm.File.Module.Mod
// Get the range to highlight for the hover.
- rng, err := source.ByteOffsetsToRange(pw.Mapper, fh.URI(), pathStart, pathEnd)
+ rng, err := pw.Mapper.OffsetRange(pathStart, pathEnd)
if err != nil {
return nil, err
}
diff --git a/gopls/internal/span/span.go b/gopls/internal/span/span.go
index 5b8d31e..333048a 100644
--- a/gopls/internal/span/span.go
+++ b/gopls/internal/span/span.go
@@ -209,7 +209,8 @@
}
}
-func (s Span) WithPosition(tf *token.File) (Span, error) {
+// (Currently unused, but we gain little yet by deleting it.)
+func (s Span) withPosition(tf *token.File) (Span, error) {
if err := s.update(tf, true, false); err != nil {
return Span{}, err
}
diff --git a/gopls/internal/span/utf16.go b/gopls/internal/span/utf16.go
index f4c93a6..0f8e1bc 100644
--- a/gopls/internal/span/utf16.go
+++ b/gopls/internal/span/utf16.go
@@ -13,6 +13,9 @@
// supplied file contents.
// This is used to convert from the native (always in bytes) column
// representation and the utf16 counts used by some editors.
+//
+// TODO(adonovan): this function is unused except by its test. Delete,
+// or consolidate with (*protocol.ColumnMapper).utf16Column.
func ToUTF16Column(p Point, content []byte) (int, error) {
if !p.HasPosition() {
return -1, fmt.Errorf("ToUTF16Column: point is missing position")