PranitInitial commit439f83c3 months agoHistory
1367 lines · 52.5 KB · Zig
RawDownload
const std = @import("std");const display_width = @import("../../core/shared/display_width.zig");const image_attachments = @import("../../core/images/image_attachments.zig");const entity_spans = @import("../../core/shared/entity_spans.zig");const skill_runtime = @import("../../core/skills/skill_runtime.zig");const types = @import("../../core/shared/types.zig");const paste_blocks = @import("../../core/input/pasted_blocks.zig");const registered_entities = @import("../../core/input/registered_entities.zig"); pub const Direction = enum { up, down };pub const BreakKind = enum { hard_newline, soft_wrap, input_end };pub const InputPrefix = struct {    bytes: []const u8,    cell_width: usize,};pub const UnitKind = union(enum) {    text,    tab,    paste_placeholder,    skill_token: usize,    image_badge: struct {        attachment_index: usize,    },}; pub const SkillTokenSpan = registered_entities.SkillTokenSpan; pub const RawRange = struct {    start: usize,    end: usize,}; pub const skill_source_separator = " · "; pub fn skillTokenSourceLabel(token: SkillTokenSpan) ?[]const u8 {    const source = token.display_source orelse return null;    return skill_runtime.skillSourceShortLabel(source);} fn skillTokenVisibleWidth(token: SkillTokenSpan) usize {    var width = display_width.visibleWidth(token.name);    if (skillTokenSourceLabel(token)) |label| {        width +|= display_width.visibleWidth(skill_source_separator);        width +|= display_width.visibleWidth(label);    }    return width;} pub const ImageTokenSpan = entity_spans.ImageTokenSpan; pub const Source = struct {    input: []const u8,    cursor: usize,    terminal_cols: u16,    /// Production invariant: unique positive attachment IDs.    images: []const types.ImageAttachment = &.{},    /// Production invariant: sorted by span start and non-overlapping.    pasted_blocks: []const paste_blocks.PastedBlock = &.{},    /// Production invariant: sorted by raw_start and non-overlapping.    image_tokens: []const ImageTokenSpan = &.{},    /// Production invariant: sorted by raw_start and non-overlapping.    skill_tokens: []const SkillTokenSpan = &.{},    selection: ?RawRange = null,}; pub const CursorPoint = struct {    raw_offset: usize,    row_index: usize,    content_column: usize,}; pub const Unit = struct {    raw_start: usize,    raw_end: usize,    row_index: usize,    content_column: usize,    cell_width: usize,    kind: UnitKind,}; pub const Row = struct {    index: usize,    raw_start: usize,    raw_end: usize,    break_kind: BreakKind,    prefix_cell_width: usize,    content_width: usize,    first_cursor_offset: usize,    last_cursor_offset: usize,}; pub const Summary = struct {    total_rows: usize,    cursor: CursorPoint,    anchor: ?CursorPoint,}; pub const VisibleWindow = struct {    first_row: usize,    row_count: usize,}; pub const Event = union(enum) {    unit: Unit,    row_end: Row,}; const RawUnit = struct {    raw_start: usize,    raw_end: usize,    cell_width: usize,    kind: UnitKind,}; pub const Iterator = struct {    source: Source,    i: usize = 0,    row_index: usize = 0,    row_start: usize = 0,    content_column: usize = 0,    last_cursor_offset: usize = 0,    previous_cursor_offset: usize = 0,    force_next_positive_wrap: bool = false,    at_word_start: bool = true,    done: bool = false,     pub fn next(self: *Iterator) ?Event {        if (self.done) return null;         if (self.i >= self.source.input.len) {            return self.finishRow(.input_end, self.source.input.len);        }         if (self.source.input[self.i] == '\n') {            return self.finishRow(.hard_newline, self.i);        }         const raw = self.peekRawUnit();        if (self.shouldBreakBefore(raw)) {            return self.finishRow(.soft_wrap, raw.raw_start);        }         return self.consumeRawUnit(raw);    }     /// Decides whether the current row soft-wraps before `raw`. Spaces never    /// wrap; they hang past the right margin (painters clip them) so    /// continuation rows start at the word itself, never a leading space.    fn shouldBreakBefore(self: *const Iterator, raw: RawUnit) bool {        if (raw.kind == .text and self.source.input[raw.raw_start] == ' ') return false;        if (self.shouldSoftWrapWord(raw)) return true;        return raw.cell_width > 0 and self.shouldSoftWrap(raw.cell_width);    }     fn finishRow(self: *Iterator, break_kind: BreakKind, raw_end: usize) Event {        const prefix = inputPrefix(self.row_index);        const last_cursor_offset = switch (break_kind) {            .soft_wrap => if (self.last_cursor_offset == raw_end) self.previous_cursor_offset else self.last_cursor_offset,            .hard_newline, .input_end => raw_end,        };        const row = Row{            .index = self.row_index,            .raw_start = self.row_start,            .raw_end = raw_end,            .break_kind = break_kind,            .prefix_cell_width = prefix.cell_width,            .content_width = self.content_column,            .first_cursor_offset = self.row_start,            .last_cursor_offset = last_cursor_offset,        };         switch (break_kind) {            .input_end => self.done = true,            .hard_newline => self.startNextRow(raw_end + 1),            .soft_wrap => self.startNextRow(raw_end),        }         return .{ .row_end = row };    }     fn startNextRow(self: *Iterator, raw_start: usize) void {        self.i = raw_start;        self.row_index += 1;        self.row_start = raw_start;        self.content_column = 0;        self.last_cursor_offset = raw_start;        self.previous_cursor_offset = raw_start;        self.force_next_positive_wrap = false;        self.at_word_start = true;    }     fn peekRawUnit(self: *const Iterator) RawUnit {        const input = self.source.input;        const start = self.i;        if (paste_blocks.registeredPlaceholderSpanStartingAt(            input,            start,            self.source.pasted_blocks,        )) |placeholder| {            return .{                .raw_start = placeholder.start,                .raw_end = placeholder.end,                .cell_width = display_width.visibleWidth(                    input[placeholder.start..placeholder.end],                ),                .kind = .paste_placeholder,            };        }         if (skillTokenStartingAt(self.source.skill_tokens, start, input.len)) |token_index| {            const token = self.source.skill_tokens[token_index];            return .{                .raw_start = token.raw_start,                .raw_end = token.raw_end,                .cell_width = skillTokenVisibleWidth(token),                .kind = .{ .skill_token = token_index },            };        }         if (input[start] == '\t') {            return .{                .raw_start = start,                .raw_end = start + 1,                .cell_width = tabAdvance(inputPrefix(self.row_index).cell_width, self.content_column, self.source.terminal_cols),                .kind = .tab,            };        }         if (imageTokenStartingAt(self.source, start)) |token| {            const attachment_index = image_attachments.findImageIndexById(                self.source.images,                token.id,            ).?;            return .{                .raw_start = token.span.raw_start,                .raw_end = token.span.raw_end,                .cell_width = image_attachments.imageBadgeVisibleWidth(token.id),                .kind = .{ .image_badge = .{ .attachment_index = attachment_index } },            };        }         const display_unit = display_width.displayUnitAt(input, start);        return .{            .raw_start = start,            .raw_end = start + display_unit.byte_len,            .cell_width = display_unit.cell_width,            .kind = .text,        };    }     fn consumeRawUnit(self: *Iterator, raw: RawUnit) Event {        const unit = Unit{            .raw_start = raw.raw_start,            .raw_end = raw.raw_end,            .row_index = self.row_index,            .content_column = self.content_column,            .cell_width = raw.cell_width,            .kind = raw.kind,        };         self.i = raw.raw_end;        self.previous_cursor_offset = self.last_cursor_offset;        self.last_cursor_offset = raw.raw_end;        self.at_word_start = switch (raw.kind) {            .text => isWordBreakByte(self.source.input[raw.raw_start]),            .tab, .paste_placeholder, .skill_token, .image_badge => true,        };        const available = self.availableContentCells();        const oversized_empty_row = available > 0 and self.content_column == 0 and raw.cell_width > available;        self.content_column +|= raw.cell_width;        if (oversized_empty_row) {            self.force_next_positive_wrap = true;        }         return .{ .unit = unit };    }     /// Word-aware wrapping: when a word starts near the right margin and    /// does not fit in the remaining cells of the current row, but would fit    /// on a fresh continuation row, wrap the whole word instead of splitting    /// it per character. Words wider than a full row still fall back to the    /// per-character soft wrap.    fn shouldSoftWrapWord(self: *const Iterator, raw: RawUnit) bool {        if (raw.kind != .text) return false;        if (!self.at_word_start) return false;        if (self.content_column == 0) return false;        if (self.force_next_positive_wrap) return false;         const available = self.availableContentCells();        if (self.content_column >= available) return false;         const remaining = available - self.content_column;        const next_available = nextRowAvailableCells(self.row_index, self.source.terminal_cols);        if (next_available == 0) return false;         const word_width = self.measureWordWidth(raw.raw_start, next_available);        return word_width > remaining and word_width <= next_available;    }     /// Measures the cell width of the word starting at `start`, stopping at    /// word breaks, atomic units, or once the width exceeds `cap`.    fn measureWordWidth(self: *const Iterator, start: usize, cap: usize) usize {        const input = self.source.input;        var width: usize = 0;        var i = start;        while (i < input.len and !self.wordEndsAt(i)) {            const display_unit = display_width.displayUnitAt(input, i);            width +|= display_unit.cell_width;            if (width > cap) break;            i += display_unit.byte_len;        }        return width;    }     /// A word ends at a break byte or at the start of an atomic unit    /// (paste, skill, or image), mirroring peekRawUnit's boundaries.    fn wordEndsAt(self: *const Iterator, i: usize) bool {        const input = self.source.input;        if (isWordBreakByte(input[i])) return true;        if (paste_blocks.registeredPlaceholderSpanStartingAt(            input,            i,            self.source.pasted_blocks,        ) != null) return true;        if (skillTokenStartingAt(self.source.skill_tokens, i, input.len) != null) return true;        if (imageTokenStartingAt(self.source, i) != null) return true;        return false;    }     fn shouldSoftWrap(self: *const Iterator, cell_width: usize) bool {        const available = self.availableContentCells();        if (available == 0) return false;        if (self.force_next_positive_wrap) return true;        if (self.content_column == 0) return false;        if (self.content_column >= available) return true;        if (cell_width > std.math.maxInt(u16)) return true;         const start_column = inputPrefix(self.row_index).cell_width + self.content_column + 1;        return display_width.shouldWrapAt(@intCast(start_column), @intCast(cell_width), self.source.terminal_cols);    }     fn availableContentCells(self: *const Iterator) usize {        const cols: usize = self.source.terminal_cols;        const prefix_width = inputPrefix(self.row_index).cell_width;        return if (cols > prefix_width) cols - prefix_width else 0;    }}; fn isWordBreakByte(byte: u8) bool {    return byte == ' ' or byte == '\t' or byte == '\n';} fn nextRowAvailableCells(current_row_index: usize, terminal_cols: u16) usize {    const cols: usize = terminal_cols;    const prefix_width = inputPrefix(current_row_index + 1).cell_width;    return if (cols > prefix_width) cols - prefix_width else 0;} fn skillTokenStartingAt(tokens: []const SkillTokenSpan, raw_start: usize, input_len: usize) ?usize {    for (tokens, 0..) |token, index| {        if (token.raw_start == raw_start and token.raw_end > token.raw_start and token.raw_end <= input_len) return index;        if (token.raw_start > raw_start) return null;    }    return null;} fn imageTokenStartingAt(source: Source, raw_start: usize) ?ImageTokenSpan {    for (source.image_tokens) |token| {        if (token.span.raw_start < raw_start) continue;        if (token.span.raw_start > raw_start) return null;        if (!token.span.isValid(source.input.len)) return null;        const match = image_attachments.matchImagePlaceholder(            source.input,            token.span.raw_start,        ) orelse return null;        if (match.id != token.id or token.span.raw_start + match.length != token.span.raw_end) {            return null;        }        if (image_attachments.findImageIndexById(source.images, token.id) == null) return null;        return token;    }    return null;} pub const VerticalScan = struct {    target: ?CursorPoint,    preferred_column: usize,    total_rows: usize,    cursor_row: usize,}; pub fn iterator(source: Source) Iterator {    return .{        .source = source,        .last_cursor_offset = 0,        .previous_cursor_offset = 0,    };} pub fn summarize(source: Source, raw_anchor: ?usize) Summary {    return .{        .total_rows = countRows(source),        .cursor = cursorPointAt(source, source.cursor),        .anchor = if (raw_anchor) |anchor| cursorPointAt(source, anchor) else null,    };} fn pointAtUnitStart(unit: Unit) CursorPoint {    return .{ .raw_offset = unit.raw_start, .row_index = unit.row_index, .content_column = unit.content_column };} fn pointAfterUnit(unit: Unit) CursorPoint {    return .{        .raw_offset = unit.raw_end,        .row_index = unit.row_index,        .content_column = unit.content_column +| unit.cell_width,    };} fn pointAtRowEnd(row: Row) CursorPoint {    return .{ .raw_offset = row.raw_end, .row_index = row.index, .content_column = row.content_width };} /// Offsets inside a unit snap to the unit start.fn pointWithinUnit(unit: Unit, target: usize) ?CursorPoint {    if (target == unit.raw_start or (target > unit.raw_start and target < unit.raw_end)) {        return pointAtUnitStart(unit);    }    return null;} /// Soft-wrap boundary offsets belong to the following row.fn pendingResolvesAt(point: CursorPoint, row: Row) bool {    return point.raw_offset < row.raw_end or row.break_kind != .soft_wrap;} pub fn cursorPointAt(source: Source, raw_offset: usize) CursorPoint {    const target = @min(raw_offset, source.input.len);    var it = iterator(source);    var pending_after: ?CursorPoint = null;     while (it.next()) |event| switch (event) {        .unit => |unit| {            if (pointWithinUnit(unit, target)) |point| return point;            if (target == unit.raw_end) pending_after = pointAfterUnit(unit);        },        .row_end => |row| {            if (target == row.raw_end) {                switch (row.break_kind) {                    .soft_wrap => pending_after = null,                    .hard_newline, .input_end => return pointAtRowEnd(row),                }            } else if (pending_after) |point| {                if (pendingResolvesAt(point, row)) return point;            }        },    };     return .{ .raw_offset = target, .row_index = 0, .content_column = 0 };} pub fn cursorPointAtPosition(    source: Source,    row_index: usize,    content_column: usize,) ?CursorPoint {    var it = iterator(source);    var target = TargetBuilder.init(content_column);     while (it.next()) |event| switch (event) {        .unit => |unit| {            if (unit.row_index == row_index) {                target.consider(pointAtUnitStart(unit));            } else if (unit.row_index > row_index) {                return null;            }        },        .row_end => |row| {            if (row.index < row_index) continue;            if (row.index > row_index) return null;            if (row.break_kind != .soft_wrap) {                target.consider(pointAtRowEnd(row));            }            return target.result();        },    };     return null;} /// Finding the cursor freezes the preferred column and arms the target:/// up resolves from the previous row, down collects from the next row.const ScanState = struct {    direction: Direction,    preferred_column: ?usize,    preferred: usize,    cursor_found: bool = false,    cursor_row: usize = 0,    target: ?CursorPoint = null,    target_row: ?usize = null,

Lines 1–500 of 1367.