internal/lsp: add mapper for go.mod files

This change adds a protocol.ColumnMapper when parsing go.mod files. This will prevent us from having to worry about line and column offsets, specifically when converting from the x/mod/modfile position to a span.Span.

Updates golang/go#31999

Change-Id: Iacdfb42d61dfea9b5f70325cf5a87c9575f8f345
Reviewed-on: https://go-review.googlesource.com/c/tools/+/214699
Run-TryBot: Rohan Challa <rohan@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
diff --git a/internal/lsp/cache/parse_mod.go b/internal/lsp/cache/parse_mod.go
index 07b219f..cddf09b 100644
--- a/internal/lsp/cache/parse_mod.go
+++ b/internal/lsp/cache/parse_mod.go
@@ -15,6 +15,7 @@
 	"golang.org/x/tools/internal/lsp/source"
 	"golang.org/x/tools/internal/lsp/telemetry"
 	"golang.org/x/tools/internal/memoize"
+	"golang.org/x/tools/internal/span"
 	"golang.org/x/tools/internal/telemetry/log"
 	"golang.org/x/tools/internal/telemetry/trace"
 	errors "golang.org/x/xerrors"
@@ -29,13 +30,14 @@
 	memoize.NoCopy
 
 	modfile *modfile.File
+	mapper  *protocol.ColumnMapper
 	err     error
 }
 
 func (c *cache) ParseModHandle(fh source.FileHandle) source.ParseModHandle {
 	h := c.store.Bind(fh.Identity(), func(ctx context.Context) interface{} {
 		data := &parseModData{}
-		data.modfile, data.err = parseMod(ctx, fh)
+		data.modfile, data.mapper, data.err = parseMod(ctx, fh)
 		return data
 	})
 	return &parseModHandle{
@@ -44,29 +46,29 @@
 	}
 }
 
-func parseMod(ctx context.Context, fh source.FileHandle) (*modfile.File, error) {
+func parseMod(ctx context.Context, fh source.FileHandle) (*modfile.File, *protocol.ColumnMapper, error) {
 	ctx, done := trace.StartSpan(ctx, "cache.parseMod", telemetry.File.Of(fh.Identity().URI.Filename()))
 	defer done()
 
 	buf, _, err := fh.Read(ctx)
 	if err != nil {
-		return nil, err
+		return nil, nil, err
 	}
-	f, err := modfile.Parse(fh.Identity().URI.Filename(), buf, nil)
+	parsed, err := modfile.Parse(fh.Identity().URI.Filename(), buf, nil)
 	if err != nil {
 		// TODO(golang/go#36486): This can be removed when modfile.Parse returns structured errors.
 		re := regexp.MustCompile(`.*:([\d]+): (.+)`)
 		matches := re.FindStringSubmatch(strings.TrimSpace(err.Error()))
 		if len(matches) < 3 {
 			log.Error(ctx, "could not parse golang/x/mod error message", err)
-			return nil, err
+			return nil, nil, err
 		}
 		line, e := strconv.Atoi(matches[1])
 		if e != nil {
-			return nil, err
+			return nil, nil, err
 		}
 		contents := strings.Split(string(buf), "\n")[line-1]
-		return nil, &source.Error{
+		return nil, nil, &source.Error{
 			Message: matches[2],
 			Range: protocol.Range{
 				Start: protocol.Position{Line: float64(line - 1), Character: float64(0)},
@@ -74,7 +76,12 @@
 			},
 		}
 	}
-	return f, nil
+	m := &protocol.ColumnMapper{
+		URI:       fh.Identity().URI,
+		Converter: span.NewContentConverter(fh.Identity().URI.Filename(), buf),
+		Content:   buf,
+	}
+	return parsed, m, nil
 }
 
 func (pgh *parseModHandle) String() string {
@@ -85,11 +92,11 @@
 	return pgh.file
 }
 
-func (pgh *parseModHandle) Parse(ctx context.Context) (*modfile.File, error) {
+func (pgh *parseModHandle) Parse(ctx context.Context) (*modfile.File, *protocol.ColumnMapper, error) {
 	v := pgh.handle.Get(ctx)
 	if v == nil {
-		return nil, errors.Errorf("no parsed file for %s", pgh.File().Identity().URI)
+		return nil, nil, errors.Errorf("no parsed file for %s", pgh.File().Identity().URI)
 	}
 	data := v.(*parseModData)
-	return data.modfile, data.err
+	return data.modfile, data.mapper, data.err
 }
diff --git a/internal/lsp/mod/diagnostics.go b/internal/lsp/mod/diagnostics.go
index 850411f..2ef87f9 100644
--- a/internal/lsp/mod/diagnostics.go
+++ b/internal/lsp/mod/diagnostics.go
@@ -15,6 +15,7 @@
 	"golang.org/x/tools/internal/lsp/protocol"
 	"golang.org/x/tools/internal/lsp/source"
 	"golang.org/x/tools/internal/lsp/telemetry"
+	"golang.org/x/tools/internal/span"
 	"golang.org/x/tools/internal/telemetry/trace"
 )
 
@@ -43,7 +44,7 @@
 		}
 	}
 
-	realMod, err := snapshot.View().Session().Cache().ParseModHandle(realfh).Parse(ctx)
+	realMod, m, err := snapshot.View().Session().Cache().ParseModHandle(realfh).Parse(ctx)
 	// If the go.mod file fails to parse, return errors right away.
 	if err, ok := err.(*source.Error); ok {
 		return map[source.FileIdentity][]source.Diagnostic{
@@ -58,7 +59,7 @@
 	if err != nil {
 		return nil, err
 	}
-	tempMod, err := snapshot.View().Session().Cache().ParseModHandle(tempfh).Parse(ctx)
+	tempMod, _, err := snapshot.View().Session().Cache().ParseModHandle(tempfh).Parse(ctx)
 	if err != nil {
 		return nil, err
 	}
@@ -84,10 +85,25 @@
 			continue
 		}
 		dep := req.Mod.Path
+
+		start, err := positionToPoint(m, req.Syntax.Start)
+		if err != nil {
+			return nil, err
+		}
+		end, err := positionToPoint(m, req.Syntax.End)
+		if err != nil {
+			return nil, err
+		}
+		spn := span.New(realfh.Identity().URI, start, end)
+		rng, err := m.Range(spn)
+		if err != nil {
+			return nil, err
+		}
+
 		diag := &source.Diagnostic{
 			Message:  fmt.Sprintf("%s is not used in this module.", dep),
 			Source:   "go mod tidy",
-			Range:    protocol.Range{Start: getPos(req.Syntax.Start), End: getPos(req.Syntax.End)},
+			Range:    rng,
 			Severity: protocol.SeverityWarning,
 		}
 		if tempReqs[dep] != nil && req.Indirect != tempReqs[dep].Indirect {
@@ -101,10 +117,10 @@
 	return reports, nil
 }
 
-// TODO: Check to see if we need to go through internal/span (for multiple byte characters).
-func getPos(pos modfile.Position) protocol.Position {
-	return protocol.Position{
-		Line:      float64(pos.Line - 1),
-		Character: float64(pos.LineRune - 1),
+func positionToPoint(m *protocol.ColumnMapper, pos modfile.Position) (span.Point, error) {
+	line, col, err := m.Converter.ToPosition(pos.Byte)
+	if err != nil {
+		return span.Point{}, err
 	}
+	return span.NewPoint(line, col, pos.Byte), nil
 }
diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go
index 83e659f..cb6995b 100644
--- a/internal/lsp/source/view.go
+++ b/internal/lsp/source/view.go
@@ -247,7 +247,7 @@
 
 	// Parse returns the parsed modifle for the go.mod file.
 	// If the file is not available, returns nil and an error.
-	Parse(ctx context.Context) (*modfile.File, error)
+	Parse(ctx context.Context) (*modfile.File, *protocol.ColumnMapper, error)
 }
 
 // ParseMode controls the content of the AST produced when parsing a source file.