neovim icon indicating copy to clipboard operation
neovim copied to clipboard

Syntax highlight flicker when same buffer open in multiple windows at different scroll positions

Open kmoschcau opened this issue 9 months ago • 20 comments

Problem

Hi,

a few days ago I got this weird and hard to diagnose behavior, that the syntax highlighting (I think it's tree sitter, not LSP) starts to flicker really fast (as in with every draw call). Sometimes this also blocks neovim from reacting to user inputs, even though the CPU usage is pretty low.

Steps to reproduce

(I will try and create a minimal repro later, right now I'm a bit pressed for time.)

  • have treesitter parser for vue and typescript or javascript installed
  • open a .vue file large enough to fill the editor viewport at least twice, vertically (ideally with the template section and the script section each taking up more space than the viewport)
  • split the buffer into a new window
  • scroll the second window, so that one window shows one injected language section and the other shows the other injected language section
  • observe flickering (do not do this, if you are suffering from epilepsy)

Expected behavior

The highlighting should not flicker and not block neovim from reacting at all.

Nvim version (nvim -v)

NVIM v0.11.0-dev-1860+g877f3b7288

Vim (not Nvim) behaves the same?

not tested

Operating system/version

Windows 11 24H2 26100.3194

Terminal name/version

wezterm 20240203-110809-5046fc22

$TERM environment variable

N/A

Installation

winget (nightly neovim)

kmoschcau avatar Feb 27 '25 14:02 kmoschcau

Here is a recording of the problem. Again: You might not want to watch this, if you are suffering from epilepsy. https://github.com/user-attachments/assets/9ad7898a-1096-49a1-997e-fa5b9170297a

The flickering is even more pronounced when I run neovide instead, due to a higher frame rate.

kmoschcau avatar Feb 27 '25 14:02 kmoschcau

Need repro steps starting with nvim --clean .

justinmk avatar Feb 27 '25 14:02 justinmk

This is likely because LanguageTree._processed_injection_range only holds a single range, and each parse attempt from the highlighter (for different windows) sets the range to that specific window's range. Maybe to allow multiple ranges, this property could be a list of ranges, or maybe something smarter like a tree that allows for efficient overlap queries

ribru17 avatar Feb 28 '25 06:02 ribru17

@ribru17 do you still need me to come up with nvim --clean repro steps? You already seem to have an idea where to look.

kmoschcau avatar Feb 28 '25 07:02 kmoschcau

Maybe @justinmk would still prefer one for testing purposes👍I can try and reproduce without, so no rush from my end

Edit: even just a minimal vue file that caused this would help

ribru17 avatar Feb 28 '25 14:02 ribru17

Sure, I got it to trigger with the following file. Just split the window and scroll one window to the top and the other to the bottom of the file. Though side note: It triggers less often as with the actual files I'm working on. If this file isn't enough, I can see about cleaning one of the work files of proprietary information and posting that as well.

In order to add nvim --clean repro steps I first need to figure out how to install the vue parser without installing nvim-treesitter first.

<script setup lang="ts">
import HelloWorld from "./components/HelloWorld.vue";
import TheWelcome from "./components/TheWelcome.vue";

const foo = [];

foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
foo.push(1);
</script>

<template>
  <header>
    <img
      alt="Vue logo"
      class="logo"
      src="./assets/logo.svg"
      width="125"
      height="125"
    />

    <div class="wrapper">
      <HelloWorld msg="You did it!" />
    </div>
  </header>

  <main>
    <TheWelcome />
  </main>
</template>

<style scoped>
header {
  line-height: 1.5;
}

.logo {
  display: block;
  margin: 0 auto 2rem;
}

@media (min-width: 1024px) {
  header {
    display: flex;
    place-items: center;
    padding-right: calc(var(--section-gap) / 2);
  }

  .logo {
    margin: 0 2rem 0 0;
  }

  header .wrapper {
    display: flex;
    place-items: flex-start;
    flex-wrap: wrap;
  }
}
</style>

kmoschcau avatar Mar 01 '25 11:03 kmoschcau

I can repro this exactly. Seeing with kickstart.nvim installed, though usually when scrolling up and down with the keyboard and only with window splits. Even when neovim is unfocussed and idle, the flicker still occasionally occurs in the lower pane. During the flicker text loses its colors and styling (bold, italic etc).

So far I've only seen this, at least obviously, with .vue SFC files.

I've noticed that occasionally the flicker can 'freeze' in the uncolored/unformatted state when the which-key popup is waiting for input.

Further tests carried out:

  • running nvim --clean the problem disappears. With --clean am I correct in assuming that treesitter is not used and neovim defers back to regex based syntax parsing?
  • disabling all other plugins except treesitter and the problem remains
  • leaving kickstart.nvim as upstream and disabling treesitter alone the problem disappears
  • with treesitter enabled, removing the nightly build of neovim and replacing with 10.4 release build, the problem disappears

It seems the issue is a change since the 10.4 release that's causing an issue with treesitter either only or mostly visible with .vue SFC files.

Running neovim latest nightly on windows 10, testing with vanilla kickstart.nvim. Tested with Wezterm and latest MS Terminal preview (same result)

Come to think of it I've only seen this issue appear over the last few weeks, and @ribru17 has made a number of commits eg. 0c9c140f91c97dfacf8648a5fee90b06d8c50bbc cbad2c662873bf791309505418407c804db219b7 to refactor/improve treesitter over that time. I don't have a nightly from a few weeks ago and don't have a build setup in windows, perhaps someone could wind back a few weeks to pinpoint? Everything is fine at neovim 10.4.

qgates avatar Mar 01 '25 22:03 qgates

Yeah, this would be due to #32482

ribru17 avatar Mar 02 '25 00:03 ribru17

I think a good way to fix this would be to store all ranges for which injections have been processed in an interval tree. We can reset the tree as we do now, and when we run the injection query over a new region we can add that range to the tree for efficient lookups in future is_valid() calls. Can make use of the data structure in #32692

ribru17 avatar Mar 02 '25 07:03 ribru17

I'm using vim.g._ts_force_sync_parsing = true as a temporary workaround.

mpal9000 avatar Mar 02 '25 13:03 mpal9000

@ribru17 I'm left wondering why this problem is particularly visible with vue files. I'm often splitting via Ctrl-W ] and only really see this issue working with them. Is this more visible in the case where different syntaxes are applied, eg. in vue files the template section being html flavoured but the script section being js/ts?

qgates avatar Mar 02 '25 17:03 qgates

Good question, I think in general this is more likely to happen if the parse attempts take place across multiple event loop iterations (are slower). Having a lot of injections definitely doesn't help, and it may be the case that the vue parser has a lot of states which would make it slower

ribru17 avatar Mar 02 '25 17:03 ribru17

Although I experience the same issue as described above, with markdown and injected syntaxes (i.e. flickering while scrolling, using splits that show different parts of a document), I see flickering also while typing (even without splits). Not sure if the cause is the same. Reproduction steps:

  1. run nvim --clean
  2. :set ft=markdown
  3. :lua vim.treesitter.start()
  4. paste the markdown content below, to the empty buffer
  5. duplicate the markdown content a few times (I can see the flickering easily, when the buffer contains ~300 lines)
  6. start typing at the bottom a new markdown paragraph

Notes:

  • the flickering is more visible when opening the same buffer, in a split window
  • :lua vim.g._ts_force_sync_parsing = true prevents the flickering in this case as well
  • nvim: v0.11.0-dev-1876+g86046c5a31, tree-sitter: v0.25.2

Sample content:

## Title

Lorem ipsum odor amet, consectetuer adipiscing elit. Sem pulvinar curae a eleifend feugiat. Potenti urna magna cursus placerat conubia gravida egestas nibh. Efficitur felis inceptos pulvinar; dapibus primis mi vivamus pharetra. Hendrerit molestie vehicula varius iaculis nisi orci porta convallis torquent. Mollis tortor fringilla non aliquet lobortis facilisi. Bibendum habitant urna quam donec posuere dis primis ad.

```lua
function greet(name)
  print("Hello, " .. name .. "!")
end

greet("Alice")

person = {
  name = "Bob",
  age = 25,
}

print("Name: " .. person.name)
print("Age: " .. person.age)

local sum = 0
for i = 1, 5 do
  sum = sum + i
end
print("Sum of numbers from 1 to 5: " .. sum)

local counter = 1
while counter <= 5 do
  print("Counter value: " .. counter)
  counter = counter + 1
end
```

mpal9000 avatar Mar 02 '25 19:03 mpal9000

I'm using vim.g._ts_force_sync_parsing = true as a temporary workaround.

I can confirm that this works as a workaround for me as well.

kmoschcau avatar Mar 03 '25 07:03 kmoschcau

I want to up this issue, because it has repro steps and :lua vim.g._ts_force_sync_parsing = true helps.

Issue can be encountered on :

NVIM v0.11.0-dev-2044+gaafbd442b2
Build type: RelWithDebInfo
LuaJIT 2.1.1741730670
Run "nvim -V1 -v" for more info

ynhhoJ avatar Mar 19 '25 14:03 ynhhoJ

I experience flicker of the current line while typing. So far it weirdly only seems to happen on one specific file. Maybe related to file size? Though I tested another file of a similar size, and the flicker did not show up.

Typing on a comment line flickers the whole line. On some other lines there is only partial flicker within for example, a string. The whole file also flickers at start when opening nvim.

If I remove 90% of the file's contents, the flickering seems to no longer occur.

File: theme.txt

Github doesn't allow adding the file with a .lua extension, so the file probably needs to be renamed manually to get treesitter to highlight it as lua.

nvim --version:

NVIM v0.11.0-dev-2064+ge0cd8cfba4
Build type: RelWithDebInfo
LuaJIT 2.1.1741730670
Run "nvim -V1 -v" for more info

Edit: This occurs with a single window/buffer.

Aumnescio avatar Mar 21 '25 14:03 Aumnescio

I'm seeing a very similar issue with Neovim v0.11. I use https://github.com/crispgm/nvim-go and issuing a :GoFormat with treesitter syntax highlighting enabled on a large .go file causes a "flash of unhighlighted text" as the file is formatted and reloaded.

Disabling async parsing fixes the issue for me:

vim.g._ts_force_sync_parsing = true 

The code for the formatting function of the nvim-go plugin is here: https://github.com/crispgm/nvim-go/blob/main/lua/go/format.lua

If there is some improvement that could be made to the plugin code to avoid the highlighting flicker while keeping treesitter highlighting async I'd be interested to hear it.

smlx avatar Mar 27 '25 06:03 smlx

If there is some improvement that could be made to the plugin code to avoid the highlighting flicker while keeping treesitter highlighting async I'd be interested to hear it.

Not sure about plugins specifically, but the core solution to prevent flicker would be to apply a double-buffering approach in the highlighter. This prevents flicker while typing (flicker for multiple windows still needs a separate parser-level change)

I have a commit which eliminates flicker for the first scenario but it requires a decent refactor, so it may take some time before it is ready

ribru17 avatar Mar 28 '25 15:03 ribru17

This new issue I created, now marked as duplicate, contains more details and a longer recording. Please have a look: https://github.com/neovim/neovim/issues/33139

Note that my case is completely unrelated to moving around. I do not think it is even related to injection, my issue has been observed in JSON for instance. But it is very notable in Markdown.

I guess my issue is closer to https://github.com/neovim/neovim/issues/32660#issuecomment-2743503801 @Aumnescio, can you please compare? 🙏

I confirm it is fixed by disabling async parsing: https://github.com/neovim/neovim/issues/32660#issuecomment-2756897985 Sadly :(

Hope it helps!

helins avatar Mar 29 '25 02:03 helins

Will be fixed by https://github.com/neovim/neovim/pull/33145

justinmk avatar Mar 29 '25 11:03 justinmk

Fixed by https://github.com/neovim/neovim/pull/36503

justinmk avatar Nov 18 '25 18:11 justinmk