fixes: #9443
Add regression coverage for concealed text that starts before the wrap column.
Caveat: I verified the logic and local tests on this Linux build, not Windows gvim specifically. The affected draw/redraw code path is shared, so this is still appropriate to mark as fixing #9443.
Known follow-up: screenline movement can still account for hidden columns. For example, with a long concealed prefix at 'conceallevel' 3, gj/gk may move through positions in the concealed text before reaching the next visible buffer line, even though the line no longer appears visually wrapped. I did not verify whether that behavior predates this PR, but it is separate from the wrapping/rendering bug fixed here.
This PR was prepared with assistance from OpenAI Codex.
https://github.com/vim/vim/pull/20472
(5 files)
—
Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS and Android. Download it today!
You are receiving this because you are subscribed to this thread.![]()
Please see #10442 (comment). Neither cases there work properly in this PR.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS and Android. Download it today!
You are receiving this because you are subscribed to this thread.![]()
Follow-up commits made the conceal wrap handling less special-casey and covered the cases the first patch missed: tabs after concealed text, visible text after concealed text, showbreak, virtual text, search/match conceal, syntax conceal edge cases, multibyte and wide characters, folded cursor lines, and screenpos() results. The final version adds conceal-aware line/column measurement in misc1.c, uses it from cursor and screenpos() code in move.c, and keeps the tab-specific wrap compensation narrow so ordinary visible text after concealed text does not double-count rows.
The tests now cover those regressions directly in test_conceal.vim and test_cursor_func.vim, including the final reviewer case where concealed text before visible wrapped text was making screenpos() report one row too far down.
Will start chipping away at CI/CD later tonight (if it keeps failing)
—
Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS and Android. Download it today!
You are receiving this because you are subscribed to this thread.![]()
I think you need to update :h screenpos().
—
Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS and Android. Download it today!
You are receiving this because you are subscribed to this thread.![]()
I think you need to update
:h screenpos().
docs have been updated!
—
Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS and Android. Download it today!
You are receiving this because you are subscribed to this thread.![]()
Performance notes:
This changes the cost model only for windows using conceallevel=3. The old line-height/cursor calculations measured display width directly. The new path must account for zero-width concealed text, so for cole=3 it may scan the line with syntax conceal, match conceal, tabs, multibyte characters, and textprop virtual text all considered.
Unaffected fast paths:
conceallevel other than 3Potentially slower:
cole=3matchadd() conceal on long linesscreenpos() calls on such linesMy belief: since this appears to be tightly scoped, I think the trade-off is acceptable. It fixes stale redraw/wrong cursor positioning for a configuration where the previous width calculation was semantically wrong (particularly problemmatic for markdown links, which is my major motivation for pursuing this PR, see MeanderingProgrammer/render-markdown.nvim#82 for one potential downstream beneficiary).
—
Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS and Android. Download it today!
You are receiving this because you are subscribed to this thread.![]()
Note: I just set up a local manual test with https://github.com/preservim/vim-markdown and there are some weird bugs / uncovered cases. So far, we cover X+ conceal, but not Markdown-style conceal of punctuation at line start combined with number, linebreak, breakindent, and showbreak. Will spend a bit of time on that
—
Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS and Android. Download it today!
You are receiving this because you are subscribed to this thread.![]()
See below for a markdown file that you can use in conjunction with something like vim-markdown.
Useful testing file# Markdown Conceal Wrapping Check This file is for manual testing of wrapped screen lines with Markdown conceal and double-width characters. Use a narrow terminal or try `:set columns=40`, `:set columns=52`, and `:set columns=60`. ## Emphasis, Links, and Wide Characters This paragraph has **bold text before 日本語**, *italic text before コンシール*, and a [concealed link title 日本語](https://example.invalid/a/very/long/path/that/should/be-hidden-by-markdown-conceal) followed by enough words to wrap several times in a narrow window. This paragraph puts the wide text later: ordinary words ordinary words ordinary words ordinary words ordinary words **bold marker hidden here** then 漢字かな交じり文 and a [second concealed link with 東京都 text](https://example.invalid/hidden-target) after the wrap point. ## Lists - A dash list item should use Markdown list formatting, and this item intentionally contains **strong text**, `inline code`, [a link with 日本語](https://example.invalid/list), and enough trailing prose to wrap with breakindent. - Another item starts normally and then places double-width characters near the middle of the wrapped display line: alpha beta gamma delta epsilon zeta eta theta 東京大阪京都神戸札幌福岡 then more ASCII words. * A star list item checks the other list marker with **bold 日本語**, *italic 日本語*, ~~struck 日本語~~, and a long link [visible title](https://example.invalid/star-list-target). + A plus list item checks the third list marker and keeps adding text after 漢字 so that screen wrapping must account for both conceal and East Asian width. ## Inline Code, Escapes, and HTML The inline code `printf("日本語 %s", value)` should hide its backticks when Markdown conceal is active, while this escaped marker \# should display as a literal hash and not make following wrapped text appear one cell early or late. Here is an HTML code tag: <code>wide_value = "日本語コンシール"</code> and a pre tag: <pre>alpha beta 日本語 gamma delta</pre> followed by enough plain text to make the line wrap after the concealed tag boundaries. ## Fenced Code ``` {style="conceal-test"} // Fence markers may be concealed by plugins; this long comment keeps double-width text near a wrap boundary: alpha beta gamma delta 日本語 epsilon zeta eta theta iota kappa lambda. const message = "The string contains 日本語 and markdown-looking **markers** that should not be treated as emphasis inside the code block."; ``` ## Boundary Stress Lines 0123456789 0123456789 0123456789 **日本語** 0123456789 0123456789 [link](https://example.invalid/hidden) 0123456789 0123456789 aaaaaaaaaa bbbbbbbbbb cccccccccc dddddddddd `日本語` eeeeeeeeee ffffffffff gggggggggg hhhhhhhhhh iiiiiiiiii Plain comparison line without Markdown conceal but with wide text near the same area: 0123456789 0123456789 0123456789 日本語 0123456789 0123456789 0123456789.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS and Android. Download it today!
You are receiving this because you are subscribed to this thread.![]()
Summary: the benchmark does not show a broad plain-wrap regression, but conceallevel=3 wrapped long-line motion has a measurable cost. Counted 3000gj was 4.8x slower, and repeated-key gj was 1.48x slower in this run.
Benchmarks were collected on July 9, 2026, comparing the PR base against the current PR head:
| Role | Commit |
|---|---|
| Baseline | 834b8d218f1db92e587ecd449e5ce88b41fbe256 |
| PR head | 6bd31c2f19feb1ac5eebe2a400ee80e24da8436d |
Both builds were clean local clones built with the default Unix make target. Both reported Vim 9.2 patches 1-782, Huge GTK3, +conceal, and gcc -O2. The machine was an x86_64 Intel Core Ultra 7 155H system.
The benchmark used an Ex-mode Vim script and reltimefloat() around only the motion command. Buffer setup, match setup, cursor reset, and redraw before each timed command were outside the measured interval.
The concealed workload used one long wrapped line or 9000 repeated lines with wrap, nolinebreak, conceallevel=3, concealcursor=nvic, columns=80, lines=40, and a matchadd() conceal rule for HIDDEN\d\+ . Each repeated text segment also included a tab and one double-width character generated with nr2char(0x3042). Plain controls used the same shape of text without concealed spans.
Two suites were run:
| Suite | Samples |
|---|---|
| Counted motions | 21 rounds, first 5 discarded, 16 samples retained |
| Repeated key motions | 10 rounds, first 2 discarded, 8 samples retained |
Raw local TSV files from this run were written under /tmp/vim-bench-results/.
These use Vim counts such as 3000gj, 6000j, or one repeated command string where shown in the benchmark name.
| Benchmark | Variant | Base median | PR median | Delta | Ratio |
|---|---|---|---|---|---|
gj_down_3000 |
conceallevel_3 |
0.015319s | 0.073570s | +380.2% | 4.80x |
gk_up_3000 |
conceallevel_3 |
0.007024s | 0.007236s | +3.0% | 1.03x |
j_down_6000 |
conceallevel_3 |
0.000008s | 0.000009s | +19.8% | 1.20x |
k_up_6000 |
conceallevel_3 |
0.000005s | 0.000005s | -9.7% | 0.90x |
g0_g$_pairs_2000 |
conceallevel_3 |
3.368108s | 0.063030s | -98.1% | 0.02x |
0_$_pairs_2000 |
conceallevel_3 |
0.000755s | 0.000656s | -13.1% | 0.87x |
l_right_20000 |
conceallevel_3 |
0.006794s | 0.066341s | +876.5% | 9.77x |
h_left_20000 |
conceallevel_3 |
0.003502s | 0.000322s | -90.8% | 0.09x |
gj_down_3000 |
plain_wrap_control |
0.006163s | 0.006312s | +2.4% | 1.02x |
gk_up_3000 |
plain_wrap_control |
0.003444s | 0.003471s | +0.8% | 1.01x |
j_down_6000 |
plain_wrap_control |
0.000007s | 0.000008s | +9.1% | 1.09x |
k_up_6000 |
plain_wrap_control |
0.000007s | 0.000007s | +3.8% | 1.04x |
l_right_20000 |
plain_wrap_control |
0.000256s | 0.000273s | +6.6% | 1.07x |
h_left_20000 |
plain_wrap_control |
0.000283s | 0.000289s | +2.1% | 1.02x |
These use repeated motion commands, for example gjgjgj..., to better approximate repeated keypresses.
| Benchmark | Variant | Base median | PR median | Delta | Ratio |
|---|---|---|---|---|---|
gj_down_30 |
conceallevel_3 |
0.108789s | 0.160932s | +47.9% | 1.48x |
gk_up_30 |
conceallevel_3 |
0.105464s | 0.101439s | -3.8% | 0.96x |
j_down_1000 |
conceallevel_3 |
0.000205s | 0.000196s | -4.1% | 0.96x |
k_up_1000 |
conceallevel_3 |
0.000196s | 0.000186s | -5.3% | 0.95x |
g0_g$_pairs_50 |
conceallevel_3 |
0.002092s | 0.033640s | +1508.0% | 16.08x |
0_$_pairs_500 |
conceallevel_3 |
0.000182s | 0.000187s | +2.9% | 1.03x |
l_right_200 |
conceallevel_3 |
0.000133s | 0.000160s | +19.9% | 1.20x |
h_left_200 |
conceallevel_3 |
0.000111s | 0.000112s | +0.8% | 1.01x |
gj_down_30 |
plain_wrap_control |
0.108162s | 0.101792s | -5.9% | 0.94x |
gk_up_30 |
plain_wrap_control |
0.104623s | 0.106862s | +2.1% | 1.02x |
j_down_1000 |
plain_wrap_control |
0.000197s | 0.000214s | +8.8% | 1.09x |
k_up_1000 |
plain_wrap_control |
0.000196s | 0.000196s | -0.2% | 1.00x |
l_right_200 |
plain_wrap_control |
0.000207s | 0.000144s | -30.2% | 0.70x |
h_left_200 |
plain_wrap_control |
0.000105s | 0.000122s | +16.4% | 1.16x |
The main cost is in wrapped visual-line and screen-column work when conceallevel=3 is active on a long line with concealed spans.
For counted gj down movement on the concealed long line, the PR is 4.8x slower than baseline in this run, adding about 58 ms to 3000gj. For repeated keypress-style gj, the PR is 1.48x slower, adding about 52 ms to 30 repeated gj motions.
Plain wrapped controls are mostly flat, so the slowdown is not a broad normal motion regression. The non-concealed gj/gk controls stayed within about 6%, and most other plain controls are so small that their percentages are noise.
The horizontal wrapped cases are position-sensitive. The counted g0_g$_pairs_2000 case, started far down the same wrapped line, improved substantially on the PR, while the shorter repeated-key g0_g$_pairs_50 case, started closer to the top of the line, regressed. That suggests this path should be treated as dependent on cursor row and cached screen-position state rather than summarized by one number.
Linewise j/k and $0 are effectively unchanged for practical purposes in this harness. Their absolute times are below a millisecond, so the percentage deltas are not meaningful.
This is a microbenchmark, not an interactive terminal or GUI redraw benchmark. It uses lazyredraw and measures motion command execution, not paint throughput. CPU frequency was not pinned, and results came from one local machine. The absolute times should be treated as local measurements; the useful signal is the relative shape across the baseline, PR, and plain-wrap controls.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS and Android. Download it today!
You are receiving this because you are subscribed to this thread.![]()
@chrisbra this ended up being quite involved, but I believe the current state is correct for the cases I could think of (motions + visual layout). And the CI/CD build finally works.
To do this right, particularly for markdown, we'd really need to cover conceallevel=1 and conceallevel=2 (which can also wrap long markdown links, hiding the bulk of their text).
Finally, there is a real performance cost to this. I can continue trying to find optimization opportunities, but I figure I'll pause here and give you a chance to weight in on current state. Eg, should we keep going down this road, or is this "fix" too invasive?
—
Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS and Android. Download it today!
You are receiving this because you are subscribed to this thread.![]()
Summary: the current PR head improves the concealed wrapped-motion slowdown compared to the earlier PR head from the July 9 benchmark. Counted 3000gj is now 61.9% faster than the old PR head, repeated-key gj is 19.9% faster, and the wrapped g0/g$ cases are 34-48% faster.
Benchmarks were collected on July 10, 2026, comparing the same baseline and old PR head from the previous report against the current PR head:
| Role | Commit |
|---|---|
| Baseline | 834b8d218f1db92e587ecd449e5ce88b41fbe256 |
| Old PR head | 6bd31c2f19feb1ac5eebe2a400ee80e24da8436d |
| Current PR head | d2edb8bd0 |
All three builds were clean local clones built with the default Unix make target. They reported Vim 9.2 patches 1-782, Huge GTK3, +conceal, and gcc -O2, on x86_64.
The original raw TSV files from the earlier comment were no longer present, so this rerun uses a reconstructed harness matching the described method and suite shape from that comment. Comparisons below should be read within this rerun.
The benchmark used an Ex-mode Vim script and reltimefloat() around only the motion command. Buffer setup, match setup, cursor reset, and redraw before each timed command were outside the measured interval.
The concealed workload used one long wrapped line or 9000 repeated lines with wrap, nolinebreak, conceallevel=3, concealcursor=nvic, columns=80, lines=40, and a matchadd() conceal rule for HIDDEN\d\+ . Plain controls used the same text shape with no active conceal.
| Suite | Samples |
|---|---|
| Counted motions | 21 rounds, first 5 discarded, 16 samples retained |
| Repeated key motions | 10 rounds, first 2 discarded, 8 samples retained |
Raw local TSV files from this run were written under /tmp/vim-bench-rerun-20472/.
| Benchmark | Variant | Base median | Old PR median | Current median | Current vs old PR |
|---|---|---|---|---|---|
gj_down_3000 |
conceallevel_3 |
0.014705s | 0.176065s | 0.067005s | -61.9% / 0.38x |
gk_up_3000 |
conceallevel_3 |
0.006919s | 0.006744s | 0.003824s | -43.3% / 0.57x |
g0_g$_pairs_2000 |
conceallevel_3 |
11.000244s | 0.086887s | 0.057233s | -34.1% / 0.66x |
l_right_20000 |
conceallevel_3 |
0.007699s | 0.064983s | 0.064041s | -1.4% / 0.99x |
gj_down_3000 |
plain_wrap_control |
0.010103s | 0.006818s | 0.009476s | +39.0% / 1.39x |
| Benchmark | Variant | Base median | Old PR median | Current median | Current vs old PR |
|---|---|---|---|---|---|
gj_down_30 |
conceallevel_3 |
0.112017s | 0.440371s | 0.352640s | -19.9% / 0.80x |
gk_up_30 |
conceallevel_3 |
0.107447s | 0.108644s | 0.099135s | -8.8% / 0.91x |
g0_g$_pairs_50 |
conceallevel_3 |
0.009255s | 0.062074s | 0.032565s | -47.5% / 0.52x |
gj_down_30 |
plain_wrap_control |
0.108290s | 0.109909s | 0.103716s | -5.6% / 0.94x |
The optimization substantially reduces the concealed wrapped-motion cost introduced by the old PR head. In this rerun, counted 3000gj drops from 0.176s to 0.067s, and repeated-key gj drops from 0.440s to 0.353s.
The current head is still slower than baseline for concealed downward wrapped gj, but the regression is materially smaller than before. The plain repeated-key controls stayed flat, so the remaining cost is still concentrated in concealed wrapped long-line motion rather than broad normal motion.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS and Android. Download it today!
You are receiving this because you are subscribed to this thread.![]()
Summary: the current PR head substantially improves concealed wrapped motion
compared to the July 10 PR head. Counted 3000gj is 81.3% faster, repeated
key gj is 93.5% faster, counted 3000gk is 89.8% faster, and the wrapped
g0/g$ cases are 93-99% faster. The latest concealed gj, gk, and g0/g$
results are also faster than baseline in this synthetic workload. Counted
horizontal l motion improved by 23.3% but remains slower than baseline.
Benchmarks were collected on July 12, 2026, comparing the same baseline and
old PR head from the previous report against both the July 10 PR head and the
current PR head:
| Role | Commit |
|---|
| Baseline | 834b8d218f1db92e587ecd449e5ce88b41fbe256 |
| Old PR head | 6bd31c2f19feb1ac5eebe2a400ee80e24da8436d |
| July 10 PR head | d2edb8bd0 |
| Current PR head | 75557c592 |
All four builds were clean local clones built with the default Unix make
target. They reported Vim 9.2 patches 1-782, Huge GTK3, +conceal, and gcc
-O2, on x86_64.
The benchmark uses the reconstructed harness from the July 10 report, so all
comparisons below were measured within this rerun.
The benchmark used an Ex-mode Vim script and reltimefloat() around only the
motion command. Buffer setup, match setup, cursor reset, and redraw before
each timed command were outside the measured interval.
The concealed workload used one long wrapped line or 9000 repeated lines with
wrap, nolinebreak, conceallevel=3, concealcursor=nvic, columns=80,
lines=40, and a matchadd() conceal rule for HIDDEN\d\+ . Plain controls
used the same text shape with no active conceal.
| Suite | Samples |
|---|---|
| Counted motions | 21 rounds, first 5 discarded, 16 samples retained |
| Repeated key motions | 10 rounds, first 2 discarded, 8 samples retained |
Raw local TSV files from this run were written under
/tmp/vim-bench-latest-20472/.
| Benchmark | Variant | Base median | Old PR median |
|---|
| July 10 median | Current median | Current vs July 10 | ||||
|---|---|---|---|---|---|---|
gj_down_3000 |
conceallevel_3 |
0.017439s | 0.136133s | 0.068372s | 0.012800s | -81.3% / 0.19x |
gk_up_3000 |
conceallevel_3 |
0.007254s | 0.007143s | 0.003600s | 0.000367s | -89.8% / 0.10x |
g0_g$_pairs_2000 |
conceallevel_3 |
11.334834s | 0.094635s | 0.057091s | 0.003924s | -93.1% / 0.07x |
l_right_20000 |
conceallevel_3 |
0.007550s | 0.066487s | 0.063373s | 0.048599s | -23.3% / 0.77x |
gj_down_3000 |
plain_wrap_control |
0.007254s | 0.006415s | 0.006307s | 0.006989s | +10.8% / 1.11x |
| Benchmark | Variant | Base median | Old PR median |
|---|
| July 10 median | Current median | Current vs July 10 | ||||
|---|---|---|---|---|---|---|
gj_down_30 |
conceallevel_3 |
0.106386s | 0.433901s | 0.387748s | 0.025355s | -93.5% / 0.07x |
gk_up_30 |
conceallevel_3 |
0.103231s | 0.112981s | 0.100116s | 0.001985s | -98.0% / 0.02x |
g0_g$_pairs_50 |
conceallevel_3 |
0.009386s | 0.059212s | 0.034601s | 0.000339s | -99.0% / 0.01x |
gj_down_30 |
plain_wrap_control |
0.110514s | 0.104264s | 0.102476s | 0.105355s | +2.8% / 1.03x |
The latest cache and screenline-motion changes remove most of the remaining
concealed wrapped-motion cost from the July 10 head. Counted 3000gj drops
from 68.4 ms to 12.8 ms, repeated-key gj drops from 387.7 ms to 25.4 ms, and
repeated-key gk drops from 100.1 ms to 2.0 ms.
For these cases the current head no longer merely narrows the regression:
concealed gj, gk, and g0/g$ are faster than the baseline medians in this
rerun. Plain repeated-key gj remains in the same band as the other builds,
which indicates that the improvement is concentrated in concealed wrapped
motion rather than broad normal motion.
Counted concealed l improves from 63.4 ms to 48.6 ms but is still 6.44x the
baseline median. The repeated l command itself remains sub-millisecond and
close to the other PR heads, so horizontal concealed motion is still the main
remaining synthetic regression rather than an interactive freeze shape.
The roughly 9-11% changes in some counted plain controls occur at only a few
milliseconds over thousands of motions and should not be treated as a broad
regression without a larger targeted benchmark.
This is a headless microbenchmark, not a terminal or GUI throughput benchmark.
It uses lazyredraw and measures motion command execution rather than paint
throughput. CPU frequency was not pinned, and results came from one local
machine. Absolute times should be treated as local measurements; the useful
signal is the relative shape across identically built commits and the plain
controls.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS and Android. Download it today!
You are receiving this because you are subscribed to this thread.![]()
related: neovim/neovim#40897
—
Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS and Android. Download it today!
You are receiving this because you are subscribed to this thread.![]()
how does this relate to the neovim patch?
—
Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS and Android. Download it today!
You are receiving this because you are subscribed to this thread.![]()
how does this relate to the neovim patch?
Was book-keeping for myself, sorry for creating noise
—
Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS and Android. Download it today!
You are receiving this because you are subscribed to this thread.![]()