๐Ÿž Bug: Text temporarily jumps right when unconcealing in linewise_hybrid_mode

#527 ยท closed ยท 12 comments

View on GitHub โ†—

daviewales

### ๐Ÿค– Which branch are you using? - [ ] dev - [x] main ### ๐Ÿ“œ Issue description: I've configured Markview so that concealed and unconcealed text has the same physical position, with the intention that it won't shift when unconcealing it. (See config in next section.) However, as you can see in the screen recording, when I move my cursor from line to line, the text temporarily jumps right when unconcealing. https://github.com/user-attachments/assets/abc7fd0d-18eb-4646-bd95-246a7d136dfa ### ๐Ÿ“‘ Reproduction steps: Add the following text to `test.md` and view in Neovim with included config: ``` md # This is an h1 header ## This is an h2 header ### This is an h3 header #### This is an h4 header ##### This is an h5 header ###### This is an h6 header This is some `inline code`. - This - is - a - list ``` ### ๐Ÿ’ป Minimal init: ```lua vim.env.LAZY_STDPATH = ".repro" load(vim.fn.system("curl -s https://raw.githubusercontent.com/folke/lazy.nvim/main/bootstrap.lua"))() require("lazy.minit").repro({ spec = { { "nvim-treesitter/nvim-treesitter", build = ":TSUpdate", ensure_installed = { "html", "latex", "markdown", "markdown_inline", "typst", "yaml" } }, { "OXY2DEV/markview.nvim", -- branch = "dev", lazy = false } }, }); require 'markview'.setup { preview = { modes = { "n", "no", "c", "i" }, hybrid_modes = { "i", "n" }, linewise_hybrid_mode = true }, code_blocks = { pad_amount = 0 }, markdown = { headings = { heading_1 = { icon = "๓ฐผ " }, heading_2 = { icon = "๓ฐŽจ " }, heading_3 = { icon = "๓ฐผ‘ " }, heading_4 = { icon = "๓ฐŽฒ " }, heading_5 = { icon = "๓ฐผ“ " }, heading_6 = { icon = "๓ฐŽด " } }, list_items = { marker_minus = { add_padding = false }, marker_plus = { add_padding = false }, marker_star = { add_padding = false }, marker_dot = { add_padding = false }, marker_parenthesis = { add_padding = false } } } } ``` ### ๐Ÿ“œ Logs: <details> ``` LOG | markview/autocmds.lua => bufHandle() -> /home/dwales/test.md | Buffer state changed. LOG | markview/actions.lua => attach() | Attached to 1. LOG | markview/actions.lua => autocmd() -> on_attach | Fired on_attach & MarkviewAttach. LOG | markview/actions.lua => set_query() | Set tree-sitter queries for 1. LOG | markview/actions.lua => autocmd() -> on_enable | Fired on_enable & MarkviewEnable. LOG | renderer.lua => clear() | Clearing: 1 LOG | renderer.lua => clear() | Clearing total(1): 0ms LOG | markview/actions.lua => render() | Rendering preview. LOG | parsers.lua => init() | Parsing(start): 1 LOG | fn() | Parsing total(1): 5ms LOG | renderer.lua => render() | Rendering: 1 LOG | renderer.lua => render() | Render(main): 1ms LOG | renderer.lua => render() | Render(post): 0ms LOG | renderer.lua => render() | Rendering total(1): 1ms LOG | renderer.lua => clear() | Clearing: 1 LOG | renderer.lua => clear() | Clearing total(1): 0ms LOG | markview/actions.lua => autocmd() -> on_hybrid_enable | Fired on_hybrid_enable & MarkviewHybridEnable. LOG | markview/actions.lua => set_query() | Set tree-sitter queries for 1. LOG | markview/autocmds.lua => cursor() -> action() | Cursor movement process. LOG | renderer.lua => clear() | Clearing: 1 LOG | renderer.lua => clear() | Clearing total(1): 0ms LOG | markview/actions.lua => render() | Rendering preview. LOG | parsers.lua => init() | Parsing(start): 1 LOG | fn() | Parsing total(1): 1ms LOG | renderer.lua => render() | Rendering: 1 LOG | renderer.lua => render() | Render(main): 0ms LOG | renderer.lua => render() | Render(post): 0ms LOG | renderer.lua => render() | Rendering total(1): 0ms LOG | renderer.lua => clear() | Clearing: 1 LOG | renderer.lua => clear() | Clearing total(1): 0ms LOG | markview/autocmds.lua => cursor() -> action() | Cursor movement process. LOG | renderer.lua => clear() | Clearing: 1 LOG | renderer.lua => clear() | Clearing total(1): 0ms LOG | markview/actions.lua => render() | Rendering preview. LOG | parsers.lua => init() | Parsing(start): 1 LOG | fn() | Parsing total(1): 1ms LOG | renderer.lua => render() | Rendering: 1 LOG | renderer.lua => render() | Render(main): 0ms LOG | renderer.lua => render() | Render(post): 0ms LOG | renderer.lua => render() | Rendering total(1): 0ms LOG | renderer.lua => clear() | Clearing: 1 LOG | renderer.lua => clear() | Clearing total(1): 0ms LOG | markview/autocmds.lua => cursor() -> action() | Cursor movement process. LOG | renderer.lua => clear() | Clearing: 1 LOG | renderer.lua => clear() | Clearing total(1): 0ms LOG | markview/actions.lua => render() | Rendering preview. LOG | parsers.lua => init() | Parsing(start): 1 LOG | fn() | Parsing total(1): 0ms LOG | renderer.lua => render() | Rendering: 1 LOG | renderer.lua => render() | Render(main): 0ms LOG | renderer.lua => render() | Render(post): 0ms LOG | renderer.lua => render() | Rendering total(1): 0ms LOG | renderer.lua => clear() | Clearing: 1 LOG | renderer.lua => clear() | Clearing total(1): 0ms LOG | markview/autocmds.lua => cursor() -> action() | Cursor movement process. LOG | renderer.lua => clear() | Clearing: 1 LOG | renderer.lua => clear() | Clearing total(1): 0ms LOG | markview/actions.lua => render() | Rendering preview. LOG | parsers.lua => init() | Parsing(start): 1 LOG | fn() | Parsing total(1): 0ms LOG | renderer.lua => render() | Rendering: 1 LOG | renderer.lua => render() | Render(main): 0ms LOG | renderer.lua => render() | Render(post): 0ms LOG | renderer.lua => render() | Rendering total(1): 0ms LOG | renderer.lua => clear() | Clearing: 1 LOG | renderer.lua => clear() | Clearing total(1): 0ms LOG | markview/autocmds.lua => cursor() -> action() | Cursor movement process. LOG | renderer.lua => clear() | Clearing: 1 LOG | renderer.lua => clear() | Clearing total(1): 0ms LOG | markview/actions.lua => render() | Rendering preview. LOG | parsers.lua => init() | Parsing(start): 1 LOG | fn() | Parsing total(1): 1ms LOG | renderer.lua => render() | Rendering: 1 LOG | renderer.lua => render() | Render(main): 0ms LOG | renderer.lua => render() | Render(post): 0ms LOG | renderer.lua => render() | Rendering total(1): 1ms LOG | renderer.lua => clear() | Clearing: 1 LOG | renderer.lua => clear() | Clearing total(1): 0ms LOG | markview/autocmds.lua => cursor() -> action() | Cursor movement process. LOG | renderer.lua => clear() | Clearing: 1 LOG | renderer.lua => clear() | Clearing total(1): 0ms LOG | markview/actions.lua => render() | Rendering preview. LOG | parsers.lua => init() | Parsing(start): 1 LOG | fn() | Parsing total(1): 0ms LOG | renderer.lua => render() | Rendering: 1 LOG | renderer.lua => render() | Render(main): 0ms LOG | renderer.lua => render() | Render(post): 0ms LOG | renderer.lua => render() | Rendering total(1): 0ms LOG | renderer.lua => clear() | Clearing: 1 LOG | renderer.lua => clear() | Clearing total(1): 0ms LOG | markview/autocmds.lua => cursor() -> action() | Cursor movement process. LOG | renderer.lua => clear() | Clearing: 1 LOG | renderer.lua => clear() | Clearing total(1): 0ms LOG | markview/actions.lua => render() | Rendering preview. LOG | parsers.lua => init() | Parsing(start): 1 LOG | fn() | Parsing total(1): 1ms LOG | renderer.lua => render() | Rendering: 1 LOG | renderer.lua => render() | Render(main): 1ms LOG | renderer.lua => render() | Render(post): 0ms LOG | renderer.lua => render() | Rendering total(1): 1ms LOG | renderer.lua => clear() | Clearing: 1 LOG | renderer.lua => clear() | Clearing total(1): 0ms LOG | markview/autocmds.lua => cursor() -> action() | Cursor movement process. LOG | renderer.lua => clear() | Clearing: 1 LOG | renderer.lua => clear() | Clearing total(1): 0ms LOG | markview/actions.lua => render() | Rendering preview. LOG | parsers.lua => init() | Parsing(start): 1 LOG | fn() | Parsing total(1): 2ms LOG | renderer.lua => render() | Rendering: 1 LOG | renderer.lua => render() | Render(main): 0ms LOG | renderer.lua => render() | Render(post): 0ms LOG | renderer.lua => render() | Rendering total(1): 0ms LOG | renderer.lua => clear() | Clearing: 1 LOG | renderer.lua => clear() | Clearing total(1): 0ms LOG | markview/autocmds.lua => cursor() -> action() | Cursor movement process. LOG | renderer.lua => clear() | Clearing: 1 LOG | renderer.lua => clear() | Clearing total(1): 0ms LOG | markview/actions.lua => render() | Rendering preview. LOG | parsers.lua => init() | Parsing(start): 1 LOG | fn() | Parsing total(1): 0ms LOG | renderer.lua => render() | Rendering: 1 LOG | renderer.lua => render() | Render(main): 0ms LOG | renderer.lua => render() | Render(post): 0ms LOG | renderer.lua => render() | Rendering total(1): 0ms LOG | renderer.lua => clear() | Clearing: 1 LOG | renderer.lua => clear() | Clearing total(1): 0ms LOG | markview/autocmds.lua => cursor() -> action() | Cursor movement process. LOG | renderer.lua => clear() | Clearing: 1 LOG | renderer.lua => clear() | Clearing total(1): 0ms LOG | markview/actions.lua => render() | Rendering preview. LOG | parsers.lua => init() | Parsing(start): 1 LOG | fn() | Parsing total(1): 0ms LOG | renderer.lua => render() | Rendering: 1 LOG | renderer.lua => render() | Render(main): 0ms LOG | renderer.lua => render() | Render(post): 0ms LOG | renderer.lua => render() | Rendering total(1): 0ms LOG | renderer.lua => clear() | Clearing: 1 LOG | renderer.lua => clear() | Clearing total(1): 0ms LOG | markview/autocmds.lua => cursor() -> action() | Cursor movement process. LOG | renderer.lua => clear() | Clearing: 1 LOG | renderer.lua => clear() | Clearing total(1): 0ms LOG | markview/actions.lua => render() | Rendering preview. LOG | parsers.lua => init() | Parsing(start): 1 LOG | fn() | Parsing total(1): 1ms LOG | renderer.lua => render() | Rendering: 1 LOG | renderer.lua => render() | Render(main): 0ms LOG | renderer.lua => render() | Render(post): 0ms LOG | renderer.lua => render() | Rendering total(1): 0ms LOG | renderer.lua => clear() | Clearing: 1 LOG | renderer.lua => clear() | Clearing total(1): 0ms LOG | markview/autocmds.lua => cursor() -> action() | Cursor movement process. LOG | renderer.lua => clear() | Clearing: 1 LOG | renderer.lua => clear() | Clearing total(1): 0ms LOG | markview/actions.lua => render() | Rendering preview. LOG | parsers.lua => init() | Parsing(start): 1 LOG | fn() | Parsing total(1): 0ms LOG | renderer.lua => render() | Rendering: 1 LOG | renderer.lua => render() | Render(main): 0ms LOG | renderer.lua => render() | Render(post): 0ms LOG | renderer.lua => render() | Rendering total(1): 0ms LOG | renderer.lua => clear() | Clearing: 1 LOG | renderer.lua => clear() | Clearing total(1): 0ms LOG | markview/autocmds.lua => cursor() -> action() | Cursor movement process. LOG | renderer.lua => clear() | Clearing: 1 LOG | renderer.lua => clear() | Clearing total(1): 0ms LOG | markview/actions.lua => render() | Rendering preview. LOG | parsers.lua => init() | Parsing(start): 1 LOG | fn() | Parsing total(1): 0ms LOG | renderer.lua => render() | Rendering: 1 LOG | renderer.lua => render() | Render(main): 0ms LOG | renderer.lua => render() | Render(post): 0ms LOG | renderer.lua => render() | Rendering total(1): 0ms LOG | renderer.lua => clear() | Clearing: 1 LOG | renderer.lua => clear() | Clearing total(1): 0ms LOG | markview/autocmds.lua => cursor() -> action() | Cursor movement process. LOG | renderer.lua => clear() | Clearing: 1 LOG | renderer.lua => clear() | Clearing total(1): 0ms LOG | markview/actions.lua => render() | Rendering preview. LOG | parsers.lua => init() | Parsing(start): 1 LOG | fn() | Parsing total(1): 0ms LOG | renderer.lua => render() | Rendering: 1 LOG | renderer.lua => render() | Render(main): 0ms LOG | renderer.lua => render() | Render(post): 0ms LOG | renderer.lua => render() | Rendering total(1): 0ms LOG | renderer.lua => clear() | Clearing: 1 LOG | renderer.lua => clear() | Clearing total(1): 0ms ``` </details> ### Neovim version: NVIM v0.12.2 Build type: Release LuaJIT 2.1.1774638290 Run "nvim -V1 -v" for more info ### โœ… Expected behavior: The text should unconceal without jumping to the right.

Comments

OXY2DEV

This isn't a bug, as by default the *preview* gets updated ~150ms. Change your [preview.debounce](https://github.com/OXY2DEV/markview.nvim/wiki/Preview#debounce), ```lua preview = { debounce = 50 } ```

daviewales

That's better, and `debounce = 0` is better still. But there's still sometimes a visible jump when unconcealing, especially when setting the cursor position with the mouse. Is it not possible to prevent the text jumping right when unconcealing? With `debounce = 0`, it's so quick that a screen recording doesn't always catch it, but you still see the jump on some frames: https://github.com/user-attachments/assets/283d134c-4dab-4100-85f2-85fcb43a8c61

OXY2DEV

Because `vim.uv.new_timer()` is used for debounce. A timer with a delay of `0` doesn't always mean it's going to run instantly. Not to mention things like getting the syntax from tree-sitter can take a while which paired with doing parsing, rendering, reading config, waiting for Neovim to *show* the decorations etc. also takes varying amount of time. So, you will still see the jump. And the delay is almost guaranteed if you are in a large text file(or if the file has some wacky syntax).

daviewales

I think we're talking about different issues? I get that it might not run instantly, even if the delay is 0. I'm confused because both the concealed and unconcealed text is in the exact same place. But something happens in between, which shows the text several characters to the right. I can understand a delay between the two states. But where is the third 'shifted right' state coming from?

OXY2DEV

> I can understand a delay between the two states. But where is the third 'shifted right' state coming from? Because that "third" state is coming from Neovim itself and I can't change that. `markview` uses `concealcursor` for previews, without it the line under the cursor won't have the raw text hidden in regular preview. Neovim applies `concealcursor` first then any decorations are placed/removed on the text. So, for a tiny amount of time both the raw text & the decorations are visible causing text to shift to the right. @daviewales that's why you see this visual bug.

daviewales

Thanks for the explanation!

daviewales

A follow-up to this. I trialled [render-markdown.nvim](https://github.com/MeanderingProgrammer/render-markdown.nvim) to see if it had the same issue. It _does_ have the same issue, but only sometimes. The distinction seems to be (as you've described), when decorations _additional_ to the native `conceal` are applied. `render-markdown` seems to apply less additional decorations, so there are some situations where `render-markdown` is less 'jumpy'. For example, it's possible to configure `render-markdown` so that headings don't jump at all. But `render-markdown` is still 'jumpy' in some cases, such as checkboxes. Including my `render-markdown` config below in case it's useful: ``` lua vim.env.LAZY_STDPATH = ".repro" load(vim.fn.system("curl -s https://raw.githubusercontent.com/folke/lazy.nvim/main/bootstrap.lua"))() require("lazy.minit").repro({ spec = { { "nvim-treesitter/nvim-treesitter", build = ":TSUpdate", ensure_installed = { "html", "latex", "markdown", "markdown_inline", "typst", "yaml" } }, { "MeanderingProgrammer/render-markdown.nvim", -- branch = "dev", lazy = false } }, }); require('render-markdown').setup { render_modes = { 'n', 'i', 'c', 't' }, code = { border = 'thin', width = 'block', min_width = 79, }, completions = { lsp = { enabled = true } }, checkbox = { right_pad = 0, unchecked = { icon = ' ๓ฐ„ฑ ' }, checked = { icon = ' ๓ฐฑ’ ' }, }, heading = { width = 'block', min_width = 79, }, link = { footnote = { icon = '๓ฐฏ” ', suffix = ' ', superscript = false, }, } } ``` https://github.com/user-attachments/assets/c95c8db0-1c60-4921-a3aa-01001d313882 In the screen recording above, note that similarly to Markview, the easiest way to reproduce the jumpiness in render-markdown is to mouse-click the text. But unlike Markview, the jumpiness only appears to affect checkbox lists. Headings and regular lists are not jumpy.

OXY2DEV

@daviewales everything in `markview.nvim` can be turned off from the config. So, you can get the same result by disabling certain decorations(e.g. `markdown.list_items.shift_width = 0`, `markdown.headings.shift_width = 0` etc). It's up to the user how much they want to customize.

daviewales

Hmm... I've tried with `shift_width = 0`, but it's still jumpy? ``` lua vim.env.LAZY_STDPATH = ".repro" load(vim.fn.system("curl -s https://raw.githubusercontent.com/folke/lazy.nvim/main/bootstrap.lua"))() require("lazy.minit").repro({ spec = { { "nvim-treesitter/nvim-treesitter", build = ":TSUpdate", ensure_installed = { "html", "latex", "markdown", "markdown_inline", "typst", "yaml" } }, { "OXY2DEV/markview.nvim", -- branch = "dev", lazy = false } }, }); require 'markview'.setup { preview = { modes = { "n", "no", "c", "i" }, hybrid_modes = { "i", "n" }, linewise_hybrid_mode = true }, code_blocks = { pad_amount = 0 }, markdown = { headings = { shift_width = 0, heading_1 = { icon = "๓ฐผ " }, heading_2 = { icon = " ๓ฐŽจ " }, heading_3 = { icon = " ๓ฐผ‘ " }, heading_4 = { icon = " ๓ฐŽฒ " }, heading_5 = { icon = " ๓ฐผ“ " }, heading_6 = { icon = " ๓ฐŽด " } }, list_items = { shift_width = 0, marker_minus = { add_padding = false }, marker_plus = { add_padding = false }, marker_star = { add_padding = false }, marker_dot = { add_padding = false }, marker_parenthesis = { add_padding = false } } } } ``` https://github.com/user-attachments/assets/7940d501-ec38-4c50-8c6d-aa3928dc5c15

OXY2DEV

@daviewales the issues with `list items` & `headings` should be resolved by now. Your config should look something like this, ```lua opts = { preview = { modes = { "n", "no", "c", "i" }, hybrid_modes = { "i", "n" }, linewise_hybrid_mode = true, }, markdown = { headings = { shift_width = 0, heading_1 = { icon = "๓ฐผ" }, heading_2 = { icon = " ๓ฐŽจ" }, heading_3 = { icon = " ๓ฐผ‘" }, heading_4 = { icon = " ๓ฐŽฒ" }, heading_5 = { icon = " ๓ฐผ“" }, heading_6 = { icon = " ๓ฐŽด" } }, list_items = { marker_minus = { add_padding = false }, marker_plus = { add_padding = false }, marker_star = { add_padding = false }, marker_dot = { add_padding = false }, marker_parenthesis = { add_padding = false } } } } ```

OXY2DEV

I am not sure if there's a "correct way" to fix the issue for code spans & other inline elements. So, I will keep fixing them on hold for now.

daviewales

That's perfect! Thanks!