699 lines · 27.5 KB · Zig
RawDownload
const std = @import("std");const unicode_data = @import("unicode_display_data.zig"); pub const DecodedRune = struct {    len: usize,    codepoint: u21,}; pub const DisplayUnit = struct {    byte_len: usize,    cell_width: usize,}; pub noinline fn visibleWidth(text: []const u8) usize {    var width: usize = 0;    var i: usize = 0;    while (i < text.len) {        const unit = displayUnitAt(text, i);        width +|= unit.cell_width;        i += unit.byte_len;    }    return width;} // Same as visibleWidth, but skips over ANSI escape sequences so callers can// measure the rendered cell width of text that already contains styling.pub noinline fn visibleWidthIgnoringAnsi(text: []const u8) usize {    var width: usize = 0;    var i: usize = 0;    while (i < text.len) {        if (text[i] == 0x1b) {            i = ansiSequenceEnd(text, i);            continue;        }        const unit = displayUnitAt(text, i);        width +|= unit.cell_width;        i += unit.byte_len;    }    return width;} // Returns the first variant whose visible width fits max_width, falling back// to the last (shortest) variant so callers always get something to clip.pub noinline fn widestFitting(variants: []const []const u8, max_width: usize) []const u8 {    std.debug.assert(variants.len > 0);    for (variants) |variant| {        if (visibleWidth(variant) <= max_width) return variant;    }    return variants[variants.len - 1];} pub noinline fn prefixByWidth(text: []const u8, max_width: usize) []const u8 {    if (max_width == 0) return "";     var width: usize = 0;    var i: usize = 0;    while (i < text.len) {        const unit = displayUnitAt(text, i);        if (unit.cell_width > max_width - width) break;        width += unit.cell_width;        i += unit.byte_len;    }    return text[0..i];} // Same as prefixByWidth, but skips ANSI escape sequences so callers can clip// text that already carries styling bytes without the escape sequences eating// into the width budget.pub noinline fn prefixByWidthIgnoringAnsi(text: []const u8, max_width: usize) []const u8 {    if (max_width == 0) return "";     var width: usize = 0;    var i: usize = 0;    while (i < text.len) {        if (text[i] == 0x1b) {            i = ansiSequenceEnd(text, i);            continue;        }        const unit = displayUnitAt(text, i);        if (unit.cell_width > max_width - width) break;        width += unit.cell_width;        i += unit.byte_len;    }    return text[0..i];} pub noinline fn suffixByWidth(text: []const u8, max_width: usize) []const u8 {    if (max_width == 0) return text[text.len..];     var remaining = visibleWidth(text);    if (remaining <= max_width) return text;     var i: usize = 0;    while (i < text.len and remaining > max_width) {        const unit = displayUnitAt(text, i);        remaining -= unit.cell_width;        i += unit.byte_len;    }    return text[i..];} // Same as suffixByWidth, but ANSI-aware. Walks forward so escape sequences// stay attached to the visible rune that follows them; splitting an OSC-8// open/close pair would leak a dangling hyperlink into the terminal.pub noinline fn suffixByWidthIgnoringAnsi(text: []const u8, max_width: usize) []const u8 {    if (max_width == 0) return text[text.len..];     const total = visibleWidthIgnoringAnsi(text);    if (total <= max_width) return text;     var remaining = total;    var i: usize = 0;    while (i < text.len and remaining > max_width) {        if (text[i] == 0x1b) {            i = ansiSequenceEnd(text, i);            continue;        }        const unit = displayUnitAt(text, i);        remaining -= unit.cell_width;        i += unit.byte_len;    }    return text[i..];} pub noinline fn displayUnitAt(text: []const u8, index: usize) DisplayUnit {    if (index >= text.len) return .{ .byte_len = 0, .cell_width = 0 };     const first_byte = text[index];    const may_start_keycap = (first_byte == '#' or first_byte == '*' or        (first_byte >= '0' and first_byte <= '9')) and        hasRgiKeycapSuffix(text, index + 1);    if (first_byte < 0x80 and !may_start_keycap) {        const cell_width: usize = if (first_byte == 0 or first_byte < 32 or first_byte == 0x7f) 0 else 1;        return .{ .byte_len = 1, .cell_width = cell_width };    }     const rgi_len = matchRgiSequence(text, index);    if (rgi_len != 0) return .{ .byte_len = rgi_len, .cell_width = 2 };     const first = decodeNextRune(text, index);    const next_index = index + first.len;    if (next_index < text.len and isInRanges(first.codepoint, unicode_data.variation_bases[0..])) {        const selector = decodeNextRune(text, next_index);        if (selector.codepoint == 0xfe0e) {            const cell_width: usize = if (isInRanges(first.codepoint, unicode_data.wide_ranges[0..])) 2 else 1;            return .{ .byte_len = first.len + selector.len, .cell_width = cell_width };        }        if (selector.codepoint == 0xfe0f) {            return .{ .byte_len = first.len + selector.len, .cell_width = 2 };        }    }     return .{ .byte_len = first.len, .cell_width = runeWidth(first.codepoint) };} fn hasRgiKeycapSuffix(text: []const u8, suffix_start: usize) bool {    const variation_selector_16 = "\xef\xb8\x8f";    const combining_keycap = "\xe2\x83\xa3";    var index = suffix_start;    if (std.mem.startsWith(u8, text[index..], variation_selector_16)) {        index += variation_selector_16.len;    }    return std.mem.startsWith(u8, text[index..], combining_keycap);} noinline fn runeWidth(codepoint: u21) usize {    if (codepoint == 0) return 0;    if (codepoint < 32 or (codepoint >= 0x7f and codepoint < 0xa0)) return 0;    if (isZeroWidthContinuation(codepoint)) return 0;    if (isInRanges(codepoint, unicode_data.wide_ranges[0..]) or        isInRanges(codepoint, unicode_data.emoji_presentation_ranges[0..])) return 2;    return 1;} /// Returns whether width `w` at one-based `col` exceeds `cols` under DECAWM-OFF./// Requires non-zero inputs and widens arithmetic to avoid `u16` overflow.pub fn shouldWrapAt(col: u16, w: u16, cols: u16) bool {    return @as(u32, col) + @as(u32, w) - 1 > @as(u32, cols);} /// Returns the next fixed eight-column tab stop, clamped to the right margin./// Columns are one-based. Callers must provide non-zero `col` and `cols`.pub fn nextTabStopColumn(col: u16, cols: u16) u16 {    std.debug.assert(col > 0);    std.debug.assert(cols > 0);    if (col >= cols) return cols;     const zero_based: u32 = @as(u32, col) - 1;    const next_stop = ((zero_based / 8) + 1) * 8 + 1;    return @intCast(@min(next_stop, @as(u32, cols)));} pub noinline fn decodeNextRune(text: []const u8, index: usize) DecodedRune {    if (index >= text.len) return .{ .len = 0, .codepoint = 0 };    const first = text[index];    if (first < 0x80) return .{ .len = 1, .codepoint = first };     const len = std.unicode.utf8ByteSequenceLength(first) catch return .{ .len = 1, .codepoint = 0xfffd };    if (index + len > text.len) return .{ .len = 1, .codepoint = 0xfffd };     const codepoint = std.unicode.utf8Decode(text[index .. index + len]) catch return .{ .len = 1, .codepoint = 0xfffd };    return .{ .len = len, .codepoint = codepoint };} pub noinline fn ansiSequenceEnd(text: []const u8, index: usize) usize {    if (index >= text.len or text[index] != 0x1b) return index;    if (index + 1 >= text.len) return text.len;     const kind = text[index + 1];    if (kind == '[') {        var i = index + 2;        while (i < text.len) : (i += 1) {            const b = text[i];            if (b >= '@' and b <= '~') return i + 1;        }        return text.len;    }     if (kind == ']') {        var i = index + 2;        while (i < text.len) : (i += 1) {            if (text[i] == 0x07) return i + 1;            if (text[i] == 0x1b and i + 1 < text.len and text[i + 1] == '\\') return i + 2;        }        return text.len;    }     return @min(index + 2, text.len);} pub noinline fn previousRuneStart(text: []const u8, end: usize) usize {    if (end == 0) return 0;    var i = end - 1;    while (i > 0 and (text[i] & 0b1100_0000) == 0b1000_0000) : (i -= 1) {}    return i;} pub noinline fn statusPrefixEnd(label: []const u8) usize {    if (label.len == 0) return 0;    const first = displayUnitAt(label, 0);    if (first.byte_len == 0 or first.byte_len >= label.len) return 0;    return if (label[first.byte_len] == ' ') first.byte_len + 1 else 0;} pub noinline fn wrapCutIgnoringAnsi(text: []const u8, max_width: usize) []const u8 {    const prefix = prefixByWidthIgnoringAnsi(text, max_width);    if (prefix.len == text.len) return prefix;     var last_space: ?usize = null;    var index: usize = 0;    while (index < prefix.len) {        const unit = displayUnitAt(prefix, index);        if (prefix[index] == ' ' or prefix[index] == '\t') last_space = index;        index += unit.byte_len;    }    if (last_space) |space_index| {        if (space_index > 0) return text[0..space_index];    }    return prefix;} pub noinline fn trimBreakWhitespace(text: []const u8) []const u8 {    var index: usize = 0;    while (index < text.len and (text[index] == ' ' or text[index] == '\t')) : (index += 1) {}    return text[index..];} fn isCombining(codepoint: u21) bool {    return (codepoint >= 0x0300 and codepoint <= 0x036f) or        (codepoint >= 0x1ab0 and codepoint <= 0x1aff) or        (codepoint >= 0x1dc0 and codepoint <= 0x1dff) or        (codepoint >= 0x20d0 and codepoint <= 0x20ff) or        (codepoint >= 0xfe20 and codepoint <= 0xfe2f) or        (codepoint >= 0xfe00 and codepoint <= 0xfe0f);} fn isZeroWidthContinuation(codepoint: u21) bool {    return isCombining(codepoint) or        isInRanges(codepoint, unicode_data.emoji_modifier_ranges[0..]) or        codepoint == 0x200c or        codepoint == 0x200d or        (codepoint >= 0x200b and codepoint <= 0x200f) or        (codepoint >= 0x202a and codepoint <= 0x202e) or        (codepoint >= 0x2060 and codepoint <= 0x206f) or        codepoint == 0xfeff or        (codepoint >= 0xe0001 and codepoint <= 0xe007f) or        (codepoint >= 0xe0100 and codepoint <= 0xe01ef);} fn isInRanges(codepoint: u21, ranges: []const unicode_data.Range) bool {    var low: usize = 0;    var high = ranges.len;    while (low < high) {        const middle = low + (high - low) / 2;        const range = ranges[middle];        if (codepoint < range.first) {            high = middle;        } else if (codepoint > range.last) {            low = middle + 1;        } else {            return true;        }    }    return false;} fn matchRgiSequence(text: []const u8, index: usize) usize {    var node_index: u32 = 0;    var cursor = index;    var longest: usize = 0;    var depth: usize = 0;     while (cursor < text.len and depth < unicode_data.max_rgi_sequence_codepoints) : (depth += 1) {        const rune = decodeNextRune(text, cursor);        node_index = findTrieChild(node_index, rune.codepoint) orelse break;        cursor += rune.len;        const node = unicode_data.rgi_trie_nodes[node_index];        if (node.terminal) longest = cursor - index;    }     return longest;} fn findTrieChild(node_index: u32, codepoint: u21) ?u32 {    const node = unicode_data.rgi_trie_nodes[node_index];    const edge_start: usize = node.edge_start;    var low = edge_start;    var high = edge_start + node.edge_len;    while (low < high) {        const middle = low + (high - low) / 2;        const edge = unicode_data.rgi_trie_edges[middle];        if (codepoint < edge.codepoint) {            high = middle;        } else if (codepoint > edge.codepoint) {            low = middle + 1;        } else {            return edge.child;        }    }    return null;} test "prefixByWidth avoids cutting emoji bytes" {    const text = "a\xf0\x9f\x98\x80b";    try std.testing.expectEqualStrings("a", prefixByWidth(text, 2));    try std.testing.expectEqualStrings("a\xf0\x9f\x98\x80", prefixByWidth(text, 3));} test "suffixByWidth keeps whole runes" {    try std.testing.expectEqualStrings("\xf0\x9f\x98\x80cd", suffixByWidth("ab\xf0\x9f\x98\x80cd", 4));} test "visibleWidth counts emoji as two cells" {    try std.testing.expectEqual(@as(usize, 4), visibleWidth("a\xf0\x9f\x98\x80b"));} test "visibleWidthIgnoringAnsi skips CSI sequences" {    try std.testing.expectEqual(@as(usize, 5), visibleWidthIgnoringAnsi("\x1b[1mhello\x1b[22m"));    try std.testing.expectEqual(@as(usize, 3), visibleWidthIgnoringAnsi("\x1b[48;5;236mfoo\x1b[49m"));    try std.testing.expectEqual(@as(usize, 7), visibleWidthIgnoringAnsi("\x1b[1mEnfoque\x1b[22m"));} test "shouldWrapAt is strictly overflow-based" {    try std.testing.expect(!shouldWrapAt(10, 1, 10));    try std.testing.expect(shouldWrapAt(10, 2, 10));    try std.testing.expect(!shouldWrapAt(1, 1, 1));     // Start at c = 2 so c - 1 stays >= 1; cols = 0 is excluded by the    // helper precondition and guarded by current callers.    var c: u16 = 2;    while (c <= 256) : (c += 1) {        try std.testing.expect(!shouldWrapAt(c, 1, c));        try std.testing.expect(shouldWrapAt(c, 1, c - 1));    }     try std.testing.expect(shouldWrapAt(65535, 2, 65535));} test "nextTabStopColumn uses absolute stops and clamps safely" {    try std.testing.expectEqual(@as(u16, 9), nextTabStopColumn(1, 80));    try std.testing.expectEqual(@as(u16, 9), nextTabStopColumn(8, 80));    try std.testing.expectEqual(@as(u16, 17), nextTabStopColumn(9, 80));    try std.testing.expectEqual(@as(u16, 8), nextTabStopColumn(1, 8));    try std.testing.expectEqual(@as(u16, 10), nextTabStopColumn(10, 10));    try std.testing.expectEqual(@as(u16, 10), nextTabStopColumn(11, 10));    try std.testing.expectEqual(@as(u16, 65535), nextTabStopColumn(65535, 65535));} test "prefixByWidthIgnoringAnsi keeps opening OSC-8 attached to visible bytes" {    // OSC-8 open does not consume any visible cells; the first two bytes of    // the hyperlink body do. Budget of 2 keeps the open + 2 cells.    const text = "\x1b]8;;file:///x\x1b\\ab\x1b]8;;\x1b\\";    const out = prefixByWidthIgnoringAnsi(text, 2);    try std.testing.expectEqual(@as(usize, 2), visibleWidthIgnoringAnsi(out));    try std.testing.expect(std.mem.find(u8, out, "\x1b]8;;file:///x\x1b\\") != null);} test "prefixByWidthIgnoringAnsi returns empty on zero budget" {    try std.testing.expectEqualStrings("", prefixByWidthIgnoringAnsi("\x1b[31mhello\x1b[0m", 0));} test "suffixByWidthIgnoringAnsi tolerates ANSI around multibyte runes" {    const text = "a\x1b]8;;file:///\xc2\xa0.png\x1b\\[Image 1]\x1b]8;;\x1b\\";    const out = suffixByWidthIgnoringAnsi(text, 9);    try std.testing.expectEqual(@as(usize, 9), visibleWidthIgnoringAnsi(out));} test "suffixByWidthIgnoringAnsi returns full text when it fits" {    const text = "\x1b[31mhi\x1b[0m";    try std.testing.expectEqualStrings(text, suffixByWidthIgnoringAnsi(text, 5));} test "decodeNextRune advances through invalid UTF-8" {    const text = "\xf0\x28\x8c\x28";    const first = decodeNextRune(text, 0);    try std.testing.expectEqual(@as(usize, 1), first.len);    try std.testing.expectEqual(@as(u21, 0xfffd), first.codepoint);     const second = decodeNextRune(text, first.len);    try std.testing.expectEqual(@as(usize, 1), second.len);    try std.testing.expectEqual(@as(u21, '('), second.codepoint);} test "ansiSequenceEnd handles OSC terminated by ESC backslash" {    const text = "\x1b]8;;file:///x\x1b\\body";    try std.testing.expectEqual(@as(usize, 16), ansiSequenceEnd(text, 0));    try std.testing.expectEqual(@as(usize, 4), visibleWidthIgnoringAnsi(text));} test "runeWidth preserves current width table semantics" {    try std.testing.expectEqual(@as(usize, 0), runeWidth(0));    try std.testing.expectEqual(@as(usize, 0), runeWidth(0x07));    try std.testing.expectEqual(@as(usize, 0), runeWidth(0x1b));    try std.testing.expectEqual(@as(usize, 0), runeWidth(0x80));    try std.testing.expectEqual(@as(usize, 0), runeWidth(0x9f));    try std.testing.expectEqual(@as(usize, 0), runeWidth(0x0301));    try std.testing.expectEqual(@as(usize, 0), runeWidth(0x1ab0));    try std.testing.expectEqual(@as(usize, 2), runeWidth(0x4e00));    try std.testing.expectEqual(@as(usize, 2), runeWidth(0x1f600));    try std.testing.expectEqual(@as(usize, 1), runeWidth(0xff66));    try std.testing.expectEqual(@as(usize, 1), runeWidth('a'));    try std.testing.expectEqual(@as(usize, 1), runeWidth(' '));    try std.testing.expectEqual(@as(usize, 0), runeWidth(0x09));} test "decodeNextRune preserves boundary and invalid-byte behavior" {    try std.testing.expectEqual(DecodedRune{ .len = 0, .codepoint = 0 }, decodeNextRune("", 0));    try std.testing.expectEqual(DecodedRune{ .len = 0, .codepoint = 0 }, decodeNextRune("a", 1));    try std.testing.expectEqual(DecodedRune{ .len = 1, .codepoint = 0xfffd }, decodeNextRune("\xff", 0));    try std.testing.expectEqual(DecodedRune{ .len = 1, .codepoint = 0xfffd }, decodeNextRune("\xf0", 0));    try std.testing.expectEqual(DecodedRune{ .len = 1, .codepoint = 0xfffd }, decodeNextRune("\xc0\x80", 0));} test "ansiSequenceEnd preserves current CSI OSC and ESC boundaries" {    try std.testing.expectEqual(@as(usize, 3), ansiSequenceEnd("\x1b[m", 0));    try std.testing.expectEqual(@as(usize, 7), ansiSequenceEnd("\x1b[1;31m", 0));    try std.testing.expectEqual(@as(usize, 3), ansiSequenceEnd("\x1b[K", 0));     const unterminated_csi = "\x1b[";    try std.testing.expectEqual(unterminated_csi.len, ansiSequenceEnd(unterminated_csi, 0));     try std.testing.expectEqual(@as(usize, 9), ansiSequenceEnd("\x1b]8;;url\x07", 0));    try std.testing.expectEqual(@as(usize, 10), ansiSequenceEnd("\x1b]8;;url\x1b\\", 0));     const unterminated_osc = "\x1b]8;;url";    try std.testing.expectEqual(unterminated_osc.len, ansiSequenceEnd(unterminated_osc, 0));     try std.testing.expectEqual(@as(usize, 2), ansiSequenceEnd("\x1bA", 0));     const bare_escape = "\x1b";    try std.testing.expectEqual(bare_escape.len, ansiSequenceEnd(bare_escape, 0));} test "previousRuneStart preserves byte-boundary behavior" {    try std.testing.expectEqual(@as(usize, 4), previousRuneStart("hello", 5));    try std.testing.expectEqual(@as(usize, 1), previousRuneStart("a\xf0\x9f\x98\x80b", 5));    try std.testing.expectEqual(@as(usize, 0), previousRuneStart("", 0));    try std.testing.expectEqual(@as(usize, 0), previousRuneStart("a", 1));} test "prefixByWidth and suffixByWidth preserve boundary behavior" {    try std.testing.expectEqualStrings("", prefixByWidth("ab", 0));    try std.testing.expectEqualStrings("a", prefixByWidth("ab", 1));    try std.testing.expectEqualStrings("ab", prefixByWidth("ab", 2));    try std.testing.expectEqualStrings("ab", prefixByWidth("ab", 100));     const suffix_zero = suffixByWidth("ab", 0);    try std.testing.expectEqual(@as(usize, 0), suffix_zero.len);    try std.testing.expectEqualStrings("b", suffixByWidth("ab", 1));    try std.testing.expectEqualStrings("\xf0\x9f\x98\x80b", suffixByWidth("a\xf0\x9f\x98\x80b", 3));} test "shouldWrapAt preserves documented edge behavior" {    try std.testing.expect(shouldWrapAt(1, 2, 1));    try std.testing.expect(!shouldWrapAt(1, 1, 80));    try std.testing.expect(!shouldWrapAt(80, 1, 80));    try std.testing.expect(shouldWrapAt(80, 2, 80));} test "statusPrefixEnd detects a leading status rune and space" {

Lines 1–500 of 699.