TUI editor and editor backend written in Zig
at main 281 lines 12 kB view raw
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}