Repository navigation
Conversation
Each highlight add rebuilt its line's spans from all of the line's records, so k adds on one line cost O(k^2) (record 076: 4,000 adds took 5.95G instructions). Spans are now a cache stamped with span_generation and built at paint by one sweep with a priority heap. Byte-addressed layers of group-set runs, which follow edits and empty on whole-text changes, feed the same builder; the highlight transaction and dirty-line set go. Editor highlights no longer invalidate the Yoga measure.
A replacement re-seeked its insert cell in the deleted text, where a joined combining mark moved it, and a layer range edge inside a character cut a run into invalid UTF-8; both now hold, asserted after every layer change. Span entries were a dense list, so one painted row deep in a document allocated an entry per earlier line; they are now keyed by the rows read. Group-set name bounds report the limit error, a no-op layer call keeps the span stamp, and three dead stamp bumps go. The span model now covers styled text, wrap, scroll, bg, attributes and links, and checks every paint against a forced full repaint.
C3.1 built byte-addressed highlight layers and Context group sets, but only tests could reach them. A host producer such as the Rust editor still had to add one display-column highlight per syntax range. ot_context_group_set interns an ordered list of style names as a Context-wide id. ot_text_buffer_set_layer and ot_edit_buffer_set_layer replace a byte range of one layer with sorted runs that name those ids, in one transaction. A fifth layer or a group set past its bounds fails with OT_OBJECT_LIMIT; malformed runs fail with OT_INVALID_ARGUMENT and change nothing. The ABI is additive: three calls and the 12-byte ot_highlight_run. The source grows by the three entry points, their Context methods and two status mappings; the next commit moves the Rust editor onto them and deletes its per-range loop. A seeded test drives both buffer kinds through the C ABI against a byte model and the header's rules: 4,096 calls, every malformed class rejected at least 64 times with the layers and span stamp unchanged.
Each syntax refresh of a Rust editor removed its highlights and added one back per styled range, measuring every range's display width through FFI first. On a 1,149-line Rust file that is 3,298 highlight calls and 6,443 measure calls per refresh. The editor now sends the ranges as byte runs of one highlight layer, in one ot_edit_buffer_set_layer call. Each style becomes a one-name group set, cached per style. Style names are now made of the style's fields, because a group set name is limited to 64 bytes and the Debug form of a style is longer. The per-range loop and its width conversion go. The refresh outside tree-sitter drops from about 13.0 million to 5.0 million instructions on that file. Frames stay the same: a seeded twin test drives the old per-range path and the layer through random edits, filetype, theme and style-table changes, clear_highlights and extmarks, and compares every frame cell for cell. tests/highlight.rs is unchanged. Source additions: EditBuffer::set_layer and Context::group_set wrap the new ABI; style_name keeps names within the group set limit.
ot_text_buffer_set_layer had no consumer: no Rust wrapper, no Core caller, and the Markdown engine sets its layers inside native code. The design lands no ABI without one, so the call goes until its first consumer needs it. The C ABI fuzz replaced C3.1's rows for run ends and range ends inside a character but only moved starts, so dropping either end check passed. It now moves any of the four edges, passes NULL with no runs as a valid clear, and has directed rows on one three-byte character. A group-set name over 64 bytes that is also not UTF-8 failed with OT_INVALID_ARGUMENT after a full scan. The length bound is now checked first, and the UTF-8 check moves into the per-name loop.
Every new theme color interns a group set that is never freed, and a failed intern went to App::report, which ends the app. A Rust editor that kept switching to themes with new colors ended after about 200 switches; main never ended an app on a theme change. The editor now interns without reporting: past the bound, runs of new styles paint in the default style and the app keeps running. The frame twin skips, by rule, frames where the old path's piece widths do not add up to the text's width: a grapheme cluster across two captures made the old columns drift, and the layer paints them right. A separate row checks those colors, and the twin's pieces now include such clusters. The public layer setter says the editor widgets own layer 1.
A syntax style had no strikethrough field, and every path that turned
a style into attributes copied bold, italic, underline and dim only. A
theme entry { strikethrough: true } was dropped without an error, so
Markdown and Code hid the tildes of ~~x~~ and drew x plain, while the
Rust widget strikes it through.
StyleDefinition, StyleDefinitionInput and theme token styles gain
strikethrough. Registration, mergeStyles, fromTheme, the tree-sitter
cascade and Markdown's chunks now pass the whole definition to
createTextAttributes, and the attribute copies loop over one list of
names, so the next attribute cannot be dropped in one place only.
Tests exceed the budget by the first paint check of strikethrough: the
Markdown theme switch now draws ~~x~~ in prose and in a table cell.
Issue: 578
A group set ORed the attribute bits of its names, so a later name could
add bold but never take it away: { bold: false } and {} composed the
same over a bold name, and the Markdown engine's nested sets (strong
inside a link, a theme that unsets one attribute) could not be drawn.
Registration now records which of the eight attribute bits a style
supplies. With the new OT_EDITOR_STYLE_ATTRIBUTE_MASK flag, bits 16..23
of attributes carry that mask; old callers send no mask, and a style
with attributes then supplies all eight bits, so its stored definition
and its paint by style id are what they were. Composition takes fg, bg
and each supplied bit from the later name. A value bit outside the mask
fails.
A seeded test registers random partial themes through the C ABI, half
of them as old callers, and checks 10,000 compositions of up to eight
names against a fold written from the design's rule, over all eight
bits; it runs in about 0.12 s in a debug build.
Source additions: the mask's flag, field, decode and fold step (the
ABI gains one flag; no call changes).
Core keeps a style's attributes as optional booleans, but registration
sent native one attribute byte, so { bold: false } and {} arrived the
same. Group sets composed natively need the difference: an unset
attribute lets an earlier name show through, a false one clears it.
registerStyle now sends which attributes the style sets, through the
new mask flag. Paint by style id and every Core renderable draw as
before; only native group-set composition reads the mask.
Source additions: the optional mask field and its encoding (one flag
and one shifted byte in the existing record).
The tree-sitter cascade built a one-field object for every defined field of every matching capture to pass to Object.assign; it now sets the field on the local style directly. createTextAttributes already treats a missing style as no attributes, so its callers need no guard.
The editor style encoder packs the attribute byte and the supplied mask into one word, but took any number for either. A value of 0x10001 with an empty mask reached native as bold both set and supplied, and a mask of 65536 or 1.5 became a different, valid mask, so native could not reject it. With a mask, both values must now be integers from 0 to 255. The registration test now checks the C record that reaches native, flags and packed word, rather than the object Core passes to its binding: dropping the mask or always sending 0xff passed before and changes a painted cell. Source additions: the byte check (the encoder had none for the mask).
With a theme that strikes markup.strikethrough, Markdown still drew some struck text plain where Code on the same document struck it. A table header cell replaced its inline attributes with the heading's, so ~~head~~ lost its strike, and the children of ~~...~~ that have a style of their own (~~**x**~~) took only that style. A header cell now keeps an inline strike unless the heading style sets strikethrough itself, and a struck span marks all its children struck. No other attribute or color changes. Source additions: one mask for the header and one loop for the span's children (no attribute reached them before). Issue: 578
Rust's Markdown widget strikes ~~x~~ through unless its styles say otherwise; Core drew it plain unless the theme asked. OpenCode's theme gives markup.strikethrough only a muted color, so users saw deleted text that looked like any other muted text. SyntaxStyle.getStyle now adds strikethrough to markup.strikethrough when the theme leaves it unset, so Markdown, Code on Markdown and mergeStyles strike it. A theme that sets strikethrough: false keeps it plain. With no style for the scope at all, the text keeps the default colors it had. Other scopes that share a theme rule, such as unchecked task boxes, do not change. Source additions: the default (Core had no Markdown style defaults).
mergeStyles cached its result under the names joined with ":", so
mergeStyles("a:b") and mergeStyles("a", "b") shared one entry, and so
did no names and one empty name; whichever ran first answered both. The
key is now the list itself, serialized, which no two lists share.
Native group sets now take each attribute bit from the later name that supplies it, and a style registered without a mask supplies all eight bits. A Rust style has no attribute that says "off", so a later name cleared the bold or italic of an earlier one, where `Style::patch` adds them. `SyntaxStyle::register` now sends the attribute mask with the bits the style sets. A group set then composes as `Style::patch`: fg and bg from the last name with one, attributes added. One-name lookups are unchanged. Source +3 lines: the mask flag, the mask bits, and the doc.
A filetyped `Textarea` named each run's style by its value, so a theme
with new colors interned new group sets that are never freed, and every
theme or style table change ran tree-sitter on the whole text again.
After about 200 themes with new colors, new styles painted plain.
Each run now names the capture names whose styles it merges, as one
group set: `default`, then the covering captures after the last `none`.
The editor registers each name as `opentui.<capture>` with its syntax
theme's style, and native paint composes the set. A theme or table
change only registers the names again: no new group sets and no
tree-sitter run. On a 1,149-line Rust file a theme switch takes 4.7M
instructions instead of 164M, and a keystroke refresh 163M instead of
167M: styles and group sets are looked up once per distinct capture
list, not once per piece.
Rust behavior changes: textareas that share one style table share the
`opentui.<capture>` names, so each name has the style the last editor
registered; give them one syntax theme. After `clear_highlights`, a
theme or table change no longer brings syntax back; the next edit does.
Extmark styles keep their `opentui.{style:?}` names from main.
Source +43 lines: the capture lists and their per-list styles, the
registration on theme and table changes, and the table lookup that
syntax and extmarks now share.
The Rust editor gives every piece of a filetyped text a layer run, also pieces the syntax theme leaves plain, so its runs do not depend on the theme. Such a run composed to an empty style but still won its columns, so an app's own priority-0 highlight under plain text disappeared; on main it showed. A layer run without a link whose group set supplies no field (no fg, no bg, no attribute bit) under the bound style is now left out of the span build where another input covers its columns, so the column falls through as if the run were absent. Legacy records keep their rule: an empty record still wins. Spans of such lines record the binding's change count and rebuild after a registration or a style swap; lines with only layer runs rebuild nothing on a theme change. Source +31 lines: the supplied-bits union in composition, the change count on style borrowers, and the skip in the span build.
A group set held at most 8 names. Core's nvim-treesitter Markdown queries stack 10 capture names on one piece (a heading with italic, strikethrough, link, bold and code text), and the Rust editor names a set after its capture stack, so such pieces lost their style. The bound is now 16 names, and the header defines both name bounds, OT_GROUP_SET_NAME_COUNT_MAX and OT_GROUP_SET_NAME_BYTE_COUNT_MAX, which native asserts against. The ABI only gains the two constants; no symbol or record changes.
Both wrappers repeated the tree-sitter feature gate around the same `highlights` call. `highlight_ranges` now merges the styles of `highlight_names`' lists, and `spans` uses the same merge, so one wrapper and one merge remain.
Editors that share a style table registered capture names each from its own syntax theme: when an edit found a new capture list, the editor registered the whole list again, so typing in one editor could repaint the other with its own theme. A theme change also touched only the names that editor had seen. And on a textarea without a filetype, a theme change made and bound the editor's own table, unbinding a table the app had set on the buffer. The table now owns its capture palette (the syntax theme set last on it) and the capture names registered from it. Setting a theme or a table restyles every name the table has seen; an edit registers its new lists' names from the table's palette, never from the editor's own theme. A theme change without a table touches nothing. A list past the group-set bounds keeps its innermost 16 names, and a capture name over 56 bytes is left out instead of the whole piece painting plain. Source +38 lines: the palette on the table and its two calls, the bounds in `group_set`, and their docs.
The twin test gets its pieces from the same capture lists as the editor, so a wrong stack rule passed on both sides, and its zero-parse row only watched the layer, so a parse moved before the content check went unseen. A table row now checks hand-written colors: a repeated capture keeps its last place, the last `none` clears what lies under it, and a three-part capture takes its longest styled prefix or, on request, its first part. The theme-churn row swaps its grammar for a lazy one that counts loads: theme and table switches load nothing, the next edit loads it once.
A line's spans depended on style definitions whenever the buffer had two layers, so one layer run off screen made every painted line rebuild after each registration or style swap: 20 needless builds in the counter row. Lines without any run paid for it too. A layer run is now checked against the inputs already on its line: only an unlinked run over an earlier input can be left out, and only then do the line's spans record the binding's change count. The two-layer counter row builds nothing on a theme change, and a row with two overlapping layers and no record shows the lower one while the upper supplies nothing.
An edit that found a new capture list registered every name in it again, also names the table already had. An app that redefined `opentui.string` in its table lost that color as soon as typing added an escape, with no theme or table change, and every such registration cleared the table's composition cache. An edit now registers only names the table has not seen; setting a palette still restyles all of them.
With the name pool full, the theme-churn row only typed text whose capture lists already had sets, so an editor that panicked on a refused set passed it. The row's grammar now also captures strings, and the first string comes after the pool is full: it paints plain while the keyword keeps its color.
# Conflicts: # packages/core/src/native-abi.generated.ts # packages/rust/crates/opentui-sys/src/generated.rs
# Conflicts: # packages/core/src/native-abi.generated.ts # packages/core/src/renderables/Markdown.ts # packages/rust/crates/opentui-sys/src/generated.rs
…ure-sets # Conflicts: # packages/core/src/native-abi.generated.ts # packages/rust/crates/opentui-sys/src/generated.rs
The Markdown engine streams a leaf's display into its text node from the first byte that changed. A text buffer could only replace its whole text or append through a new registry slot, which runs out after 255 appends and then copies the whole document each time. replaceTail keeps the text in one growable slot the buffer owns: a write from byte b drops the rope leaves past b, lays out only the new bytes (and a grapheme they continue), and clips layers at b. The rope is rebuilt into a fresh arena when its arena outgrows twice the live rope. 8,192 one-byte writes copy 8,192 bytes and keep one slot. Source +158: C4's internal tail primitive, which M2's bound Markdown nodes need (design: native-markdown-parser.md, "Text nodes").
A hooked leaf's flag is off while its pending line is parsed again (the provisional layout is dropped before the hook latches anew). A binding that never skipped a write, because the leaf latched after the doc's last write, then took a tail write over the host's text, and any binding took the whole write of a closing leaf. A binding now also notes a text change since the doc's last write (the buffer's content epoch), and only a bind or an option change writes a node that skipped: closing writes no longer do. A table header that takes a hooked paragraph's last line can leave the paragraph without its hook. Its node held the host's text, and the doc never wrote it; it does now. The ABI property test's host writes the nodes it owns and checks after each call that the doc left them alone.
Each append to an open list refreshed it: every item's children were collected again to decide tightness (items times blocks), and every marker was formatted and set again. A 30-byte append to list.md took 830 allocations, rising to 1,230 in the second half. Tightness is now one pass over the list's blocks in preorder, and a marker is set only when its text changes; the text is formatted into one reused buffer. The list is still walked once per refresh, but without allocations: list.md takes 26 allocations per append, flat.
The pattern lost its rust-blocks and rust-stream tags in the Rust engine commit and now fits on one line, as oxfmt writes it.
d8dc9fc styled the space before a concealed link's ` (url)` and the `]` of a link's source as `markup.link`, as Core's marked preview does. The frames OpenCode shows keep both as text (oc-rust's U.4a and U.4c fixtures pin it, and the Rust widget drew them so before), and the marked preview goes in M3. Both are text again.
adopt freed the old document and moved the new one in before it wrote the bound nodes. A failed node write then returned an error, and the caller's cleanup freed the storage the document had just taken: a replace or option change that ran out of memory there freed live memory. A rejected append or finish also left the nodes it had written showing text the rolled-back document no longer had, and listed the pending line's blocks again. The new state now becomes the document's by a swap that a failed write undoes. The nodes a rejected call wrote are written whole again from the document as it is; one that fails again is written at the next call. A rejected bind leaves the bindings as they were, and the pending line's blocks keep their listing across a rejected call. A test fails each allocation of an option change, an append, a replace and a finish in turn and checks the source, the pending changes and both bound nodes.
Each write to a bound paragraph computed the runs of every live token, from the first. Behind an opener that never closes the tokens are never folded, so a paragraph of `*open ` and 512 code spans (2,054 bytes) took 262,656 token visits, four times as many for twice the bytes. The live leaf now keeps the styles in force before each token and restyles from the first token whose style may have changed: the first changed token, or the first opener a new match took. The parser reports that opener, since an opener drawn as closed all along does not change its own display when it matches. The same paragraph takes about one visit per token. The repeated-construct model gates the run token visits at 2n plus one per call and gains this family, and a row streams bigboi's paragraph.
A host writes hooked prose, table cells and every leaf in source mode from read_leaf, which resolves colors through the document's style. set_options with another style changed that output but listed no block, so the host kept the old colors. A style swap now lists every leaf the document does not write itself. The seeded ABI test now always runs seed 647, which finds a hooked leaf written over by the document, so the separate host-ownership test that replayed it goes.
Three regressions against the old widget, all in the inline hook:
- A hook that read the document with `markdown()` panicked: a change
held the source's exclusive borrow while it showed its blocks. The
change now holds it only while native code changes the document,
and a shared one while hooks and renderers run.
- A hook that shows the inline source kept `*hi*` after
`set_markdown("_hi_")`: the widget skipped the hook when read_leaf
was unchanged. It now compares the inline source too.
- Where native code writes the text (terminals with hyperlinks), a
removed hook's targets stayed clickable. They now go once a frame
paints the engine's text.
TestApp::with_hyperlinks makes a headless app whose terminal shows
hyperlinks, so tests reach the path that native code writes.
Ending the stream ran again only the renderers whose last call returned a widget. A renderer that kept the default widget while streaming and has its own for the finished document (a preview that waits for the end) was never asked again when the closed fence did not change. Every code block runs its renderer again now.
set_markdown always parsed the whole text again. oc-rust streams reasoning, groups, compaction and mini's scrollback by setting the whole text each time, so each call cost the size of the document: on an 18 KB document, 788k instructions per call against 57k to append. While the document streams, text that extends the content now appends its new tail; the engine's streamed parse equals the fresh one. The Rust model gains that command, and checks click targets with or without a hook. Its theme commands set only what they change, and it runs a second time on a terminal with hyperlinks, where native code writes the text: no Rust test reached that path before, so dropping the bind of a reused paragraph passed CI. An exhaustive run of every three hook, rerun, finish, replace, append and conceal changes covers who writes the text and who owns the targets.
A removal refreshed the table or list around each removed block, which the engine lists itself, and taking back a removed widget checked that the block's parent stays, which holds once taking back stops at the first container. Both survive every test and opus's twelve removal probes.
The inline parser leaves the last word of an open paragraph to B's pending drawing, which conceals `*`, `_` and `~` runs but styled none of the text between them. A stream that never ends, such as OpenCode's compaction summary `The user is fixing the **parser**.`, showed `parser` without bold for good, and a complete line showed it so until the next word arrived. B now notes the runs it conceals, and the runs that close one another style the text between them, as process emphasis matches them; an opener that waits still styles nothing. read_leaf and the live node take the same spans, and under a hold cut neither counts a run at the cut.
…ument A host that commits rows (mini's scrollback) needs the committed count to leave out a block whose last line is pending. The ABI property test now compares the count every call returns with a fresh document's, and checks that no committed top-level block is open.
The live node counted a pending run under a held pipe by display offset, read_leaf by the run's end in the content, so a run that reached the pipe (`$_ses_ | ` with math on, model seed 467) styled the node and not read_leaf. Both now drop the runs that reach the pipe. The repeated-construct model also fails on a node that differs from read_leaf; it only printed it before.
…rack/m2-rust-markdown-engine # Conflicts: # packages/native/src/tests/markdown_test.zig
Rows C, E and F of the tear prototype close a setext leaf with conceal or hyperlinks off; they aborted in liveRestyle before f86891f and need M2's options, so they live here. M1's filter takes at most sixteen items now: the header says so, and set_options and create reject a seventeenth.
Bytes that only grow the pending line extend its layout without reading it again. In a list item a fence opener's info string is the rest of that line, so the code block's info range stayed where the layout put it and the host was not told the block changed (seeded ABI test, seed 965, after M1's soup gained the tear bytes). A line whose layout opens a fence now takes a new layout for new bytes.
…lity reply change_options read the engine flags before change took in a new hyperlink capability, then sent those stale flags after the capability's own option call. Removing a hook just after the terminal reported hyperlinks left native code thinking the widget wrote the text and the widget thinking native code did, so later appends never showed. The flags are now read inside the change. (bigboi-r2-3)
Ending the stream showed the blocks the finish listed, which ran their renderers for the finished document, and then ran every renderer again. An open fence's renderer was asked twice with the same finished state, and a renderer that answered differently the second time changed what showed. The widget now notes the blocks the finish already rendered and leaves them out of the second pass. (bigboi-r2-4)
Nested closers (`begin `, k times `*a `, then k times `b* `, one piece per call) took 561, 2,145 and 8,385 run token visits at k = 16, 32 and 64: each closer's match restyled from its opener, through the inner matches' interiors, which were italic already. At k = 64 that is 390 bytes, where the work property allows 973. The bound was right; the restyle was quadratic. When a call's new matches share one kind, the walk now jumps over the interior of an older match of that kind, whose styles cannot change, and writes the runs before and after it as separate segments (a node write may now end its runs before the text's end). The skipped tokens' noted styles are marked stale, so a later write that would start in them starts before them. Matches nest and do not cross links, so the styles at the inner closer are those after its opener. The same stream takes about one visit per token. The repeated-construct model generates this shape (several marks, prefixes and interiors), and a row streams bigboi's k = 64 stream. (bigboi-r2-2)
An option change that turns the live display on takes over the nodes the host wrote and writes them whole. If the call then ran out of memory it was rejected, and the source and changes were as before, but a node already written showed the doc's text, and the host's text was gone: the doc cannot rebuild it, so the rejected-call resync could not restore it. During a call, a write that would take a node over from the host now only marks it; the call writes those nodes once everything else in it succeeded, and a rejected call drops the marks. A take-over write that fails after that leaves the node stale for the next call to write. The allocation-failure row also starts from host-written nodes and fails each allocation of the live option change. (bigboi-r2-1)
51a955c let a restyle walk jump from an opener to the closer of an older match of the call's one kind, and go on with the styles it had after the opener. A `***` opens two matches with different closers, and a closer can close matches opened inside the gap, so that state was wrong: `begin *out ***a** b* tail q* e` streamed a byte at a time left bold on after its closer. bigboi's prefix-frame differential found 20 of 240 streams differing from a fresh document; none did before 51a955c. The rule now follows from the matcher's own result instead of a guess about the call's kinds. The walk keeps how many of the call's matches of each kind cover the token it is past. It jumps from an opener to its innermost closer only when that match is older than the call (so no match of the call starts or ends inside: matches nest), every kind the call's matches add there is one the opener's matches give already, and no bytes or link there changed. Before the closer, the styles are those after the opener plus the closer's own matches whose openers lie in the gap; the walk goes on from there with them. The parser reports the first bracket a link took, the one style change without new bytes. The differential is now a seeded test (60 documents of mixed and shared delimiters, links and code, at strides 1, 3, 5 and 8: 240 streams), with bigboi's two strings as its first rows, and the repeated-construct model's nested family mixes `*`, `**`, `***`, `_`, `__` and `~~`. (bigboi-r3-1)
64 nested `***` pairs, closed one per call, still took 8,385 run token visits where the work property allows 1,485. Each closer marked its opener's display changed (a three-star run is not matched by one kind at once), so the text was rewritten from the opener, and no jump can pass written bytes. A match never changes its opener's bytes: matched or still waiting with what is left, a run is concealed. The style change the mark stood for is what `matched_from` reports, so the mark goes. The same stream now takes about one visit per token, inside the stated bound: run token visits ≤ 2n + calls + 64, as before. A walk passes a token when the token is new or written again, or when the call's matches add a kind to it that no older match around it gives; the jump passes the rest. A row streams bigboi's 64 `***` pairs. (bigboi-r3-2)
8cb56f3 dropped the pending runs that reach the leaf's held pipe, but read_leaf holds a pipe only in a paragraph's last line, where it may start a table header. In a heading the live node then left `*e*` after a code span with a pipe unstyled (mixed-fragment differential, seed 601 at a wide budget). Only a paragraph's runs stop at a pipe now; the nested differential carries the row.
ec11923 wrote a host's nodes at the end of the call that made the doc write them, and a write that failed there left the node stale for the next call. That next call wrote it before it checked its own input, so an invalid UTF-8 append was rejected but changed the node from the host's text to the doc's (bigboi-r3-3). A doc now never writes a node the host wrote, in any call: when a call would (options that turn the live display on, a leaf that loses its hook), it unbinds the node and lists the leaf once the call succeeds, and the host binds the node again to hand it over. A text change since the doc's last write counts as the host's. The bind's own write cannot fail once the text changes: the layer's storage is reserved first, so a rejected bind leaves the node and the bindings as they were. There is no state in which a node shows text the doc wanted to replace. The Rust widget binds a node again when native code writes a leaf the widget wrote. The allocation-failure row checks the accepted hand-off and an invalid append after it; a new row fails each allocation of a bind over host text.
The repeated-construct model at 1,000 seeds (seed 81) took more run token visits than 2n + calls once its nested family mixed `*`, `_`, `**`, `__`, `***` and `~~`. Two things were wrong. The jump asked the opener at hand for every kind the call's matches add. When one call closes a `~~` and the `**` around it, and the older levels inside give the two kinds by two matches, no single opener gives both, so the walk went down every level on every call. The jump now asks all the older matches around the position: the walk's styles count every match there and `new_active` the call's, so an older one gives a kind when its count is the larger. And 2n + calls holds only for one kind. A walk passes a token when it arrives and once more for each kind it gains: the bound proved is (1 + k)n + calls, where k is the number of match kinds the doc's closers made (at most 3). The counters record those kinds, and the work property uses that bound. The `***` row still checks 2n + calls itself, since a `***` closer adds its two kinds in one call. A row streams 64 `**a ~~b ` levels closed `c~~ d** ` per call. (bigboi-r3-2)
18b8d37 bounded run token visits by (1 + k)n + calls, k the match kinds the doc's closers made. With no match at all (seed 247 at 1,000 seeds: openers that never close, code spans split across appends) that left n + calls, but a token is walked again when a later call writes it again (a code span that closes, a hold that moves), which the old 2n covered. The bound is (1 + max(1, k))n + calls: 2n for one kind or none, as before, and n more for each further kind.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #2037 (the native Markdown engine) and #2030 (with #2025–#2027, highlight layers). Merge those first. The base is #2037's branch, so until #2025–#2030 merge, the diff also shows their changes.
The Rust
Markdownwidget parsed with its own line parser. Each append parsed the open tail again and a streamedlist parsed the whole list again (564 times the bytes of a 34 KB list, about 5,000 allocations per 30-byte
append), and its output drifted from Core's.
The widget now runs on the native engine through a new
ot_markdown_*ABI. The engine reads each byte once,reports the blocks that changed, and writes streamed paragraphs and headings to their text nodes itself; the
widget builds or updates only what changed. A list append takes 26 allocations, flat, and
set_markdownwithtext that extends a stream appends the new tail. The public Rust API is unchanged; output changes where the spec
decides (
- - -rules, setext headings).Tests: an ABI property test whose host learns blocks only from
read_changesand must match a fresh documentafter every call under every option set, with a depth-2 sweep and an allocation-failure row; seeded and
exhaustive Rust models against a fresh widget, on terminals with and without hyperlinks.