gopls/internal/lsp/protocol: simplify ColumnMapper
This change decouples ColumnMapper from go/token.
Its TokFile field is now unexported and fully
encapsulated and will be removed in a follow-up;
it serves only as a line-number table.
ColumnMapper now provides only mapping between
byte offsets and columns, in three different
units (UTF-8, UTF-16, and runes).
Three operations that require both a Mapper and
a token.File--and require then to be consistent with
each other--have been moved to ParsedGoFile:
(Pos, PosRange, and RangeToSpanRange).
This is another step to keeping the use of token.Pos
close to its token.File or FileSet, and using
byte offsets and ColumnMappers more broadly.
MappedRange now holds a ParsedGoFile and (internally)
a start/end Pos pair, making it self-contained for all
conversions. (The File field is unfortunately public
for now due to one tricky use; fixing it would have
expanded this already large CL.)
I'm not sure whether MappedRange carries its weight;
I think it might be clearer for all users to simply
expand it out (i.e. hold a ColumnMapper and two byte
offsets), making one less creature in the zoo.
Numerous calls to NewMappedRange followed by .Range()
have been reduced to pgf.PosRange().
Also:
- New ColumnMapper methods:
OffsetSpan
OffsetPoint
- safetoken.Offsets(start, end) is the plural of Offset(pos).
- span.ToPosition renamed span.OffsetToLineCol8.
- span.NewTokenFile inlined into sole caller.
- avoid embedding of MappedRange, as it makes the references
hard to see. (Embedded fields are both a def and a ref but
gopls cross-references is confused by that.)
- findLinksInString uses offsets now.
Change-Id: I2c775e181e456604e2ce977d618b0f1ec8e76903
Reviewed-on: https://go-review.googlesource.com/c/tools/+/460615
Run-TryBot: Alan Donovan <adonovan@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Robert Findley <rfindley@google.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
diff --git a/gopls/internal/lsp/cache/analysis.go b/gopls/internal/lsp/cache/analysis.go
index 61e868c..8bbe2ed 100644
--- a/gopls/internal/lsp/cache/analysis.go
+++ b/gopls/internal/lsp/cache/analysis.go
@@ -1024,7 +1024,7 @@
if end == token.NoPos {
end = start
}
- rng, err := p.Mapper.PosRange(start, end)
+ rng, err := p.PosRange(start, end)
if err != nil {
return protocol.Location{}, err
}
diff --git a/gopls/internal/lsp/cache/check.go b/gopls/internal/lsp/cache/check.go
index cfac5ff..c52a2f8 100644
--- a/gopls/internal/lsp/cache/check.go
+++ b/gopls/internal/lsp/cache/check.go
@@ -683,7 +683,7 @@
}
for _, imp := range allImports[item] {
- rng, err := source.NewMappedRange(imp.cgf.Mapper, imp.imp.Pos(), imp.imp.End()).Range()
+ rng, err := imp.cgf.PosRange(imp.imp.Pos(), imp.imp.End())
if err != nil {
return nil, err
}
diff --git a/gopls/internal/lsp/cache/errors.go b/gopls/internal/lsp/cache/errors.go
index 7ca4f07..6777193 100644
--- a/gopls/internal/lsp/cache/errors.go
+++ b/gopls/internal/lsp/cache/errors.go
@@ -86,17 +86,12 @@
if err != nil {
return nil, err
}
- pos := pgf.Tok.Pos(e.Pos.Offset)
- spn, err := span.NewRange(pgf.Tok, pos, pos).Span()
- if err != nil {
- return nil, err
- }
- rng, err := spanToRange(pkg, spn)
+ rng, err := pgf.Mapper.OffsetRange(e.Pos.Offset, e.Pos.Offset)
if err != nil {
return nil, err
}
return []*source.Diagnostic{{
- URI: spn.URI(),
+ URI: pgf.URI,
Range: rng,
Severity: protocol.SeverityError,
Source: source.ParseError,
@@ -327,7 +322,7 @@
if !end.IsValid() || end == start {
end = analysisinternal.TypeErrorEndPos(fset, pgf.Src, start)
}
- spn, err := span.FileSpan(pgf.Mapper.TokFile, start, end)
+ spn, err := span.FileSpan(pgf.Tok, start, end)
if err != nil {
return 0, span.Span{}, err
}
@@ -379,7 +374,11 @@
// Search file imports for the import that is causing the import cycle.
for _, imp := range cgf.File.Imports {
if imp.Path.Value == circImp {
- spn, err := span.NewRange(cgf.Tok, imp.Pos(), imp.End()).Span()
+ start, end, err := safetoken.Offsets(cgf.Tok, imp.Pos(), imp.End())
+ if err != nil {
+ return msg, span.Span{}, false
+ }
+ spn, err := cgf.Mapper.OffsetSpan(start, end)
if err != nil {
return msg, span.Span{}, false
}
diff --git a/gopls/internal/lsp/cache/load.go b/gopls/internal/lsp/cache/load.go
index f79109a..b072aaf 100644
--- a/gopls/internal/lsp/cache/load.go
+++ b/gopls/internal/lsp/cache/load.go
@@ -391,10 +391,7 @@
if pgf, err := s.ParseGo(ctx, fh, source.ParseHeader); err == nil {
// Check that we have a valid `package foo` range to use for positioning the error.
if pgf.File.Package.IsValid() && pgf.File.Name != nil && pgf.File.Name.End().IsValid() {
- pkgDecl := span.NewRange(pgf.Tok, pgf.File.Package, pgf.File.Name.End())
- if spn, err := pkgDecl.Span(); err == nil {
- rng, _ = pgf.Mapper.Range(spn)
- }
+ rng, _ = pgf.PosRange(pgf.File.Package, pgf.File.Name.End())
}
}
case source.Mod:
diff --git a/gopls/internal/lsp/cache/mod.go b/gopls/internal/lsp/cache/mod.go
index 757bb5e..a3d207d 100644
--- a/gopls/internal/lsp/cache/mod.go
+++ b/gopls/internal/lsp/cache/mod.go
@@ -402,11 +402,12 @@
if pm.File.Module == nil {
return span.New(pm.URI, span.NewPoint(1, 1, 0), span.Point{}), false, nil
}
- spn, err := spanFromPositions(pm.Mapper, pm.File.Module.Syntax.Start, pm.File.Module.Syntax.End)
+ syntax := pm.File.Module.Syntax
+ spn, err := pm.Mapper.OffsetSpan(syntax.Start.Byte, syntax.End.Byte)
return spn, false, err
}
- spn, err := spanFromPositions(pm.Mapper, reference.Start, reference.End)
+ spn, err := pm.Mapper.OffsetSpan(reference.Start.Byte, reference.End.Byte)
return spn, true, err
}
diff --git a/gopls/internal/lsp/cache/mod_tidy.go b/gopls/internal/lsp/cache/mod_tidy.go
index fa30df1..c9c02ca 100644
--- a/gopls/internal/lsp/cache/mod_tidy.go
+++ b/gopls/internal/lsp/cache/mod_tidy.go
@@ -8,7 +8,6 @@
"context"
"fmt"
"go/ast"
- "go/token"
"io/ioutil"
"os"
"path/filepath"
@@ -263,7 +262,7 @@
if !ok {
return nil, fmt.Errorf("no missing module fix for %q (%q)", importPath, req.Mod.Path)
}
- srcErr, err := missingModuleForImport(pgf.Tok, m, imp, req, fixes)
+ srcErr, err := missingModuleForImport(pgf, imp, req, fixes)
if err != nil {
return nil, err
}
@@ -423,16 +422,16 @@
// missingModuleForImport creates an error for a given import path that comes
// from a missing module.
-func missingModuleForImport(file *token.File, m *protocol.ColumnMapper, imp *ast.ImportSpec, req *modfile.Require, fixes []source.SuggestedFix) (*source.Diagnostic, error) {
+func missingModuleForImport(pgf *source.ParsedGoFile, imp *ast.ImportSpec, req *modfile.Require, fixes []source.SuggestedFix) (*source.Diagnostic, error) {
if req.Syntax == nil {
return nil, fmt.Errorf("no syntax for %v", req)
}
- rng, err := m.PosRange(imp.Path.Pos(), imp.Path.End())
+ rng, err := pgf.PosRange(imp.Path.Pos(), imp.Path.End())
if err != nil {
return nil, err
}
return &source.Diagnostic{
- URI: m.URI,
+ URI: pgf.URI,
Range: rng,
Severity: protocol.SeverityError,
Source: source.ModTidyError,
@@ -441,25 +440,6 @@
}, nil
}
-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)
- if err != nil {
- return span.Point{}, err
- }
- return span.NewPoint(l, c, offset), nil
- }
- start, err := toPoint(s.Byte)
- if err != nil {
- return span.Span{}, err
- }
- end, err := toPoint(e.Byte)
- if err != nil {
- return span.Span{}, err
- }
- return span.New(m.URI, start, end), nil
-}
-
// parseImports parses the headers of the specified files and returns
// the set of strings that appear in import declarations within
// GoFiles. Errors are ignored.
diff --git a/gopls/internal/lsp/cache/parse.go b/gopls/internal/lsp/cache/parse.go
index 83f18da..7451cc3 100644
--- a/gopls/internal/lsp/cache/parse.go
+++ b/gopls/internal/lsp/cache/parse.go
@@ -202,17 +202,13 @@
}
return &source.ParsedGoFile{
- URI: fh.URI(),
- Mode: mode,
- Src: src,
- Fixed: fixed,
- File: file,
- Tok: tok,
- Mapper: &protocol.ColumnMapper{
- URI: fh.URI(),
- TokFile: tok,
- Content: src,
- },
+ URI: fh.URI(),
+ Mode: mode,
+ Src: src,
+ Fixed: fixed,
+ File: file,
+ Tok: tok,
+ Mapper: protocol.NewColumnMapper(fh.URI(), src),
ParseErr: parseErr,
}, nil
}
@@ -900,11 +896,7 @@
}
// Try to extract a statement from the BadExpr.
- start, err := safetoken.Offset(tok, bad.Pos())
- if err != nil {
- return
- }
- end, err := safetoken.Offset(tok, bad.End()-1)
+ start, end, err := safetoken.Offsets(tok, bad.Pos(), bad.End()-1)
if err != nil {
return
}
@@ -989,11 +981,7 @@
// Avoid doing tok.Offset(to) since that panics if badExpr ends at EOF.
// It also panics if the position is not in the range of the file, and
// badExprs may not necessarily have good positions, so check first.
- fromOffset, err := safetoken.Offset(tok, from)
- if err != nil {
- return false
- }
- toOffset, err := safetoken.Offset(tok, to-1)
+ fromOffset, toOffset, err := safetoken.Offsets(tok, from, to-1)
if err != nil {
return false
}
@@ -1150,18 +1138,13 @@
}
}
- fromOffset, err := safetoken.Offset(tok, from)
+ fromOffset, toOffset, err := safetoken.Offsets(tok, from, to)
if err != nil {
return false
}
if !from.IsValid() || fromOffset >= len(src) {
return false
}
-
- toOffset, err := safetoken.Offset(tok, to)
- if err != nil {
- return false
- }
if !to.IsValid() || toOffset >= len(src) {
return false
}
diff --git a/gopls/internal/lsp/cmd/cmd.go b/gopls/internal/lsp/cmd/cmd.go
index 3aa74d0..c221913 100644
--- a/gopls/internal/lsp/cmd/cmd.go
+++ b/gopls/internal/lsp/cmd/cmd.go
@@ -11,7 +11,6 @@
"context"
"flag"
"fmt"
- "go/token"
"io/ioutil"
"log"
"os"
@@ -385,8 +384,7 @@
type cmdClient struct {
protocol.Server
- app *Application
- fset *token.FileSet
+ app *Application
diagnosticsMu sync.Mutex
diagnosticsDone chan struct{}
@@ -407,7 +405,6 @@
return &connection{
Client: &cmdClient{
app: app,
- fset: token.NewFileSet(),
files: make(map[span.URI]*cmdFile),
},
}
@@ -541,19 +538,12 @@
c.files[uri] = file
}
if file.mapper == nil {
- fname := uri.Filename()
- content, err := ioutil.ReadFile(fname)
+ content, err := ioutil.ReadFile(uri.Filename())
if err != nil {
file.err = fmt.Errorf("getFile: %v: %v", uri, err)
return file
}
- f := c.fset.AddFile(fname, -1, len(content))
- f.SetLinesForContent(content)
- file.mapper = &protocol.ColumnMapper{
- URI: uri,
- TokFile: f,
- Content: content,
- }
+ file.mapper = protocol.NewColumnMapper(uri, content)
}
return file
}
diff --git a/gopls/internal/lsp/code_action.go b/gopls/internal/lsp/code_action.go
index 0767d43..5e0a778 100644
--- a/gopls/internal/lsp/code_action.go
+++ b/gopls/internal/lsp/code_action.go
@@ -316,7 +316,7 @@
if err != nil {
return nil, fmt.Errorf("getting file for Identifier: %w", err)
}
- srng, err := pgf.Mapper.RangeToSpanRange(rng)
+ srng, err := pgf.RangeToSpanRange(rng)
if err != nil {
return nil, err
}
diff --git a/gopls/internal/lsp/completion.go b/gopls/internal/lsp/completion.go
index c967c1f..e443a3c 100644
--- a/gopls/internal/lsp/completion.go
+++ b/gopls/internal/lsp/completion.go
@@ -60,6 +60,8 @@
// internal/span, as the latter treats end of file as the beginning of the
// next line, even when it's not newline-terminated. See golang/go#41029 for
// more details.
+ // TODO(adonovan): make completion retain the pgf.Mapper
+ // so we can convert to rng without reading.
src, err := fh.Read()
if err != nil {
return nil, err
diff --git a/gopls/internal/lsp/definition.go b/gopls/internal/lsp/definition.go
index d2ad474..d83512a 100644
--- a/gopls/internal/lsp/definition.go
+++ b/gopls/internal/lsp/definition.go
@@ -58,13 +58,13 @@
if ident.Type.Object == nil {
return nil, fmt.Errorf("no type definition for %s", ident.Name)
}
- identRange, err := ident.Type.Range()
+ identRange, err := ident.Type.MappedRange.Range()
if err != nil {
return nil, err
}
return []protocol.Location{
{
- URI: protocol.URIFromSpanURI(ident.Type.URI()),
+ URI: protocol.URIFromSpanURI(ident.Type.MappedRange.URI()),
Range: identRange,
},
}, nil
diff --git a/gopls/internal/lsp/diagnostics.go b/gopls/internal/lsp/diagnostics.go
index 863c142..6ec7a08 100644
--- a/gopls/internal/lsp/diagnostics.go
+++ b/gopls/internal/lsp/diagnostics.go
@@ -577,7 +577,7 @@
if !pgf.File.Name.Pos().IsValid() {
return nil
}
- rng, err := pgf.Mapper.PosRange(pgf.File.Name.Pos(), pgf.File.Name.End())
+ rng, err := pgf.PosRange(pgf.File.Name.Pos(), pgf.File.Name.End())
if err != nil {
return nil
}
diff --git a/gopls/internal/lsp/folding_range.go b/gopls/internal/lsp/folding_range.go
index 4a2d828..86469d3 100644
--- a/gopls/internal/lsp/folding_range.go
+++ b/gopls/internal/lsp/folding_range.go
@@ -28,7 +28,7 @@
func toProtocolFoldingRanges(ranges []*source.FoldingRangeInfo) ([]protocol.FoldingRange, error) {
result := make([]protocol.FoldingRange, 0, len(ranges))
for _, info := range ranges {
- rng, err := info.Range()
+ rng, err := info.MappedRange.Range()
if err != nil {
return nil, err
}
diff --git a/gopls/internal/lsp/link.go b/gopls/internal/lsp/link.go
index 011f0e4..e7bdb60 100644
--- a/gopls/internal/lsp/link.go
+++ b/gopls/internal/lsp/link.go
@@ -17,8 +17,8 @@
"golang.org/x/mod/modfile"
"golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/lsp/safetoken"
"golang.org/x/tools/gopls/internal/lsp/source"
- "golang.org/x/tools/gopls/internal/span"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/event/tag"
)
@@ -48,7 +48,6 @@
if err != nil {
return nil, err
}
- tokFile := pm.Mapper.TokFile
var links []protocol.DocumentLink
for _, req := range pm.File.Require {
@@ -60,16 +59,15 @@
continue
}
dep := []byte(req.Mod.Path)
- s, e := req.Syntax.Start.Byte, req.Syntax.End.Byte
- i := bytes.Index(pm.Mapper.Content[s:e], dep)
+ start, end := req.Syntax.Start.Byte, req.Syntax.End.Byte
+ i := bytes.Index(pm.Mapper.Content[start:end], dep)
if i == -1 {
continue
}
// Shift the start position to the location of the
// dependency within the require statement.
- start, end := tokFile.Pos(s+i), tokFile.Pos(s+i+len(dep))
target := source.BuildLink(snapshot.View().Options().LinkTarget, "mod/"+req.Mod.String(), "")
- l, err := toProtocolLink(tokFile, pm.Mapper, target, start, end)
+ l, err := toProtocolLink(pm.Mapper, target, start+i, start+i+len(dep))
if err != nil {
return nil, err
}
@@ -89,8 +87,7 @@
}
for _, section := range [][]modfile.Comment{comments.Before, comments.Suffix, comments.After} {
for _, comment := range section {
- start := tokFile.Pos(comment.Start.Byte)
- l, err := findLinksInString(urlRegexp, comment.Token, start, tokFile, pm.Mapper)
+ l, err := findLinksInString(urlRegexp, comment.Token, comment.Start.Byte, pm.Mapper)
if err != nil {
return nil, err
}
@@ -145,11 +142,13 @@
urlPath = strings.Replace(urlPath, m.Module.Path, m.Module.Path+"@"+m.Module.Version, 1)
}
- // Account for the quotation marks in the positions.
- start := imp.Path.Pos() + 1
- end := imp.Path.End() - 1
+ start, end, err := safetoken.Offsets(pgf.Tok, imp.Path.Pos(), imp.Path.End())
+ if err != nil {
+ return nil, err
+ }
targetURL := source.BuildLink(view.Options().LinkTarget, urlPath, "")
- l, err := toProtocolLink(pgf.Tok, pgf.Mapper, targetURL, start, end)
+ // Account for the quotation marks in the positions.
+ l, err := toProtocolLink(pgf.Mapper, targetURL, start+len(`"`), end-len(`"`))
if err != nil {
return nil, err
}
@@ -173,7 +172,11 @@
return true
})
for _, s := range str {
- l, err := findLinksInString(urlRegexp, s.Value, s.Pos(), pgf.Tok, pgf.Mapper)
+ strOffset, err := safetoken.Offset(pgf.Tok, s.Pos())
+ if err != nil {
+ return nil, err
+ }
+ l, err := findLinksInString(urlRegexp, s.Value, strOffset, pgf.Mapper)
if err != nil {
return nil, err
}
@@ -183,7 +186,11 @@
// Gather links found in comments.
for _, commentGroup := range pgf.File.Comments {
for _, comment := range commentGroup.List {
- l, err := findLinksInString(urlRegexp, comment.Text, comment.Pos(), pgf.Tok, pgf.Mapper)
+ commentOffset, err := safetoken.Offset(pgf.Tok, comment.Pos())
+ if err != nil {
+ return nil, err
+ }
+ l, err := findLinksInString(urlRegexp, comment.Text, commentOffset, pgf.Mapper)
if err != nil {
return nil, err
}
@@ -203,13 +210,11 @@
}
// urlRegexp is the user-supplied regular expression to match URL.
-// tokFile may be a throwaway File for non-Go files.
-func findLinksInString(urlRegexp *regexp.Regexp, src string, pos token.Pos, tokFile *token.File, m *protocol.ColumnMapper) ([]protocol.DocumentLink, error) {
+// srcOffset is the start offset of 'src' within m's file.
+func findLinksInString(urlRegexp *regexp.Regexp, src string, srcOffset int, m *protocol.ColumnMapper) ([]protocol.DocumentLink, error) {
var links []protocol.DocumentLink
for _, index := range urlRegexp.FindAllIndex([]byte(src), -1) {
start, end := index[0], index[1]
- startPos := token.Pos(int(pos) + start)
- endPos := token.Pos(int(pos) + end)
link := src[start:end]
linkURL, err := url.Parse(link)
// Fallback: Linkify IP addresses as suggested in golang/go#18824.
@@ -227,7 +232,8 @@
if !acceptedSchemes[linkURL.Scheme] {
continue
}
- l, err := toProtocolLink(tokFile, m, linkURL.String(), startPos, endPos)
+
+ l, err := toProtocolLink(m, linkURL.String(), srcOffset+start, srcOffset+end)
if err != nil {
return nil, err
}
@@ -237,15 +243,13 @@
r := getIssueRegexp()
for _, index := range r.FindAllIndex([]byte(src), -1) {
start, end := index[0], index[1]
- startPos := token.Pos(int(pos) + start)
- endPos := token.Pos(int(pos) + end)
matches := r.FindStringSubmatch(src)
if len(matches) < 4 {
continue
}
org, repo, number := matches[1], matches[2], matches[3]
targetURL := fmt.Sprintf("https://github.com/%s/%s/issues/%s", org, repo, number)
- l, err := toProtocolLink(tokFile, m, targetURL, startPos, endPos)
+ l, err := toProtocolLink(m, targetURL, srcOffset+start, srcOffset+end)
if err != nil {
return nil, err
}
@@ -266,12 +270,8 @@
issueRegexp *regexp.Regexp
)
-func toProtocolLink(tokFile *token.File, m *protocol.ColumnMapper, targetURL string, start, end token.Pos) (protocol.DocumentLink, error) {
- spn, err := span.NewRange(tokFile, start, end).Span()
- if err != nil {
- return protocol.DocumentLink{}, err
- }
- rng, err := m.Range(spn)
+func toProtocolLink(m *protocol.ColumnMapper, targetURL string, start, end int) (protocol.DocumentLink, error) {
+ rng, err := m.OffsetRange(start, end)
if err != nil {
return protocol.DocumentLink{}, err
}
diff --git a/gopls/internal/lsp/protocol/span.go b/gopls/internal/lsp/protocol/span.go
index 78e180c..160b925 100644
--- a/gopls/internal/lsp/protocol/span.go
+++ b/gopls/internal/lsp/protocol/span.go
@@ -8,12 +8,12 @@
//
// Imports: source --> lsppos --> protocol --> span --> token
//
-// source.MappedRange = (span.Range, protocol.ColumnMapper)
+// source.MappedRange = (*ParsedGoFile, start/end token.Pos)
//
// lsppos.TokenMapper = (token.File, lsppos.Mapper)
// lsppos.Mapper = (line offset table, content)
//
-// protocol.ColumnMapper = (URI, token.File, content)
+// protocol.ColumnMapper = (URI, Content). Does all offset <=> column conversions.
// protocol.Location = (URI, protocol.Range)
// protocol.Range = (start, end Position)
// protocol.Position = (line, char uint32) 0-based UTF-16
@@ -24,12 +24,19 @@
//
// token.Pos
// token.FileSet
+// token.File
// 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.
+// TODO(adonovan): simplify this picture:
+// - Eliminate the optionality of span.{Span,Point}'s position and offset fields?
+// - Move span.Range to package safetoken. Can we eliminate it?
+// Without a ColumnMapper it's not really self-contained.
+// It is mostly used by completion. Given access to complete.mapper,
+// it could use a pair byte offsets instead.
+// - Merge lsppos.Mapper and protocol.ColumnMapper.
+// - Replace all uses of lsppos.TokenMapper by the underlying ParsedGoFile,
+// which carries a token.File and a ColumnMapper.
+// - Then delete lsppos package.
package protocol
@@ -41,35 +48,52 @@
"strings"
"unicode/utf8"
- "golang.org/x/tools/gopls/internal/lsp/safetoken"
"golang.org/x/tools/gopls/internal/span"
"golang.org/x/tools/internal/bug"
)
-// A ColumnMapper maps between UTF-8 oriented positions (e.g. token.Pos,
-// span.Span) and the UTF-16 oriented positions used by the LSP.
+// A ColumnMapper wraps the content of a file and provides mapping
+// from byte offsets to and from other notations of position:
+//
+// - (line, col8) pairs, where col8 is a 1-based UTF-8 column number (bytes),
+// as used by go/token;
+//
+// - (line, col16) pairs, where col16 is a 1-based UTF-16 column number,
+// as used by the LSP protocol;
+//
+// - (line, colRune) pairs, where colRune is a rune index, as used by ParseWork.
+//
+// This type does not depend on or use go/token-based representations.
+// Use safetoken to map between token.Pos <=> byte offsets.
type ColumnMapper struct {
URI span.URI
- TokFile *token.File
Content []byte
- // File content is only really needed for UTF-16 column
- // computation, which could be be achieved more compactly.
- // For example, one could record only the lines for which
- // UTF-16 columns differ from the UTF-8 ones, or only the
- // indices of the non-ASCII characters.
+ // This field provides a line-number table, nothing more.
+ // The public API of ColumnMapper doesn't mention go/token,
+ // nor should it. It need not be consistent with any
+ // other token.File or FileSet.
//
- // TODO(adonovan): consider not retaining the entire file
- // content, or at least not exposing the fact that we
- // currently retain it.
+ // TODO(adonovan): eliminate this field in a follow-up
+ // by inlining the line-number table. Then merge this
+ // type with the nearly identical lsspos.Mapper.
+ //
+ // TODO(adonovan): opt: quick experiments suggest that
+ // ColumnMappers are created for thousands of files but the
+ // m.lines field is accessed only for a small handful.
+ // So it would make sense to allocate it lazily.
+ lines *token.File
}
// NewColumnMapper creates a new column mapper for the given uri and content.
func NewColumnMapper(uri span.URI, content []byte) *ColumnMapper {
- tf := span.NewTokenFile(uri.Filename(), content)
+ fset := token.NewFileSet()
+ tf := fset.AddFile(uri.Filename(), -1, len(content))
+ tf.SetLinesForContent(content)
+
return &ColumnMapper{
URI: uri,
- TokFile: tf,
+ lines: tf,
Content: content,
}
}
@@ -105,7 +129,7 @@
return Range{}, bug.Errorf("column mapper is for file %q instead of %q", m.URI, s.URI())
}
- s, err := s.WithOffset(m.TokFile)
+ s, err := s.WithOffset(m.lines)
if err != nil {
return Range{}, err
}
@@ -135,17 +159,20 @@
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)
+// OffsetSpan converts a pair of byte offsets to a Span.
+func (m *ColumnMapper) OffsetSpan(start, end int) (span.Span, error) {
+ if start > end {
+ return span.Span{}, fmt.Errorf("start offset (%d) > end (%d)", start, end)
}
- endOffset, err := safetoken.Offset(m.TokFile, end)
+ startPoint, err := m.OffsetPoint(start)
if err != nil {
- return Range{}, fmt.Errorf("end: %v", err)
+ return span.Span{}, err
}
- return m.OffsetRange(startOffset, endOffset)
+ endPoint, err := m.OffsetPoint(end)
+ if err != nil {
+ return span.Span{}, err
+ }
+ return span.New(m.URI, startPoint, endPoint), nil
}
// Position returns the protocol position for the specified point,
@@ -160,14 +187,14 @@
// OffsetPosition returns the protocol position of the specified
// offset within m.Content.
func (m *ColumnMapper) OffsetPosition(offset int) (Position, error) {
- // We use span.ToPosition for its "line+1 at EOF" workaround.
- line, _, err := span.ToPosition(m.TokFile, offset)
+ // We use span.OffsetToLineCol8 for its "line+1 at EOF" workaround.
+ line, _, err := span.OffsetToLineCol8(m.lines, offset)
if err != nil {
return Position{}, fmt.Errorf("OffsetPosition: %v", err)
}
// If that workaround executed, skip the usual column computation.
char := 0
- if offset != m.TokFile.Size() {
+ if offset != m.lines.Size() {
char = m.utf16Column(offset)
}
return Position{
@@ -224,24 +251,7 @@
if err != nil {
return span.Span{}, err
}
- return span.New(m.URI, start, end).WithAll(m.TokFile)
-}
-
-func (m *ColumnMapper) RangeToSpanRange(r Range) (span.Range, error) {
- spn, err := m.RangeSpan(r)
- if err != nil {
- return span.Range{}, err
- }
- return spn.Range(m.TokFile)
-}
-
-// 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
- }
- return safetoken.Pos(m.TokFile, start.Offset())
+ return span.New(m.URI, start, end).WithAll(m.lines)
}
// Offset returns the utf-8 byte offset of p within the mapped file.
@@ -253,13 +263,23 @@
return start.Offset(), nil
}
+// OffsetPoint returns the span.Point for the given byte offset.
+func (m *ColumnMapper) OffsetPoint(offset int) (span.Point, error) {
+ // We use span.ToPosition for its "line+1 at EOF" workaround.
+ line, col8, err := span.OffsetToLineCol8(m.lines, offset)
+ if err != nil {
+ return span.Point{}, fmt.Errorf("OffsetPoint: %v", err)
+ }
+ return span.NewPoint(line, col8, offset), nil
+}
+
// 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)
+ offset, err := span.ToOffset(m.lines, line, 1)
if err != nil {
return span.Point{}, err
}
diff --git a/gopls/internal/lsp/references.go b/gopls/internal/lsp/references.go
index 6f4e3ee..390e290 100644
--- a/gopls/internal/lsp/references.go
+++ b/gopls/internal/lsp/references.go
@@ -27,12 +27,12 @@
}
var locations []protocol.Location
for _, ref := range references {
- refRange, err := ref.Range()
+ refRange, err := ref.MappedRange.Range()
if err != nil {
return nil, err
}
locations = append(locations, protocol.Location{
- URI: protocol.URIFromSpanURI(ref.URI()),
+ URI: protocol.URIFromSpanURI(ref.MappedRange.URI()),
Range: refRange,
})
}
diff --git a/gopls/internal/lsp/safetoken/safetoken.go b/gopls/internal/lsp/safetoken/safetoken.go
index fa9c675..29cc1b1 100644
--- a/gopls/internal/lsp/safetoken/safetoken.go
+++ b/gopls/internal/lsp/safetoken/safetoken.go
@@ -41,6 +41,19 @@
return int(pos) - f.Base(), nil
}
+// Offsets returns Offset(start) and Offset(end).
+func Offsets(f *token.File, start, end token.Pos) (int, int, error) {
+ startOffset, err := Offset(f, start)
+ if err != nil {
+ return 0, 0, fmt.Errorf("start: %v", err)
+ }
+ endOffset, err := Offset(f, end)
+ if err != nil {
+ return 0, 0, fmt.Errorf("end: %v", err)
+ }
+ return startOffset, endOffset, nil
+}
+
// Pos returns f.Pos(offset), but first checks that the offset is
// non-negative and not larger than the size of the file.
func Pos(f *token.File, offset int) (token.Pos, error) {
diff --git a/gopls/internal/lsp/selection_range.go b/gopls/internal/lsp/selection_range.go
index 314f224..b7a0cdb 100644
--- a/gopls/internal/lsp/selection_range.go
+++ b/gopls/internal/lsp/selection_range.go
@@ -41,7 +41,7 @@
result := make([]protocol.SelectionRange, len(params.Positions))
for i, protocolPos := range params.Positions {
- pos, err := pgf.Mapper.Pos(protocolPos)
+ pos, err := pgf.Pos(protocolPos)
if err != nil {
return nil, err
}
@@ -51,7 +51,7 @@
tail := &result[i] // tail of the Parent linked list, built head first
for j, node := range path {
- rng, err := pgf.Mapper.PosRange(node.Pos(), node.End())
+ rng, err := pgf.PosRange(node.Pos(), node.End())
if err != nil {
return nil, err
}
diff --git a/gopls/internal/lsp/semantic.go b/gopls/internal/lsp/semantic.go
index 4117eb7..728f61d 100644
--- a/gopls/internal/lsp/semantic.go
+++ b/gopls/internal/lsp/semantic.go
@@ -182,8 +182,7 @@
return
}
// want a line and column from start (in LSP coordinates). Ignore line directives.
- rng := source.NewMappedRange(e.pgf.Mapper, start, start+token.Pos(leng))
- lspRange, err := rng.Range()
+ lspRange, err := e.pgf.PosRange(start, start+token.Pos(leng))
if err != nil {
event.Error(e.ctx, "failed to convert to range", err)
return
diff --git a/gopls/internal/lsp/source/call_hierarchy.go b/gopls/internal/lsp/source/call_hierarchy.go
index 076aed0..cce8a13 100644
--- a/gopls/internal/lsp/source/call_hierarchy.go
+++ b/gopls/internal/lsp/source/call_hierarchy.go
@@ -86,12 +86,12 @@
// once in the result but highlight all calls using FromRanges (ranges at which the calls occur)
var incomingCalls = map[protocol.Location]*protocol.CallHierarchyIncomingCall{}
for _, ref := range refs {
- refRange, err := ref.Range()
+ refRange, err := ref.MappedRange.Range()
if err != nil {
return nil, err
}
- callItem, err := enclosingNodeCallItem(snapshot, ref.pkg, ref.URI(), ref.ident.NamePos)
+ callItem, err := enclosingNodeCallItem(snapshot, ref.pkg, ref.MappedRange.URI(), ref.ident.NamePos)
if err != nil {
event.Error(ctx, "error getting enclosing node", err, tag.Method.Of(ref.Name))
continue
@@ -155,7 +155,7 @@
nameStart, nameEnd = funcLit.Type.Func, funcLit.Type.Params.Pos()
kind = protocol.Function
}
- rng, err := NewMappedRange(pgf.Mapper, nameStart, nameEnd).Range()
+ rng, err := pgf.PosRange(nameStart, nameEnd)
if err != nil {
return protocol.CallHierarchyItem{}, err
}
@@ -199,7 +199,7 @@
if len(identifier.Declaration.MappedRange) == 0 {
return nil, nil
}
- callExprs, err := collectCallExpressions(identifier.Declaration.MappedRange[0].m, node)
+ callExprs, err := collectCallExpressions(identifier.Declaration.MappedRange[0].File, node)
if err != nil {
return nil, err
}
@@ -208,7 +208,7 @@
}
// collectCallExpressions collects call expression ranges inside a function.
-func collectCallExpressions(mapper *protocol.ColumnMapper, node ast.Node) ([]protocol.Range, error) {
+func collectCallExpressions(pgf *ParsedGoFile, node ast.Node) ([]protocol.Range, error) {
type callPos struct {
start, end token.Pos
}
@@ -238,7 +238,7 @@
callRanges := []protocol.Range{}
for _, call := range callPositions {
- callRange, err := NewMappedRange(mapper, call.start, call.end).Range()
+ callRange, err := pgf.PosRange(call.start, call.end)
if err != nil {
return nil, err
}
diff --git a/gopls/internal/lsp/source/code_lens.go b/gopls/internal/lsp/source/code_lens.go
index c87f416..f929256 100644
--- a/gopls/internal/lsp/source/code_lens.go
+++ b/gopls/internal/lsp/source/code_lens.go
@@ -67,7 +67,7 @@
return nil, err
}
// add a code lens to the top of the file which runs all benchmarks in the file
- rng, err := NewMappedRange(pgf.Mapper, pgf.File.Package, pgf.File.Package).Range()
+ rng, err := pgf.PosRange(pgf.File.Package, pgf.File.Package)
if err != nil {
return nil, err
}
@@ -111,7 +111,7 @@
continue
}
- rng, err := NewMappedRange(pgf.Mapper, fn.Pos(), fn.End()).Range()
+ rng, err := pgf.PosRange(fn.Pos(), fn.End())
if err != nil {
return out, err
}
@@ -177,7 +177,7 @@
if !strings.HasPrefix(l.Text, ggDirective) {
continue
}
- rng, err := NewMappedRange(pgf.Mapper, l.Pos(), l.Pos()+token.Pos(len(ggDirective))).Range()
+ rng, err := pgf.PosRange(l.Pos(), l.Pos()+token.Pos(len(ggDirective)))
if err != nil {
return nil, err
}
@@ -214,7 +214,7 @@
if c == nil {
return nil, nil
}
- rng, err := NewMappedRange(pgf.Mapper, c.Pos(), c.End()).Range()
+ rng, err := pgf.PosRange(c.Pos(), c.End())
if err != nil {
return nil, err
}
@@ -235,7 +235,7 @@
// Without a package name we have nowhere to put the codelens, so give up.
return nil, nil
}
- rng, err := NewMappedRange(pgf.Mapper, pgf.File.Package, pgf.File.Package).Range()
+ rng, err := pgf.PosRange(pgf.File.Package, pgf.File.Package)
if err != nil {
return nil, err
}
diff --git a/gopls/internal/lsp/source/completion/completion.go b/gopls/internal/lsp/source/completion/completion.go
index 19d16a1..4d2da2e 100644
--- a/gopls/internal/lsp/source/completion/completion.go
+++ b/gopls/internal/lsp/source/completion/completion.go
@@ -291,6 +291,10 @@
content string
cursor token.Pos // relative to rng.TokFile
rng span.Range
+ // TODO(adonovan): keep the ColumnMapper (completer.mapper)
+ // nearby so we can convert rng to protocol form without
+ // needing to read the file again, as the sole caller of
+ // Selection.Range() must currently do.
}
func (p Selection) Content() string {
@@ -441,7 +445,7 @@
}
return items, surrounding, nil
}
- pos, err := pgf.Mapper.Pos(protoPos)
+ pos, err := pgf.Pos(protoPos)
if err != nil {
return nil, nil, err
}
diff --git a/gopls/internal/lsp/source/completion/package.go b/gopls/internal/lsp/source/completion/package.go
index 70d98df..de2b75d 100644
--- a/gopls/internal/lsp/source/completion/package.go
+++ b/gopls/internal/lsp/source/completion/package.go
@@ -37,7 +37,7 @@
return nil, nil, err
}
- pos, err := pgf.Mapper.Pos(position)
+ pos, err := pgf.Pos(position)
if err != nil {
return nil, nil, err
}
diff --git a/gopls/internal/lsp/source/completion/util.go b/gopls/internal/lsp/source/completion/util.go
index 72877a3..4b6ec09 100644
--- a/gopls/internal/lsp/source/completion/util.go
+++ b/gopls/internal/lsp/source/completion/util.go
@@ -312,13 +312,9 @@
}
func (c *completer) editText(from, to token.Pos, newText string) ([]protocol.TextEdit, error) {
- start, err := safetoken.Offset(c.tokFile, from)
+ start, end, err := safetoken.Offsets(c.tokFile, from, to)
if err != nil {
- return nil, err // can't happen: from came from c
- }
- end, err := safetoken.Offset(c.tokFile, to)
- if err != nil {
- return nil, err // can't happen: to came from c
+ return nil, err // can't happen: from/to came from c
}
return source.ToProtocolEdits(c.mapper, []diff.Edit{{
Start: start,
diff --git a/gopls/internal/lsp/source/extract.go b/gopls/internal/lsp/source/extract.go
index 0ac18e1..31a8598 100644
--- a/gopls/internal/lsp/source/extract.go
+++ b/gopls/internal/lsp/source/extract.go
@@ -134,11 +134,7 @@
// line of code on which the insertion occurs.
func calculateIndentation(content []byte, tok *token.File, insertBeforeStmt ast.Node) (string, error) {
line := tok.Line(insertBeforeStmt.Pos())
- lineOffset, err := safetoken.Offset(tok, tok.LineStart(line))
- if err != nil {
- return "", err
- }
- stmtOffset, err := safetoken.Offset(tok, insertBeforeStmt.Pos())
+ lineOffset, stmtOffset, err := safetoken.Offsets(tok, tok.LineStart(line), insertBeforeStmt.Pos())
if err != nil {
return "", err
}
@@ -405,11 +401,7 @@
// We put the selection in a constructed file. We can then traverse and edit
// the extracted selection without modifying the original AST.
- startOffset, err := safetoken.Offset(tok, rng.Start)
- if err != nil {
- return nil, err
- }
- endOffset, err := safetoken.Offset(tok, rng.End)
+ startOffset, endOffset, err := safetoken.Offsets(tok, rng.Start, rng.End)
if err != nil {
return nil, err
}
@@ -605,11 +597,7 @@
// We're going to replace the whole enclosing function,
// so preserve the text before and after the selected block.
- outerStart, err := safetoken.Offset(tok, outer.Pos())
- if err != nil {
- return nil, err
- }
- outerEnd, err := safetoken.Offset(tok, outer.End())
+ outerStart, outerEnd, err := safetoken.Offsets(tok, outer.Pos(), outer.End())
if err != nil {
return nil, err
}
diff --git a/gopls/internal/lsp/source/fix.go b/gopls/internal/lsp/source/fix.go
index 34a6fe8..873db7f 100644
--- a/gopls/internal/lsp/source/fix.go
+++ b/gopls/internal/lsp/source/fix.go
@@ -15,6 +15,7 @@
"golang.org/x/tools/gopls/internal/lsp/analysis/fillstruct"
"golang.org/x/tools/gopls/internal/lsp/analysis/undeclaredname"
"golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/lsp/safetoken"
"golang.org/x/tools/gopls/internal/span"
"golang.org/x/tools/internal/bug"
)
@@ -93,6 +94,10 @@
if !end.IsValid() {
end = edit.Pos
}
+ startOffset, endOffset, err := safetoken.Offsets(tokFile, edit.Pos, end)
+ if err != nil {
+ return nil, err
+ }
fh, err := snapshot.GetVersionedFile(ctx, span.URIFromPath(tokFile.Name()))
if err != nil {
return nil, err
@@ -109,12 +114,13 @@
}
editsPerFile[fh.URI()] = te
}
+ // TODO(adonovan): once FileHandle has a ColumnMapper, eliminate this.
content, err := fh.Read()
if err != nil {
return nil, err
}
- m := protocol.ColumnMapper{URI: fh.URI(), TokFile: tokFile, Content: content}
- rng, err := m.PosRange(edit.Pos, end)
+ m := protocol.NewColumnMapper(fh.URI(), content)
+ rng, err := m.OffsetRange(startOffset, endOffset)
if err != nil {
return nil, err
}
@@ -137,7 +143,7 @@
if err != nil {
return nil, span.Range{}, nil, nil, nil, nil, fmt.Errorf("getting file for Identifier: %w", err)
}
- rng, err := pgf.Mapper.RangeToSpanRange(pRng)
+ rng, err := pgf.RangeToSpanRange(pRng)
if err != nil {
return nil, span.Range{}, nil, nil, nil, nil, err
}
diff --git a/gopls/internal/lsp/source/folding_range.go b/gopls/internal/lsp/source/folding_range.go
index dacb5ae..01107fd 100644
--- a/gopls/internal/lsp/source/folding_range.go
+++ b/gopls/internal/lsp/source/folding_range.go
@@ -16,8 +16,8 @@
// FoldingRangeInfo holds range and kind info of folding for an ast.Node
type FoldingRangeInfo struct {
- MappedRange
- Kind protocol.FoldingRangeKind
+ MappedRange MappedRange
+ Kind protocol.FoldingRangeKind
}
// FoldingRange gets all of the folding range for f.
@@ -42,10 +42,10 @@
}
// Get folding ranges for comments separately as they are not walked by ast.Inspect.
- ranges = append(ranges, commentsFoldingRange(pgf.Mapper, pgf.File)...)
+ ranges = append(ranges, commentsFoldingRange(pgf)...)
visit := func(n ast.Node) bool {
- rng := foldingRangeFunc(pgf.Tok, pgf.Mapper, n, lineFoldingOnly)
+ rng := foldingRangeFunc(pgf, n, lineFoldingOnly)
if rng != nil {
ranges = append(ranges, rng)
}
@@ -55,8 +55,8 @@
ast.Inspect(pgf.File, visit)
sort.Slice(ranges, func(i, j int) bool {
- irng, _ := ranges[i].Range()
- jrng, _ := ranges[j].Range()
+ irng, _ := ranges[i].MappedRange.Range()
+ jrng, _ := ranges[j].MappedRange.Range()
return protocol.CompareRange(irng, jrng) < 0
})
@@ -64,7 +64,7 @@
}
// foldingRangeFunc calculates the line folding range for ast.Node n
-func foldingRangeFunc(tokFile *token.File, m *protocol.ColumnMapper, n ast.Node, lineFoldingOnly bool) *FoldingRangeInfo {
+func foldingRangeFunc(pgf *ParsedGoFile, n ast.Node, lineFoldingOnly bool) *FoldingRangeInfo {
// TODO(suzmue): include trailing empty lines before the closing
// parenthesis/brace.
var kind protocol.FoldingRangeKind
@@ -76,7 +76,7 @@
if num := len(n.List); num != 0 {
startList, endList = n.List[0].Pos(), n.List[num-1].End()
}
- start, end = validLineFoldingRange(tokFile, n.Lbrace, n.Rbrace, startList, endList, lineFoldingOnly)
+ start, end = validLineFoldingRange(pgf.Tok, n.Lbrace, n.Rbrace, startList, endList, lineFoldingOnly)
case *ast.CaseClause:
// Fold from position of ":" to end.
start, end = n.Colon+1, n.End()
@@ -92,7 +92,7 @@
if num := len(n.List); num != 0 {
startList, endList = n.List[0].Pos(), n.List[num-1].End()
}
- start, end = validLineFoldingRange(tokFile, n.Opening, n.Closing, startList, endList, lineFoldingOnly)
+ start, end = validLineFoldingRange(pgf.Tok, n.Opening, n.Closing, startList, endList, lineFoldingOnly)
case *ast.GenDecl:
// If this is an import declaration, set the kind to be protocol.Imports.
if n.Tok == token.IMPORT {
@@ -103,7 +103,7 @@
if num := len(n.Specs); num != 0 {
startSpecs, endSpecs = n.Specs[0].Pos(), n.Specs[num-1].End()
}
- start, end = validLineFoldingRange(tokFile, n.Lparen, n.Rparen, startSpecs, endSpecs, lineFoldingOnly)
+ start, end = validLineFoldingRange(pgf.Tok, n.Lparen, n.Rparen, startSpecs, endSpecs, lineFoldingOnly)
case *ast.BasicLit:
// Fold raw string literals from position of "`" to position of "`".
if n.Kind == token.STRING && len(n.Value) >= 2 && n.Value[0] == '`' && n.Value[len(n.Value)-1] == '`' {
@@ -115,7 +115,7 @@
if num := len(n.Elts); num != 0 {
startElts, endElts = n.Elts[0].Pos(), n.Elts[num-1].End()
}
- start, end = validLineFoldingRange(tokFile, n.Lbrace, n.Rbrace, startElts, endElts, lineFoldingOnly)
+ start, end = validLineFoldingRange(pgf.Tok, n.Lbrace, n.Rbrace, startElts, endElts, lineFoldingOnly)
}
// Check that folding positions are valid.
@@ -123,11 +123,11 @@
return nil
}
// in line folding mode, do not fold if the start and end lines are the same.
- if lineFoldingOnly && tokFile.Line(start) == tokFile.Line(end) {
+ if lineFoldingOnly && pgf.Tok.Line(start) == pgf.Tok.Line(end) {
return nil
}
return &FoldingRangeInfo{
- MappedRange: NewMappedRange(m, start, end),
+ MappedRange: NewMappedRange(pgf, start, end),
Kind: kind,
}
}
@@ -157,9 +157,9 @@
// commentsFoldingRange returns the folding ranges for all comment blocks in file.
// The folding range starts at the end of the first line of the comment block, and ends at the end of the
// comment block and has kind protocol.Comment.
-func commentsFoldingRange(m *protocol.ColumnMapper, file *ast.File) (comments []*FoldingRangeInfo) {
- tokFile := m.TokFile
- for _, commentGrp := range file.Comments {
+func commentsFoldingRange(pgf *ParsedGoFile) (comments []*FoldingRangeInfo) {
+ tokFile := pgf.Tok
+ for _, commentGrp := range pgf.File.Comments {
startGrpLine, endGrpLine := tokFile.Line(commentGrp.Pos()), tokFile.Line(commentGrp.End())
if startGrpLine == endGrpLine {
// Don't fold single line comments.
@@ -176,7 +176,7 @@
}
comments = append(comments, &FoldingRangeInfo{
// Fold from the end of the first line comment to the end of the comment block.
- MappedRange: NewMappedRange(m, endLinePos, commentGrp.End()),
+ MappedRange: NewMappedRange(pgf, endLinePos, commentGrp.End()),
Kind: protocol.Comment,
})
}
diff --git a/gopls/internal/lsp/source/format.go b/gopls/internal/lsp/source/format.go
index 6662137..a272218 100644
--- a/gopls/internal/lsp/source/format.go
+++ b/gopls/internal/lsp/source/format.go
@@ -199,7 +199,7 @@
fixedData = append(fixedData, '\n') // ApplyFixes may miss the newline, go figure.
}
edits := snapshot.View().Options().ComputeEdits(left, string(fixedData))
- return protocolEditsFromSource([]byte(left), edits, pgf.Mapper.TokFile)
+ return protocolEditsFromSource([]byte(left), edits)
}
// importPrefix returns the prefix of the given file content through the final
@@ -314,7 +314,7 @@
// protocolEditsFromSource converts text edits to LSP edits using the original
// source.
-func protocolEditsFromSource(src []byte, edits []diff.Edit, tf *token.File) ([]protocol.TextEdit, error) {
+func protocolEditsFromSource(src []byte, edits []diff.Edit) ([]protocol.TextEdit, error) {
m := lsppos.NewMapper(src)
var result []protocol.TextEdit
for _, edit := range edits {
diff --git a/gopls/internal/lsp/source/highlight.go b/gopls/internal/lsp/source/highlight.go
index d073fff..9bd90e5 100644
--- a/gopls/internal/lsp/source/highlight.go
+++ b/gopls/internal/lsp/source/highlight.go
@@ -28,7 +28,7 @@
return nil, fmt.Errorf("getting package for Highlight: %w", err)
}
- pos, err := pgf.Mapper.Pos(position)
+ pos, err := pgf.Pos(position)
if err != nil {
return nil, err
}
diff --git a/gopls/internal/lsp/source/hover.go b/gopls/internal/lsp/source/hover.go
index dd6ce40..2cf369b 100644
--- a/gopls/internal/lsp/source/hover.go
+++ b/gopls/internal/lsp/source/hover.go
@@ -83,7 +83,7 @@
if err != nil {
return nil, err
}
- rng, err := ident.Range()
+ rng, err := ident.MappedRange.Range()
if err != nil {
return nil, err
}
@@ -104,11 +104,7 @@
ctx, done := event.Start(ctx, "source.hoverRune")
defer done()
- r, mrng, err := findRune(ctx, snapshot, fh, position)
- if err != nil {
- return nil, err
- }
- rng, err := mrng.Range()
+ r, rng, err := findRune(ctx, snapshot, fh, position)
if err != nil {
return nil, err
}
@@ -138,18 +134,18 @@
var ErrNoRuneFound = errors.New("no rune found")
// findRune returns rune information for a position in a file.
-func findRune(ctx context.Context, snapshot Snapshot, fh FileHandle, position protocol.Position) (rune, MappedRange, error) {
+func findRune(ctx context.Context, snapshot Snapshot, fh FileHandle, position protocol.Position) (rune, protocol.Range, error) {
fh, err := snapshot.GetFile(ctx, fh.URI())
if err != nil {
- return 0, MappedRange{}, err
+ return 0, protocol.Range{}, err
}
pgf, err := snapshot.ParseGo(ctx, fh, ParseFull)
if err != nil {
- return 0, MappedRange{}, err
+ return 0, protocol.Range{}, err
}
- pos, err := pgf.Mapper.Pos(position)
+ pos, err := pgf.Pos(position)
if err != nil {
- return 0, MappedRange{}, err
+ return 0, protocol.Range{}, err
}
// Find the basic literal enclosing the given position, if there is one.
@@ -166,7 +162,7 @@
return lit == nil // descend unless target is found
})
if lit == nil {
- return 0, MappedRange{}, ErrNoRuneFound
+ return 0, protocol.Range{}, ErrNoRuneFound
}
var r rune
@@ -177,26 +173,26 @@
if err != nil {
// If the conversion fails, it's because of an invalid syntax, therefore
// there is no rune to be found.
- return 0, MappedRange{}, ErrNoRuneFound
+ return 0, protocol.Range{}, ErrNoRuneFound
}
r, _ = utf8.DecodeRuneInString(s)
if r == utf8.RuneError {
- return 0, MappedRange{}, fmt.Errorf("rune error")
+ return 0, protocol.Range{}, fmt.Errorf("rune error")
}
start, end = lit.Pos(), lit.End()
case token.INT:
// It's an integer, scan only if it is a hex litteral whose bitsize in
// ranging from 8 to 32.
if !(strings.HasPrefix(lit.Value, "0x") && len(lit.Value[2:]) >= 2 && len(lit.Value[2:]) <= 8) {
- return 0, MappedRange{}, ErrNoRuneFound
+ return 0, protocol.Range{}, ErrNoRuneFound
}
v, err := strconv.ParseUint(lit.Value[2:], 16, 32)
if err != nil {
- return 0, MappedRange{}, err
+ return 0, protocol.Range{}, err
}
r = rune(v)
if r == utf8.RuneError {
- return 0, MappedRange{}, fmt.Errorf("rune error")
+ return 0, protocol.Range{}, fmt.Errorf("rune error")
}
start, end = lit.Pos(), lit.End()
case token.STRING:
@@ -205,17 +201,17 @@
var found bool
litOffset, err := safetoken.Offset(pgf.Tok, lit.Pos())
if err != nil {
- return 0, MappedRange{}, err
+ return 0, protocol.Range{}, err
}
offset, err := safetoken.Offset(pgf.Tok, pos)
if err != nil {
- return 0, MappedRange{}, err
+ return 0, protocol.Range{}, err
}
for i := offset - litOffset; i > 0; i-- {
// Start at the cursor position and search backward for the beginning of a rune escape sequence.
rr, _ := utf8.DecodeRuneInString(lit.Value[i:])
if rr == utf8.RuneError {
- return 0, MappedRange{}, fmt.Errorf("rune error")
+ return 0, protocol.Range{}, fmt.Errorf("rune error")
}
if rr == '\\' {
// Got the beginning, decode it.
@@ -223,7 +219,7 @@
r, _, tail, err = strconv.UnquoteChar(lit.Value[i:], '"')
if err != nil {
// If the conversion fails, it's because of an invalid syntax, therefore is no rune to be found.
- return 0, MappedRange{}, ErrNoRuneFound
+ return 0, protocol.Range{}, ErrNoRuneFound
}
// Only the rune escape sequence part of the string has to be highlighted, recompute the range.
runeLen := len(lit.Value) - (int(i) + len(tail))
@@ -235,12 +231,16 @@
}
if !found {
// No escape sequence found
- return 0, MappedRange{}, ErrNoRuneFound
+ return 0, protocol.Range{}, ErrNoRuneFound
}
default:
- return 0, MappedRange{}, ErrNoRuneFound
+ return 0, protocol.Range{}, ErrNoRuneFound
}
- return r, NewMappedRange(pgf.Mapper, start, end), nil
+ rng, err := pgf.PosRange(start, end)
+ if err != nil {
+ return 0, protocol.Range{}, err
+ }
+ return r, rng, nil
}
func HoverIdentifier(ctx context.Context, i *IdentifierInfo) (*HoverJSON, error) {
@@ -580,11 +580,7 @@
var spec ast.Spec
for _, s := range node.Specs {
// Avoid panics by guarding the calls to token.Offset (golang/go#48249).
- start, err := safetoken.Offset(fullTok, s.Pos())
- if err != nil {
- return nil, err
- }
- end, err := safetoken.Offset(fullTok, s.End())
+ start, end, err := safetoken.Offsets(fullTok, s.Pos(), s.End())
if err != nil {
return nil, err
}
diff --git a/gopls/internal/lsp/source/identifier.go b/gopls/internal/lsp/source/identifier.go
index ad826da..bfd853f 100644
--- a/gopls/internal/lsp/source/identifier.go
+++ b/gopls/internal/lsp/source/identifier.go
@@ -24,13 +24,13 @@
// IdentifierInfo holds information about an identifier in Go source.
type IdentifierInfo struct {
- Name string
- Snapshot Snapshot // only needed for .View(); TODO(adonovan): reduce.
- MappedRange
+ Name string
+ Snapshot Snapshot // only needed for .View(); TODO(adonovan): reduce.
+ MappedRange MappedRange
Type struct {
- MappedRange
- Object types.Object
+ MappedRange MappedRange // TODO(adonovan): strength-reduce to a protocol.Location
+ Object types.Object
}
Inferred *types.Signature
@@ -83,7 +83,7 @@
if err != nil {
return nil, err
}
- pos, err := pgf.Mapper.Pos(position)
+ pos, err := pgf.Pos(position)
if err != nil {
return nil, err
}
@@ -114,24 +114,15 @@
// Special case for package declarations, since they have no
// corresponding types.Object.
if ident == file.Name {
- rng, err := posToMappedRange(pkg, file.Name.Pos(), file.Name.End())
- if err != nil {
- return nil, err
- }
- var declAST *ast.File
+ rng := NewMappedRange(pgf, file.Name.Pos(), file.Name.End())
+ // If there's no package documentation, just use current file.
+ decl := pgf
for _, pgf := range pkg.CompiledGoFiles() {
if pgf.File.Doc != nil {
- declAST = pgf.File
+ decl = pgf
}
}
- // If there's no package documentation, just use current file.
- if declAST == nil {
- declAST = file
- }
- declRng, err := posToMappedRange(pkg, declAST.Name.Pos(), declAST.Name.End())
- if err != nil {
- return nil, err
- }
+ declRng := NewMappedRange(decl, decl.File.Name.Pos(), decl.File.Name.End())
return &IdentifierInfo{
Name: file.Name.Name,
ident: file.Name,
@@ -140,7 +131,7 @@
qf: qf,
Snapshot: snapshot,
Declaration: Declaration{
- node: declAST.Name,
+ node: decl.File.Name,
MappedRange: []MappedRange{declRng},
},
}, nil
@@ -199,7 +190,7 @@
// The builtin package isn't in the dependency graph, so the usual
// utilities won't work here.
- rng := NewMappedRange(builtin.Mapper, decl.Pos(), decl.Pos()+token.Pos(len(result.Name)))
+ rng := NewMappedRange(builtin, decl.Pos(), decl.Pos()+token.Pos(len(result.Name)))
result.Declaration.MappedRange = append(result.Declaration.MappedRange, rng)
return result, nil
}
@@ -240,7 +231,7 @@
}
name := method.Names[0].Name
result.Declaration.node = method
- rng := NewMappedRange(builtin.Mapper, method.Pos(), method.Pos()+token.Pos(len(name)))
+ rng := NewMappedRange(builtin, method.Pos(), method.Pos()+token.Pos(len(name)))
result.Declaration.MappedRange = append(result.Declaration.MappedRange, rng)
return result, nil
}
diff --git a/gopls/internal/lsp/source/inlay_hint.go b/gopls/internal/lsp/source/inlay_hint.go
index ade2f5f..3958e4b 100644
--- a/gopls/internal/lsp/source/inlay_hint.go
+++ b/gopls/internal/lsp/source/inlay_hint.go
@@ -111,7 +111,7 @@
start, end := pgf.File.Pos(), pgf.File.End()
if pRng.Start.Line < pRng.End.Line || pRng.Start.Character < pRng.End.Character {
// Adjust start and end for the specified range.
- rng, err := pgf.Mapper.RangeToSpanRange(pRng)
+ rng, err := pgf.RangeToSpanRange(pRng)
if err != nil {
return nil, err
}
diff --git a/gopls/internal/lsp/source/references.go b/gopls/internal/lsp/source/references.go
index a1dcc54..c7529e8 100644
--- a/gopls/internal/lsp/source/references.go
+++ b/gopls/internal/lsp/source/references.go
@@ -23,8 +23,8 @@
// ReferenceInfo holds information about reference to an identifier in Go source.
type ReferenceInfo struct {
- Name string
- MappedRange
+ Name string
+ MappedRange MappedRange
ident *ast.Ident
obj types.Object
pkg Package
@@ -74,7 +74,7 @@
if rdep.DepsByImpPath[UnquoteImportPath(imp)] == targetPkg.ID {
refs = append(refs, &ReferenceInfo{
Name: pgf.File.Name.Name,
- MappedRange: NewMappedRange(f.Mapper, imp.Pos(), imp.End()),
+ MappedRange: NewMappedRange(f, imp.Pos(), imp.End()),
})
}
}
@@ -93,7 +93,7 @@
}
refs = append(refs, &ReferenceInfo{
Name: pgf.File.Name.Name,
- MappedRange: NewMappedRange(f.Mapper, f.File.Name.Pos(), f.File.Name.End()),
+ MappedRange: NewMappedRange(f, f.File.Name.Pos(), f.File.Name.End()),
})
}
@@ -120,7 +120,7 @@
}
sort.Slice(toSort, func(i, j int) bool {
x, y := toSort[i], toSort[j]
- if cmp := strings.Compare(string(x.URI()), string(y.URI())); cmp != 0 {
+ if cmp := strings.Compare(string(x.MappedRange.URI()), string(y.MappedRange.URI())); cmp != 0 {
return cmp < 0
}
return x.ident.Pos() < y.ident.Pos()
@@ -137,8 +137,8 @@
return nil, false, err
}
// Careful: because we used ParseHeader,
- // Mapper.Pos(ppos) may be beyond EOF => (0, err).
- pos, _ := pgf.Mapper.Pos(ppos)
+ // pgf.Pos(ppos) may be beyond EOF => (0, err).
+ pos, _ := pgf.Pos(ppos)
return pgf, pgf.File.Name.Pos() <= pos && pos <= pgf.File.Name.End(), nil
}
@@ -261,11 +261,11 @@
if includeInterfaceRefs && !isType {
// TODO(adonovan): opt: don't go back into the position domain:
// we have complete type information already.
- declRange, err := declIdent.Range()
+ declRange, err := declIdent.MappedRange.Range()
if err != nil {
return nil, err
}
- fh, err := snapshot.GetFile(ctx, declIdent.URI())
+ fh, err := snapshot.GetFile(ctx, declIdent.MappedRange.URI())
if err != nil {
return nil, err
}
diff --git a/gopls/internal/lsp/source/rename.go b/gopls/internal/lsp/source/rename.go
index 37fb2d5..8cb50e8 100644
--- a/gopls/internal/lsp/source/rename.go
+++ b/gopls/internal/lsp/source/rename.go
@@ -107,7 +107,7 @@
}
// Return the location of the package declaration.
- rng, err := pgf.Mapper.PosRange(pgf.File.Name.Pos(), pgf.File.Name.End())
+ rng, err := pgf.PosRange(pgf.File.Name.Pos(), pgf.File.Name.End())
if err != nil {
return nil, err, err
}
@@ -434,7 +434,7 @@
if f.File.Name == nil {
continue // no package declaration
}
- rng, err := f.Mapper.PosRange(f.File.Name.Pos(), f.File.Name.End())
+ rng, err := f.PosRange(f.File.Name.Pos(), f.File.Name.End())
if err != nil {
return err
}
@@ -493,8 +493,7 @@
}
// Create text edit for the import path (string literal).
- impPathMappedRange := NewMappedRange(f.Mapper, imp.Path.Pos(), imp.Path.End())
- rng, err := impPathMappedRange.Range()
+ rng, err := f.PosRange(imp.Path.Pos(), imp.Path.End())
if err != nil {
return err
}
@@ -667,7 +666,7 @@
return nil, err
}
for _, ref := range r.refs {
- refSpan, err := ref.Span()
+ refSpan, err := ref.MappedRange.Span()
if err != nil {
return nil, err
}
@@ -726,8 +725,7 @@
for _, locs := range docRegexp.FindAllIndex([]byte(line), -1) {
// The File.Offset static check complains
// even though these uses are manifestly safe.
- start, _ := safetoken.Offset(tokFile, lineStart+token.Pos(locs[0]))
- end, _ := safetoken.Offset(tokFile, lineStart+token.Pos(locs[1]))
+ start, end, _ := safetoken.Offsets(tokFile, lineStart+token.Pos(locs[0]), lineStart+token.Pos(locs[1]))
result[uri] = append(result[uri], diff.Edit{
Start: start,
End: end,
@@ -813,15 +811,14 @@
// Replace the portion (possibly empty) of the spec before the path:
// local "path" or "path"
// -> <- -><-
- rng := span.NewRange(tokFile, spec.Pos(), spec.Path.Pos())
- spn, err := rng.Span()
+ start, end, err := safetoken.Offsets(tokFile, spec.Pos(), spec.Path.Pos())
if err != nil {
return nil, err
}
return &diff.Edit{
- Start: spn.Start().Offset(),
- End: spn.End().Offset(),
+ Start: start,
+ End: end,
New: newText,
}, nil
}
diff --git a/gopls/internal/lsp/source/signature_help.go b/gopls/internal/lsp/source/signature_help.go
index 3f12d90..a751b29 100644
--- a/gopls/internal/lsp/source/signature_help.go
+++ b/gopls/internal/lsp/source/signature_help.go
@@ -24,7 +24,7 @@
if err != nil {
return nil, 0, fmt.Errorf("getting file for SignatureHelp: %w", err)
}
- pos, err := pgf.Mapper.Pos(position)
+ pos, err := pgf.Pos(position)
if err != nil {
return nil, 0, err
}
diff --git a/gopls/internal/lsp/source/source_test.go b/gopls/internal/lsp/source/source_test.go
index ed8d9a4..fccb91e 100644
--- a/gopls/internal/lsp/source/source_test.go
+++ b/gopls/internal/lsp/source/source_test.go
@@ -431,11 +431,11 @@
}
func conflict(t *testing.T, a, b *source.FoldingRangeInfo) bool {
- arng, err := a.Range()
+ arng, err := a.MappedRange.Range()
if err != nil {
t.Fatal(err)
}
- brng, err := b.Range()
+ brng, err := b.MappedRange.Range()
if err != nil {
t.Fatal(err)
}
@@ -450,7 +450,7 @@
// to preserve the offsets.
for i := len(ranges) - 1; i >= 0; i-- {
fRange := ranges[i]
- spn, err := fRange.Span()
+ spn, err := fRange.MappedRange.Span()
if err != nil {
return "", err
}
@@ -552,7 +552,7 @@
t.Fatal(err)
}
if d.IsType {
- rng, err = ident.Type.Range()
+ rng, err = ident.Type.MappedRange.Range()
if err != nil {
t.Fatal(err)
}
@@ -725,7 +725,7 @@
}
got := make(map[span.Span]bool)
for _, refInfo := range refs {
- refSpan, err := refInfo.Span()
+ refSpan, err := refInfo.MappedRange.Span()
if err != nil {
t.Fatal(err)
}
diff --git a/gopls/internal/lsp/source/stub.go b/gopls/internal/lsp/source/stub.go
index 2568bd0..0d94d48 100644
--- a/gopls/internal/lsp/source/stub.go
+++ b/gopls/internal/lsp/source/stub.go
@@ -109,12 +109,11 @@
// Return the diff.
diffs := snapshot.View().Options().ComputeEdits(string(parsedConcreteFile.Src), source.String())
- tf := parsedConcreteFile.Mapper.TokFile
var edits []analysis.TextEdit
for _, edit := range diffs {
edits = append(edits, analysis.TextEdit{
- Pos: tf.Pos(edit.Start),
- End: tf.Pos(edit.End),
+ Pos: parsedConcreteFile.Tok.Pos(edit.Start),
+ End: parsedConcreteFile.Tok.Pos(edit.End),
NewText: []byte(edit.New),
})
}
@@ -223,7 +222,7 @@
if err != nil {
return nil, 0, err
}
- rng, err := spn.Range(pgf.Mapper.TokFile)
+ rng, err := spn.Range(pgf.Tok)
if err != nil {
return nil, 0, err
}
diff --git a/gopls/internal/lsp/source/util.go b/gopls/internal/lsp/source/util.go
index face4c9..0b7f3e4 100644
--- a/gopls/internal/lsp/source/util.go
+++ b/gopls/internal/lsp/source/util.go
@@ -20,66 +20,46 @@
"golang.org/x/tools/gopls/internal/lsp/protocol"
"golang.org/x/tools/gopls/internal/lsp/safetoken"
"golang.org/x/tools/gopls/internal/span"
- "golang.org/x/tools/internal/bug"
"golang.org/x/tools/internal/typeparams"
)
-// MappedRange provides mapped protocol.Range for a span.Range, accounting for
-// UTF-16 code points.
+// A MappedRange represents an interval within a parsed Go file and
+// has the ability to convert to protocol.Range or span.Span form.
//
-// TOOD(adonovan): eliminate this type. Replace all uses by an
-// explicit pair (span.Range, protocol.ColumnMapper), and an operation
-// to map both to a protocol.Range.
+// TOOD(adonovan): eliminate this type by inlining it: make callers
+// hold the triple themselves, or convert to Mapper + start/end offsets
+// and hold that.
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)
+ // TODO(adonovan): eliminate sole tricky direct use of this,
+ // which is entangled with IdentifierInfo.Declaration.node.
+ File *ParsedGoFile
+ start, end token.Pos
}
// NewMappedRange returns a MappedRange for the given file and
-// start/end positions, which must be valid within m.TokFile.
-func NewMappedRange(m *protocol.ColumnMapper, start, end token.Pos) MappedRange {
- return MappedRange{
- spanRange: span.NewRange(m.TokFile, start, end),
- m: m,
- }
+// start/end positions, which must be valid within the file.
+func NewMappedRange(pgf *ParsedGoFile, start, end token.Pos) MappedRange {
+ _ = span.NewRange(pgf.Tok, start, end) // just for assertions
+ return MappedRange{File: pgf, start: start, end: end}
}
// Range returns the LSP range in the edited source.
-//
-// See the documentation of NewMappedRange for information on edited vs
-// compiled source.
func (s MappedRange) Range() (protocol.Range, error) {
- if s.m == nil {
- return protocol.Range{}, bug.Errorf("invalid range")
- }
- spn, err := span.FileSpan(s.spanRange.TokFile, s.spanRange.Start, s.spanRange.End)
- if err != nil {
- return protocol.Range{}, err
- }
- return s.m.Range(spn)
+ return s.File.PosRange(s.start, s.end)
}
-// Span returns the span corresponding to the mapped range in the edited
-// source.
-//
-// See the documentation of NewMappedRange for information on edited vs
-// compiled source.
+// Span returns the span corresponding to the mapped range in the edited source.
func (s MappedRange) Span() (span.Span, error) {
- // In the past, some code-paths have relied on Span returning an error if s
- // is the zero value (i.e. s.m is nil). But this should be treated as a bug:
- // observe that s.URI() would panic in this case.
- if s.m == nil {
- return span.Span{}, bug.Errorf("invalid range")
+ start, end, err := safetoken.Offsets(s.File.Tok, s.start, s.end)
+ if err != nil {
+ return span.Span{}, err
}
- return span.FileSpan(s.spanRange.TokFile, s.spanRange.Start, s.spanRange.End)
+ return s.File.Mapper.OffsetSpan(start, end)
}
// URI returns the URI of the edited file.
-//
-// See the documentation of NewMappedRange for information on edited vs
-// compiled source.
func (s MappedRange) URI() span.URI {
- return s.m.URI
+ return s.File.Mapper.URI
}
func IsGenerated(ctx context.Context, snapshot Snapshot, uri span.URI) bool {
@@ -126,6 +106,10 @@
// posToMappedRange returns the MappedRange for the given [start, end) span,
// which must be among the transitive dependencies of pkg.
+//
+// TODO(adonovan): many of the callers need only the ParsedGoFile so
+// that they can call pgf.PosRange(pos, end) to get a Range; they
+// don't actually need a MappedRange.
func posToMappedRange(pkg Package, pos, end token.Pos) (MappedRange, error) {
if !pos.IsValid() {
return MappedRange{}, fmt.Errorf("invalid start position")
@@ -139,7 +123,7 @@
if err != nil {
return MappedRange{}, err
}
- return NewMappedRange(pgf.Mapper, pos, end), nil
+ return NewMappedRange(pgf, pos, end), nil
}
// FindPackageFromPos returns the Package for the given position, which must be
diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go
index 0b73c74..21e9969 100644
--- a/gopls/internal/lsp/source/view.go
+++ b/gopls/internal/lsp/source/view.go
@@ -23,6 +23,7 @@
"golang.org/x/tools/go/packages"
"golang.org/x/tools/gopls/internal/govulncheck"
"golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/lsp/safetoken"
"golang.org/x/tools/gopls/internal/span"
"golang.org/x/tools/internal/event/label"
"golang.org/x/tools/internal/event/tag"
@@ -380,6 +381,33 @@
ParseErr scanner.ErrorList
}
+// Pos returns the token.Pos of protocol position p within the file.
+func (pgf *ParsedGoFile) Pos(p protocol.Position) (token.Pos, error) {
+ point, err := pgf.Mapper.Point(p)
+ if err != nil {
+ return token.NoPos, err
+ }
+ return safetoken.Pos(pgf.Tok, point.Offset())
+}
+
+// PosRange returns a protocol Range for the token.Pos interval in this file.
+func (pgf *ParsedGoFile) PosRange(start, end token.Pos) (protocol.Range, error) {
+ startOffset, endOffset, err := safetoken.Offsets(pgf.Tok, start, end)
+ if err != nil {
+ return protocol.Range{}, err
+ }
+ return pgf.Mapper.OffsetRange(startOffset, endOffset)
+}
+
+// RangeToSpanRange parses a protocol Range back into the go/token domain.
+func (pgf *ParsedGoFile) RangeToSpanRange(r protocol.Range) (span.Range, error) {
+ spn, err := pgf.Mapper.RangeSpan(r)
+ if err != nil {
+ return span.Range{}, err
+ }
+ return spn.Range(pgf.Tok)
+}
+
// A ParsedModule contains the results of parsing a go.mod file.
type ParsedModule struct {
URI span.URI
diff --git a/gopls/internal/lsp/work/completion.go b/gopls/internal/lsp/work/completion.go
index 623d2ce..b3682e1 100644
--- a/gopls/internal/lsp/work/completion.go
+++ b/gopls/internal/lsp/work/completion.go
@@ -8,15 +8,14 @@
"context"
"errors"
"fmt"
- "go/token"
"os"
"path/filepath"
"sort"
"strings"
- "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 Completion(ctx context.Context, snapshot source.Snapshot, fh source.VersionedFileHandle, position protocol.Position) (*protocol.CompletionList, error) {
@@ -28,18 +27,17 @@
if err != nil {
return nil, fmt.Errorf("getting go.work file handle: %w", err)
}
- pos, err := pw.Mapper.Pos(position)
+ cursor, err := pw.Mapper.Offset(position)
if err != nil {
- return nil, fmt.Errorf("computing cursor position: %w", err)
+ return nil, fmt.Errorf("computing cursor offset: %w", err)
}
// Find the use statement the user is in.
- cursor := pos - 1
use, pathStart, _ := usePath(pw, cursor)
if use == nil {
return &protocol.CompletionList{}, nil
}
- completingFrom := use.Path[:cursor-token.Pos(pathStart)]
+ completingFrom := use.Path[:cursor-pathStart]
// We're going to find the completions of the user input
// (completingFrom) by doing a walk on the innermost directory
diff --git a/gopls/internal/lsp/work/hover.go b/gopls/internal/lsp/work/hover.go
index 641028b..a29d59c 100644
--- a/gopls/internal/lsp/work/hover.go
+++ b/gopls/internal/lsp/work/hover.go
@@ -8,7 +8,6 @@
"bytes"
"context"
"fmt"
- "go/token"
"golang.org/x/mod/modfile"
"golang.org/x/tools/gopls/internal/lsp/protocol"
@@ -30,14 +29,14 @@
if err != nil {
return nil, fmt.Errorf("getting go.work file handle: %w", err)
}
- pos, err := pw.Mapper.Pos(position)
+ offset, err := pw.Mapper.Offset(position)
if err != nil {
- return nil, fmt.Errorf("computing cursor position: %w", err)
+ return nil, fmt.Errorf("computing cursor offset: %w", err)
}
// Confirm that the cursor is inside a use statement, and then find
// the position of the use statement's directory path.
- use, pathStart, pathEnd := usePath(pw, pos)
+ use, pathStart, pathEnd := usePath(pw, offset)
// The cursor position is not on a use statement.
if use == nil {
@@ -70,7 +69,7 @@
}, nil
}
-func usePath(pw *source.ParsedWorkFile, pos token.Pos) (use *modfile.Use, pathStart, pathEnd int) {
+func usePath(pw *source.ParsedWorkFile, offset int) (use *modfile.Use, pathStart, pathEnd int) {
for _, u := range pw.File.Use {
path := []byte(u.Path)
s, e := u.Syntax.Start.Byte, u.Syntax.End.Byte
@@ -82,7 +81,7 @@
// Shift the start position to the location of the
// module directory within the use statement.
pathStart, pathEnd = s+i, s+i+len(path)
- if token.Pos(pathStart) <= pos && pos <= token.Pos(pathEnd) {
+ if pathStart <= offset && offset <= pathEnd {
return u, pathStart, pathEnd
}
}
diff --git a/gopls/internal/regtest/misc/failures_test.go b/gopls/internal/regtest/misc/failures_test.go
index b016966..f996514 100644
--- a/gopls/internal/regtest/misc/failures_test.go
+++ b/gopls/internal/regtest/misc/failures_test.go
@@ -50,7 +50,7 @@
func TestFailingDiagnosticClearingOnEdit(t *testing.T) {
t.Skip("line directives //line ")
// badPackageDup contains a duplicate definition of the 'a' const.
- // This is a minor variant of TestDiagnosticClearingOnEditfrom from
+ // This is a minor variant of TestDiagnosticClearingOnEdit from
// diagnostics_test.go, with a line directive, which makes no difference.
const badPackageDup = `
-- go.mod --
diff --git a/gopls/internal/span/span.go b/gopls/internal/span/span.go
index 0c24a2d..36629f0 100644
--- a/gopls/internal/span/span.go
+++ b/gopls/internal/span/span.go
@@ -36,9 +36,9 @@
}
type point struct {
- Line int `json:"line"`
- Column int `json:"column"`
- Offset int `json:"offset"`
+ Line int `json:"line"` // 1-based line number
+ Column int `json:"column"` // 1-based, UTF-8 codes (bytes)
+ Offset int `json:"offset"` // 0-based byte offset
}
// Invalid is a span that reports false from IsValid
@@ -274,12 +274,12 @@
}
func (p *point) updatePosition(tf *token.File) error {
- line, col, err := ToPosition(tf, p.Offset)
+ line, col8, err := OffsetToLineCol8(tf, p.Offset)
if err != nil {
return err
}
p.Line = line
- p.Column = col
+ p.Column = col8
return nil
}
diff --git a/gopls/internal/span/token.go b/gopls/internal/span/token.go
index ca78d67..2e71cba 100644
--- a/gopls/internal/span/token.go
+++ b/gopls/internal/span/token.go
@@ -15,6 +15,8 @@
// Range represents a source code range in token.Pos form.
// It also carries the token.File that produced the positions, so that it is
// self contained.
+//
+// TODO(adonovan): move to safetoken.Range (but the Range.Span function must stay behind).
type Range struct {
TokFile *token.File // non-nil
Start, End token.Pos // both IsValid()
@@ -55,14 +57,6 @@
}
}
-// NewTokenFile returns a token.File for the given file content.
-func NewTokenFile(filename string, content []byte) *token.File {
- fset := token.NewFileSet()
- f := fset.AddFile(filename, -1, len(content))
- f.SetLinesForContent(content)
- return f
-}
-
// IsPoint returns true if the range represents a single point.
func (r Range) IsPoint() bool {
return r.Start == r.End
@@ -95,8 +89,6 @@
if err != nil {
return Span{}, err
}
- // In the presence of line directives, a single File can have sections from
- // multiple file names.
if endFilename != startFilename {
return Span{}, fmt.Errorf("span begins in file %q but ends in %q", startFilename, endFilename)
}
@@ -160,11 +152,13 @@
}, nil
}
-// ToPosition converts a byte offset in the file corresponding to tf into
+// OffsetToLineCol8 converts a byte offset in the file corresponding to tf into
// 1-based line and utf-8 column indexes.
-func ToPosition(tf *token.File, offset int) (int, int, error) {
- _, line, col, err := positionFromOffset(tf, offset)
- return line, col, err
+//
+// TODO(adonovan): move to safetoken package for consistency?
+func OffsetToLineCol8(tf *token.File, offset int) (int, int, error) {
+ _, line, col8, err := positionFromOffset(tf, offset)
+ return line, col8, err
}
// ToOffset converts a 1-based line and utf-8 column index into a byte offset