Preserve semantic highlights during mid-edit via decoration provider #22

Closed
opened 2026-09-21 18:54:13 +00:00 by barrettruth · 0 comments
Owner

Original issue: barrettruth/canola.nvim#129
Original author: barrettruth
Original date: 2026-03-15T16:52:39Z

Problem

When editing file or directory names in a canola buffer (e.g. renaming with cw), semantic highlights — OilDir, OilFile, OilExecutable, OilLink, OilHidden, permission column bits, mtime, size — disappear mid-edit and only return after :w triggers a full re-render. This is because highlights are currently applied as static extmarks via nvim_buf_set_extmark() during util.set_highlights(). When the user edits a line, those extmarks shift or are invalidated by the buffer change.

stevearc noted this as a "technical limitation" of extmarks (stevearc/oil.nvim#254). That is accurate for the current static approach, but nvim_set_decoration_provider offers a different architecture that sidesteps the problem.

Upstream context

Proposed solution: nvim_set_decoration_provider

Replace static extmarks with a decoration provider — a Neovim API used by gitsigns.nvim and the built-in treesitter highlighter. The provider registers callbacks that fire during every redraw cycle:

  • on_win(ns, winid, bufnr, toprow, botrow) — called once per visible window range each redraw
  • on_line(ns, winid, bufnr, row) — called per visible line within that range

Highlights become ephemeral: recalculated each frame, only for the lines currently visible. The provider never writes to buffer content and never stores extmarks persistently. Because the highlights are recomputed on every redraw rather than stored, editing a line does not invalidate them — the next frame simply recomputes them from the current parse result.

Flow per visible line

  1. Read the raw line text from the buffer
  2. Parse the concealed ID prefix to get the entry ID (parser.parse_line)
  3. Look up the entry in cache by ID (cache.get_entry_by_id)
  4. Apply highlights transiently via nvim_buf_set_extmark with ephemeral = true
  5. If the ID prefix has been destroyed (user deleted it), skip gracefully — no highlight is better than a wrong one

Highlight cache

A per-buffer table (bufnr → lnum → { hl_group, col_start, col_end }[]) stores the highlight metadata produced during the last full render. The decoration provider reads this cache for column highlights (icon hl, permission bits, mtime, size) that require column layout knowledge to compute accurately. For type-based highlights (OilDir, OilFile, etc.) the provider can recompute directly from the cache entry without the layout table.

Affected highlight groups

  • OilDir, OilFile, OilExecutable, OilLink, OilOrphan, OilHidden
  • OilExecutableHidden, OilLinkTarget, OilLinkTargetError
  • Icon column highlights (from the icon provider)
  • Permission column segment highlights
  • OilMtime, OilSize, and any other column-specific groups
  • Results from user-supplied highlight_filename callbacks

Interaction with virtual text columns (#142)

Virtual text columns (issue #142) also need decoration-provider infrastructure to render without modifying buffer content. The same provider registration serves both: the on_line callback handles semantic highlights for existing columns and virtual text rendering for virtual columns. Implementing this issue first establishes the provider pattern that #142 builds on.

Performance

The provider processes only visible lines per frame, not the entire buffer. For a directory with thousands of entries, only the ~50 visible lines are touched per redraw. Cache lookup by lnum is O(1). This is comparable to or faster than the current approach, which applies extmarks for every entry in the buffer on each render, including off-screen entries.

The main cost is the ID prefix parse on every redraw for every visible line. This can be optimized by caching the last-seen line text and skipping the parse if the line is unchanged since the previous frame.

Key files

  • lua/oil/util.lua — set_highlights() (line 366), render_table() (line 319)
  • lua/oil/view.lua — format_entry_cols() (line 892), render_buffer() (line 749)
  • lua/oil/mutator/parser.lua — parse_line() for ID prefix extraction
  • syntax/oil.vim — concealment of the ID prefix (unchanged by this work)
> Original issue: barrettruth/canola.nvim#129 > Original author: `barrettruth` > Original date: 2026-03-15T16:52:39Z ## Problem When editing file or directory names in a canola buffer (e.g. renaming with `cw`), semantic highlights — `OilDir`, `OilFile`, `OilExecutable`, `OilLink`, `OilHidden`, permission column bits, mtime, size — disappear mid-edit and only return after `:w` triggers a full re-render. This is because highlights are currently applied as static extmarks via `nvim_buf_set_extmark()` during `util.set_highlights()`. When the user edits a line, those extmarks shift or are invalidated by the buffer change. stevearc noted this as a \"technical limitation\" of extmarks (stevearc/oil.nvim#254). That is accurate for the current static approach, but `nvim_set_decoration_provider` offers a different architecture that sidesteps the problem. ## Upstream context - stevearc/oil.nvim#254 ## Proposed solution: `nvim_set_decoration_provider` Replace static extmarks with a decoration provider — a Neovim API used by gitsigns.nvim and the built-in treesitter highlighter. The provider registers callbacks that fire during every redraw cycle: - `on_win(ns, winid, bufnr, toprow, botrow)` — called once per visible window range each redraw - `on_line(ns, winid, bufnr, row)` — called per visible line within that range Highlights become ephemeral: recalculated each frame, only for the lines currently visible. The provider never writes to buffer content and never stores extmarks persistently. Because the highlights are recomputed on every redraw rather than stored, editing a line does not invalidate them — the next frame simply recomputes them from the current parse result. ### Flow per visible line 1. Read the raw line text from the buffer 2. Parse the concealed ID prefix to get the entry ID (`parser.parse_line`) 3. Look up the entry in cache by ID (`cache.get_entry_by_id`) 4. Apply highlights transiently via `nvim_buf_set_extmark` with `ephemeral = true` 5. If the ID prefix has been destroyed (user deleted it), skip gracefully — no highlight is better than a wrong one ### Highlight cache A per-buffer table (`bufnr → lnum → { hl_group, col_start, col_end }[]`) stores the highlight metadata produced during the last full render. The decoration provider reads this cache for column highlights (icon hl, permission bits, mtime, size) that require column layout knowledge to compute accurately. For type-based highlights (`OilDir`, `OilFile`, etc.) the provider can recompute directly from the cache entry without the layout table. ### Affected highlight groups - `OilDir`, `OilFile`, `OilExecutable`, `OilLink`, `OilOrphan`, `OilHidden` - `OilExecutableHidden`, `OilLinkTarget`, `OilLinkTargetError` - Icon column highlights (from the icon provider) - Permission column segment highlights - `OilMtime`, `OilSize`, and any other column-specific groups - Results from user-supplied `highlight_filename` callbacks ### Interaction with virtual text columns (#142) Virtual text columns (issue #142) also need decoration-provider infrastructure to render without modifying buffer content. The same provider registration serves both: the `on_line` callback handles semantic highlights for existing columns and virtual text rendering for virtual columns. Implementing this issue first establishes the provider pattern that #142 builds on. ### Performance The provider processes only visible lines per frame, not the entire buffer. For a directory with thousands of entries, only the ~50 visible lines are touched per redraw. Cache lookup by lnum is O(1). This is comparable to or faster than the current approach, which applies extmarks for every entry in the buffer on each render, including off-screen entries. The main cost is the ID prefix parse on every redraw for every visible line. This can be optimized by caching the last-seen line text and skipping the parse if the line is unchanged since the previous frame. ## Key files - `lua/oil/util.lua` — `set_highlights()` (line 366), `render_table()` (line 319) - `lua/oil/view.lua` — `format_entry_cols()` (line 892), `render_buffer()` (line 749) - `lua/oil/mutator/parser.lua` — `parse_line()` for ID prefix extraction - `syntax/oil.vim` — concealment of the ID prefix (unchanged by this work)
barrettruth added this to the v1.1 milestone 2026-09-21 18:54:13 +00:00
Sign in to join this conversation.
No description provided.