TUI editor and editor backend written in Zig
at main 259 lines 10 kB view raw
1//! A `Selection` contains 2 `Pos` structs, one represents the position of the cursor and the other 2//! is an anchor. `Selection`s are semantically different from `Range`s. A `Range`s `Pos`itions are 3//! arbitrary points, where neither has semantic meaning, unlike the `cursor` and `anchor` in a 4//! `Selection`. 5 6const std = @import("std"); 7 8const Pos = @import("pos.zig").Pos; 9const Range = @import("Range.zig"); 10 11/// A span from one cursor to another counts as a selection. 12const Selection = @This(); 13 14/// The edge of the selection that's considered to be a cursor. This is typically the "end" 15/// of the selection, or where the cursor (bar, beam, block, underline, etc.) is located. The 16/// cursor is not guaranteed to come after the anchor since selections are bi-directional. 17cursor: Pos, 18/// The edge of the selection that's considered to be an anchor. This is typically the "start" 19/// of the selection, or where the cursor (bar, beam, block, underline, etc.) is not located. 20/// The anchor is not guaranteed to come before the cursor since selections are bi-directional. 21anchor: Pos, 22 23pub const init: Selection = .{ .cursor = .init, .anchor = .init }; 24 25pub fn createCursor(pos: Pos) Selection { 26 return .{ .cursor = pos, .anchor = pos }; 27} 28 29/// Returns `true` if this selection is a cursor. A selection is considered a cursor if it's 30/// empty. 31pub fn isCursor(self: Selection) bool { 32 return self.cursor.eql(self.anchor); 33} 34 35/// Returns a Range based on this Selection. The Range will go from the anchor to the cursor. 36pub fn toRange(self: Selection) Range { 37 return .{ .from = self.anchor, .to = self.cursor }; 38} 39 40/// Returns a Selection based on the provided Range. The Selection will anchor to the Range's 41/// `.from` value and the cursor will be at the Range's `.to` value. 42pub fn fromRange(range: Range) Selection { 43 return .{ .cursor = range.to, .anchor = range.from }; 44} 45 46/// Returns `true` if a and b cover the same areas of the text editor. The order of the `.cursor` 47/// and `.anchor` positions within each selection does not matter. 48pub fn eql(a: Selection, b: Selection) bool { 49 return a.toRange().eql(b.toRange()); 50} 51 52/// Returns `true` if and only if a.cursor == b.cursor and a.anchor == b.anchor. In other words; the 53/// order of `.anchor` and `.cursor` positions within the selection matters. 54pub fn strictEql(a: Selection, b: Selection) bool { 55 return a.cursor.eql(b.cursor) and a.anchor.eql(b.anchor); 56} 57 58/// Returns `true` if there's an overlap between the provided selections. In other words; at least 59/// one edge from either selection is inside the other. 60pub fn hasOverlap(a: Selection, b: Selection) bool { 61 return a.toRange().hasOverlap(b.toRange()); 62} 63 64/// Returns `true` if `a` comes before `b`. Asserts that `a` and `b` don't overlap. 65pub fn comesBefore(a: Selection, b: Selection) bool { 66 std.debug.assert(!a.hasOverlap(b)); 67 return a.toRange().after().comesBefore(b.toRange().before()); 68} 69 70/// Merges the provided Selections into a new selection. Asserts that the selections overlap. 71pub fn merge(a: Selection, b: Selection) Selection { 72 std.debug.assert(a.hasOverlap(b)); 73 74 // 1. If either range contains the other, return the container range. 75 76 if (a.toRange().containsRange(b.toRange())) return a; 77 if (b.toRange().containsRange(a.toRange())) return b; 78 79 // 2. If `a` is a cursor we know it's not in `b`, so we know we only have to merge the correct 80 // edge. 81 82 if (a.isCursor()) { 83 if (a.anchor.comesBefore(b.toRange().before())) 84 return .{ .anchor = a.anchor, .cursor = b.toRange().after() }; 85 86 return .{ .anchor = b.toRange().before(), .cursor = a.cursor }; 87 } 88 89 // 3. Now we know we have overlapping ranges. `b` might still be a cursor, but it does not 90 // matter at this point since it doesn't change how we handle `b`. Now we just make sure we 91 // merge the ranges such that the anchor and cursor are in the right positions. 92 93 // 4. Handle what happens when anchor comes before the cursor. 94 95 if (a.anchor.comesBefore(a.cursor)) { 96 // If the anchor comes after the first position in `b` we use the first position in `b` as 97 // the new anchor. 98 if (a.anchor.comesAfter(b.toRange().before())) 99 return .{ .anchor = b.toRange().before(), .cursor = a.cursor }; 100 101 // Otherwise, we use the anchor from a, and set the cursor to the latter position in `b`. 102 return .{ .anchor = a.anchor, .cursor = b.toRange().after() }; 103 } 104 105 // 5. Otherwise the cursor comes before the anchor. 106 107 // If the cursor comes after the first position in `b` we know we need to extend the cursor to 108 // `b`s first position. 109 if (a.cursor.comesAfter(b.toRange().before())) 110 return .{ .anchor = a.anchor, .cursor = b.toRange().before() }; 111 112 // Otherwise, we extend the anchor to `b`s latter position. 113 return .{ .anchor = b.toRange().after(), .cursor = a.cursor }; 114} 115 116pub fn lessThan(_: void, lhs: Selection, rhs: Selection) bool { 117 return lhs.cursor.comesBefore(rhs.cursor); 118} 119 120pub fn lessThanPtr(_: void, lhs: *Selection, rhs: *Selection) bool { 121 return Selection.lessThan({}, lhs.*, rhs.*); 122} 123 124test merge { 125 // 1. If one selection contains the other, just return the containing selection. 126 127 try std.testing.expectEqual( 128 Selection{ .anchor = .init, .cursor = .{ .row = 0, .col = 5 } }, 129 Selection.merge( 130 .{ .anchor = .{ .row = 0, .col = 1 }, .cursor = .{ .row = 0, .col = 1 } }, 131 .{ .anchor = .init, .cursor = .{ .row = 0, .col = 5 } }, 132 ), 133 ); 134 try std.testing.expectEqual( 135 Selection{ .anchor = .init, .cursor = .{ .row = 0, .col = 5 } }, 136 Selection.merge( 137 .{ .anchor = .init, .cursor = .{ .row = 0, .col = 5 } }, 138 .{ .anchor = .{ .row = 0, .col = 1 }, .cursor = .{ .row = 0, .col = 1 } }, 139 ), 140 ); 141 142 // 2. Merging a cursor and a selection. 143 144 try std.testing.expectEqual( 145 Selection{ .anchor = .{ .row = 0, .col = 1 }, .cursor = .{ .row = 0, .col = 5 } }, 146 Selection.merge( 147 .{ .anchor = .{ .row = 0, .col = 1 }, .cursor = .{ .row = 0, .col = 1 } }, 148 .{ .anchor = .{ .row = 0, .col = 1 }, .cursor = .{ .row = 0, .col = 5 } }, 149 ), 150 ); 151 try std.testing.expectEqual( 152 Selection{ .anchor = .init, .cursor = .{ .row = 0, .col = 1 } }, 153 Selection.merge( 154 .{ .anchor = .init, .cursor = .{ .row = 0, .col = 1 } }, 155 .{ .anchor = .{ .row = 0, .col = 1 }, .cursor = .{ .row = 0, .col = 1 } }, 156 ), 157 ); 158 159 // 3. Merging overlapping selections. 160 161 try std.testing.expectEqual( 162 Selection{ .anchor = .init, .cursor = .{ .row = 1, .col = 4 } }, 163 Selection.merge( 164 .{ .anchor = .init, .cursor = .{ .row = 0, .col = 5 } }, 165 .{ .anchor = .{ .row = 0, .col = 4 }, .cursor = .{ .row = 1, .col = 4 } }, 166 ), 167 ); 168 try std.testing.expectEqual( 169 Selection{ .anchor = .init, .cursor = .{ .row = 1, .col = 4 } }, 170 Selection.merge( 171 .{ .anchor = .{ .row = 0, .col = 4 }, .cursor = .{ .row = 1, .col = 4 } }, 172 .{ .anchor = .init, .cursor = .{ .row = 0, .col = 5 } }, 173 ), 174 ); 175 try std.testing.expectEqual( 176 Selection{ .cursor = .init, .anchor = .{ .row = 1, .col = 4 } }, 177 Selection.merge( 178 .{ .cursor = .{ .row = 0, .col = 4 }, .anchor = .{ .row = 1, .col = 4 } }, 179 .{ .cursor = .init, .anchor = .{ .row = 0, .col = 5 } }, 180 ), 181 ); 182 try std.testing.expectEqual( 183 Selection{ .cursor = .init, .anchor = .{ .row = 1, .col = 4 } }, 184 Selection.merge( 185 .{ .cursor = .init, .anchor = .{ .row = 0, .col = 5 } }, 186 .{ .cursor = .{ .row = 0, .col = 4 }, .anchor = .{ .row = 1, .col = 4 } }, 187 ), 188 ); 189 try std.testing.expectEqual( 190 Selection{ .cursor = .init, .anchor = .{ .row = 1, .col = 4 } }, 191 Selection.merge( 192 .{ .cursor = .init, .anchor = .{ .row = 0, .col = 5 } }, 193 .{ .anchor = .{ .row = 0, .col = 4 }, .cursor = .{ .row = 1, .col = 4 } }, 194 ), 195 ); 196} 197 198test isCursor { 199 const empty: Selection = .{ 200 .anchor = .{ .row = 0, .col = 1 }, 201 .cursor = .{ .row = 0, .col = 1 }, 202 }; 203 const not_empty: Selection = .{ 204 .anchor = .{ .row = 0, .col = 1 }, 205 .cursor = .{ .row = 0, .col = 2 }, 206 }; 207 208 try std.testing.expect(empty.isCursor()); 209 try std.testing.expect(!not_empty.isCursor()); 210} 211 212test eql { 213 const a: Selection = .{ .anchor = .init, .cursor = .{ .row = 0, .col = 3 } }; 214 const b: Selection = .{ .anchor = .{ .row = 0, .col = 3 }, .cursor = .init }; 215 const c: Selection = .{ .anchor = .{ .row = 1, .col = 2 }, .cursor = .{ .row = 1, .col = 5 } }; 216 217 try std.testing.expect(Selection.eql(a, a)); 218 try std.testing.expect(Selection.eql(b, b)); 219 try std.testing.expect(Selection.eql(c, c)); 220 221 try std.testing.expect(Selection.eql(a, b)); 222 try std.testing.expect(Selection.eql(b, a)); 223 224 try std.testing.expect(!Selection.eql(c, a)); 225 try std.testing.expect(!Selection.eql(a, c)); 226 try std.testing.expect(!Selection.eql(c, b)); 227 try std.testing.expect(!Selection.eql(b, c)); 228} 229 230test comesBefore { 231 const a: Selection = .{ .anchor = .init, .cursor = .{ .row = 0, .col = 3 } }; 232 const b: Selection = .{ .anchor = .{ .row = 0, .col = 4 }, .cursor = .{ .row = 1, .col = 2 } }; 233 const c: Selection = .{ .anchor = .{ .row = 2, .col = 3 }, .cursor = .{ .row = 1, .col = 5 } }; 234 235 try std.testing.expect(a.comesBefore(b)); 236 try std.testing.expect(b.comesBefore(c)); 237} 238 239test strictEql { 240 const a: Selection = .{ .anchor = .init, .cursor = .{ .row = 0, .col = 3 } }; 241 const b: Selection = .{ .anchor = .{ .row = 0, .col = 3 }, .cursor = .init }; 242 const c: Selection = .{ .anchor = .{ .row = 1, .col = 2 }, .cursor = .{ .row = 1, .col = 5 } }; 243 244 try std.testing.expect(Selection.strictEql(a, a)); 245 try std.testing.expect(Selection.strictEql(b, b)); 246 try std.testing.expect(Selection.strictEql(c, c)); 247 248 try std.testing.expect(!Selection.strictEql(a, b)); 249 try std.testing.expect(!Selection.strictEql(b, a)); 250 251 try std.testing.expect(!Selection.strictEql(c, a)); 252 try std.testing.expect(!Selection.strictEql(a, c)); 253 try std.testing.expect(!Selection.strictEql(c, b)); 254 try std.testing.expect(!Selection.strictEql(b, c)); 255} 256 257test "refAllDecls" { 258 std.testing.refAllDecls(@This()); 259}