lib/pretty/core/src/position.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2
3 pub const Position = struct {
4 line: usize = 0,
5 column: usize = 0,
6 };
7
8 pub const Reset = struct {
9 line_feeds: usize,
10 trailing_cells: usize,
11 };
12
13 pub const Summary = union(enum) {
14 inline_cells: usize,
15 reset: Reset,
16
17 pub fn fromBytes(bytes: []const u8) Summary {
18 var line_feeds: usize = 0;
19 var last_reset: ?usize = null;
20 for (bytes, 0..) |byte, index| {
21 switch (byte) {
22 '\n' => {
23 line_feeds +|= 1;
24 last_reset = index;
25 },
26 '\r' => last_reset = index,
27 else => {},
28 }
29 }
30 const reset_index = last_reset orelse
31 return .{ .inline_cells = bytes.len };
32 return .{ .reset = .{
33 .line_feeds = line_feeds,
34 .trailing_cells = bytes.len - (reset_index + 1),
35 } };
36 }
37
38 pub fn append(left: Summary, right: Summary) Summary {
39 return switch (left) {
40 .inline_cells => |left_cells| switch (right) {
41 .inline_cells => |right_cells| .{
42 .inline_cells = left_cells +| right_cells,
43 },
44 .reset => right,
45 },
46 .reset => |left_reset| switch (right) {
47 .inline_cells => |right_cells| .{ .reset = .{
48 .line_feeds = left_reset.line_feeds,
49 .trailing_cells = left_reset.trailing_cells +| right_cells,
50 } },
51 .reset => |right_reset| .{ .reset = .{
52 .line_feeds = left_reset.line_feeds +| right_reset.line_feeds,
53 .trailing_cells = right_reset.trailing_cells,
54 } },
55 },
56 };
57 }
58
59 pub fn advance(self: Summary, start: Position) Position {
60 return switch (self) {
61 .inline_cells => |cells| .{
62 .line = start.line,
63 .column = start.column +| cells,
64 },
65 .reset => |reset| .{
66 .line = start.line +| reset.line_feeds,
67 .column = reset.trailing_cells,
68 },
69 };
70 }
71
72 pub fn flatWidth(self: Summary) ?usize {
73 return switch (self) {
74 .inline_cells => |cells| cells,
75 .reset => null,
76 };
77 }
78 };
79
80 pub fn consumeFlatBytes(bytes: []const u8, budget: *usize) bool {
81 if (bytes.len > budget.*) return false;
82 if (std.mem.findAny(u8, bytes, "\r\n") != null) return false;
83 budget.* -= bytes.len;
84 return true;
85 }
86
87 test "flat byte consumption agrees with position summaries across byte values" {
88 var bytes: [129]u8 = undefined;
89 for (0..256) |value| {
90 @memset(&bytes, @intCast(value));
91 for (0..bytes.len + 1) |length| {
92 const text = bytes[0..length];
93 const width = Summary.fromBytes(text).flatWidth();
94 for ([_]usize{ 0, length -| 1, length, length + 1, std.math.maxInt(usize) }) |limit| {
95 var budget = limit;
96 const fits = if (width) |cells| cells <= limit else false;
97 try std.testing.expectEqual(fits, consumeFlatBytes(text, &budget));
98 try std.testing.expectEqual(if (fits) limit - length else limit, budget);
99 }
100 }
101 }
102 }
103
104 test "position summaries compose across split CRLF" {
105 const left = Summary.fromBytes("ab\r");
106 const right = Summary.fromBytes("\ncd");
107 const whole = Summary.fromBytes("ab\r\ncd");
108 try std.testing.expectEqual(whole, left.append(right));
109
110 const start = Position{ .line = 7, .column = std.math.maxInt(usize) };
111 try std.testing.expectEqual(
112 whole.advance(start),
113 right.advance(left.advance(start)),
114 );
115 try std.testing.expectEqual(
116 Position{ .line = 8, .column = 2 },
117 whole.advance(start),
118 );
119 }
120
121 test "position advancement saturates additive fields" {
122 const maximum = std.math.maxInt(usize);
123 const start = Position{ .line = maximum, .column = maximum };
124 try std.testing.expectEqual(
125 start,
126 (Summary{ .inline_cells = 1 }).advance(start),
127 );
128 try std.testing.expectEqual(
129 Position{ .line = maximum, .column = maximum },
130 (Summary{ .reset = .{
131 .line_feeds = 1,
132 .trailing_cells = maximum,
133 } }).advance(start),
134 );
135 try std.testing.expectEqual(
136 Summary{ .reset = .{
137 .line_feeds = maximum,
138 .trailing_cells = maximum,
139 } },
140 (Summary{ .reset = .{
141 .line_feeds = maximum,
142 .trailing_cells = maximum,
143 } }).append(.{ .inline_cells = 1 }),
144 );
145 }
146
147 test "flat byte consumption rejects every position reset" {
148 var exact_budget: usize = 3;
149 try std.testing.expect(consumeFlatBytes("abc", &exact_budget));
150 try std.testing.expectEqual(@as(usize, 0), exact_budget);
151
152 var line_feed_budget: usize = 3;
153 try std.testing.expect(!consumeFlatBytes("a\nb", &line_feed_budget));
154 try std.testing.expectEqual(@as(usize, 3), line_feed_budget);
155
156 var carriage_return_budget: usize = 3;
157 try std.testing.expect(!consumeFlatBytes("a\rb", &carriage_return_budget));
158 try std.testing.expectEqual(@as(usize, 3), carriage_return_budget);
159 }