TUI editor and editor backend written in Zig
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}