PranitInitial commit439f83c3 months agoHistory
302 lines Β· 11.9 KB Β· Zig
RawDownload
const std = @import("std");const display_width = @import("../shared/display_width.zig"); pub fn nextCharacterEnd(text: []const u8, start: usize) usize {    var end = @min(start, text.len);    if (end == text.len) return end;     const first = display_width.displayUnitAt(text, end);    end += @max(first.byte_len, 1);    if (isControlBoundary(text, start)) return @min(end, text.len);    while (end < text.len) {        const continuation = display_width.displayUnitAt(text, end);        if (continuation.cell_width != 0 or isControlBoundary(text, end)) break;        end += @max(continuation.byte_len, 1);    }    return @min(end, text.len);} pub fn previousCharacterStart(text: []const u8, end: usize) usize {    const target = @min(end, text.len);    if (target == 0) return 0;     var current: usize = 0;    var previous: usize = 0;    while (current < target) {        previous = current;        const next = nextCharacterEnd(text, current);        if (next >= target) return previous;        current = next;    }    return previous;} pub fn previousWordStart(text: []const u8, cursor: usize) usize {    var pos = @min(cursor, text.len);    while (pos > 0) {        const previous = previousCharacterStart(text, pos);        if (isWordCharacterAt(text, previous)) break;        pos = previous;    }    while (pos > 0) {        const previous = previousCharacterStart(text, pos);        if (!isWordCharacterAt(text, previous)) break;        pos = previous;    }    return pos;} pub fn nextWordEnd(text: []const u8, cursor: usize) usize {    var pos = @min(cursor, text.len);    while (pos < text.len and !isWordCharacterAt(text, pos)) {        pos = nextCharacterEnd(text, pos);    }    while (pos < text.len and isWordCharacterAt(text, pos)) {        pos = nextCharacterEnd(text, pos);    }    return pos;} pub fn nextWordDeleteEnd(text: []const u8, cursor: usize) usize {    var pos = @min(cursor, text.len);    while (pos < text.len and isWordCharacterAt(text, pos)) {        pos = nextCharacterEnd(text, pos);    }    while (pos < text.len and !isWordCharacterAt(text, pos)) {        pos = nextCharacterEnd(text, pos);    }    return pos;} pub fn isWordCharacter(codepoint: u21) bool {    if (codepoint == '_') return true;    if (codepoint >= 'a' and codepoint <= 'z') return true;    if (codepoint >= 'A' and codepoint <= 'Z') return true;    if (codepoint >= '0' and codepoint <= '9') return true;    return codepoint >= 0xC0;} pub fn logicalLineStart(text: []const u8, cursor: usize) usize {    var start = @min(cursor, text.len);    while (start > 0 and text[start - 1] != '\n') start -= 1;    return start;} pub fn logicalLineEnd(text: []const u8, cursor: usize) usize {    var end = @min(cursor, text.len);    while (end < text.len and text[end] != '\n') end += 1;    return end;} fn lineIsBlank(text: []const u8, start: usize, end: usize) bool {    for (text[start..end]) |byte| switch (byte) {        ' ', '\t', '\r' => {},        else => return false,    };    return true;} fn previousLineStart(text: []const u8, line_start: usize) ?usize {    if (line_start == 0) return null;    return logicalLineStart(text, line_start - 1);} pub fn previousParagraphStart(text: []const u8, cursor: usize) usize {    var line_start = logicalLineStart(text, cursor);    var line_end = logicalLineEnd(text, line_start);     while (lineIsBlank(text, line_start, line_end)) {        line_start = previousLineStart(text, line_start) orelse return 0;        line_end = logicalLineEnd(text, line_start);    }     var paragraph_start = line_start;    while (previousLineStart(text, paragraph_start)) |candidate| {        if (lineIsBlank(text, candidate, logicalLineEnd(text, candidate))) break;        paragraph_start = candidate;    }    if (cursor > paragraph_start) return paragraph_start;     var previous = previousLineStart(text, paragraph_start) orelse return 0;    while (lineIsBlank(text, previous, logicalLineEnd(text, previous))) {        previous = previousLineStart(text, previous) orelse return 0;    }    var previous_start = previous;    while (previousLineStart(text, previous_start)) |candidate| {        if (lineIsBlank(text, candidate, logicalLineEnd(text, candidate))) break;        previous_start = candidate;    }    return previous_start;} pub fn nextParagraphStart(text: []const u8, cursor: usize) usize {    var line_start = logicalLineStart(text, cursor);    var line_end = logicalLineEnd(text, line_start);     while (!lineIsBlank(text, line_start, line_end)) {        if (line_end == text.len) return text.len;        line_start = line_end + 1;        line_end = logicalLineEnd(text, line_start);    }    while (lineIsBlank(text, line_start, line_end)) {        if (line_end == text.len) return text.len;        line_start = line_end + 1;        line_end = logicalLineEnd(text, line_start);    }    return line_start;} pub fn previousWhitespaceDelimitedTokenStart(    text: []const u8,    cursor: usize,    lower_bound: usize,) usize {    var pos = @min(cursor, text.len);    const lower = @min(lower_bound, pos);    while (pos > lower) {        const previous = previousCharacterStart(text, pos);        if (!isWhitespaceCharacter(text, previous)) break;        pos = previous;    }    while (pos > lower) {        const previous = previousCharacterStart(text, pos);        if (isWhitespaceCharacter(text, previous)) break;        pos = previous;    }    return pos;} fn isControlBoundary(text: []const u8, start: usize) bool {    const codepoint = display_width.decodeNextRune(text, start).codepoint;    return codepoint < 0x20 or codepoint == 0x7F;} fn isWhitespaceCharacter(text: []const u8, start: usize) bool {    const codepoint = display_width.decodeNextRune(text, start).codepoint;    return codepoint <= 0x7F and std.ascii.isWhitespace(@intCast(codepoint));} fn isWordCharacterAt(text: []const u8, start: usize) bool {    const codepoint = display_width.decodeNextRune(text, start).codepoint;    return isWordCharacter(codepoint);} test "character boundaries group terminal character continuations" {    const decomposed = "Ae\u{301}B";    try std.testing.expectEqual(@as(usize, 4), nextCharacterEnd(decomposed, 1));    try std.testing.expectEqual(@as(usize, 1), previousCharacterStart(decomposed, 4));     const flag = "AπŸ‡ΊπŸ‡ΈB";    try std.testing.expectEqual(@as(usize, 1 + "πŸ‡ΊπŸ‡Έ".len), nextCharacterEnd(flag, 1));    try std.testing.expectEqual(@as(usize, 1), previousCharacterStart(flag, 1 + "πŸ‡ΊπŸ‡Έ".len));     const modifier = "AπŸ‘πŸ½B";    try std.testing.expectEqual(@as(usize, 1 + "πŸ‘πŸ½".len), nextCharacterEnd(modifier, 1));    try std.testing.expectEqual(@as(usize, 1), previousCharacterStart(modifier, 1 + "πŸ‘πŸ½".len));     const family = "AπŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦B";    try std.testing.expectEqual(@as(usize, 1 + "πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦".len), nextCharacterEnd(family, 1));    try std.testing.expectEqual(@as(usize, 1), previousCharacterStart(family, 1 + "πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦".len));} test "character boundaries preserve ascii and precomposed utf8" {    const text = "AΓ©B";    try std.testing.expectEqual(@as(usize, 1), nextCharacterEnd(text, 0));    try std.testing.expectEqual(@as(usize, 3), nextCharacterEnd(text, 1));    try std.testing.expectEqual(@as(usize, 1), previousCharacterStart(text, 3));    try std.testing.expectEqual(@as(usize, 3), previousCharacterStart(text, text.len));    try std.testing.expectEqual(@as(usize, 0), previousCharacterStart(text, 0));    try std.testing.expectEqual(text.len, nextCharacterEnd(text, text.len + 10));} test "control bytes remain independent character boundaries" {    const text = "A\n\u{301}\tB";    try std.testing.expectEqual(@as(usize, 1), nextCharacterEnd(text, 0));    try std.testing.expectEqual(@as(usize, 2), nextCharacterEnd(text, 1));    try std.testing.expectEqual(@as(usize, 4), nextCharacterEnd(text, 2));    try std.testing.expectEqual(@as(usize, 5), nextCharacterEnd(text, 4));    try std.testing.expectEqual(@as(usize, 4), previousCharacterStart(text, 5));    try std.testing.expectEqual(@as(usize, 2), previousCharacterStart(text, 4));    try std.testing.expectEqual(@as(usize, 1), previousCharacterStart(text, 2));} test "word boundaries use whole terminal characters" {    const text = "alpha beta gamma";    try std.testing.expectEqual(@as(usize, 11), previousWordStart(text, text.len));    try std.testing.expectEqual(@as(usize, 6), previousWordStart(text, 11));    try std.testing.expectEqual(@as(usize, 5), nextWordEnd(text, 0));    try std.testing.expectEqual(@as(usize, 10), nextWordEnd(text, 5));    try std.testing.expectEqual(text.len, nextWordEnd(text, 11));     const unicode = "one e\u{301}lan πŸ‡ΊπŸ‡Έ flag";    const flag_start = std.mem.find(u8, unicode, "πŸ‡ΊπŸ‡Έ").?;    try std.testing.expectEqual(flag_start + "πŸ‡ΊπŸ‡Έ".len, nextWordEnd(unicode, "one e\u{301}lan".len));    try std.testing.expectEqual(@as(usize, 4), previousWordStart(unicode, flag_start));} test "word deletion boundary consumes the current word and following separators" {    const text = "alpha  beta gamma";    try std.testing.expectEqual(@as(usize, 7), nextWordDeleteEnd(text, 0));    try std.testing.expectEqual(@as(usize, 7), nextWordDeleteEnd(text, 5));    try std.testing.expectEqual(@as(usize, 12), nextWordDeleteEnd(text, 7));    try std.testing.expectEqual(text.len, nextWordDeleteEnd(text, 12));    try std.testing.expectEqual(@as(usize, 7), nextWordDeleteEnd("alpha, beta", 0));     const unicode = "Γ©lan πŸ™‚ next";    try std.testing.expectEqual(@as(usize, "Γ©lan ".len), nextWordDeleteEnd(unicode, 0));} test "logical line boundaries clamp to the current line" {    const text = "alpha\nbeta\ngamma";    try std.testing.expectEqual(@as(usize, 0), logicalLineStart(text, 5));    try std.testing.expectEqual(@as(usize, 6), logicalLineStart(text, 8));    try std.testing.expectEqual(@as(usize, 10), logicalLineEnd(text, 6));    try std.testing.expectEqual(text.len, logicalLineEnd(text, text.len + 10));} test "paragraph boundaries move across blank-line-delimited blocks" {    const text = "alpha\nbeta\n \t\ngamma\ndelta\n\nlast";    const gamma = std.mem.find(u8, text, "gamma").?;    const last = std.mem.find(u8, text, "last").?;     try std.testing.expectEqual(@as(usize, 0), previousParagraphStart(text, "alpha\nbeta".len));    try std.testing.expectEqual(@as(usize, 0), previousParagraphStart(text, gamma));    try std.testing.expectEqual(gamma, nextParagraphStart(text, 0));    try std.testing.expectEqual(last, nextParagraphStart(text, gamma));    try std.testing.expectEqual(text.len, nextParagraphStart(text, last));} test "whitespace delimited token boundary stays within its logical line" {    const text = "alpha\n  beta gamma";    const line_start = logicalLineStart(text, text.len);    try std.testing.expectEqual(@as(usize, 13), previousWhitespaceDelimitedTokenStart(text, text.len, line_start));    try std.testing.expectEqual(@as(usize, 8), previousWhitespaceDelimitedTokenStart(text, 13, line_start));    try std.testing.expectEqual(line_start, previousWhitespaceDelimitedTokenStart(text, 8, line_start));     const unicode = "one\te\u{301}lan";    try std.testing.expectEqual(@as(usize, 4), previousWhitespaceDelimitedTokenStart(unicode, unicode.len, 0));} test "character boundary offsets stay ordered and in bounds" {    const cases = [_][]const u8{        "",        "ascii",        "e\u{301}",        "πŸ‡ΊπŸ‡Έ",        "πŸ‘πŸ½",        "πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦",        "\u{301}\u{302}x",    };     for (cases) |text| {        var cursor: usize = 0;        while (cursor < text.len) {            const next = nextCharacterEnd(text, cursor);            try std.testing.expect(next > cursor);            try std.testing.expect(next <= text.len);            try std.testing.expectEqual(cursor, previousCharacterStart(text, next));            cursor = next;        }        try std.testing.expectEqual(text.len, cursor);    }}