summaryrefslogtreecommitdiff
path: root/src/panes.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/panes.zig')
-rw-r--r--src/panes.zig307
1 files changed, 2 insertions, 305 deletions
diff --git a/src/panes.zig b/src/panes.zig
index 0fb49636..d494cfe4 100644
--- a/src/panes.zig
+++ b/src/panes.zig
@@ -3126,7 +3126,7 @@ pub const Output = struct {
return openRead(p, id, .errors, "", content);
}
- fn resetBody(p: *Pardes, pane: *Pane) void {
+ pub fn resetBody(p: *Pardes, pane: *Pane) void {
pane.file.?.scroll = 0;
pane.cur_row = 0;
pane.cur_col = 0;
@@ -3174,310 +3174,7 @@ pub const Output = struct {
}
};
-pub const Mini = struct {
- pub const max_input_bytes = 4 * 1024 * 1024;
- pub const max_output_bytes = 4 * 1024 * 1024;
-
- pub const State = struct {
- source: []u8,
- colors: []u8,
-
- pub fn deinit(state: *State, gpa: std.mem.Allocator) void {
- gpa.free(state.source);
- gpa.free(state.colors);
- state.* = undefined;
- }
- };
-
- pub const Result = struct {
- content: []u8,
- colors: []u8,
-
- pub fn deinit(result: Result, gpa: std.mem.Allocator) void {
- gpa.free(result.content);
- gpa.free(result.colors);
- }
- };
-
- const Row = struct {
- text: []const u8 = "",
- base: usize = 0,
- at: usize = 0,
- left: usize = 0,
- ink: bool = false,
- color: u8 = 0,
-
- fn dot(row: *Row, styles: []const u8) ?u8 {
- if (row.left == 0) {
- if (row.at == row.text.len) return null;
- const end = modal.nextGrapheme(row.text, row.at);
- const grapheme = row.text[row.at..end];
- row.left = File.graphemeDisplayWidth(grapheme);
- const n = std.unicode.utf8ByteSequenceLength(grapheme[0]) catch unreachable;
- const cp = std.unicode.utf8Decode(grapheme[0..n]) catch unreachable;
- const blank = switch (cp) {
- '\t'...'\r', ' ', 0x85, 0xa0, 0x1680, 0x2000...0x200a, 0x2028, 0x2029, 0x202f, 0x205f, 0x3000 => true,
- else => false,
- };
- row.ink = !blank or grapheme.len != n;
- row.color = if (styles.len == 0) 0 else styles[row.base + row.at];
- row.at = end;
- }
- row.left -= 1;
- return if (row.ink) row.color else null;
- }
- };
-
- fn render(output: ?Result, source: []const u8, styles: []const u8) !usize {
- if (source.len == 0) return 0;
- const end = source.len - @intFromBool(source[source.len - 1] == '\n');
- var lines = std.mem.splitScalar(u8, source[0..end], '\n');
- var offset: usize = 0;
- while (lines.peek() != null) {
- var rows: [4]Row = @splat(.{});
- for (&rows) |*row| {
- const line = lines.next() orelse break;
- row.text = std.mem.trimEnd(u8, line, "\r");
- row.base = @intFromPtr(line.ptr) - @intFromPtr(source.ptr);
- }
- var spaces: usize = 0;
- while (true) {
- var more = false;
- for (rows) |row| more = more or row.at < row.text.len or row.left > 0;
- if (!more) break;
- const bits = [4][2]u3{ .{ 0, 3 }, .{ 1, 4 }, .{ 2, 5 }, .{ 6, 7 } };
- var mask: u8 = 0;
- var counts: [5]u8 = @splat(0);
- for (&rows, 0..) |*row, y| {
- for (0..2) |x| {
- if (row.dot(styles)) |color| {
- mask |= @as(u8, 1) << bits[y][x];
- counts[color] += 1;
- }
- }
- }
- if (mask == 0) {
- spaces += 1;
- continue;
- }
- if (spaces + 3 > max_output_bytes - offset) return error.MiniTooLarge;
- var color: u8 = 0;
- var most: u8 = 0;
- for (counts[1..], 1..) |count, i| {
- if (count > most) {
- color = @intCast(i);
- most = count;
- }
- }
- if (output) |out| {
- @memset(out.content[offset..][0..spaces], ' ');
- @memset(out.colors[offset..][0..spaces], 0);
- _ = std.unicode.utf8Encode(@as(u21, 0x2800) + mask, out.content[offset + spaces ..][0..3]) catch unreachable;
- @memset(out.colors[offset + spaces ..][0..3], color);
- }
- offset += spaces + 3;
- spaces = 0;
- }
- if (offset == max_output_bytes) return error.MiniTooLarge;
- if (output) |out| {
- out.content[offset] = '\n';
- out.colors[offset] = 0;
- }
- offset += 1;
- }
- return offset;
- }
-
- pub fn generate(gpa: std.mem.Allocator, source: []const u8, styles: []const u8) !Result {
- if (source.len > max_input_bytes) return error.MiniTooLarge;
- if (!std.unicode.utf8ValidateSlice(source)) return error.InvalidUtf8;
- if (styles.len != 0 and styles.len != source.len) return error.InvalidMiniColors;
- for (styles) |color| if (color > @intFromEnum(syntax.Syn.comment)) return error.InvalidMiniColors;
- const len = try render(null, source, styles);
- const content = try gpa.alloc(u8, len);
- errdefer gpa.free(content);
- const colors = try gpa.alloc(u8, len);
- errdefer gpa.free(colors);
- const result: Result = .{ .content = content, .colors = colors };
- const written = try render(result, source, styles);
- std.debug.assert(written == len);
- return result;
- }
-
- pub fn open(p: *Pardes, id: usize, argument: []const u8) !void {
- const caller = p.panes[id] orelse return error.MissingPane;
- const word = std.mem.trim(u8, argument, " \t\r\n");
- if (word.len == 0) return error.MissingPath;
- var path_buf: [4096]u8 = undefined;
- const target = filesystem.resolve(p, word, Pardes.paneDir(caller), &path_buf) orelse return error.FileNotFound;
- if (target.dir) return error.NotAFile;
- const source = try p.gpa.dupe(u8, target.path);
- errdefer p.gpa.free(source);
- const input = try filesystem.readLimit(p, source, max_input_bytes);
- defer p.gpa.free(input);
- const styles = try syntax.highlightFileRange(p.tree_sitter_gpa, source, input, 0, input.len);
- defer p.tree_sitter_gpa.free(styles);
- const result = try generate(p.gpa, input, styles);
- errdefer result.deinit(p.gpa);
- for (p.panes, 0..) |slot, i| {
- const pane = slot orelse continue;
- const file = if (pane.file) |*f| f else continue;
- const old = file.mini orelse continue;
- if (!std.mem.eql(u8, old.source, source)) continue;
- if (std.mem.eql(u8, file.content, result.content) and std.mem.eql(u8, old.colors, result.colors)) {
- p.gpa.free(source);
- result.deinit(p.gpa);
- } else {
- File.setContent(p, file, result.content);
- file.mini = .{ .source = source, .colors = result.colors };
- file.syntax_dirty = false;
- Output.resetBody(p, pane);
- }
- p.active = i;
- return;
- }
- const free = p.freeSlot() orelse return error.NoPaneSlots;
- const dir = std.fs.path.dirname(source) orelse "/";
- const path = try std.fmt.allocPrint(p.gpa, "{s}/Mini {s}", .{ std.mem.trimEnd(u8, dir, "/"), std.fs.path.basename(source) });
- errdefer p.gpa.free(path);
- const history = try File.History.create(p.gpa);
- errdefer p.gpa.destroy(history);
- const pane = try p.newDocPane(free);
- pane.file = .{
- .path = path,
- .content = result.content,
- .output = .{ .from = .{ .cmd = .Mini } },
- .mini = .{ .source = source, .colors = result.colors },
- .history = history,
- .syntax_dirty = false,
- };
- pane.cur_pinned = true;
- p.placeDoc(id, free, pane);
- p.active = free;
- }
-
- test "Mini maps every braille dot and partial line group" {
- const gpa = std.testing.allocator;
- const bits = [4][2]u3{ .{ 0, 3 }, .{ 1, 4 }, .{ 2, 5 }, .{ 6, 7 } };
- for (0..256) |mask| {
- var source: [12]u8 = undefined;
- for (0..4) |row| {
- for (0..2) |col| source[row * 3 + col] = if (mask & (@as(usize, 1) << bits[row][col]) != 0) 'x' else ' ';
- source[row * 3 + 2] = '\n';
- }
- const result = try generate(gpa, &source, "");
- defer result.deinit(gpa);
- var expected: [4]u8 = undefined;
- const len: usize = if (mask == 0) 0 else try std.unicode.utf8Encode(@as(u21, 0x2800) + @as(u21, @intCast(mask)), &expected);
- expected[len] = '\n';
- try std.testing.expectEqualStrings(expected[0 .. len + 1], result.content);
- try std.testing.expectEqual(result.content.len, result.colors.len);
- for (result.colors) |color| try std.testing.expectEqual(@as(u8, 0), color);
- }
- for ([_]struct { source: []const u8, expected: []const u8 }{
- .{ .source = "", .expected = "" },
- .{ .source = "x", .expected = "⠁\n" },
- .{ .source = "xx\n", .expected = "⠉\n" },
- .{ .source = "x \n", .expected = "⠁\n" },
- .{ .source = "\n\n\n\nx", .expected = "\n⠁\n" },
- .{ .source = "x\r\nx\r\n", .expected = "⠃\n" },
- }) |case| {
- const result = try generate(gpa, case.source, "");
- defer result.deinit(gpa);
- try std.testing.expectEqualStrings(case.expected, result.content);
- }
- }
-
- test "Mini uses display cells for tabs combining text and wide characters" {
- const gpa = std.testing.allocator;
- for ([_]struct { source: []const u8, expected: []const u8 }{
- .{ .source = "e\u{301}界\n", .expected = "⠉⠁\n" },
- .{ .source = " x\n", .expected = " ⠁\n" },
- .{ .source = "\u{a0}x\n", .expected = "⠈\n" },
- }) |case| {
- const result = try generate(gpa, case.source, "");
- defer result.deinit(gpa);
- try std.testing.expectEqualStrings(case.expected, result.content);
- }
- const tabs = try generate(gpa, "\tx\n", "");
- defer tabs.deinit(gpa);
- const spaces = config.tab_width / 2;
- for (tabs.content[0..spaces]) |byte| try std.testing.expectEqual(@as(u8, ' '), byte);
- try std.testing.expectEqualStrings(if (config.tab_width % 2 == 0) "⠁\n" else "⠈\n", tabs.content[spaces..]);
- }
-
- test "Mini chooses highlighted dots over plain ink with stable color ties" {
- const gpa = std.testing.allocator;
- const source = "xx\nxx\nxx\nxx\n";
- var styles: [source.len]u8 = @splat(0);
- styles[0] = @intFromEnum(syntax.Syn.keyword);
- const rare = try generate(gpa, source, &styles);
- defer rare.deinit(gpa);
- try std.testing.expectEqualStrings("⣿\n", rare.content);
- try std.testing.expectEqualSlices(u8, &.{ 1, 1, 1, 0 }, rare.colors);
- styles[0] = @intFromEnum(syntax.Syn.string);
- styles[1] = @intFromEnum(syntax.Syn.number);
- const tied = try generate(gpa, source, &styles);
- defer tied.deinit(gpa);
- try std.testing.expectEqualSlices(u8, &.{ 2, 2, 2, 0 }, tied.colors);
- styles[3] = @intFromEnum(syntax.Syn.number);
- const majority = try generate(gpa, source, &styles);
- defer majority.deinit(gpa);
- try std.testing.expectEqualSlices(u8, &.{ 3, 3, 3, 0 }, majority.colors);
- }
-
- test "Mini generation bounds and allocation failures leave no partial result" {
- const Case = struct {
- fn run(gpa: std.mem.Allocator) !void {
- const result = try generate(gpa, "alpha\nbeta\ngamma\ndelta\nepsilon\n", "");
- defer result.deinit(gpa);
- }
- };
- try std.testing.checkAllAllocationFailures(std.testing.allocator, Case.run, .{});
- try std.testing.expectError(error.InvalidMiniColors, generate(std.testing.allocator, "x", &.{5}));
- try std.testing.expectError(error.InvalidMiniColors, generate(std.testing.allocator, "xx", &.{0}));
- var allocator = std.testing.FailingAllocator.init(std.testing.allocator, .{ .fail_index = 0 });
- const source = try std.testing.allocator.alloc(u8, max_input_bytes + 1);
- defer std.testing.allocator.free(source);
- try std.testing.expectError(error.MiniTooLarge, generate(allocator.allocator(), source, ""));
- for (source[0..max_input_bytes], 0..) |*byte, i| byte.* = if (i % 2 == 0) 'x' else ' ';
- try std.testing.expectError(error.MiniTooLarge, generate(allocator.allocator(), source[0..max_input_bytes], ""));
- try std.testing.expect(!allocator.has_induced_failure);
- }
-
- test "Mini publishes only complete snapshots and content replacement drops metadata" {
- const Case = struct {
- fn run(gpa: std.mem.Allocator, path: []const u8) !void {
- const p = try Pardes.init(gpa, .{ .tty_only = true });
- defer p.deinit();
- const source = try p.setTestFile("untouched\n");
- const free = p.freeSlot();
- open(p, 0, path) catch |err| {
- try std.testing.expectEqual(@as(usize, 0), p.active);
- try std.testing.expectEqual(free, p.freeSlot());
- try std.testing.expectEqual(source, p.panes[0].?);
- try std.testing.expectEqualStrings("untouched\n", source.file.?.content);
- return err;
- };
- const file = &p.panes[p.active].?.file.?;
- try std.testing.expectEqualStrings("⠉\n", file.content);
- try std.testing.expectEqualStrings(path, file.mini.?.source);
- const replacement = try gpa.dupe(u8, "plain\n");
- File.setContent(p, file, replacement);
- try std.testing.expect(file.mini == null);
- try std.testing.expectEqualStrings("plain\n", file.content);
- }
- };
- var tmp = std.testing.tmpDir(.{});
- defer tmp.cleanup();
- try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "mini.txt", .data = "xx\n" });
- var dir_buf: [4096]u8 = undefined;
- const dir = dir_buf[0..try tmp.dir.realPath(std.testing.io, &dir_buf)];
- var path_buf: [4096]u8 = undefined;
- const path = try std.fmt.bufPrint(&path_buf, "{s}/mini.txt", .{dir});
- try std.testing.checkAllAllocationFailures(std.testing.allocator, Case.run, .{path});
- }
-};
+pub const Mini = @import("Mini.zig");
pub const Image = struct {
const GridKey = struct {