text_view: Parse large full replacements off the UI thread (#2741)
## Description Full-replacement `TextView` updates currently parse their entire Markdown or HTML source synchronously so the first layout has an exact content height. That preserves the scrollbar fix from #2470 for ordinary content, but it also lets a large document block the UI thread before construction or layout can return. This change keeps the synchronous path for full replacements at or below a private 4 KiB byte budget. Larger replacements are sent through the existing background parser and applied through its existing revision checks. Documents are still parsed and displayed in full; this is a scheduling boundary, not a document-length limit. The 4 KiB value is an intentionally conservative private scheduling budget. It preserves the exact first-layout path for short TextViews and can be adjusted later without changing the public API. The background mode distinguishes an unapplied `Replace` from `BaselineAck` and append-compatible work. If an append is coalesced into an async replacement, the merged update remains a replacement so selection and layout state are reset when the result is applied. This is intentionally limited to `crates/ui/src/text/state.rs`. It does not change LSP popovers, `render_markdown`, parser behavior, public APIs, coalescing limits, or task cancellation. Closes #2740 ## Before and after The same 250 KB Markdown document (`"# x\n\n"` repeated 50,000 times) was tested on `main` at `da4f93696dc2` and on this branch. The durations are machine-specific and are reported as reproduction evidence, not test thresholds. | Metric | Before | After | | --- | --- | --- | | UI-thread construction latency | `3410ms`, including the full parse | `<1ms` (reported as `0ms`); no large parser work before returning | | State when construction returns | `source_len=250000`, `blocks=50000` | `source_len=0`, `blocks=0`, with the complete input already stored in `text` | | First parse completion | During construction | `3573ms` later on the background executor, then `source_len=250000`, `blocks=50000` | | Observed test runtime | `7.13s` | `3.61s` | The `3410ms` and `3573ms` samples are individual debug-build parser runs, not evidence of a parser throughput regression. Their 163ms difference is normal run-to-run variation. Before this change, a large full replacement is parsed once synchronously and then queued as `BaselineAck`, causing the background worker to parse the same document again to establish its baseline. The old test's `7.13s` total is consistent with those two parses. After this change, the large replacement is parsed once by the background worker and the test finishes in `3.61s`; most importantly, construction returns without waiting for the full parse. For documents at or below 4 KiB, behavior is unchanged: parsing completes synchronously so the first layout retains its exact height. For larger documents, the previous or empty parsed document may be shown initially, followed by a relayout when the current background result is applied. ## Security impact This bounds the amount of attacker-controlled Markdown or HTML parsed synchronously on the UI thread. In particular, large LSP hover, completion, or diagnostic contents can no longer make `TextView` synchronously parse the whole document during popover construction. This change does not attempt to bound total background CPU use from a sustained stream of large documents; that is separate from the synchronous UI-thread availability issue addressed here. ## Tests added - Small full replacements are parsed before the background executor runs. - Over-budget Markdown and HTML wait for the background executor and are then applied in full. - An async full replacement immediately followed by `push_str` preserves the complete source. - `Replace` followed by a coalesced append remains `ParseMode::Replace`. The existing `outer_list_content_total_stable_while_scrolling` regression test is unchanged and continues to pass. ## Breaking changes None. The byte budget and `ParseMode` are private implementation details. ## How to test Passed locally on Arch Linux x86_64 with Rust 1.96.1: ```shell cargo fmt --all -- --check cargo test --locked -p gpui-component text::state::tests -- --nocapture cargo test --locked -p gpui-component \ text::text_view::tests::outer_list_content_total_stable_while_scrolling \ -- --nocapture cargo test --locked -p gpui-component cargo check --locked -p gpui-component --no-default-features cargo clippy --locked -p gpui-component --no-deps -- --deny warnings cargo test --locked --all ``` The repository-wide CI Clippy command currently stops on two pre-existing `clippy::nonminimal_bool` diagnostics in `crates/base/src/calendar.rs:131` with Rust 1.96.1. This PR does not modify that file; Clippy for the changed `gpui-component` crate passes with warnings denied. ## AI assistance AI assisted with root-cause analysis, patch drafting, test design, and PR wording. I reviewed the final diff and ran all commands listed above locally. ## Checklist - [x] I have read the `CONTRIBUTING.md` document and followed the guidelines. - [x] Reviewed the AI-assisted changes and confirmed the described behavior. - [x] Added deterministic tests for the changed scheduling and merge semantics. - [x] Kept the change to one problem and one production source file. - [ ] Ran the Story app manually; not run because this change is covered by state and layout regression tests. - [ ] Tested macOS and Windows; Linux validation passed and the platform matrix remains for GitHub Actions.
L
luren committed
f9a70b6967c79f2c4c951d98d0ed66b286da8b3c
Parent: 8c9a8f9
Committed by GitHub <noreply@github.com>
on 8/17/2026, 9:42:09 AM