From 9f425311c38fada4dc25f758c31933fe7b0b4dfe Mon Sep 17 00:00:00 2001 From: Volodymyr Chernetskyi Date: Mon, 7 Sep 2026 18:47:48 +0200 Subject: [PATCH] fix(lsp): semantic tokens ignore 'fileformat' when sizing line endings #41733 Problem: The eol_offset used to decode multiline semantic tokens is computed as vim.bo.fileformat[bufnr] == 'dos' and 2 or 1 which indexes the option value rather than the buffer, so it is never "dos" and the offset is always 1. A token that crosses a line boundary in a 'fileformat' of "dos" is then decoded one code unit short per line ending, and the highlight extends past the end of the token. Solution: Read the option with the buffer-scoped form, vim.bo[bufnr].fileformat. The existing multiline test covers both formats now. Its token length of 82 counts the line endings it spans, so the same token reaches four characters less far once the buffer is "dos". AI-assisted --- runtime/lua/vim/lsp/semantic_tokens.lua | 2 +- .../plugin/lsp/semantic_tokens_spec.lua | 30 ++++++++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/runtime/lua/vim/lsp/semantic_tokens.lua b/runtime/lua/vim/lsp/semantic_tokens.lua index 428cce9360..e23abafb97 100644 --- a/runtime/lua/vim/lsp/semantic_tokens.lua +++ b/runtime/lua/vim/lsp/semantic_tokens.lua @@ -88,7 +88,7 @@ local function tokens_to_ranges(data, bufnr, client, request, ranges) local encoding = client.offset_encoding local lines = api.nvim_buf_get_lines(bufnr, 0, -1, false) -- For all encodings, \r\n takes up two code points, and \n (or \r) takes up one. - local eol_offset = vim.bo.fileformat[bufnr] == 'dos' and 2 or 1 + local eol_offset = vim.bo[bufnr].fileformat == 'dos' and 2 or 1 local version = request.version local request_id = request.request_id local last_insert_idx = 1 diff --git a/test/functional/plugin/lsp/semantic_tokens_spec.lua b/test/functional/plugin/lsp/semantic_tokens_spec.lua index 22fc95c416..255bfa50ea 100644 --- a/test/functional/plugin/lsp/semantic_tokens_spec.lua +++ b/test/functional/plugin/lsp/semantic_tokens_spec.lua @@ -181,8 +181,9 @@ describe('semantic token highlighting', function() exec_lua(function() local bufnr = vim.api.nvim_get_current_buf() vim.api.nvim_win_set_buf(0, bufnr) + vim.bo[bufnr].fileformat = 'unix' vim.bo[bufnr].filetype = 'some-filetype' - _G._start_server(_G.server2) + _G.test_client_id = _G._start_server(_G.server2) end) screen:expect { @@ -203,6 +204,33 @@ describe('semantic token highlighting', function() | ]], } + + -- The same token reaches four characters less far in a 'dos' buffer, because each + -- of the four line endings it spans takes two characters instead of one. + exec_lua(function() + vim.lsp.get_client_by_id(assert(_G.test_client_id)):stop() + vim.bo[vim.api.nvim_get_current_buf()].fileformat = 'dos' + _G._start_server(_G.server2) + end) + + screen:expect { + grid = [[ + #include | + | + int main() | + { | + int x; | + {2:#ifdef __cplusplus} | + {2: std::cout << x << "\n";} | + {2:#else} | + {2: printf("%d\n", x);} | + {2:#e}ndif | + } | + ^} | + {1:~ }|*3 + | + ]], + } end) it('calls both range and full when range is supported', function()