TUI editor and editor backend written in Zig
1//! A `Range` contains 2 `Pos` structs. It can be used to represent a range somewhere in the file.
2//! It's semantically different from `Selection`. A `Selection` has a cursor and anchor, each of
3//! which has their own semantic meaning, while a `Range` is simply 2 arbitrary `Pos`itions.
4
5const std = @import("std");
6
7const Pos = @import("pos.zig").Pos;
8
9/// A pair of positions count as a range.
10const Range = @This();
11
12from: Pos,
13to: Pos,
14
15/// Returns `true` if and only if a.from == b.from and a.to == b.to. In other words; the order
16/// of `.to` and `.from` positions within the range matters.
17pub fn strictEql(a: Range, b: Range) bool {
18 return a.from.eql(b.from) and a.to.eql(b.to);
19}
20
21/// Returns `true` if a and b cover the same areas of the text editor. The order of the `.to`
22/// and `.from` positions within each range does not matter.
23pub fn eql(a: Range, b: Range) bool {
24 return a.before().eql(b.before()) and a.after().eql(b.after());
25}
26
27/// Returns whichever position in the range that comes earlier in the text.
28pub fn before(self: Range) Pos {
29 if (self.from.comesBefore(self.to)) return self.from;
30
31 return self.to;
32}
33
34/// Returns whichever position in the range that comes later in the text.
35pub fn after(self: Range) Pos {
36 if (self.from.comesBefore(self.to)) return self.to;
37
38 return self.from;
39}
40
41/// Returns `true` if the range has 0 width, i.e. the `from` and `to` positions are the same.
42pub fn isEmpty(self: Range) bool {
43 return self.from.eql(self.to);
44}
45
46/// Returns `true` if the provided positions sits within the range. A position on the edges of
47/// the range counts as being inside the range. For example: a position {0,0} is considered to
48/// be contained by a range from {0,0} to {0,1}.
49pub fn containsPos(self: Range, pos: Pos) bool {
50 // 1. Check if the provided position is inside the range.
51
52 if (self.before().comesBefore(pos) and self.after().comesAfter(pos)) return true;
53
54 // 2. Check if the provided position is on the edge of the range, which we also think of as
55 // containing the position.
56 return self.from.eql(pos) or self.to.eql(pos);
57}
58
59/// Returns `true` if the provided range sits within this range. This uses the same logic as
60/// `containsPos` and the same rules apply. Equal ranges are considered to contain each other.
61pub fn containsRange(self: Range, other: Range) bool {
62 return self.containsPos(other.from) and self.containsPos(other.to);
63}
64
65/// Returns `true` if there's an overlap between the provided ranges. In other words; at least
66/// one edge from either range is inside the other.
67pub fn hasOverlap(a: Range, b: Range) bool {
68 // Ranges overlap if one contains the other.
69 if (a.containsRange(b) or b.containsRange(a)) return true;
70
71 // If one range does not contain the other then it's enough to check if one range contains a
72 // position from the other. We don't need to check both because at least one edge from one is
73 // guaranteed to be outside the other.
74 return a.containsPos(b.from) or a.containsPos(b.to);
75}
76
77test eql {
78 const a: Range = .{ .from = .{ .row = 0, .col = 0 }, .to = .{ .row = 0, .col = 3 } };
79 const b: Range = .{ .from = .{ .row = 0, .col = 3 }, .to = .{ .row = 0, .col = 0 } };
80 const c: Range = .{ .from = .{ .row = 0, .col = 1 }, .to = .{ .row = 1, .col = 2 } };
81
82 try std.testing.expectEqual(true, Range.eql(a, a));
83 try std.testing.expectEqual(true, Range.eql(b, b));
84 try std.testing.expectEqual(true, Range.eql(c, c));
85
86 try std.testing.expectEqual(true, Range.eql(a, b));
87 try std.testing.expectEqual(true, Range.eql(b, a));
88
89 try std.testing.expectEqual(false, Range.eql(c, a));
90 try std.testing.expectEqual(false, Range.eql(a, c));
91 try std.testing.expectEqual(false, Range.eql(c, b));
92 try std.testing.expectEqual(false, Range.eql(b, c));
93}
94
95test strictEql {
96 const a: Range = .{ .from = .{ .row = 0, .col = 0 }, .to = .{ .row = 0, .col = 3 } };
97 const b: Range = .{ .from = .{ .row = 0, .col = 3 }, .to = .{ .row = 0, .col = 0 } };
98 const c: Range = .{ .from = .{ .row = 0, .col = 1 }, .to = .{ .row = 1, .col = 2 } };
99
100 try std.testing.expectEqual(true, Range.strictEql(a, a));
101 try std.testing.expectEqual(true, Range.strictEql(b, b));
102 try std.testing.expectEqual(true, Range.strictEql(c, c));
103
104 try std.testing.expectEqual(false, Range.strictEql(a, b));
105 try std.testing.expectEqual(false, Range.strictEql(b, a));
106
107 try std.testing.expectEqual(false, Range.strictEql(c, a));
108 try std.testing.expectEqual(false, Range.strictEql(a, c));
109 try std.testing.expectEqual(false, Range.strictEql(c, b));
110 try std.testing.expectEqual(false, Range.strictEql(b, c));
111}
112
113test isEmpty {
114 const empty: Range = .{ .from = .{ .row = 0, .col = 1 }, .to = .{ .row = 0, .col = 1 } };
115 const not_empty: Range = .{ .from = .{ .row = 0, .col = 1 }, .to = .{ .row = 0, .col = 2 } };
116
117 try std.testing.expectEqual(true, empty.isEmpty());
118 try std.testing.expectEqual(false, not_empty.isEmpty());
119}
120
121test containsPos {
122 const range: Range = .{ .from = .{ .row = 0, .col = 1 }, .to = .{ .row = 1, .col = 2 } };
123
124 try std.testing.expect(range.containsPos(.{ .row = 0, .col = 1 }));
125 try std.testing.expect(range.containsPos(.{ .row = 0, .col = 10 }));
126 try std.testing.expect(range.containsPos(.{ .row = 1, .col = 0 }));
127 try std.testing.expect(range.containsPos(.{ .row = 1, .col = 2 }));
128
129 try std.testing.expect(!range.containsPos(.{ .row = 0, .col = 0 }));
130 try std.testing.expect(!range.containsPos(.{ .row = 1, .col = 3 }));
131 try std.testing.expect(!range.containsPos(.{ .row = 4, .col = 4 }));
132}
133
134test containsRange {
135 // const a: Range = .{ .from = Pos.fromInt(2), .to = Pos.fromInt(10) };
136 const a: Range = .{ .from = .{ .row = 0, .col = 2 }, .to = .{ .row = 1, .col = 5 } };
137 const same_line: Range = .{ .from = .{ .row = 0, .col = 2 }, .to = .{ .row = 0, .col = 10 } };
138
139 // 1. Ranges contain themselves and equal ranges.
140
141 try std.testing.expect(a.containsRange(a));
142
143 // 2. Ranges contain other ranges that fall within themselves.
144
145 const in_a_1: Range = .{ .from = a.from, .to = .{ .row = 0, .col = 8 } };
146 const in_same_line_1: Range = .{ .from = same_line.from, .to = .{ .row = 0, .col = 8 } };
147 // From inside to end edge.
148 const in_a_2: Range = .{ .from = .{ .row = 1, .col = 3 }, .to = a.to };
149 const in_same_line_2: Range = .{ .from = .{ .row = 0, .col = 5 }, .to = a.to };
150 // Completely inside.
151 const in_a_3: Range = .{ .from = .{ .row = 0, .col = 9 }, .to = .{ .row = 1, .col = 1 } };
152 const in_same_line_3: Range = .{
153 .from = .{ .row = 0, .col = 4 },
154 .to = .{ .row = 0, .col = 8 },
155 };
156
157 try std.testing.expect(Range.hasOverlap(a, in_a_1));
158 try std.testing.expect(Range.hasOverlap(a, in_a_2));
159 try std.testing.expect(Range.hasOverlap(a, in_a_3));
160 try std.testing.expect(Range.hasOverlap(a, in_same_line_1));
161 try std.testing.expect(Range.hasOverlap(a, in_same_line_2));
162 try std.testing.expect(Range.hasOverlap(a, in_same_line_3));
163
164 // 3. Ranges do not contain other ranges where one edge is outside.
165
166 // Start edge is outside.
167 const outside_a_1: Range = .{
168 .from = .{ .row = a.from.row, .col = a.from.col -| 2 },
169 .to = .{ .row = 1, .col = 10 },
170 };
171 // End edge is outside.
172 const outside_a_2: Range = .{
173 .from = .{ .row = 0, .col = 10 },
174 .to = .{ .row = a.to.row, .col = a.to.col +| 4 },
175 };
176
177 try std.testing.expect(!a.containsRange(outside_a_1));
178 try std.testing.expect(!a.containsRange(outside_a_2));
179
180 // 4. Ranges do not contain other ranges that are entirely outside.
181
182 // Outside start, edges are touching.
183 const outside_a_3: Range = .{ .from = .{ .row = 0, .col = 0 }, .to = a.from };
184 // Outside end, edges are touching.
185 const outside_a_4: Range = .{ .from = a.to, .to = .{ .row = a.to.row, .col = a.to.col +| 4 } };
186 // Outside start, edges not touching.
187 const outside_a_5: Range = .{ .from = .{ .row = 0, .col = 0 }, .to = .{ .row = 0, .col = 1 } };
188 // Outside end, edges not touching.
189 const outside_a_6: Range = .{
190 .from = .{ .row = a.to.row, .col = a.to.col +| 1 },
191 .to = .{ .row = a.to.row, .col = a.to.col +| 4 },
192 };
193
194 try std.testing.expect(!a.containsRange(outside_a_3));
195 try std.testing.expect(!a.containsRange(outside_a_4));
196 try std.testing.expect(!a.containsRange(outside_a_5));
197 try std.testing.expect(!a.containsRange(outside_a_6));
198}
199
200test hasOverlap {
201 // const a: Range = .{ .from = Pos.fromInt(2), .to = Pos.fromInt(10) };
202 const a: Range = .{ .from = .{ .row = 0, .col = 2 }, .to = .{ .row = 1, .col = 5 } };
203 const same_line: Range = .{ .from = .{ .row = 0, .col = 2 }, .to = .{ .row = 0, .col = 10 } };
204
205 // 1. Ranges overlap themselves and equal ranges.
206
207 try std.testing.expect(Range.hasOverlap(a, a));
208
209 // 2. Ranges overlap containing ranges.
210
211 // From start edge to inside.
212 const in_a_1: Range = .{ .from = a.from, .to = .{ .row = 0, .col = 8 } };
213 const in_same_line_1: Range = .{ .from = same_line.from, .to = .{ .row = 0, .col = 8 } };
214 // From inside to end edge.
215 const in_a_2: Range = .{ .from = .{ .row = 1, .col = 3 }, .to = a.to };
216 const in_same_line_2: Range = .{ .from = .{ .row = 0, .col = 5 }, .to = a.to };
217 // Completely inside.
218 const in_a_3: Range = .{ .from = .{ .row = 0, .col = 9 }, .to = .{ .row = 1, .col = 1 } };
219 const in_same_line_3: Range = .{
220 .from = .{ .row = 0, .col = 4 },
221 .to = .{ .row = 0, .col = 8 },
222 };
223
224 try std.testing.expect(Range.hasOverlap(a, in_a_1));
225 try std.testing.expect(Range.hasOverlap(a, in_a_2));
226 try std.testing.expect(Range.hasOverlap(a, in_a_3));
227 try std.testing.expect(Range.hasOverlap(a, in_same_line_1));
228 try std.testing.expect(Range.hasOverlap(a, in_same_line_2));
229 try std.testing.expect(Range.hasOverlap(a, in_same_line_3));
230
231 // 3. Ranges overlap when only one edge is inside the other.
232
233 // Start edge is outside.
234 const outside_a_1: Range = .{
235 .from = .{ .row = a.from.row, .col = a.from.col -| 2 },
236 .to = .{ .row = 1, .col = 10 },
237 };
238 // End edge is outside.
239 const outside_a_2: Range = .{
240 .from = .{ .row = 0, .col = 10 },
241 .to = .{ .row = a.to.row, .col = a.to.col +| 4 },
242 };
243
244 try std.testing.expectEqual(true, Range.hasOverlap(a, outside_a_1));
245 try std.testing.expectEqual(true, Range.hasOverlap(a, outside_a_2));
246
247 // 4. Ranges overlap when edges are touching.
248 // TODO: Maybe they shouldn't be considered to be overlapping here? It's just easier to
249 // implement this if they do, so leaving this as is for now.
250
251 // Outside start, edges are touching.
252 const outside_a_3: Range = .{ .from = .{ .row = 0, .col = 0 }, .to = a.from };
253 // Outside end, edges are touching.
254 const outside_a_4: Range = .{ .from = a.to, .to = .{ .row = a.to.row, .col = a.to.col +| 4 } };
255
256 try std.testing.expectEqual(true, Range.hasOverlap(a, outside_a_3));
257 try std.testing.expectEqual(true, Range.hasOverlap(a, outside_a_4));
258
259 // 5. Ranges do not overlap when one does not contain an edge from the other.
260
261 // Outside start, edges not touching.
262 const outside_a_5: Range = .{ .from = .{ .row = 0, .col = 0 }, .to = .{ .row = 0, .col = 1 } };
263 // Outside end, edges not touching.
264 const outside_a_6: Range = .{
265 .from = .{ .row = a.to.row, .col = a.to.col +| 1 },
266 .to = .{ .row = a.to.row, .col = a.to.col +| 4 },
267 };
268
269 try std.testing.expectEqual(false, Range.hasOverlap(a, outside_a_5));
270 try std.testing.expectEqual(false, Range.hasOverlap(a, outside_a_6));
271
272 // 6. The latter parameter to hasOverlap contains the former parameter, but not vice versa.
273
274 const former: Range = .{ .from = .{ .row = 0, .col = 1 }, .to = .{ .row = 0, .col = 1 } };
275 const latter: Range = .{ .from = .{ .row = 0, .col = 0 }, .to = .{ .row = 0, .col = 5 } };
276 try std.testing.expect(Range.hasOverlap(former, latter));
277}
278
279test "refAllDecls" {
280 std.testing.refAllDecls(@This());
281}