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. Load more
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,