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 }