1package hcl
2
3import "fmt"
4
5// Pos represents a single position in a source file, by addressing the
6// start byte of a unicode character encoded in UTF-8.
7//
8// Pos is generally used only in the context of a Range, which then defines
9// which source file the position is within.
10type Pos struct {
11	// Line is the source code line where this position points. Lines are
12	// counted starting at 1 and incremented for each newline character
13	// encountered.
14	Line int
15
16	// Column is the source code column where this position points, in
17	// unicode characters, with counting starting at 1.
18	//
19	// Column counts characters as they appear visually, so for example a
20	// latin letter with a combining diacritic mark counts as one character.
21	// This is intended for rendering visual markers against source code in
22	// contexts where these diacritics would be rendered in a single character
23	// cell. Technically speaking, Column is counting grapheme clusters as
24	// used in unicode normalization.
25	Column int
26
27	// Byte is the byte offset into the file where the indicated character
28	// begins. This is a zero-based offset to the first byte of the first
29	// UTF-8 codepoint sequence in the character, and thus gives a position
30	// that can be resolved _without_ awareness of Unicode characters.
31	Byte int
32}
33
34// InitialPos is a suitable position to use to mark the start of a file.
35var InitialPos = Pos{Byte: 0, Line: 1, Column: 1}
36
37// Range represents a span of characters between two positions in a source
38// file.
39//
40// This struct is usually used by value in types that represent AST nodes,
41// but by pointer in types that refer to the positions of other objects,
42// such as in diagnostics.
43type Range struct {
44	// Filename is the name of the file into which this range's positions
45	// point.
46	Filename string
47
48	// Start and End represent the bounds of this range. Start is inclusive
49	// and End is exclusive.
50	Start, End Pos
51}
52
53// RangeBetween returns a new range that spans from the beginning of the
54// start range to the end of the end range.
55//
56// The result is meaningless if the two ranges do not belong to the same
57// source file or if the end range appears before the start range.
58func RangeBetween(start, end Range) Range {
59	return Range{
60		Filename: start.Filename,
61		Start:    start.Start,
62		End:      end.End,
63	}
64}
65
66// RangeOver returns a new range that covers both of the given ranges and
67// possibly additional content between them if the two ranges do not overlap.
68//
69// If either range is empty then it is ignored. The result is empty if both
70// given ranges are empty.
71//
72// The result is meaningless if the two ranges to not belong to the same
73// source file.
74func RangeOver(a, b Range) Range {
75	if a.Empty() {
76		return b
77	}
78	if b.Empty() {
79		return a
80	}
81
82	var start, end Pos
83	if a.Start.Byte < b.Start.Byte {
84		start = a.Start
85	} else {
86		start = b.Start
87	}
88	if a.End.Byte > b.End.Byte {
89		end = a.End
90	} else {
91		end = b.End
92	}
93	return Range{
94		Filename: a.Filename,
95		Start:    start,
96		End:      end,
97	}
98}
99
100// ContainsPos returns true if and only if the given position is contained within
101// the receiving range.
102//
103// In the unlikely case that the line/column information disagree with the byte
104// offset information in the given position or receiving range, the byte
105// offsets are given priority.
106func (r Range) ContainsPos(pos Pos) bool {
107	return r.ContainsOffset(pos.Byte)
108}
109
110// ContainsOffset returns true if and only if the given byte offset is within
111// the receiving Range.
112func (r Range) ContainsOffset(offset int) bool {
113	return offset >= r.Start.Byte && offset < r.End.Byte
114}
115
116// Ptr returns a pointer to a copy of the receiver. This is a convenience when
117// ranges in places where pointers are required, such as in Diagnostic, but
118// the range in question is returned from a method. Go would otherwise not
119// allow one to take the address of a function call.
120func (r Range) Ptr() *Range {
121	return &r
122}
123
124// String returns a compact string representation of the receiver.
125// Callers should generally prefer to present a range more visually,
126// e.g. via markers directly on the relevant portion of source code.
127func (r Range) String() string {
128	if r.Start.Line == r.End.Line {
129		return fmt.Sprintf(
130			"%s:%d,%d-%d",
131			r.Filename,
132			r.Start.Line, r.Start.Column,
133			r.End.Column,
134		)
135	} else {
136		return fmt.Sprintf(
137			"%s:%d,%d-%d,%d",
138			r.Filename,
139			r.Start.Line, r.Start.Column,
140			r.End.Line, r.End.Column,
141		)
142	}
143}
144
145func (r Range) Empty() bool {
146	return r.Start.Byte == r.End.Byte
147}
148
149// CanSliceBytes returns true if SliceBytes could return an accurate
150// sub-slice of the given slice.
151//
152// This effectively tests whether the start and end offsets of the range
153// are within the bounds of the slice, and thus whether SliceBytes can be
154// trusted to produce an accurate start and end position within that slice.
155func (r Range) CanSliceBytes(b []byte) bool {
156	switch {
157	case r.Start.Byte < 0 || r.Start.Byte > len(b):
158		return false
159	case r.End.Byte < 0 || r.End.Byte > len(b):
160		return false
161	case r.End.Byte < r.Start.Byte:
162		return false
163	default:
164		return true
165	}
166}
167
168// SliceBytes returns a sub-slice of the given slice that is covered by the
169// receiving range, assuming that the given slice is the source code of the
170// file indicated by r.Filename.
171//
172// If the receiver refers to any byte offsets that are outside of the slice
173// then the result is constrained to the overlapping portion only, to avoid
174// a panic. Use CanSliceBytes to determine if the result is guaranteed to
175// be an accurate span of the requested range.
176func (r Range) SliceBytes(b []byte) []byte {
177	start := r.Start.Byte
178	end := r.End.Byte
179	if start < 0 {
180		start = 0
181	} else if start > len(b) {
182		start = len(b)
183	}
184	if end < 0 {
185		end = 0
186	} else if end > len(b) {
187		end = len(b)
188	}
189	if end < start {
190		end = start
191	}
192	return b[start:end]
193}
194
195// Overlaps returns true if the receiver and the other given range share any
196// characters in common.
197func (r Range) Overlaps(other Range) bool {
198	switch {
199	case r.Filename != other.Filename:
200		// If the ranges are in different files then they can't possibly overlap
201		return false
202	case r.Empty() || other.Empty():
203		// Empty ranges can never overlap
204		return false
205	case r.ContainsOffset(other.Start.Byte) || r.ContainsOffset(other.End.Byte):
206		return true
207	case other.ContainsOffset(r.Start.Byte) || other.ContainsOffset(r.End.Byte):
208		return true
209	default:
210		return false
211	}
212}
213
214// Overlap finds a range that is either identical to or a sub-range of both
215// the receiver and the other given range. It returns an empty range
216// within the receiver if there is no overlap between the two ranges.
217//
218// A non-empty result is either identical to or a subset of the receiver.
219func (r Range) Overlap(other Range) Range {
220	if !r.Overlaps(other) {
221		// Start == End indicates an empty range
222		return Range{
223			Filename: r.Filename,
224			Start:    r.Start,
225			End:      r.Start,
226		}
227	}
228
229	var start, end Pos
230	if r.Start.Byte > other.Start.Byte {
231		start = r.Start
232	} else {
233		start = other.Start
234	}
235	if r.End.Byte < other.End.Byte {
236		end = r.End
237	} else {
238		end = other.End
239	}
240
241	return Range{
242		Filename: r.Filename,
243		Start:    start,
244		End:      end,
245	}
246}
247
248// PartitionAround finds the portion of the given range that overlaps with
249// the reciever and returns three ranges: the portion of the reciever that
250// precedes the overlap, the overlap itself, and then the portion of the
251// reciever that comes after the overlap.
252//
253// If the two ranges do not overlap then all three returned ranges are empty.
254//
255// If the given range aligns with or extends beyond either extent of the
256// reciever then the corresponding outer range will be empty.
257func (r Range) PartitionAround(other Range) (before, overlap, after Range) {
258	overlap = r.Overlap(other)
259	if overlap.Empty() {
260		return overlap, overlap, overlap
261	}
262
263	before = Range{
264		Filename: r.Filename,
265		Start:    r.Start,
266		End:      overlap.Start,
267	}
268	after = Range{
269		Filename: r.Filename,
270		Start:    overlap.End,
271		End:      r.End,
272	}
273
274	return before, overlap, after
275}
276