From 9a256d34ebca317e3007adf71c2b94016059d4a3 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Wed, 30 Sep 2026 18:40:41 -0300 Subject: A selection deep in a long line costs a render what one at its start does: 5 MB in, /screen goes from 14.7 s to 5 ms Every render measured a selection's and the cursor's columns from the start of their line (lineDisplayOffset walked the whole prefix twice), so a selection near the end of a 5 MB line stalled each frame, 3 s at 1 MB in and 14.7 s at 5 MB. The cost grew with the offset, with Wrap on or off. Three changes make it independent of where the selection is: - display width adds (a tab is tab_width wherever it stands), so the offset between two columns is the width of the text between them, never the prefix; - the selection's rows are clamped to what is shown: a row wholly before the selection is skipped, a start before the row's first column starts there, and an end past its last column ends there; - a head or cursor whose column lies past its visible row (the last visible row of a line owns every column after its start) is not measured, since it is not drawn. Measured on a Debug build, 5 MB line: offset 10, 1 MB and 5 MB now take 11, 6 and 5 ms per /screen. A new perf gate case, deep-sel, renders with the selection 2 MB into a line: 83 us on Debug. The baselines were re-recorded for the new harness. Co-Authored-By: Claude Opus 5.5 --- src/File.zig | 15 ++++++++++---- src/body_layer.zig | 35 +++++++++++++++++++++++++++++++-- test/perf-baseline-gui-Debug.json | 2 +- test/perf-baseline-tty-Debug.json | 2 +- test/perf-baseline-tty-ReleaseFast.json | 2 +- test/perf.zig | 28 +++++++++++++++++++++++--- 6 files changed, 72 insertions(+), 12 deletions(-) diff --git a/src/File.zig b/src/File.zig index 78c84287..a89db613 100644 --- a/src/File.zig +++ b/src/File.zig @@ -352,11 +352,18 @@ pub fn byteAtDisplayFrom(line_text: []const u8, from_raw: usize, display_col: us return from +| rawAtDisplay(line_text[from..], display_col); } +/// The display columns from `from_raw` to `to_raw` on a line. Widths add +/// (a tab is `tab_width` wherever it stands), so only the text between +/// the two is measured, never the line from its start: a selection a +/// million bytes into a line draws as fast as one at its start. pub fn lineDisplayOffset(line_text: []const u8, from_raw: usize, to_raw: usize) i32 { - const from_display = rawDisplayCol(line_text, from_raw); - const to_display = rawDisplayCol(line_text, to_raw); - if (to_display >= from_display) return @intCast(to_display - from_display); - return -@as(i32, @intCast(from_display - to_display)); + const lo = @min(from_raw, to_raw); + const hi = @max(from_raw, to_raw); + const a = modal.graphemeStart(line_text, @min(lo, line_text.len)); + const b = modal.graphemeStart(line_text, @min(hi, line_text.len)); + const past = (hi -| line_text.len) - (lo -| line_text.len); + const width: usize = displayWidth(line_text[a..b]) +| past; + return if (to_raw >= from_raw) @intCast(width) else -@as(i32, @intCast(width)); } pub fn lineDisplayEndOffset(line_text: []const u8, from_raw: usize, at_raw: usize) i32 { diff --git a/src/body_layer.zig b/src/body_layer.zig index 8e7cd030..914694e7 100644 --- a/src/body_layer.zig +++ b/src/body_layer.zig @@ -83,6 +83,17 @@ fn paintTerminalSelection(p: *Pardes, s: *Surface, pane: *Pane, r: Rect, rows: [ } } +/// Whether `col` lies past the last column its visible row shows: the last +/// visible row of a line owns every column after its start (wrapRow), so a +/// head millions of bytes along a long line is on that row, off the view. +fn pastView(pane: *Pane, body: []const u8, line: i32, row: i32, at: i32, col: i32, tw: u16) bool { + const text = if (pane.file != null) panes.File.sourceLine(pane, line) else modal.lineSlice(body, @intCast(@max(0, row))); + const from: usize = @intCast(@max(0, at)); + const to: usize = @intCast(@max(0, col)); + if (from >= text.len) return to >= from +| tw; + return to >= from + panes.File.byteAtDisplay(text[from..], tw) and to < text.len; +} + fn paintSourceSelection(p: *Pardes, s: *Surface, pane: *Pane, r: Rect, rows: []const Pane.PointerRow, bg: [3]u8, fg: ?[3]u8) void { if (pane.isTerminal()) return paintTerminalSelection(p, s, pane, r, rows, bg, fg); const terminal_lines = if (pane.file == null) panes.terminal.cursorLines(p, pane) catch return else &.{}; @@ -371,14 +382,26 @@ pub fn renderBody(p: *Pardes, s: *Surface, arena: std.mem.Allocator, pane: *Pane const w = pane.wrapAt(vr); if (w.line < bnd.lo_row or w.line > bnd.hi_row) continue; const visible_line = modal.lineSlice(body, @intCast(vr)); - const cstart: i32 = if (w.line == bnd.lo_row) + // Only the visible columns are measured: a selection that + // starts before this row's first column starts at it, and + // one that ends past its last ends there. + const row_text = if (pane.file != null) panes.File.sourceLine(pane, w.line) else visible_line; + const row_at: usize = @intCast(@max(0, w.at)); + // The byte past the row's last column, walked from its first: + // bounded by the view, not the line. + // A line that ends inside the view has no column past it. + const shown_to = if (row_at >= row_text.len) row_text.len else row_at + panes.File.byteAtDisplay(row_text[row_at..], tw); + const past_view = if (shown_to >= row_text.len) std.math.maxInt(usize) else shown_to; + // A row that ends before the selection starts has none of it. + if (w.line == bnd.lo_row and @as(usize, @intCast(@max(0, bnd.lo_col))) >= past_view) continue; + const cstart: i32 = if (w.line == bnd.lo_row and bnd.lo_col > w.at) (if (pane.file != null) panes.File.displayOffset(pane, w.line, w.at, bnd.lo_col) else panes.File.lineDisplayOffset(visible_line, @intCast(@max(0, w.at)), @intCast(@max(0, bnd.lo_col)))) + vpfx else vpfx; - const cend: i32 = if (w.line == bnd.hi_row) + const cend: i32 = if (w.line == bnd.hi_row and @as(usize, @intCast(@max(0, bnd.hi_col))) < past_view) (if (pane.file != null) panes.File.displayEndOffset(pane, w.line, w.at, bnd.hi_col) else @@ -396,6 +419,10 @@ pub fn renderBody(p: *Pardes, s: *Surface, arena: std.mem.Allocator, pane: *Pane if (primary and !preview and !hover_only) continue; // the hardware cursor IS the primary's if (hover_only) continue; // quiet preview preserves the source ink const cw = pane.wrapRow(sr.row, sr.col); + // A head off the view is measured not at all: its row is not + // drawn, and its column may be millions of bytes along. + if (cw.row < 0 or cw.row + @as(i32, pane.tag_rows) >= @as(i32, body_bottom)) continue; + if (pastView(pane, body, sr.row, cw.row, cw.at, sr.col, tw)) continue; const crow = cw.row + @as(i32, pane.tag_rows); const ccol = (if (pane.file != null) panes.File.displayOffset(pane, sr.row, cw.at, sr.col) @@ -447,6 +474,9 @@ pub fn renderBody(p: *Pardes, s: *Surface, arena: std.mem.Allocator, pane: *Pane const ccol = if (pane.body.cur_pinned) pane.body.cur_col else @as(i32, @intCast(cur.x)); const cwp = pane.wrapRow(crow, ccol); const prow = cwp.row + @as(i32, pane.tag_rows); + // Off the view, the cursor is not measured (its column may be + // millions of bytes along a line whose start is shown). + if (cwp.row >= 0 and prow < body_bottom and !pastView(pane, body, crow, cwp.row, cwp.at, ccol, tw)) { // Files store source-byte columns; the Surface stores display // cells, so account for every expanded tab before the cursor. const cx = if (pane.file != null) @@ -461,6 +491,7 @@ pub fn renderBody(p: *Pardes, s: *Surface, arena: std.mem.Allocator, pane: *Pane ccol; if (prow >= pane.tag_rows and cx >= 0 and prow < body_bottom and cx < tw) s.cursor = .{ .x = tx + @as(u16, @intCast(cx)), .y = body_y + @as(u16, @intCast(prow - pane.tag_rows)), .bar = pane.body.mode == .insert }; + } } else if (panes.terminal.visibleCursor(pane)) |cur| { if (cur.y + pane.tag_rows < body_bottom and cur.x < tw) s.cursor = .{ .x = tx + cur.x, .y = body_y + cur.y }; diff --git a/test/perf-baseline-gui-Debug.json b/test/perf-baseline-gui-Debug.json index 8808dd55..ace9a967 100644 --- a/test/perf-baseline-gui-Debug.json +++ b/test/perf-baseline-gui-Debug.json @@ -1 +1 @@ -{"harness":"f420fe5db60d6e97","build":{"compiler":"0.16.0","target":"x86_64-linux.5.10...6.19-gnu.2.38","cpu":"x86_64","driver_optimize":"ReleaseFast","core_optimize":"Debug","c_optimize":"ReleaseFast","terminal_optimize":"ReleaseSafe","platform":"gui","grammars":"full","mupdf":true,"jpx":true,"tracy":false,"quic":false,"theme_animation":true,"terminal_simd":true,"prebuilt_shaders":false},"cols":120,"rows":40,"reps":10,"only":"medium","fixtures":[{"name":"small","lines":1000,"cols":60,"bytes":62694},{"name":"medium","lines":50000,"cols":60,"bytes":3201944},{"name":"large","lines":300000,"cols":60,"bytes":19464444},{"name":"longline","lines":400,"cols":8000,"bytes":3200400},{"name":"esp32p4","lines":12,"cols":240,"bytes":2892}],"cells":[{"op":"open","fixture":"medium","min_us":1740,"med_us":2009,"p90_us":2077,"max_us":2079},{"op":"render","fixture":"medium","min_us":76,"med_us":80,"p90_us":90,"max_us":101},{"op":"key-down","fixture":"medium","min_us":77,"med_us":81,"p90_us":94,"max_us":95},{"op":"key-right","fixture":"medium","min_us":77,"med_us":82,"p90_us":86,"max_us":89},{"op":"page-down","fixture":"medium","min_us":77,"med_us":78,"p90_us":86,"max_us":99},{"op":"wheel","fixture":"medium","min_us":76,"med_us":76,"p90_us":80,"max_us":89},{"op":"edit-char","fixture":"medium","min_us":924,"med_us":1330,"p90_us":1522,"max_us":1566},{"op":"body-2m","fixture":"medium","min_us":4888,"med_us":8435,"p90_us":10635,"max_us":11460},{"op":"body-appends","fixture":"medium","min_us":119102,"med_us":187377,"p90_us":230392,"max_us":245721}]} +{"harness":"e63a8b27a0bcb261","build":{"compiler":"0.16.0","target":"x86_64-linux.5.10...6.19-gnu.2.38","cpu":"x86_64","driver_optimize":"ReleaseFast","core_optimize":"Debug","c_optimize":"ReleaseFast","terminal_optimize":"ReleaseSafe","platform":"gui","grammars":"full","mupdf":true,"jpx":true,"tracy":false,"quic":false,"theme_animation":true,"terminal_simd":true,"prebuilt_shaders":false},"cols":120,"rows":40,"reps":10,"only":"medium","fixtures":[{"name":"small","lines":1000,"cols":60,"bytes":62694},{"name":"medium","lines":50000,"cols":60,"bytes":3201944},{"name":"large","lines":300000,"cols":60,"bytes":19464444},{"name":"longline","lines":400,"cols":8000,"bytes":3200400},{"name":"esp32p4","lines":12,"cols":240,"bytes":2892}],"cells":[{"op":"open","fixture":"medium","min_us":2100,"med_us":2437,"p90_us":2875,"max_us":3207},{"op":"render","fixture":"medium","min_us":82,"med_us":84,"p90_us":88,"max_us":89},{"op":"key-down","fixture":"medium","min_us":88,"med_us":95,"p90_us":102,"max_us":114},{"op":"key-right","fixture":"medium","min_us":84,"med_us":88,"p90_us":94,"max_us":105},{"op":"page-down","fixture":"medium","min_us":83,"med_us":86,"p90_us":88,"max_us":89},{"op":"wheel","fixture":"medium","min_us":82,"med_us":85,"p90_us":90,"max_us":99},{"op":"edit-char","fixture":"medium","min_us":1276,"med_us":1769,"p90_us":2239,"max_us":2754},{"op":"body-2m","fixture":"medium","min_us":5333,"med_us":9734,"p90_us":12257,"max_us":14666},{"op":"body-appends","fixture":"medium","min_us":135052,"med_us":262059,"p90_us":295829,"max_us":304576},{"op":"deep-sel","fixture":"medium","min_us":77,"med_us":79,"p90_us":83,"max_us":118}]} diff --git a/test/perf-baseline-tty-Debug.json b/test/perf-baseline-tty-Debug.json index ef42f0ad..6b5c0bd5 100644 --- a/test/perf-baseline-tty-Debug.json +++ b/test/perf-baseline-tty-Debug.json @@ -1 +1 @@ -{"harness":"f420fe5db60d6e97","build":{"compiler":"0.16.0","target":"x86_64-linux.5.10...6.19-gnu.2.38","cpu":"x86_64","driver_optimize":"ReleaseFast","core_optimize":"Debug","c_optimize":"ReleaseFast","terminal_optimize":"ReleaseSafe","platform":"tty","grammars":"full","mupdf":true,"jpx":true,"tracy":false,"quic":false,"theme_animation":true,"terminal_simd":true,"prebuilt_shaders":false},"cols":120,"rows":40,"reps":10,"only":"medium","fixtures":[{"name":"small","lines":1000,"cols":60,"bytes":62694},{"name":"medium","lines":50000,"cols":60,"bytes":3201944},{"name":"large","lines":300000,"cols":60,"bytes":19464444},{"name":"longline","lines":400,"cols":8000,"bytes":3200400},{"name":"esp32p4","lines":12,"cols":240,"bytes":2892}],"cells":[{"op":"open","fixture":"medium","min_us":1743,"med_us":2011,"p90_us":2152,"max_us":2217},{"op":"render","fixture":"medium","min_us":76,"med_us":79,"p90_us":80,"max_us":84},{"op":"key-down","fixture":"medium","min_us":77,"med_us":78,"p90_us":85,"max_us":89},{"op":"key-right","fixture":"medium","min_us":77,"med_us":80,"p90_us":83,"max_us":95},{"op":"page-down","fixture":"medium","min_us":78,"med_us":78,"p90_us":87,"max_us":95},{"op":"wheel","fixture":"medium","min_us":76,"med_us":80,"p90_us":88,"max_us":90},{"op":"edit-char","fixture":"medium","min_us":1122,"med_us":1501,"p90_us":1812,"max_us":2440},{"op":"body-2m","fixture":"medium","min_us":4949,"med_us":8504,"p90_us":10909,"max_us":11452},{"op":"body-appends","fixture":"medium","min_us":115605,"med_us":190785,"p90_us":226244,"max_us":245258}]} +{"harness":"e63a8b27a0bcb261","build":{"compiler":"0.16.0","target":"x86_64-linux.5.10...6.19-gnu.2.38","cpu":"x86_64","driver_optimize":"ReleaseFast","core_optimize":"Debug","c_optimize":"ReleaseFast","terminal_optimize":"ReleaseSafe","platform":"tty","grammars":"full","mupdf":true,"jpx":true,"tracy":false,"quic":false,"theme_animation":true,"terminal_simd":true,"prebuilt_shaders":false},"cols":120,"rows":40,"reps":10,"only":"medium","fixtures":[{"name":"small","lines":1000,"cols":60,"bytes":62694},{"name":"medium","lines":50000,"cols":60,"bytes":3201944},{"name":"large","lines":300000,"cols":60,"bytes":19464444},{"name":"longline","lines":400,"cols":8000,"bytes":3200400},{"name":"esp32p4","lines":12,"cols":240,"bytes":2892}],"cells":[{"op":"open","fixture":"medium","min_us":1830,"med_us":2222,"p90_us":2264,"max_us":2287},{"op":"render","fixture":"medium","min_us":78,"med_us":83,"p90_us":86,"max_us":94},{"op":"key-down","fixture":"medium","min_us":80,"med_us":84,"p90_us":89,"max_us":98},{"op":"key-right","fixture":"medium","min_us":78,"med_us":81,"p90_us":88,"max_us":96},{"op":"page-down","fixture":"medium","min_us":78,"med_us":82,"p90_us":89,"max_us":95},{"op":"wheel","fixture":"medium","min_us":77,"med_us":85,"p90_us":96,"max_us":109},{"op":"edit-char","fixture":"medium","min_us":1125,"med_us":1307,"p90_us":1558,"max_us":1584},{"op":"body-2m","fixture":"medium","min_us":5189,"med_us":9224,"p90_us":10859,"max_us":14741},{"op":"body-appends","fixture":"medium","min_us":108477,"med_us":198709,"p90_us":227756,"max_us":252672},{"op":"deep-sel","fixture":"medium","min_us":73,"med_us":77,"p90_us":80,"max_us":91}]} diff --git a/test/perf-baseline-tty-ReleaseFast.json b/test/perf-baseline-tty-ReleaseFast.json index 66421ba3..87a31754 100644 --- a/test/perf-baseline-tty-ReleaseFast.json +++ b/test/perf-baseline-tty-ReleaseFast.json @@ -1 +1 @@ -{"harness":"f420fe5db60d6e97","build":{"compiler":"0.16.0","target":"x86_64-linux.5.10...6.19-gnu.2.38","cpu":"x86_64","driver_optimize":"ReleaseFast","core_optimize":"ReleaseFast","c_optimize":"ReleaseFast","terminal_optimize":"ReleaseFast","platform":"tty","grammars":"full","mupdf":true,"jpx":true,"tracy":false,"quic":false,"theme_animation":true,"terminal_simd":true,"prebuilt_shaders":false},"cols":120,"rows":40,"reps":10,"only":"medium","fixtures":[{"name":"small","lines":1000,"cols":60,"bytes":62694},{"name":"medium","lines":50000,"cols":60,"bytes":3201944},{"name":"large","lines":300000,"cols":60,"bytes":19464444},{"name":"longline","lines":400,"cols":8000,"bytes":3200400},{"name":"esp32p4","lines":12,"cols":240,"bytes":2892}],"cells":[{"op":"open","fixture":"medium","min_us":1751,"med_us":2134,"p90_us":2183,"max_us":2415},{"op":"render","fixture":"medium","min_us":76,"med_us":77,"p90_us":91,"max_us":92},{"op":"key-down","fixture":"medium","min_us":77,"med_us":78,"p90_us":82,"max_us":83},{"op":"key-right","fixture":"medium","min_us":79,"med_us":85,"p90_us":100,"max_us":102},{"op":"page-down","fixture":"medium","min_us":78,"med_us":82,"p90_us":86,"max_us":95},{"op":"wheel","fixture":"medium","min_us":76,"med_us":79,"p90_us":83,"max_us":87},{"op":"edit-char","fixture":"medium","min_us":1063,"med_us":1290,"p90_us":1527,"max_us":1564},{"op":"body-2m","fixture":"medium","min_us":4950,"med_us":9414,"p90_us":10945,"max_us":13152},{"op":"body-appends","fixture":"medium","min_us":117839,"med_us":189253,"p90_us":232943,"max_us":247720}]} +{"harness":"e63a8b27a0bcb261","build":{"compiler":"0.16.0","target":"x86_64-linux.5.10...6.19-gnu.2.38","cpu":"x86_64","driver_optimize":"ReleaseFast","core_optimize":"ReleaseFast","c_optimize":"ReleaseFast","terminal_optimize":"ReleaseFast","platform":"tty","grammars":"full","mupdf":true,"jpx":true,"tracy":false,"quic":false,"theme_animation":true,"terminal_simd":true,"prebuilt_shaders":false},"cols":120,"rows":40,"reps":10,"only":"medium","fixtures":[{"name":"small","lines":1000,"cols":60,"bytes":62694},{"name":"medium","lines":50000,"cols":60,"bytes":3201944},{"name":"large","lines":300000,"cols":60,"bytes":19464444},{"name":"longline","lines":400,"cols":8000,"bytes":3200400},{"name":"esp32p4","lines":12,"cols":240,"bytes":2892}],"cells":[{"op":"open","fixture":"medium","min_us":2042,"med_us":2280,"p90_us":2451,"max_us":2478},{"op":"render","fixture":"medium","min_us":82,"med_us":84,"p90_us":87,"max_us":104},{"op":"key-down","fixture":"medium","min_us":83,"med_us":86,"p90_us":90,"max_us":93},{"op":"key-right","fixture":"medium","min_us":84,"med_us":87,"p90_us":91,"max_us":104},{"op":"page-down","fixture":"medium","min_us":84,"med_us":87,"p90_us":93,"max_us":96},{"op":"wheel","fixture":"medium","min_us":81,"med_us":85,"p90_us":89,"max_us":89},{"op":"edit-char","fixture":"medium","min_us":1309,"med_us":1613,"p90_us":1840,"max_us":2128},{"op":"body-2m","fixture":"medium","min_us":5386,"med_us":11064,"p90_us":11706,"max_us":12374},{"op":"body-appends","fixture":"medium","min_us":144772,"med_us":232307,"p90_us":273586,"max_us":281169},{"op":"deep-sel","fixture":"medium","min_us":79,"med_us":83,"p90_us":87,"max_us":130}]} diff --git a/test/perf.zig b/test/perf.zig index 95b55748..94076a25 100644 --- a/test/perf.zig +++ b/test/perf.zig @@ -53,6 +53,10 @@ const Op = enum { /// close, as a shell's `>>` loop makes: each costs its own bytes, not a /// pass over the whole body for its cursor, line index and saved hash. body_appends, + /// A render with the selection 2 MB into one line: only the columns + /// shown are measured, so it costs what a selection at the line's + /// start does (4.5 s a frame at 5 MB before). + deep_selection, fn label(o: Op) []const u8 { return switch (o) { @@ -65,6 +69,7 @@ const Op = enum { .edit_char => "edit-char", .body_write => "body-2m", .body_appends => "body-appends", + .deep_selection => "deep-sel", }; } }; @@ -83,6 +88,16 @@ fn bodyWrite(core: *pardes.Pardes, serial: u32) void { _ = core.serveFs(.{ .tag = 4, .op = .release, .node = body, .handle = h, .opened = true }); } +/// One line of 2 MB appended to the file, no newline in it. +fn longLine(core: *pardes.Pardes, serial: u32) void { + const Node = pardes.ctlfs.Node; + const body = Node.of(serial, .body); + @memset(&body_chunk, 'z'); + const h = core.serveFs(.{ .tag = 1, .op = .open, .node = body, .omode = 1 }).handle; + for (0..8) |_| _ = core.serveFs(.{ .tag = 2, .op = .write, .node = body, .handle = h, .data = &body_chunk }); + _ = core.serveFs(.{ .tag = 4, .op = .release, .node = body, .handle = h, .opened = true }); +} + const append_chunk_len = 8 * 1024; const append_count = 64; @@ -534,9 +549,16 @@ fn measure(op: Op, fx: Fixture, path: []const u8, expected_bytes: usize, reps: u core.update(.{ .key = .{ .cp = 'i', .text = "i" } }); pump(core); } + if (op == .deep_selection) { + longLine(core, pane.serial); + pump(core); + const len: u32 = @intCast(pane.file.?.content.len); + pardes.ctlfs.pane.setDot(pane, .{ .q0 = len - 10, .q1 = len - 5 }); + pump(core); + } for (0..warmup + reps) |n| { - seek(pane, fx, n); + if (op != .deep_selection) seek(pane, fx, n); _ = try frame(core); // settle the view at the new spot, untimed const before = .{ .row = pane.body.cur_row, .col = pane.body.cur_col, .scroll = pane.scroll(), .len = pane.file.?.content.len }; const rect = core.rects[id]; @@ -545,7 +567,7 @@ fn measure(op: Op, fx: Fixture, path: []const u8, expected_bytes: usize, reps: u const t0 = nowNs(); switch (op) { .open => unreachable, - .render => {}, + .render, .deep_selection => {}, .key_down => core.update(.{ .key = .{ .cp = 'j', .text = "j" } }), .key_right => core.update(.{ .key = .{ .cp = 'l', .text = "l" } }), .page_down => core.update(.{ .key = .{ .cp = pardes.Key.page_down } }), @@ -561,7 +583,7 @@ fn measure(op: Op, fx: Fixture, path: []const u8, expected_bytes: usize, reps: u .open => unreachable, .body_write => if (pane.file.?.content.len != before.len + 8 * body_chunk.len) return error.BodyWriteWasNotAppended, .body_appends => if (pane.file.?.content.len != before.len + append_count * append_chunk_len) return error.BodyAppendsWereNotAppended, - .render => if (pane.body.cur_row != before.row or pane.body.cur_col != before.col or pane.file.?.content.len != before.len) + .render, .deep_selection => if (pane.body.cur_row != before.row or pane.body.cur_col != before.col or pane.file.?.content.len != before.len) return error.RenderChangedDocument, .key_down => if (pane.body.cur_row != before.row + 1) return error.DownDidNotMove, .key_right => if (pane.body.cur_row != before.row or pane.body.cur_col != before.col + 1) return error.RightDidNotMove, -- cgit v1.3