Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions plugins/markdown/src/md/render_ast.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1949,8 +1949,16 @@ fn renderTopLevel(doc_node: ast.Node, ids: *IdGen, ctx: RenderContext) void {
// everything below jumps, and next frame's anchor "corrects" it — the flash that
// only happens when scrolling up. Pin the *emitted* height to the table; `record` still
// learns the real size from the unconstrained inner box (see `measured` below).
const pin_for_above = known_h > 0 and !width_in_flux and y <= anchor_y;
const pin_h: ?f32 = if (known_h > 0 and !width_in_flux and
//
// Only a virtualized render pins. With no viewport (`drawInline`: a chat bubble, a
// tooltip) every block is laid out every frame, so no block comes back from a spacer and
// there is nothing for a pin to hide. There a pin does harm: an edit re-parses, which
// leaves the edited block with an estimate (biased low) or the deferred height its first
// frame filed, and next frame's pin emits that in place of its real height. The caller
// sees the block shrink by a line and grow back; a chat streaming a reply re-parses on
// every chunk, so its conversation bounced by a line each time.
const pin_for_above = virtualize and known_h > 0 and !width_in_flux and y <= anchor_y;
const pin_h: ?f32 = if (virtualize and known_h > 0 and !width_in_flux and
(pin_for_above or
(frozen and block_state != .estimated) or
pinnable)) known_h else null;
Expand Down
18 changes: 18 additions & 0 deletions tests/integration.zig
Original file line number Diff line number Diff line change
Expand Up @@ -7281,6 +7281,24 @@ test "headless: the whole editor comes up, opens a file, plays the tour and goes
for (0..3) |_| _ = try dvui.testing.step(inlineMarkdownFrame);
try std.testing.expect(inline_md_height > one_para * 2);
inline_md.?.release(0x1d1e);

// Streamed in a few bytes a frame, as a chat reply arrives, it only ever grows. Each
// re-parse left the edited block with an estimate that a pin then emitted for a frame, so
// a streaming reply shrank by a line and grew back, over and over.
const reply = "Sure. Let me look at how the view lays out a message while it streams, because that is where the motion comes from.\n\nThe **scroll area** sizes its content from last frame's sizes, and a message that grows by a line is a frame late.\n\n- one point\n- and a second that goes on long enough to wrap at this width, and then some more";
var last: f32 = 0;
var n: usize = 0;
while (n < reply.len) {
n = @min(reply.len, n + 9);
inline_md_text = reply[0..n];
_ = try dvui.testing.step(inlineMarkdownFrame);
if (inline_md_height + 0.5 < last) {
std.debug.print("inline markdown shrank from {d:.1} to {d:.1} at byte {d}\n", .{ last, inline_md_height, n });
return error.TestUnexpectedResult;
}
last = inline_md_height;
}
inline_md.?.release(0x1d1e);
}

// The `plugins` service: what each plugin's loading did. A built-in is loaded; a reload that
Expand Down
Loading