VulCode
Blog

vscode-neovim: visual selections no longer cut an emoji in half

A selection ending on an emoji or a character like 𝕜 stopped in the middle of a UTF-16 surrogate pair. The end of the selection now steps over the whole character.

DualFroz - VulCode CEODualFroz - VulCode CEO1 min read
vscode-neovim: visual selections no longer cut an emoji in half - DualFroz - VulCode CEO
Title in the repository
Keep visual selections whole over characters outside the BMP
Merged
23 Sep 2026
Written up here
6 Oct 2026

The note was written 13 days after the change was merged.

vscode-neovim
Technologies:LuaTypeScriptUnicode

What was broken

The vscode-neovim extension runs a real Neovim underneath VS Code. Neovim counts columns in UTF-8 bytes and VS Code in UTF-16 units, so every selection has to be converted. Characters outside the Basic Multilingual Plane, such as most emoji or 𝕜 (U+1D55C), take two UTF-16 units, a surrogate pair.

In visual mode the end of a selection is inclusive, while in VS Code it is exclusive, so the code added one UTF-16 unit to the end column. With a two-unit character the selection ended in the middle of the pair (issue #2535).

What we changed

Instead of adding one UTF-16 unit, the code first moves the end of the selection past the whole character in bytes, and only then converts the column to UTF-16:

lua
if vim.o.selection ~= "exclusive" then
  -- step over the whole character: adding one UTF-16 unit splits a surrogate pair
  end_col = byte_after_char(end_text, end_col)
end
local end_char = M.utf16_col(end_text, end_col)

byte_after_char uses vim.str_utf_end to find the last byte of the character and clamps at the end of the line.

How we tested it

We added three integration tests: a selection over one character outside the BMP, over two of them in a row, and over a mix of ordinary multi-byte characters and characters outside the BMP. We could not run the integration suite locally and said so in the pull request, so the project's CI was its first run.

The result

The change was merged on 23 September 2026. Selections in text with emoji and mathematical characters now cover whole characters.

Vim mode for VS Code where the editing is done by a real Neovim.

Language
TypeScript
GitHub stars
7,753

The same case before and after the change

On the example of the note: what came out before the fix and what comes out now.

A visual mode selection ends on the character
𝕜  U+1D55C

Neovim counts this character as 4 UTF-8 bytes (F0 9D 95 9C), VS Code as 2 UTF-16 units (D835 DD5C).

Before the change
D835

One UTF-16 unit was added to the end of the selection, so it ended in the middle of the surrogate pair (issue #2535).

After the change
D835 DD5C

The end of the selection steps over the whole character in bytes and only then is converted to UTF-16, so it covers both units.

What it taught us

JavaScript measures strings in UTF-16 units, and an emoji takes two. Every cursor offset has to be counted in characters, not units.

The character and the description come from the note; the bytes and units are counted when the page is built. The integration tests of pull request #2683 cover one such character, two in a row and a mix with ordinary multi-byte characters.

Questions about change #2683

Where the change is, how we tested it and who merged it.