blob: 4753d534350c1a83c1dea4059764eff21a5683fe [file] [log] [blame]
// Copyright 2019 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package cmd
// span and point represent positions and ranges in text files.
import (
"encoding/json"
"fmt"
"path"
"sort"
"strings"
"golang.org/x/tools/gopls/internal/protocol"
)
// A span represents a range of text within a source file. The start
// and end points of a valid span may be hold either its byte offset,
// or its (line, column) pair, or both. Columns are measured in bytes.
//
// Spans are appropriate in user interfaces (e.g. command-line tools)
// and tests where a position is notated without access to the content
// of the file.
//
// Use protocol.Mapper to convert between span and other
// representations, such as go/token (also UTF-8) or the LSP protocol
// (UTF-16). The latter requires access to file contents.
//
// See overview comments at ../protocol/mapper.go.
type span struct {
v _span
}
// point represents a single point within a file.
// In general this should only be used as part of a span, as on its own it
// does not carry enough information.
type point struct {
v _point
}
// The span_/point_ types have public fields to support JSON encoding,
// but the span/point types hide these fields by defining methods that
// shadow them. (This is used by a few of the command-line tool
// subcommands, which emit spans and have a -json flag.)
//
// TODO(adonovan): simplify now that it's all internal to cmd.
type _span struct {
URI protocol.DocumentURI `json:"uri"`
Start _point `json:"start"`
End _point `json:"end"`
}
type _point struct {
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
}
func newSpan(uri protocol.DocumentURI, start, end point) span {
s := span{v: _span{URI: uri, Start: start.v, End: end.v}}
s.v.clean()
return s
}
func newPoint(line, col, offset int) point {
p := point{v: _point{Line: line, Column: col, Offset: offset}}
p.v.clean()
return p
}
// sortSpans sorts spans into a stable but unspecified order.
func sortSpans(spans []span) {
sort.SliceStable(spans, func(i, j int) bool {
return compare(spans[i], spans[j]) < 0
})
}
// compare implements a three-valued ordered comparison of Spans.
func compare(a, b span) int {
// This is a textual comparison. It does not perform path
// cleaning, case folding, resolution of symbolic links,
// testing for existence, or any I/O.
if cmp := strings.Compare(string(a.URI()), string(b.URI())); cmp != 0 {
return cmp
}
if cmp := comparePoint(a.v.Start, b.v.Start); cmp != 0 {
return cmp
}
return comparePoint(a.v.End, b.v.End)
}
func comparePoint(a, b _point) int {
if !a.hasPosition() {
if a.Offset < b.Offset {
return -1
}
if a.Offset > b.Offset {
return 1
}
return 0
}
if a.Line < b.Line {
return -1
}
if a.Line > b.Line {
return 1
}
if a.Column < b.Column {
return -1
}
if a.Column > b.Column {
return 1
}
return 0
}
func (s span) HasPosition() bool { return s.v.Start.hasPosition() }
func (s span) HasOffset() bool { return s.v.Start.hasOffset() }
func (s span) IsValid() bool { return s.v.Start.isValid() }
func (s span) IsPoint() bool { return s.v.Start == s.v.End }
func (s span) URI() protocol.DocumentURI { return s.v.URI }
func (s span) Start() point { return point{s.v.Start} }
func (s span) End() point { return point{s.v.End} }
func (s *span) MarshalJSON() ([]byte, error) { return json.Marshal(&s.v) }
func (s *span) UnmarshalJSON(b []byte) error { return json.Unmarshal(b, &s.v) }
func (p point) HasPosition() bool { return p.v.hasPosition() }
func (p point) HasOffset() bool { return p.v.hasOffset() }
func (p point) IsValid() bool { return p.v.isValid() }
func (p *point) MarshalJSON() ([]byte, error) { return json.Marshal(&p.v) }
func (p *point) UnmarshalJSON(b []byte) error { return json.Unmarshal(b, &p.v) }
func (p point) Line() int {
if !p.v.hasPosition() {
panic(fmt.Errorf("position not set in %v", p.v))
}
return p.v.Line
}
func (p point) Column() int {
if !p.v.hasPosition() {
panic(fmt.Errorf("position not set in %v", p.v))
}
return p.v.Column
}
func (p point) Offset() int {
if !p.v.hasOffset() {
panic(fmt.Errorf("offset not set in %v", p.v))
}
return p.v.Offset
}
func (p _point) hasPosition() bool { return p.Line > 0 }
func (p _point) hasOffset() bool { return p.Offset >= 0 }
func (p _point) isValid() bool { return p.hasPosition() || p.hasOffset() }
func (p _point) isZero() bool {
return (p.Line == 1 && p.Column == 1) || (!p.hasPosition() && p.Offset == 0)
}
func (s *_span) clean() {
//this presumes the points are already clean
if !s.End.isValid() || (s.End == _point{}) {
s.End = s.Start
}
}
func (p *_point) clean() {
if p.Line < 0 {
p.Line = 0
}
if p.Column <= 0 {
if p.Line > 0 {
p.Column = 1
} else {
p.Column = 0
}
}
if p.Offset == 0 && (p.Line > 1 || p.Column > 1) {
p.Offset = -1
}
}
// Format implements fmt.Formatter to print the Location in a standard form.
// The format produced is one that can be read back in using parseSpan.
//
// TODO(adonovan): this is esoteric, and the formatting options are
// never used outside of TestFormat. Replace with something simpler
// along the lines of MappedRange.String.
func (s span) Format(f fmt.State, c rune) {
fullForm := f.Flag('+')
preferOffset := f.Flag('#')
// we should always have a uri, simplify if it is file format
//TODO: make sure the end of the uri is unambiguous
uri := string(s.v.URI)
if c == 'f' {
uri = path.Base(uri)
} else if !fullForm {
uri = s.v.URI.Path()
}
fmt.Fprint(f, uri)
if !s.IsValid() || (!fullForm && s.v.Start.isZero() && s.v.End.isZero()) {
return
}
// see which bits of start to write
printOffset := s.HasOffset() && (fullForm || preferOffset || !s.HasPosition())
printLine := s.HasPosition() && (fullForm || !printOffset)
printColumn := printLine && (fullForm || (s.v.Start.Column > 1 || s.v.End.Column > 1))
fmt.Fprint(f, ":")
if printLine {
fmt.Fprintf(f, "%d", s.v.Start.Line)
}
if printColumn {
fmt.Fprintf(f, ":%d", s.v.Start.Column)
}
if printOffset {
fmt.Fprintf(f, "#%d", s.v.Start.Offset)
}
// start is written, do we need end?
if s.IsPoint() {
return
}
// we don't print the line if it did not change
printLine = fullForm || (printLine && s.v.End.Line > s.v.Start.Line)
fmt.Fprint(f, "-")
if printLine {
fmt.Fprintf(f, "%d", s.v.End.Line)
}
if printColumn {
if printLine {
fmt.Fprint(f, ":")
}
fmt.Fprintf(f, "%d", s.v.End.Column)
}
if printOffset {
fmt.Fprintf(f, "#%d", s.v.End.Offset)
}
}