4250 lines · 150.1 KB · Zig
RawDownload
// Host contract:// - fields: shadow_vt, transcript, entries, layout, viewport_top_row,//   owned_top_row, pending_scroll_compact,//   min_visible_viewport_rows, reflow_clear_guard_rows,//   viewport_clear_pending,//   replaceable_last_line, replaceable_row, replaceable_start,//   last_rendered_cols, last_paint_bottom_reserved_rows,//   last_visible_transcript_*, last_viewport_selection,//   paint_trace, has_painted_transcript, resize_bottom_anchor_pending,//   footer_viewport, full_transcript, folded_command_blocks// - methods: ensurePaintReservation, beginPaint, endPaint,//   resetCursorTracking, advanceCursor,//   compactAfterCollapsedResize, footerTopRowForExtra const std = @import("std");const debug_trace = @import("../../core/shared/debug_trace.zig");const types = @import("../../core/shared/types.zig");const command_output_content = @import("../../core/tooling/command_output_content.zig");const render_engine = @import("../render_engine.zig");const source_preparation = @import("source_preparation.zig");const transcript_release = @import("../../core/output/transcript_release.zig");const transcript_writer = @import("writer.zig");const vt_emulator = @import("../../core/terminal/engine.zig"); const Allocator = std.mem.Allocator;const Metrics = types.Metrics;const frame_surface = render_engine.frame_surface;const paint_plan = render_engine.paint_plan;const transcript_blocks = render_engine.transcript_blocks;const viewport_selection = render_engine.viewport_selection; const HardLineStarts = viewport_selection.HardLineStarts;const LineRef = viewport_selection.LineRef;const TranscriptBuffer = viewport_selection.TranscriptBuffer;const TranscriptRef = viewport_selection.TranscriptRef;const VisibleTranscriptLine = viewport_selection.VisibleTranscriptLine;const VisibleTranscriptBatch = viewport_selection.VisibleTranscriptBatch;const ViewportSelection = viewport_selection.ViewportSelection;const buildHardLineStarts = viewport_selection.buildHardLineStarts;const hardLineRefAt = viewport_selection.hardLineRefAt;const selectedSourceLineIndex = viewport_selection.selectedSourceLineIndex;const visibleFromTranscript = viewport_selection.visibleFromTranscript;const buildViewportSelection = viewport_selection.buildViewportSelection;const isVisuallyBlankLine = transcript_blocks.isVisuallyBlankLine;const nextCursorColForLine = transcript_blocks.nextCursorColForLine;const skipVisualRowsInLine = transcript_blocks.skipVisualRowsInLine;const stripTrailingNewline = transcript_blocks.stripTrailingNewline;const visualRowsForLine = transcript_blocks.visualRowsForLine; const blank_transcript_row_bytes = "\x1b[2K"; const ResizeHistoryPolicy = enum {    apply,    skip,}; fn rowForVisibleLineIndex(    selection: ViewportSelection,    line_visual_rows: []const u16,    target_line: usize,    max_row: u16,) ?u16 {    if (target_line >= line_visual_rows.len) return null;     if (selection.split_active) {        const prefix_lines = selection.split_prefix_lines;        const suffix_start = selection.split_suffix_start_line;        var row = selection.top_row;        var line_index: usize = 0;        const target_visible = if (target_line < prefix_lines)            target_line        else blk: {            if (target_line < suffix_start) return null;            while (line_index < prefix_lines) : (line_index += 1) {                row +|= line_visual_rows[line_index];            }            line_index = suffix_start;            break :blk target_line;        };         while (line_index < target_visible) : (line_index += 1) {            row +|= line_visual_rows[line_index];        }        if (row > max_row) return null;        return row;    }     if (target_line < selection.start_line) return null;     var row = selection.top_row;    var line_index = selection.start_line;    var skip_rows = selection.partial_skip_rows;    while (line_index < target_line) : (line_index += 1) {        const rows = line_visual_rows[line_index];        if (skip_rows >= rows) {            skip_rows -= rows;            continue;        }        row +|= rows - skip_rows;        skip_rows = 0;    }    if (row > max_row) return null;    return row;} const RenderedTranscriptRow = struct {    row: u16,    line_index: usize,    bytes: []const u8,    rows_painted: u16,    partial_skip_rows: u16,    folded: bool,    blank: bool,}; const RenderedTranscriptRows = struct {    rows: std.ArrayList(RenderedTranscriptRow) = .empty,    owned_bytes: std.ArrayList([]u8) = .empty,    first_row: u16 = 0,    last_visible_line: []const u8 = "",    last_visible_row: u16 = 0,    last_visible_row_blank: bool = false,    last_line_is_folded: bool = false,    replaceable_start_row: u16 = 0,    replaceable_line_index: usize = 0,    painted_rows: u16 = 0,     fn deinit(self: *RenderedTranscriptRows, alloc: Allocator) void {        for (self.owned_bytes.items) |bytes| alloc.free(bytes);        self.owned_bytes.deinit(alloc);        self.rows.deinit(alloc);    }     fn rememberOwnedBytes(self: *RenderedTranscriptRows, alloc: Allocator, bytes: []u8) ![]const u8 {        errdefer alloc.free(bytes);        try self.owned_bytes.append(alloc, bytes);        return bytes;    }}; fn applyRenderedRowsToSelection(selection: *ViewportSelection, rendered_rows: *const RenderedTranscriptRows) void {    selection.last_visible_row = rendered_rows.last_visible_row;    selection.last_visible_row_blank = rendered_rows.last_visible_row_blank;    selection.replaceable_start_row = rendered_rows.replaceable_start_row;    if (rendered_rows.last_visible_row > 0) {        selection.bottom_row = rendered_rows.last_visible_row;    }} const ViewportCursorResult = struct {    cursor_row: u16,    cursor_col: u16,    replaceable_row: u16,}; const TranscriptPreparationSource = source_preparation.TranscriptPreparationSource; pub const MeasuredHistoryEndpoint = struct {    span_start_flow_offset: usize,    flow_offset: usize,    hard_row_cols: u16,}; pub const MeasuredHistoryOrigin = struct {    source_cols: u16,    source_visual_offset: u32,    endpoint: ?MeasuredHistoryEndpoint = null,}; pub const MeasuredHistoryOriginProjection = struct {    origin: MeasuredHistoryOrigin,    current_cols: u16,}; pub const MeasuredResizeTargetProvenance = enum {    none,    stable,    recovering,}; fn renderTranscriptRows(    self: anytype,    alloc: Allocator,    batch: VisibleTranscriptBatch,    line_visual_rows: []const u16,    total_lines: usize,    start_line: usize,    partial_skip_rows: u16,    top_row: u16,    bottom_row: u16,    max_visual_rows: ?u16,    projection_resume_bytes: []const u8,    projection_start_flow_offset: ?usize,) !RenderedTranscriptRows {    var rendered: RenderedTranscriptRows = .{        .first_row = top_row,        .replaceable_start_row = top_row,        .replaceable_line_index = total_lines,    };    errdefer rendered.deinit(alloc);     var current_row: u32 = top_row;    var line_index: usize = start_line;    var remaining_visual_rows = max_visual_rows;    if (remaining_visual_rows == null and projection_start_flow_offset != null) {        remaining_visual_rows = bottom_row - top_row + 1;    }    while (line_index < total_lines and        current_row <= @as(u32, bottom_row) and        (remaining_visual_rows == null or remaining_visual_rows.? > 0)) : (line_index += 1)    {        const vis = batch.lines[line_index];        const is_partial_tail = line_index == start_line and partial_skip_rows > 0;        const line_rows = line_visual_rows[line_index];        std.debug.assert(!is_partial_tail or partial_skip_rows < line_rows);        const exact_tail = if (line_index == start_line) switch (vis) {            .transcript => |line| projectionTailFromFlowOffset(                batch,                line.ref.ref,                projection_start_flow_offset,            ),            .folded_line => null,        } else null;        const available_rows: u16 = if (exact_tail) |tail|            visualRowsForLine(tail, self.layout.cols)        else if (is_partial_tail)            line_rows - partial_skip_rows        else            line_rows;        const rows_painted = if (remaining_visual_rows) |remaining|            @min(available_rows, remaining)        else            available_rows;        const projection_truncates_line = rows_painted < available_rows;        const paint_row: u16 = @intCast(current_row);         switch (vis) {            .transcript => |line| {                const text = batch.resolve(vis);                const tail = exact_tail orelse                    transcriptRowBytes(self, text, is_partial_tail, partial_skip_rows);                const visible_bytes = if (projection_truncates_line)                    tail[0..skipVisualRowsInLine(tail, self.layout.cols, rows_painted)]                else                    tail;                const paint_bytes = if (visible_bytes.len == 0)                    blank_transcript_row_bytes                else if (isVisuallyBlankLine(visible_bytes)) blk: {                    const materialized = try std.mem.concat(                        alloc,                        u8,                        &.{ visible_bytes, blank_transcript_row_bytes },                    );                    break :blk try rendered.rememberOwnedBytes(alloc, materialized);                } else visible_bytes;                var emitted_bytes = paint_bytes;                if (line_index == start_line and projection_resume_bytes.len > 0) {                    const resumed = try alloc.alloc(                        u8,                        projection_resume_bytes.len + paint_bytes.len,                    );                    @memcpy(resumed[0..projection_resume_bytes.len], projection_resume_bytes);                    @memcpy(resumed[projection_resume_bytes.len..], paint_bytes);                    emitted_bytes = try rendered.rememberOwnedBytes(alloc, resumed);                }                const cursor_line = if (projection_truncates_line or exact_tail != null)                    visible_bytes                else                    text;                const blank = isVisuallyBlankLine(if (exact_tail != null)                    cursor_line                else                    text);                logRenderedTranscriptRow(paint_row, rows_painted, line_index, "transcript", if (is_partial_tail) partial_skip_rows else 0, text);                try rendered.rows.append(alloc, .{                    .row = paint_row,                    .line_index = line_index,                    .bytes = emitted_bytes,                    .rows_painted = rows_painted,                    .partial_skip_rows = if (is_partial_tail) partial_skip_rows else 0,                    .folded = false,                    .blank = blank,                });                if (self.replaceable_last_line and line.ref.ref.start <= self.replaceable_start) {                    rendered.replaceable_line_index = line_index;                }                rendered.last_visible_line = cursor_line;                rendered.last_line_is_folded = false;                rendered.last_visible_row_blank = blank;            },            .folded_line => |folded| {                var folded_bytes = try foldedLineBytes(alloc, folded.text, folded.stream);                var folded_bytes_needs_free = true;                errdefer if (folded_bytes_needs_free) alloc.free(folded_bytes);                const rows_touched = visualRowsForLine(folded_bytes, self.layout.cols);                if (rows_touched < rows_painted) {                    const padded = try padAnsiBytesToRows(alloc, folded_bytes, rows_painted - rows_touched);                    alloc.free(folded_bytes);                    folded_bytes = padded;                }                folded_bytes_needs_free = false;                const owned = try rendered.rememberOwnedBytes(alloc, folded_bytes);                const tail = if (is_partial_tail)                    foldedPartialTailBytes(self, owned, partial_skip_rows)                else                    owned;                const bytes = if (projection_truncates_line)                    tail[0..skipVisualRowsInLine(tail, self.layout.cols, rows_painted)]                else                    tail;                logRenderedTranscriptRow(                    paint_row,                    rows_painted,                    line_index,                    if (folded.stream == .stderr) "folded_stderr" else "folded_stdout",                    if (is_partial_tail) partial_skip_rows else 0,                    folded.text,                );                try rendered.rows.append(alloc, .{                    .row = paint_row,                    .line_index = line_index,                    .bytes = if (bytes.len == 0) blank_transcript_row_bytes else bytes,                    .rows_painted = rows_painted,                    .partial_skip_rows = if (is_partial_tail) partial_skip_rows else 0,                    .folded = true,                    .blank = false,                });                rendered.last_visible_line = if (projection_truncates_line) bytes else "";                rendered.last_line_is_folded = !projection_truncates_line;                rendered.last_visible_row_blank = false;            },        }         if (line_index == rendered.replaceable_line_index) {            rendered.replaceable_start_row = paint_row;        }        rendered.last_visible_row = @intCast(            current_row + @as(u32, rows_painted) - 1,        );        rendered.painted_rows += rows_painted;        current_row += @as(u32, rows_painted);        if (remaining_visual_rows) |*remaining| remaining.* -= rows_painted;    }     return rendered;} fn projectionTailFromFlowOffset(    batch: VisibleTranscriptBatch,    line: LineRef,    flow_offset: ?usize,) ?[]const u8 {    const offset = flow_offset orelse return null;    const start: usize = line.start;    const end = start + @as(usize, line.len);    if (offset < start or offset > end) return null;    return batch.transcript.bytes[offset..end];} fn transcriptRowBytes(self: anytype, text: []const u8, is_partial_tail: bool, partial_skip_rows: u16) []const u8 {    if (!is_partial_tail or text.len == 0) return text;     const offset = skipVisualRowsInLine(text, self.layout.cols, partial_skip_rows);    const tail_bytes: usize = if (offset < text.len) text.len - offset else 0;    debug_trace.logf(        "transcript.sgr_drop_partial_tail",        "active sgr before skip offset not walked, rows_skipped={d} cols={d} tail_bytes={d}",        .{ partial_skip_rows, self.layout.cols, tail_bytes },    );    return if (offset < text.len) text[offset..] else "";} fn foldedPartialTailBytes(self: anytype, bytes: []const u8, partial_skip_rows: u16) []const u8 {    if (partial_skip_rows == 0 or bytes.len == 0) return bytes;    const offset = skipVisualRowsInLine(bytes, self.layout.cols, partial_skip_rows);    return if (offset < bytes.len) bytes[offset..] else "";} fn foldedLineBytes(alloc: Allocator, text: []const u8, stream: command_output_content.Stream) ![]u8 {    const style = if (stream == .stderr) "\x1b[38;5;252m" else "\x1b[38;5;245m";    const trimmed = stripTrailingNewline(text);    const reset = "\x1b[0m";    const bytes = try alloc.alloc(u8, style.len + "│ ".len + trimmed.len + reset.len);    var offset: usize = 0;    @memcpy(bytes[offset .. offset + style.len], style);    offset += style.len;    @memcpy(bytes[offset .. offset + "│ ".len], "│ ");    offset += "│ ".len;    @memcpy(bytes[offset .. offset + trimmed.len], trimmed);    offset += trimmed.len;    @memcpy(bytes[offset .. offset + reset.len], reset);    return bytes;} fn padAnsiBytesToRows(alloc: Allocator, bytes: []const u8, missing_rows: u16) ![]u8 {    std.debug.assert(missing_rows > 0);    return std.fmt.allocPrint(alloc, "{s}\x1b[{d}B\x1b[2K", .{ bytes, missing_rows });} fn logRenderedTranscriptRow(row: u16, rows_painted: u16, line_index: usize, kind_name: []const u8, partial_skip_rows: u16, text: []const u8) void {    debug_trace.logf(        "paint.rows",        "transcript_line row={d} rows={d} line_index={d} kind={s} partial_skip={d} bytes={d} hash={x}",        .{            row,            rows_painted,            line_index,            kind_name,            partial_skip_rows,            text.len,            std.hash.Wyhash.hash(0, text),        },    );} fn calculateViewportCursor(    layout: types.Layout,    bottom_row: u16,    total_lines: usize,    start_line: usize,    repl_line_idx: usize,    replaceable_start_row: u16,    last_visible_line: []const u8,    last_visible_row: u16,    last_line_is_folded: bool,    top_row: u16,    transcript_ends_with_newline: bool,    replaceable_last_line: bool,    current_replaceable_row: u16,) ViewportCursorResult {    _ = bottom_row;    var cursor_row = top_row;    var cursor_col: u16 = 1;    var replaceable_row = current_replaceable_row;    if (total_lines == 0) return .{ .cursor_row = cursor_row, .cursor_col = cursor_col, .replaceable_row = replaceable_row };     const treat_last_row_as_terminated = last_line_is_folded or transcript_ends_with_newline;    if (treat_last_row_as_terminated and last_visible_row < layout.content_bottom) {        cursor_row = last_visible_row + 1;        cursor_col = 1;    } else {        cursor_row = last_visible_row;        cursor_col = nextCursorColForLine(last_visible_line, layout.cols);    }    if (cursor_row < 1) cursor_row = 1;    if (cursor_col < 1) cursor_col = 1;    if (replaceable_last_line) {        replaceable_row = if (repl_line_idx < start_line or repl_line_idx >= total_lines) top_row else replaceable_start_row;    }    return .{ .cursor_row = cursor_row, .cursor_col = cursor_col, .replaceable_row = replaceable_row };} fn recordPreFrameTranscriptInvalidation(    self: anytype,    reason: paint_plan.FrameInvalidationReason,    top: u16,    bottom: u16,) void {    if (top == 0 or bottom == 0 or bottom < top) return;    const Shell = @TypeOf(self.*);    if (comptime !@hasDecl(Shell, "recordFrameInvalidation")) return;    self.recordFrameInvalidation(.{ .reason = reason, .top = top, .bottom = bottom }) catch |err| {        debug_trace.logf(            "frame_plan",            "pre_frame_transcript_invalidation_drop reason={s} top={d} bottom={d} err={s}",            .{ @tagName(reason), top, bottom, @errorName(err) },        );    };} fn compactAfterCollapsedResize(self: anytype, metrics: *Metrics) !void {    _ = metrics;    if (!self.pending_scroll_compact) return;    self.pending_scroll_compact = false;    if (self.owned_top_row <= 1) return;     const scroll_rows = self.owned_top_row - 1;    debug_trace.logf("render", "viewport_scroll_compact rows={d} owned_top={d} layout={d}x{d}", .{ scroll_rows, self.owned_top_row, self.layout.cols, self.layout.rows });     self.owned_top_row = 1;    self.viewport_top_row = 1;    self.cursor_row = 1;    self.cursor_col = 1;    recordPreFrameTranscriptInvalidation(self, .viewport_reanchor, 1, self.layout.rows);    debug_trace.logf(        "scroll",        "viewport_scroll_compact_frame_deferred rows={d} layout={d}x{d}",        .{ scroll_rows, self.layout.cols, self.layout.rows },    );} fn visualOffsetForSelection(line_visual_rows: []const u16, selection: ViewportSelection) u32 {    var offset: u32 = 0;    const end = @min(selection.start_line, line_visual_rows.len);    for (line_visual_rows[0..end]) |rows| offset += rows;    return offset + selection.partial_skip_rows;} fn totalVisualRows(line_visual_rows: []const u16) u32 {    var total: u32 = 0;

Lines 1–500 of 4250.