mirror of
https://github.com/neovim/neovim.git
synced 2026-06-15 16:23:48 +00:00
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>
131 lines
3.6 KiB
Lua
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
|