1// Copyright 2019 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5// Package span contains support for representing with positions and ranges in
6// text files.
7package span
8
9import (
10	"encoding/json"
11	"fmt"
12	"path"
13)
14
15// Span represents a source code range in standardized form.
16type Span struct {
17	v span
18}
19
20// Point represents a single point within a file.
21// In general this should only be used as part of a Span, as on its own it
22// does not carry enough information.
23type Point struct {
24	v point
25}
26
27type span struct {
28	URI   URI   `json:"uri"`
29	Start point `json:"start"`
30	End   point `json:"end"`
31}
32
33type point struct {
34	Line   int `json:"line"`
35	Column int `json:"column"`
36	Offset int `json:"offset"`
37}
38
39// Invalid is a span that reports false from IsValid
40var Invalid = Span{v: span{Start: invalidPoint.v, End: invalidPoint.v}}
41
42var invalidPoint = Point{v: point{Line: 0, Column: 0, Offset: -1}}
43
44// Converter is the interface to an object that can convert between line:column
45// and offset forms for a single file.
46type Converter interface {
47	//ToPosition converts from an offset to a line:column pair.
48	ToPosition(offset int) (int, int, error)
49	//ToOffset converts from a line:column pair to an offset.
50	ToOffset(line, col int) (int, error)
51}
52
53func New(uri URI, start Point, end Point) Span {
54	s := Span{v: span{URI: uri, Start: start.v, End: end.v}}
55	s.v.clean()
56	return s
57}
58
59func NewPoint(line, col, offset int) Point {
60	p := Point{v: point{Line: line, Column: col, Offset: offset}}
61	p.v.clean()
62	return p
63}
64
65func Compare(a, b Span) int {
66	if r := CompareURI(a.URI(), b.URI()); r != 0 {
67		return r
68	}
69	if r := comparePoint(a.v.Start, b.v.Start); r != 0 {
70		return r
71	}
72	return comparePoint(a.v.End, b.v.End)
73}
74
75func ComparePoint(a, b Point) int {
76	return comparePoint(a.v, b.v)
77}
78
79func comparePoint(a, b point) int {
80	if !a.hasPosition() {
81		if a.Offset < b.Offset {
82			return -1
83		}
84		if a.Offset > b.Offset {
85			return 1
86		}
87		return 0
88	}
89	if a.Line < b.Line {
90		return -1
91	}
92	if a.Line > b.Line {
93		return 1
94	}
95	if a.Column < b.Column {
96		return -1
97	}
98	if a.Column > b.Column {
99		return 1
100	}
101	return 0
102}
103
104func (s Span) HasPosition() bool             { return s.v.Start.hasPosition() }
105func (s Span) HasOffset() bool               { return s.v.Start.hasOffset() }
106func (s Span) IsValid() bool                 { return s.v.Start.isValid() }
107func (s Span) IsPoint() bool                 { return s.v.Start == s.v.End }
108func (s Span) URI() URI                      { return s.v.URI }
109func (s Span) Start() Point                  { return Point{s.v.Start} }
110func (s Span) End() Point                    { return Point{s.v.End} }
111func (s *Span) MarshalJSON() ([]byte, error) { return json.Marshal(&s.v) }
112func (s *Span) UnmarshalJSON(b []byte) error { return json.Unmarshal(b, &s.v) }
113
114func (p Point) HasPosition() bool             { return p.v.hasPosition() }
115func (p Point) HasOffset() bool               { return p.v.hasOffset() }
116func (p Point) IsValid() bool                 { return p.v.isValid() }
117func (p *Point) MarshalJSON() ([]byte, error) { return json.Marshal(&p.v) }
118func (p *Point) UnmarshalJSON(b []byte) error { return json.Unmarshal(b, &p.v) }
119func (p Point) Line() int {
120	if !p.v.hasPosition() {
121		panic(fmt.Errorf("position not set in %v", p.v))
122	}
123	return p.v.Line
124}
125func (p Point) Column() int {
126	if !p.v.hasPosition() {
127		panic(fmt.Errorf("position not set in %v", p.v))
128	}
129	return p.v.Column
130}
131func (p Point) Offset() int {
132	if !p.v.hasOffset() {
133		panic(fmt.Errorf("offset not set in %v", p.v))
134	}
135	return p.v.Offset
136}
137
138func (p point) hasPosition() bool { return p.Line > 0 }
139func (p point) hasOffset() bool   { return p.Offset >= 0 }
140func (p point) isValid() bool     { return p.hasPosition() || p.hasOffset() }
141func (p point) isZero() bool {
142	return (p.Line == 1 && p.Column == 1) || (!p.hasPosition() && p.Offset == 0)
143}
144
145func (s *span) clean() {
146	//this presumes the points are already clean
147	if !s.End.isValid() || (s.End == point{}) {
148		s.End = s.Start
149	}
150}
151
152func (p *point) clean() {
153	if p.Line < 0 {
154		p.Line = 0
155	}
156	if p.Column <= 0 {
157		if p.Line > 0 {
158			p.Column = 1
159		} else {
160			p.Column = 0
161		}
162	}
163	if p.Offset == 0 && (p.Line > 1 || p.Column > 1) {
164		p.Offset = -1
165	}
166}
167
168// Format implements fmt.Formatter to print the Location in a standard form.
169// The format produced is one that can be read back in using Parse.
170func (s Span) Format(f fmt.State, c rune) {
171	fullForm := f.Flag('+')
172	preferOffset := f.Flag('#')
173	// we should always have a uri, simplify if it is file format
174	//TODO: make sure the end of the uri is unambiguous
175	uri := string(s.v.URI)
176	if c == 'f' {
177		uri = path.Base(uri)
178	} else if !fullForm {
179		uri = s.v.URI.Filename()
180	}
181	fmt.Fprint(f, uri)
182	if !s.IsValid() || (!fullForm && s.v.Start.isZero() && s.v.End.isZero()) {
183		return
184	}
185	// see which bits of start to write
186	printOffset := s.HasOffset() && (fullForm || preferOffset || !s.HasPosition())
187	printLine := s.HasPosition() && (fullForm || !printOffset)
188	printColumn := printLine && (fullForm || (s.v.Start.Column > 1 || s.v.End.Column > 1))
189	fmt.Fprint(f, ":")
190	if printLine {
191		fmt.Fprintf(f, "%d", s.v.Start.Line)
192	}
193	if printColumn {
194		fmt.Fprintf(f, ":%d", s.v.Start.Column)
195	}
196	if printOffset {
197		fmt.Fprintf(f, "#%d", s.v.Start.Offset)
198	}
199	// start is written, do we need end?
200	if s.IsPoint() {
201		return
202	}
203	// we don't print the line if it did not change
204	printLine = fullForm || (printLine && s.v.End.Line > s.v.Start.Line)
205	fmt.Fprint(f, "-")
206	if printLine {
207		fmt.Fprintf(f, "%d", s.v.End.Line)
208	}
209	if printColumn {
210		if printLine {
211			fmt.Fprint(f, ":")
212		}
213		fmt.Fprintf(f, "%d", s.v.End.Column)
214	}
215	if printOffset {
216		fmt.Fprintf(f, "#%d", s.v.End.Offset)
217	}
218}
219
220func (s Span) WithPosition(c Converter) (Span, error) {
221	if err := s.update(c, true, false); err != nil {
222		return Span{}, err
223	}
224	return s, nil
225}
226
227func (s Span) WithOffset(c Converter) (Span, error) {
228	if err := s.update(c, false, true); err != nil {
229		return Span{}, err
230	}
231	return s, nil
232}
233
234func (s Span) WithAll(c Converter) (Span, error) {
235	if err := s.update(c, true, true); err != nil {
236		return Span{}, err
237	}
238	return s, nil
239}
240
241func (s *Span) update(c Converter, withPos, withOffset bool) error {
242	if !s.IsValid() {
243		return fmt.Errorf("cannot add information to an invalid span")
244	}
245	if withPos && !s.HasPosition() {
246		if err := s.v.Start.updatePosition(c); err != nil {
247			return err
248		}
249		if s.v.End.Offset == s.v.Start.Offset {
250			s.v.End = s.v.Start
251		} else if err := s.v.End.updatePosition(c); err != nil {
252			return err
253		}
254	}
255	if withOffset && (!s.HasOffset() || (s.v.End.hasPosition() && !s.v.End.hasOffset())) {
256		if err := s.v.Start.updateOffset(c); err != nil {
257			return err
258		}
259		if s.v.End.Line == s.v.Start.Line && s.v.End.Column == s.v.Start.Column {
260			s.v.End.Offset = s.v.Start.Offset
261		} else if err := s.v.End.updateOffset(c); err != nil {
262			return err
263		}
264	}
265	return nil
266}
267
268func (p *point) updatePosition(c Converter) error {
269	line, col, err := c.ToPosition(p.Offset)
270	if err != nil {
271		return err
272	}
273	p.Line = line
274	p.Column = col
275	return nil
276}
277
278func (p *point) updateOffset(c Converter) error {
279	offset, err := c.ToOffset(p.Line, p.Column)
280	if err != nil {
281		return err
282	}
283	p.Offset = offset
284	return nil
285}
286