| // Copyright 2019 The Go Authors. | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); | |
| // you may not use this file except in compliance with the License. | |
| // You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| // See the License for the specific language governing permissions and | |
| // limitations under the License. | |
| // Package span contains support for representing with positions and ranges in | |
| // text files. | |
| package span | |
| import ( | |
| "encoding/json" | |
| "fmt" | |
| "path" | |
| ) | |
| // Span represents a source code range in standardized form. | |
| 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 | |
| } | |
| type span struct { | |
| URI URI `json:"uri"` | |
| Start point `json:"start"` | |
| End point `json:"end"` | |
| } | |
| type point struct { | |
| Line int `json:"line"` | |
| Column int `json:"column"` | |
| Offset int `json:"offset"` | |
| } | |
| // Invalid is a span that reports false from IsValid | |
| var Invalid = Span{v: span{Start: invalidPoint.v, End: invalidPoint.v}} | |
| var invalidPoint = Point{v: point{Line: 0, Column: 0, Offset: -1}} | |
| // Converter is the interface to an object that can convert between line:column | |
| // and offset forms for a single file. | |
| type Converter interface { | |
| //ToPosition converts from an offset to a line:column pair. | |
| ToPosition(offset int) (int, int, error) | |
| //ToOffset converts from a line:column pair to an offset. | |
| ToOffset(line, col int) (int, error) | |
| } | |
| func New(uri URI, start Point, 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 | |
| } | |
| func Compare(a, b Span) int { | |
| if r := CompareURI(a.URI(), b.URI()); r != 0 { | |
| return r | |
| } | |
| if r := comparePoint(a.v.Start, b.v.Start); r != 0 { | |
| return r | |
| } | |
| return comparePoint(a.v.End, b.v.End) | |
| } | |
| func ComparePoint(a, b Point) int { | |
| return comparePoint(a.v, b.v) | |
| } | |
| 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() URI { 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 Parse. | |
| 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.Filename() | |
| } | |
| 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) | |
| } | |
| } | |
| func (s Span) WithPosition(c Converter) (Span, error) { | |
| if err := s.update(c, true, false); err != nil { | |
| return Span{}, err | |
| } | |
| return s, nil | |
| } | |
| func (s Span) WithOffset(c Converter) (Span, error) { | |
| if err := s.update(c, false, true); err != nil { | |
| return Span{}, err | |
| } | |
| return s, nil | |
| } | |
| func (s Span) WithAll(c Converter) (Span, error) { | |
| if err := s.update(c, true, true); err != nil { | |
| return Span{}, err | |
| } | |
| return s, nil | |
| } | |
| func (s *Span) update(c Converter, withPos, withOffset bool) error { | |
| if !s.IsValid() { | |
| return fmt.Errorf("cannot add information to an invalid span") | |
| } | |
| if withPos && !s.HasPosition() { | |
| if err := s.v.Start.updatePosition(c); err != nil { | |
| return err | |
| } | |
| if s.v.End.Offset == s.v.Start.Offset { | |
| s.v.End = s.v.Start | |
| } else if err := s.v.End.updatePosition(c); err != nil { | |
| return err | |
| } | |
| } | |
| if withOffset && (!s.HasOffset() || (s.v.End.hasPosition() && !s.v.End.hasOffset())) { | |
| if err := s.v.Start.updateOffset(c); err != nil { | |
| return err | |
| } | |
| if s.v.End.Line == s.v.Start.Line && s.v.End.Column == s.v.Start.Column { | |
| s.v.End.Offset = s.v.Start.Offset | |
| } else if err := s.v.End.updateOffset(c); err != nil { | |
| return err | |
| } | |
| } | |
| return nil | |
| } | |
| func (p *point) updatePosition(c Converter) error { | |
| line, col, err := c.ToPosition(p.Offset) | |
| if err != nil { | |
| return err | |
| } | |
| p.Line = line | |
| p.Column = col | |
| return nil | |
| } | |
| func (p *point) updateOffset(c Converter) error { | |
| offset, err := c.ToOffset(p.Line, p.Column) | |
| if err != nil { | |
| return err | |
| } | |
| p.Offset = offset | |
| return nil | |
| } |