Something went wrong. Try again.
My version of https://github.com/vercel-labs/fx
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347const 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 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,};
fn skillTokenVisibleWidth(token: SkillTokenSpan) usize { return display_width.visibleWidth(token.name);}
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, content_width: 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 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, .content_width = self.content_column, .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, target_builder: TargetBuilder = .{ .preferred_column = 0 },
fn init(direction: Direction, preferred_column: ?usize) ScanState { return .{ .direction = direction, .preferred_column = preferred_column, .preferred = preferred_column orelse 0, }; }
fn foundCursor( self: *ScanState, point: CursorPoint, previous_row_checkpoint: ?Iterator, previous_row_target: ?CursorPoint, ) void { self.cursor_found = true; self.cursor_row = point.row_index; if (self.preferred_column == null) self.preferred = point.content_column; switch (self.direction) { .up => if (point.row_index > 0) { self.target = if (self.preferred_column == null) targetPointInCheckpoint(previous_row_checkpoint.?, point.row_index - 1, self.preferred) else previous_row_target; }, .down => { self.target_row = point.row_index + 1; self.target_builder = TargetBuilder.init(self.preferred); }, } }
fn considerTargetUnit(self: *ScanState, unit: Unit) void { const row_index = self.target_row orelse return; if (row_index != unit.row_index) return; self.target_builder.consider(pointAtUnitStart(unit)); }
fn finishTargetRow(self: *ScanState, row: Row) void { const row_index = self.target_row orelse return; if (row_index != row.index) return; if (row.break_kind != .soft_wrap) self.target_builder.consider(pointAtRowEnd(row)); self.target = self.target_builder.result(); self.target_row = null; }};
pub fn scanAdjacentRow(source: Source, direction: Direction, preferred_column: ?usize) VerticalScan { const target_offset = @min(source.cursor, source.input.len); var it = iterator(source); var current_row_checkpoint = it; var previous_row_checkpoint: ?Iterator = null;
var total_rows: usize = 0; var pending_after: ?CursorPoint = null; var previous_row_target: ?CursorPoint = null; var scan = ScanState.init(direction, preferred_column); var row_builder = TargetBuilder.init(scan.preferred);
while (it.next()) |event| switch (event) { .unit => |unit| { if (preferred_column != null) row_builder.consider(pointAtUnitStart(unit)); scan.considerTargetUnit(unit); if (!scan.cursor_found) { if (pointWithinUnit(unit, target_offset)) |point| { scan.foundCursor(point, previous_row_checkpoint, previous_row_target); } else if (target_offset == unit.raw_end) { pending_after = pointAfterUnit(unit); } } }, .row_end => |row| { total_rows += 1;
if (preferred_column != null and row.break_kind != .soft_wrap) { row_builder.consider(pointAtRowEnd(row)); }
scan.finishTargetRow(row);
if (!scan.cursor_found) { if (target_offset == row.raw_end) { switch (row.break_kind) { .soft_wrap => pending_after = null, .hard_newline, .input_end => scan.foundCursor( pointAtRowEnd(row), previous_row_checkpoint, previous_row_target, ), } } else if (pending_after) |point| { if (pendingResolvesAt(point, row)) { scan.foundCursor(point, previous_row_checkpoint, previous_row_target); } } }
if (preferred_column != null) { previous_row_target = row_builder.result(); row_builder = TargetBuilder.init(scan.preferred); } previous_row_checkpoint = current_row_checkpoint; current_row_checkpoint = it; }, };
return .{ .target = scan.target, .preferred_column = scan.preferred, .total_rows = total_rows, .cursor_row = scan.cursor_row, };}
pub fn scanRowDelta( source: Source, direction: Direction, row_count: usize, preferred_column: ?usize,) VerticalScan { const summary = summarize(source, null); const preferred = preferred_column orelse summary.cursor.content_column; const target_row = switch (direction) { .up => summary.cursor.row_index -| row_count, .down => @min( summary.cursor.row_index +| row_count, summary.total_rows -| 1, ), }; return .{ .target = if (target_row == summary.cursor.row_index) null else cursorPointAtPosition(source, target_row, preferred), .preferred_column = preferred, .total_rows = summary.total_rows, .cursor_row = summary.cursor.row_index, };}
fn countRows(source: Source) usize { var it = iterator(source); var rows: usize = 0; while (it.next()) |event| switch (event) { .row_end => rows += 1, else => {}, }; return rows;}
const TargetBuilder = struct { preferred_column: usize, first: ?CursorPoint = null, best: ?CursorPoint = null,
fn init(preferred_column: usize) TargetBuilder { return .{ .preferred_column = preferred_column }; }
fn consider(self: *TargetBuilder, point: CursorPoint) void { if (self.first == null) self.first = point; if (point.content_column > self.preferred_column) return; if (self.best) |current| { if (point.content_column > current.content_column or (point.content_column == current.content_column and point.raw_offset > current.raw_offset)) { self.best = point; } } else { self.best = point; } }
fn result(self: TargetBuilder) ?CursorPoint { return self.best orelse self.first; }};
fn targetPointInCheckpoint(checkpoint: Iterator, row_index: usize, preferred_column: usize) ?CursorPoint { var it = checkpoint; var builder = TargetBuilder.init(preferred_column);
while (it.next()) |event| switch (event) { .unit => |unit| { if (unit.row_index != row_index) continue; builder.consider(pointAtUnitStart(unit)); }, .row_end => |row| { if (row.index != row_index) continue; if (row.break_kind != .soft_wrap) builder.consider(pointAtRowEnd(row)); return builder.result(); }, };
return builder.result();}
pub fn visibleWindow(cursor_row: usize, total_rows: usize, row_limit: usize) VisibleWindow { if (total_rows == 0) return .{ .first_row = 0, .row_count = 0 }; const count = @max(row_limit, 1); const clamped_cursor = @min(cursor_row, total_rows - 1); const first = (clamped_cursor + 1) -| count; return .{ .first_row = first, .row_count = @min(count, total_rows - first) };}
pub fn inputPrefix(row_index: usize) InputPrefix { return if (row_index == 0) .{ .bytes = "❯ ", .cell_width = 2 } else .{ .bytes = " ", .cell_width = 2 };}
pub fn terminalColumn(point: CursorPoint, terminal_cols: u16) u16 { if (terminal_cols == 0) return 1; return @intCast(@min( @as(usize, terminal_cols), inputPrefix(point.row_index).cell_width +| point.content_column +| 1, ));}
pub fn projectedAnchorColumn(summary: Summary, terminal_cols: u16) u16 { const anchor = summary.anchor orelse return 1; if (anchor.row_index == summary.cursor.row_index) return terminalColumn(anchor, terminal_cols); return terminalColumn(.{ .raw_offset = summary.cursor.raw_offset, .row_index = summary.cursor.row_index, .content_column = 0, }, terminal_cols);}
pub fn tabAdvance(prefix_cell_width: usize, content_column: usize, terminal_cols: u16) usize { if (terminal_cols == 0) return 0; const cols: usize = terminal_cols; const absolute_zero_based = prefix_cell_width +| content_column; const right_margin = cols - 1; const current_zero_based = @min(absolute_zero_based, right_margin); const current_col: u16 = @intCast(current_zero_based + 1); return display_width.nextTabStopColumn(current_col, terminal_cols) - current_col;}
fn testSource(input: []const u8, cursor: usize, cols: u16) Source { return .{ .input = input, .cursor = cursor, .terminal_cols = cols };}
fn expectPoint(point: CursorPoint, raw_offset: usize, row_index: usize, content_column: usize) !void { try std.testing.expectEqual(raw_offset, point.raw_offset); try std.testing.expectEqual(row_index, point.row_index); try std.testing.expectEqual(content_column, point.content_column);}
fn expectCursor(input: []const u8, raw_offset: usize, cols: u16, row_index: usize, content_column: usize) !void { try expectPoint(cursorPointAt(testSource(input, raw_offset, cols), raw_offset), raw_offset, row_index, content_column);}
fn rowAt(source: Source, index: usize) ?Row { var it = iterator(source); while (it.next()) |event| switch (event) { .row_end => |row| if (row.index == index) return row, else => {}, }; return null;}
test "visual layout exposes exact input prefixes and terminal projection" { const first = inputPrefix(0); const continuation = inputPrefix(1); try std.testing.expectEqualStrings("❯ ", first.bytes); try std.testing.expectEqual(@as(usize, 4), first.bytes.len); try std.testing.expectEqual(@as(usize, 2), first.cell_width); try std.testing.expectEqual(first.cell_width, display_width.visibleWidth(first.bytes)); try std.testing.expectEqualStrings(" ", continuation.bytes); try std.testing.expectEqual(@as(usize, 2), continuation.bytes.len); try std.testing.expectEqual(@as(usize, 2), continuation.cell_width); try std.testing.expectEqual(continuation.cell_width, display_width.visibleWidth(continuation.bytes));
try std.testing.expectEqual(@as(u16, 3), terminalColumn(.{ .raw_offset = 0, .row_index = 0, .content_column = 0 }, 80)); try std.testing.expectEqual(@as(u16, 3), terminalColumn(.{ .raw_offset = 0, .row_index = 1, .content_column = 0 }, 80)); try std.testing.expectEqual(@as(u16, 6), terminalColumn(.{ .raw_offset = 3, .row_index = 0, .content_column = 3 }, 80)); try std.testing.expectEqual(@as(u16, 1), terminalColumn(.{ .raw_offset = 3, .row_index = 0, .content_column = 3 }, 0));}
test "visual layout preserves empty input hard newlines and trailing empty rows" { const empty = summarize(testSource("", 0, 80), null); try std.testing.expectEqual(@as(usize, 1), empty.total_rows); try expectPoint(empty.cursor, 0, 0, 0); try std.testing.expectEqual(BreakKind.input_end, rowAt(testSource("", 0, 80), 0).?.break_kind);
try std.testing.expectEqual(@as(usize, 2), summarize(testSource("a\nb", 0, 80), null).total_rows); try expectCursor("a\nb", 1, 80, 0, 1); try expectCursor("a\nb", 2, 80, 1, 0);
const multi = summarize(testSource("a\n\nb\n", 5, 80), null); try std.testing.expectEqual(@as(usize, 4), multi.total_rows); try expectPoint(multi.cursor, 5, 3, 0);}
test "visual layout targets registered paste placeholders only at boundaries" { const input = "x\n[Pasted text #7, 1 line]"; const blocks = [_]paste_blocks.PastedBlock{.{ .id = 7, .text = @constCast("P" ** 1001), .line_count = 1, .span = .{ .raw_start = "x\n".len, .raw_end = input.len, }, }}; const scan = scanAdjacentRow(.{ .input = input, .cursor = "x".len, .terminal_cols = 80, .pasted_blocks = &blocks, }, .down, null);
try std.testing.expect(scan.target != null); try std.testing.expectEqual(@as(usize, "x\n".len), scan.target.?.raw_offset);}
test "visual layout assigns soft-wrap boundaries to the following row" { const source = testSource("abcd", 3, 5); const summary = summarize(source, null); try std.testing.expectEqual(@as(usize, 2), summary.total_rows); try expectPoint(summary.cursor, 3, 1, 0);
const first = rowAt(source, 0).?; try std.testing.expectEqual(BreakKind.soft_wrap, first.break_kind); try std.testing.expectEqual(@as(usize, 0), first.raw_start); try std.testing.expectEqual(@as(usize, 3), first.raw_end); try std.testing.expect(first.last_cursor_offset < first.raw_end);}
test "visual layout zero-capacity widths split only on hard newlines" { inline for (.{ @as(u16, 0), 1, 2 }) |cols| { const source = testSource("abc\ndef", "abc\ndef".len, cols); const summary = summarize(source, null); try std.testing.expectEqual(@as(usize, 2), summary.total_rows); try expectPoint(summary.cursor, "abc\ndef".len, 1, 3); try std.testing.expectEqual(BreakKind.hard_newline, rowAt(source, 0).?.break_kind); }}
test "visual layout handles wide runes oversized rows combining marks and invalid utf8" { try expectCursor("a界", 1, 4, 1, 0); try expectPoint(cursorPointAt(testSource("a界", 2, 4), 2), 1, 1, 0);
try std.testing.expectEqual(@as(usize, 2), summarize(testSource("界a", 0, 3), null).total_rows); inline for (.{ @as(u16, 0), 1, 2 }) |cols| { try std.testing.expectEqual(@as(usize, 1), summarize(testSource("界a", 0, cols), null).total_rows); }
const combining = "a\xcc\x81\nb"; try expectCursor(combining, "a\xcc\x81".len, 80, 0, 1); const down = scanAdjacentRow(testSource(combining, 0, 80), .down, null); try std.testing.expectEqual(@as(usize, 2), down.total_rows); try std.testing.expectEqual(@as(usize, 0), down.cursor_row); try expectPoint(down.target.?, "a\xcc\x81\n".len, 1, 0);
const combining_target = scanAdjacentRow(testSource("x\na\xcc\x81\nz", "x".len, 80), .down, null); try std.testing.expectEqual(@as(usize, 3), combining_target.total_rows); try expectPoint(combining_target.target.?, "x\na\xcc\x81".len, 1, 1);
try expectCursor("\xffx", 1, 80, 0, 1);
const invalid = "\xff\xfex"; var it = iterator(testSource(invalid, invalid.len, 80)); var invalid_units: usize = 0; while (it.next()) |event| switch (event) { .unit => |unit| { if (invalid_units < 2) { try std.testing.expectEqual(invalid_units, unit.raw_start); try std.testing.expectEqual(invalid_units + 1, unit.raw_end); try std.testing.expectEqual(invalid_units, unit.content_column); try std.testing.expectEqual(@as(usize, 1), unit.cell_width); } invalid_units += 1; }, else => {}, }; try std.testing.expectEqual(@as(usize, 3), invalid_units);}
test "visual layout keeps Unicode display units atomic at the right margin" { const emoji = "\u{1F44D}\u{1F3FD}"; const input = "a" ++ emoji ++ "b"; const source = testSource(input, 0, 5);
try std.testing.expectEqual(@as(usize, 2), summarize(source, null).total_rows); try expectPoint(cursorPointAt(source, 1), 1, 0, 1); try expectPoint(cursorPointAt(source, 2), 1, 0, 1); try expectPoint(cursorPointAt(source, 1 + emoji.len), 1 + emoji.len, 1, 0); try expectPoint(cursorPointAt(source, input.len), input.len, 1, 1);
var it = iterator(source); while (it.next()) |event| switch (event) { .unit => |unit| if (unit.raw_start == 1) { try std.testing.expectEqual(@as(usize, 1 + emoji.len), unit.raw_end); try std.testing.expectEqual(@as(usize, 2), unit.cell_width); try std.testing.expectEqual(UnitKind.text, unit.kind); return; }, else => {}, }; return error.TestExpectedEqual;}
test "visual layout tab units use absolute stops and strict following overflow" { try std.testing.expectEqual(@as(usize, 6), tabAdvance(2, 0, 10)); try std.testing.expectEqual(@as(usize, 1), tabAdvance(2, 6, 10)); try std.testing.expectEqual(@as(usize, 0), tabAdvance(2, 7, 10)); try std.testing.expectEqual(@as(usize, 0), tabAdvance(2, 0, 0));
try std.testing.expectEqual(@as(usize, 1), summarize(testSource("\tX", 0, 10), null).total_rows); try std.testing.expectEqual(@as(usize, 2), summarize(testSource("aaaaaa\t界", 0, 10), null).total_rows); try std.testing.expectEqual(@as(usize, 2), summarize(testSource("aaaaaa\tXY", 0, 10), null).total_rows); try expectCursor("\tX", 1, 10, 0, 6);}
test "visual layout treats only registered image placeholders as atomic badges" { const images = [_]types.ImageAttachment{ .{ .id = 5, .path = @constCast("/tmp/five.png"), .media_type = @constCast("image/png") }, }; const input = "[Image #5][Image #5] [Image #6] [Image #999999999999999999999999999999999999]"; const image_tokens = [_]ImageTokenSpan{.{ .id = 5, .span = .{ .raw_start = "[Image #5]".len, .raw_end = "[Image #5][Image #5]".len }, }}; const source = Source{ .input = input, .cursor = 3, .terminal_cols = 80, .images = &images, .image_tokens = &image_tokens, }; try expectPoint(cursorPointAt(source, 3), 3, 0, 3);
var it = iterator(source); var badges: usize = 0; while (it.next()) |event| switch (event) { .unit => |unit| switch (unit.kind) { .image_badge => |badge| { badges += 1; // Both placeholders name the same attachment, so both badges are identical. try std.testing.expectEqual(@as(usize, 0), badge.attachment_index); try std.testing.expectEqual(image_attachments.imageBadgeVisibleWidth(5), unit.cell_width); }, else => {}, }, else => {}, }; try std.testing.expectEqual(@as(usize, 1), badges);}
test "visual layout resolves each image placeholder to its own attachment by id" { const images = [_]types.ImageAttachment{ .{ .id = 2, .path = @constCast("/tmp/two.png"), .media_type = @constCast("image/png") }, .{ .id = 1, .path = @constCast("/tmp/one.png"), .media_type = @constCast("image/png") }, }; const image_tokens = [_]ImageTokenSpan{ .{ .id = 2, .span = .{ .raw_start = 0, .raw_end = "[Image #2]".len } }, .{ .id = 1, .span = .{ .raw_start = "[Image #2] ".len, .raw_end = "[Image #2] [Image #1]".len, }, }, }; const expected_ids = [_]usize{ 2, 1 }; var it = iterator(.{ .input = "[Image #2] [Image #1]", .cursor = 0, .terminal_cols = 80, .images = &images, .image_tokens = &image_tokens, }); var badges: usize = 0; while (it.next()) |event| switch (event) { .unit => |unit| switch (unit.kind) { .image_badge => |badge| { const id = expected_ids[badges]; try std.testing.expectEqual(id, images[badge.attachment_index].id); try std.testing.expectEqual(image_attachments.imageBadgeVisibleWidth(id), unit.cell_width); badges += 1; }, else => {}, }, else => {}, }; try std.testing.expectEqual(@as(usize, 2), badges);}
test "visual layout measures a badge from the attachment id, not its appearance ordinal" { const images = [_]types.ImageAttachment{ .{ .id = 10, .path = @constCast("/tmp/ten.png"), .media_type = @constCast("image/png") }, }; const image_tokens = [_]ImageTokenSpan{.{ .id = 10, .span = .{ .raw_start = 0, .raw_end = "[Image #10]".len }, }}; var it = iterator(.{ .input = "[Image #10]", .cursor = 0, .terminal_cols = 80, .images = &images, .image_tokens = &image_tokens, }); var badges: usize = 0; while (it.next()) |event| switch (event) { .unit => |unit| switch (unit.kind) { .image_badge => |badge| { badges += 1; try std.testing.expectEqual(@as(usize, 10), images[badge.attachment_index].id); try std.testing.expectEqual(image_attachments.imageBadgeVisibleWidth(10), unit.cell_width); }, else => {}, }, else => {}, }; try std.testing.expectEqual(@as(usize, 1), badges);}
test "visual layout exposes zero-cell tab targets without forcing soft wraps" { const at_margin = testSource("\tX", 0, 3); try std.testing.expectEqual(@as(usize, 1), summarize(at_margin, null).total_rows); try expectPoint(cursorPointAt(at_margin, 0), 0, 0, 0); try expectPoint(cursorPointAt(at_margin, 1), 1, 0, 0); try expectPoint(cursorPointAt(at_margin, 2), 2, 0, 1);
inline for (.{ @as(u16, 0), 1, 2 }) |cols| { const source = testSource("\tX", 2, cols); try std.testing.expectEqual(@as(usize, 1), summarize(source, null).total_rows); try expectPoint(cursorPointAt(source, 1), 1, 0, 0); try expectPoint(cursorPointAt(source, 2), 2, 0, 1); }}
test "visual layout treats unknown and overflowing image placeholders as literal text" { const images = [_]types.ImageAttachment{ .{ .id = 5, .path = @constCast("/tmp/five.png"), .media_type = @constCast("image/png") }, }; const unknown = Source{ .input = "[Image #6]", .cursor = "[Image #6]".len, .terminal_cols = 80, .images = &images }; try std.testing.expectEqual(@as(usize, 1), summarize(unknown, null).total_rows); try expectPoint(cursorPointAt(unknown, 1), 1, 0, 1); try expectPoint(cursorPointAt(unknown, unknown.input.len), unknown.input.len, 0, unknown.input.len);
const overflowing_input = "[Image #999999999999999999999999999999999999]"; const overflowing = Source{ .input = overflowing_input, .cursor = overflowing_input.len, .terminal_cols = 80, .images = &images }; try std.testing.expectEqual(@as(usize, 1), summarize(overflowing, null).total_rows); try expectPoint(cursorPointAt(overflowing, 1), 1, 0, 1);}
test "visual layout badge width follows multi-digit image ids" { const images = [_]types.ImageAttachment{ .{ .id = 1, .path = @constCast("/tmp/one.png"), .media_type = @constCast("image/png") }, .{ .id = 10, .path = @constCast("/tmp/ten.png"), .media_type = @constCast("image/png") }, .{ .id = 100, .path = @constCast("/tmp/hundred.png"), .media_type = @constCast("image/png") }, }; const input = "[Image #1][Image #10][Image #100]"; const image_tokens = [_]ImageTokenSpan{ .{ .id = 1, .span = .{ .raw_start = 0, .raw_end = "[Image #1]".len } }, .{ .id = 10, .span = .{ .raw_start = "[Image #1]".len, .raw_end = "[Image #1][Image #10]".len, }, }, .{ .id = 100, .span = .{ .raw_start = "[Image #1][Image #10]".len, .raw_end = input.len, }, }, }; const expected_ids = [_]usize{ 1, 10, 100 }; var it = iterator(.{ .input = input, .cursor = 0, .terminal_cols = 200, .images = &images, .image_tokens = &image_tokens, }); var badges: usize = 0; while (it.next()) |event| switch (event) { .unit => |unit| switch (unit.kind) { .image_badge => |badge| { const id = expected_ids[badges]; try std.testing.expectEqual(id, images[badge.attachment_index].id); try std.testing.expectEqual(image_attachments.imageBadgeVisibleWidth(id), unit.cell_width); badges += 1; }, else => {}, }, else => {}, }; try std.testing.expectEqual(@as(usize, 3), badges);}
test "visual layout oversized badges wrap only with positive capacity" { const images = [_]types.ImageAttachment{ .{ .id = 1, .path = @constCast("/tmp/one.png"), .media_type = @constCast("image/png") }, }; const input = "[Image #1]x"; const image_tokens = [_]ImageTokenSpan{.{ .id = 1, .span = .{ .raw_start = 0, .raw_end = "[Image #1]".len }, }}; const positive = Source{ .input = input, .cursor = input.len, .terminal_cols = 10, .images = &images, .image_tokens = &image_tokens, }; try std.testing.expectEqual(@as(usize, 2), summarize(positive, null).total_rows); try std.testing.expectEqual(BreakKind.soft_wrap, rowAt(positive, 0).?.break_kind); try expectPoint(cursorPointAt(positive, "[Image #1]".len), "[Image #1]".len, 1, 0);
inline for (.{ @as(u16, 0), 1, 2 }) |cols| { const clipped = Source{ .input = input, .cursor = input.len, .terminal_cols = cols, .images = &images, .image_tokens = &image_tokens, }; try std.testing.expectEqual(@as(usize, 1), summarize(clipped, null).total_rows); try expectPoint(cursorPointAt(clipped, "[Image #1]".len), "[Image #1]".len, 0, image_attachments.imageBadgeVisibleWidth(1)); }}
test "visual layout clamps cursor beyond input and short target rows" { try expectPoint(cursorPointAt(testSource("abc", 99, 80), 99), 3, 0, 3);
const short = scanAdjacentRow(testSource("abcdef\nx", "abcdef".len, 80), .down, null); try std.testing.expectEqual(@as(usize, 6), short.preferred_column); try expectPoint(short.target.?, "abcdef\nx".len, 1, 1);}
test "visual layout maps pointer cells to legal cursor boundaries" { const ascii = testSource("abc", 0, 80); try expectPoint(cursorPointAtPosition(ascii, 0, 0).?, 0, 0, 0); try expectPoint(cursorPointAtPosition(ascii, 0, 1).?, 1, 0, 1); try expectPoint(cursorPointAtPosition(ascii, 0, 99).?, 3, 0, 3); try std.testing.expect(cursorPointAtPosition(ascii, 1, 0) == null);
const hard = testSource("ab\nc", 0, 80); try expectPoint(cursorPointAtPosition(hard, 0, 99).?, 2, 0, 2); try expectPoint(cursorPointAtPosition(hard, 1, 0).?, 3, 1, 0);
const wrapped = testSource("abcdef", 0, 6); try expectPoint(cursorPointAtPosition(wrapped, 0, 99).?, 3, 0, 3); try expectPoint(cursorPointAtPosition(wrapped, 1, 0).?, 4, 1, 0);
const wide = testSource("a界b", 0, 80); try expectPoint(cursorPointAtPosition(wide, 0, 1).?, 1, 0, 1); try expectPoint(cursorPointAtPosition(wide, 0, 2).?, 1, 0, 1); try expectPoint(cursorPointAtPosition(wide, 0, 3).?, 4, 0, 3);}
test "visual layout pointer mapping keeps skill tokens atomic" { const token = SkillTokenSpan{ .raw_start = 0, .raw_end = "$review".len, .name = "review", .path = "/tmp/review/SKILL.md", }; const source = Source{ .input = "$review x", .cursor = 0, .terminal_cols = 80, .skill_tokens = &.{token}, };
try expectPoint(cursorPointAtPosition(source, 0, 3).?, 0, 0, 0); try expectPoint( cursorPointAtPosition(source, 0, display_width.visibleWidth(token.name)).?, "$review".len, 0, display_width.visibleWidth(token.name), );}
test "visual layout soft-wrap target end stays owned by the source row" { const scan = scanAdjacentRow(testSource("abcd", "abcd".len, 5), .up, 99); try std.testing.expectEqual(@as(usize, 2), scan.total_rows); try std.testing.expectEqual(@as(usize, 1), scan.cursor_row); try expectPoint(scan.target.?, 2, 0, 2);}
test "visual layout first vertical move captures source column with target" { const up = scanAdjacentRow(testSource("\nabcdef", "\nabcdef".len, 80), .up, null); try std.testing.expectEqual(@as(usize, 6), up.preferred_column); try expectPoint(up.target.?, 0, 0, 0);
const down = scanAdjacentRow(testSource("abcdef\n", "abcdef".len, 80), .down, null); try std.testing.expectEqual(@as(usize, 6), down.preferred_column); try expectPoint(down.target.?, "abcdef\n".len, 1, 0);}
test "visual layout target selection and vertical scan report complete row facts" { const hard = scanAdjacentRow(testSource("abc\nde", "abc\nde".len, 80), .up, null); try std.testing.expectEqual(@as(usize, 2), hard.total_rows); try std.testing.expectEqual(@as(usize, 1), hard.cursor_row); try expectPoint(hard.target.?, 2, 0, 2); try std.testing.expectEqual(hard.total_rows, summarize(testSource("abc\nde", "abc\nde".len, 80), null).total_rows);
const soft = scanAdjacentRow(testSource("abcdefghij", 1, 5), .down, null); try std.testing.expect(soft.target != null); try std.testing.expectEqual(@as(usize, 4), soft.total_rows); try std.testing.expectEqual(@as(usize, 0), soft.cursor_row); try std.testing.expectEqual(soft.total_rows, summarize(testSource("abcdefghij", 1, 5), null).total_rows);
const none = scanAdjacentRow(testSource("abc", 0, 80), .up, null); try std.testing.expect(none.target == null); try std.testing.expectEqual(@as(usize, 1), none.total_rows); try std.testing.expectEqual(@as(usize, 0), none.cursor_row);}
test "visual layout row delta reuses the measured row coordinate model" { const source = testSource("one\ntwo\nthree\nfour", 1, 80); const down = scanRowDelta(source, .down, 2, null); try std.testing.expectEqual(@as(usize, 4), down.total_rows); try std.testing.expectEqual(@as(usize, 0), down.cursor_row); try std.testing.expectEqual(@as(usize, "one\ntwo\n".len + 1), down.target.?.raw_offset); try std.testing.expectEqual(@as(usize, 1), down.preferred_column);
var bottom = source; bottom.cursor = bottom.input.len; const clamped = scanRowDelta(bottom, .down, 10, null); try std.testing.expect(clamped.target == null);}
test "visual layout projects picker anchors relative to the cursor row" { const same = Summary{ .total_rows = 1, .cursor = .{ .raw_offset = 10, .row_index = 0, .content_column = 10 }, .anchor = .{ .raw_offset = 10, .row_index = 0, .content_column = 10 }, }; try std.testing.expectEqual(@as(u16, 13), projectedAnchorColumn(same, 80));
const earlier = Summary{ .total_rows = 2, .cursor = .{ .raw_offset = 8, .row_index = 1, .content_column = 3 }, .anchor = .{ .raw_offset = 2, .row_index = 0, .content_column = 2 }, }; try std.testing.expectEqual(@as(u16, 3), projectedAnchorColumn(earlier, 80)); try std.testing.expectEqual(@as(u16, 1), projectedAnchorColumn(earlier, 0));}
test "visual layout visibleWindow is defensive and cursor-containing" { try std.testing.expectEqual(VisibleWindow{ .first_row = 0, .row_count = 0 }, visibleWindow(99, 0, 3)); try std.testing.expectEqual(VisibleWindow{ .first_row = 7, .row_count = 3 }, visibleWindow(9, 10, 3)); try std.testing.expectEqual(VisibleWindow{ .first_row = 7, .row_count = 3 }, visibleWindow(99, 10, 3)); try std.testing.expectEqual(VisibleWindow{ .first_row = 0, .row_count = 1 }, visibleWindow(0, 5, 0));
const max_rows = std.math.maxInt(usize); try std.testing.expectEqual( VisibleWindow{ .first_row = 0, .row_count = max_rows }, visibleWindow(max_rows, max_rows, max_rows), );}
test "visual layout wraps whole words at the soft margin" { // cols = 12, prefix = 2, so 10 content cells per row. // "hello brave world" splits per word: "hello " (6) + "brave" would end // past column 10, so "brave" moves whole to the next row, and "world" // (5 cells after "brave " = 6) wraps whole again. const input = "hello brave world"; const source = testSource(input, input.len, 12); try std.testing.expectEqual(@as(usize, 3), summarize(source, null).total_rows);
const first = rowAt(source, 0).?; try std.testing.expectEqual(BreakKind.soft_wrap, first.break_kind); try std.testing.expectEqualStrings("hello ", input[first.raw_start..first.raw_end]);
const second = rowAt(source, 1).?; try std.testing.expectEqualStrings("brave ", input[second.raw_start..second.raw_end]);
const third = rowAt(source, 2).?; try std.testing.expectEqualStrings("world", input[third.raw_start..third.raw_end]);
// The first character of the wrapped word lands at column 0 of row 1. try expectPoint(cursorPointAt(source, "hello ".len), "hello ".len, 1, 0);}
test "visual layout still splits words wider than a full row per character" { const input = "ab cdefghijklm"; const source = testSource(input, input.len, 12); // "cdefghijklm" is 11 cells wide, wider than the 10 available content // cells, so it must fall back to character splitting instead of leaving // the first row almost empty. try std.testing.expectEqual(@as(usize, 2), summarize(source, null).total_rows); const first = rowAt(source, 0).?; try std.testing.expectEqual(BreakKind.soft_wrap, first.break_kind); try std.testing.expectEqual(@as(usize, 10), first.content_width);}
test "visual layout hangs margin spaces so continuation rows never start with a space" { // cols = 12, prefix = 2, so 10 content cells. "abcdefghij" fills row 0 // exactly; the following space hangs past the margin instead of wrapping, // so row 1 starts at "kl" with no leading space. const input = "abcdefghij kl"; const source = testSource(input, input.len, 12); try std.testing.expectEqual(@as(usize, 2), summarize(source, null).total_rows);
const first = rowAt(source, 0).?; try std.testing.expectEqual(BreakKind.soft_wrap, first.break_kind); try std.testing.expectEqualStrings("abcdefghij ", input[first.raw_start..first.raw_end]);
const second = rowAt(source, 1).?; try std.testing.expectEqualStrings("kl", input[second.raw_start..second.raw_end]);
// Multiple margin spaces all hang on the previous row. const multi = "abcdefghij kl"; const multi_source = testSource(multi, multi.len, 12); try std.testing.expectEqual(@as(usize, 2), summarize(multi_source, null).total_rows); const multi_second = rowAt(multi_source, 1).?; try std.testing.expectEqualStrings("kl", multi[multi_second.raw_start..multi_second.raw_end]);}
test "visual layout word wrap keeps trailing space on the previous row" { // Typing "aaaa bbbb " at cols 12: after the wrap, continuing to type // starts words on the second row rather than mid-split. const input = "aaaa bbbbbb cc"; const source = testSource(input, input.len, 12); try std.testing.expectEqual(@as(usize, 2), summarize(source, null).total_rows); const first = rowAt(source, 0).?; try std.testing.expectEqualStrings("aaaa ", input[first.raw_start..first.raw_end]); const second = rowAt(source, 1).?; try std.testing.expectEqualStrings("bbbbbb cc", input[second.raw_start..second.raw_end]);}
test "visual layout handles direct-limit and longer restored input without allocation" { const direct = "x" ** 4096; const longer = "y" ** 5000; const direct_summary = summarize(testSource(direct, direct.len, 80), null); const longer_summary = summarize(testSource(longer, longer.len, 80), null); try std.testing.expect(direct_summary.total_rows > 1); try std.testing.expect(longer_summary.total_rows > direct_summary.total_rows); _ = visibleWindow(longer_summary.cursor.row_index, longer_summary.total_rows, 4); try std.testing.expectEqual(@as(usize, countRows(testSource(direct, direct.len, 80))), direct_summary.total_rows);}
test "skill token width ignores internal source metadata" { const unique = SkillTokenSpan{ .raw_start = 0, .raw_end = "$review".len, .name = "review", .path = "/tmp/review", }; const ambiguous = SkillTokenSpan{ .raw_start = 0, .raw_end = "$review".len, .name = "review", .path = "/tmp/review", .display_source = .workspace_codex, };
try std.testing.expectEqual(@as(usize, "review".len), skillTokenVisibleWidth(unique)); try std.testing.expectEqual(@as(usize, "review".len), skillTokenVisibleWidth(ambiguous));}