Files
neovim/runtime/lua/vim/uri.lua
Justin M. Keyes 4b424a06c5 backport fix(lsp): send didClose, didOpen when languageId changes (#39519)
fix(lsp): send didClose, didOpen when languageId changes

Problem:
If a buffer's filetype changes after the LSP client has already
attached (e.g. from json to jsonc via a modeline), but the client
supports both filetypes, it stays attached. It does not notify the
server of the new languageId, causing the server to incorrectly process
the file using the old languageId.

Solution:
Save the languageId used during textDocument/didOpen, and send
textDocument/didClose + textDocument/didOpen when buffer's languageId
changed.

Lsp spec:
0003fb53f1/_specifications/lsp/3.18/textDocument/didOpen.md (L5)
> If the language id of a document changes, the client
> needs to send a textDocument/didClose to the server followed by a
> textDocument/didOpen with the new language id if the server handles
> the new language id as well.

AI-assisted: Gemini 3.1 Pro

Co-authored-by: phanium <91544758+phanen@users.noreply.github.com>
2026-04-30 13:09:55 +00:00

131 lines
3.6 KiB
Lua

-- TODO: This is implemented only for files currently.
-- https://tools.ietf.org/html/rfc3986
-- https://tools.ietf.org/html/rfc2732
-- https://tools.ietf.org/html/rfc2396
local M = {}
local sbyte = string.byte
local schar = string.char
local tohex = require('bit').tohex
local URI_SCHEME_PATTERN = '^([a-zA-Z]+[a-zA-Z0-9.+-]*):.*'
local WINDOWS_URI_SCHEME_PATTERN = '^([a-zA-Z]+[a-zA-Z0-9.+-]*):[a-zA-Z]:.*'
local PATTERNS = {
-- RFC 2396
-- https://tools.ietf.org/html/rfc2396#section-2.2
rfc2396 = "^A-Za-z0-9%-_.!~*'()",
-- RFC 2732
-- https://tools.ietf.org/html/rfc2732
rfc2732 = "^A-Za-z0-9%-_.!~*'()%[%]",
-- RFC 3986
-- https://tools.ietf.org/html/rfc3986#section-2.2
rfc3986 = "^A-Za-z0-9%-._~!$&'()*+,;=:@/",
}
---Converts hex to char
---@param hex string
---@return string
local function hex_to_char(hex)
return schar(vim._assert_integer(hex, 16))
end
---@param char string
---@return string
local function percent_encode_char(char)
return '%' .. tohex(sbyte(char), 2)
end
---@param uri string
---@return boolean
local function is_windows_file_uri(uri)
return uri:match('^file:/+[a-zA-Z]:') ~= nil
end
---URI-encodes a string using percent escapes.
---@param str string string to encode
---@param rfc "rfc2396" | "rfc2732" | "rfc3986" | nil
---@return string encoded string
function M.uri_encode(str, rfc)
local pattern = PATTERNS[rfc] or PATTERNS.rfc3986
return (str:gsub('([' .. pattern .. '])', percent_encode_char)) -- clamped to 1 retval with ()
end
---URI-decodes a string containing percent escapes.
---@param str string string to decode
---@return string decoded string
function M.uri_decode(str)
return (str:gsub('%%([a-fA-F0-9][a-fA-F0-9])', hex_to_char)) -- clamped to 1 retval with ()
end
---Gets a URI from a file path.
---@param path string Path to file
---@return string URI
function M.uri_from_fname(path)
local volume_path, fname = path:match('^([a-zA-Z]:)(.*)') ---@type string?, string?
local is_windows = volume_path ~= nil
if is_windows then
assert(fname)
path = volume_path .. M.uri_encode(fname:gsub('\\', '/'))
else
path = M.uri_encode(path)
end
local uri_parts = { 'file://' }
if is_windows then
table.insert(uri_parts, '/')
end
table.insert(uri_parts, path)
return table.concat(uri_parts)
end
---Gets a URI from a bufnr.
---@param buf integer
---@return string URI
function M.uri_from_bufnr(buf)
local fname = vim.api.nvim_buf_get_name(buf)
local volume_path = fname:match('^([a-zA-Z]:).*')
local is_windows = volume_path ~= nil
local scheme ---@type string?
if is_windows then
fname = fname:gsub('\\', '/')
scheme = fname:match(WINDOWS_URI_SCHEME_PATTERN)
else
scheme = fname:match(URI_SCHEME_PATTERN)
end
if scheme then
return fname
else
return M.uri_from_fname(fname)
end
end
---Gets a filename from a URI.
---@param uri string
---@return string filename or unchanged URI for non-file URIs
function M.uri_to_fname(uri)
local scheme = uri:match(URI_SCHEME_PATTERN) or error('URI must contain a scheme: ' .. uri)
if scheme ~= 'file' then
return uri
end
local fragment_index = uri:find('#')
if fragment_index ~= nil then
uri = uri:sub(1, fragment_index - 1)
end
uri = M.uri_decode(uri)
--TODO improve this.
if is_windows_file_uri(uri) then
uri = uri:gsub('^file:/+', ''):gsub('/', '\\') --- @type string
else
uri = uri:gsub('^file:/+', '/') ---@type string
end
return uri
end
---Gets the buffer for a uri.
---Creates a new unloaded buffer if no buffer for the uri already exists.
---@param uri string
---@return integer bufnr
function M.uri_to_bufnr(uri)
return vim.fn.bufadd(M.uri_to_fname(uri))
end
return M