From 4b424a06c5bb642e8df40cc4195447ee1ccf5935 Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Thu, 30 Apr 2026 09:09:55 -0400 Subject: [PATCH] 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: https://github.com/microsoft/language-server-protocol/blob/0003fb53f18dc7a4ac7e51d0eb518deeea90fde5/_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> --- runtime/doc/lsp.txt | 3 +- runtime/doc/lua.txt | 22 ++-- runtime/doc/news.txt | 1 + runtime/doc/treesitter.txt | 41 ++++--- runtime/lua/editorconfig.lua | 16 +-- runtime/lua/vim/_core/shared.lua | 10 +- runtime/lua/vim/_inspector.lua | 26 ++--- runtime/lua/vim/_meta/regex.lua | 6 +- runtime/lua/vim/filetype.lua | 24 ++-- runtime/lua/vim/hl.lua | 18 +-- runtime/lua/vim/lsp.lua | 17 ++- runtime/lua/vim/lsp/client.lua | 23 ++-- runtime/lua/vim/snippet.lua | 16 +-- runtime/lua/vim/treesitter.lua | 84 ++++++------- runtime/lua/vim/treesitter/_fold.lua | 6 +- runtime/lua/vim/treesitter/dev.lua | 30 +++-- runtime/lua/vim/treesitter/languagetree.lua | 8 +- runtime/lua/vim/uri.lua | 6 +- src/gen/lint.lua | 123 ++------------------ test/functional/plugin/lsp_spec.lua | 46 ++++++++ 20 files changed, 243 insertions(+), 283 deletions(-) diff --git a/runtime/doc/lsp.txt b/runtime/doc/lsp.txt index dedc3744b8..8f805255b3 100644 --- a/runtime/doc/lsp.txt +++ b/runtime/doc/lsp.txt @@ -1706,7 +1706,8 @@ Lua module: vim.lsp.client *lsp-client* *vim.lsp.Client* Fields: ~ - • {attached_buffers} (`table`) + • {attached_buffers} (`table`) Each buffer's last + used `languageId`. • {cancel_request} (`fun(self: vim.lsp.Client, id: integer): boolean`) See |Client:cancel_request()|. • {capabilities} (`lsp.ClientCapabilities`) Capabilities diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt index b3662f7d82..28bd2c11eb 100644 --- a/runtime/doc/lua.txt +++ b/runtime/doc/lua.txt @@ -1523,7 +1523,7 @@ vim.str_utfindex({s}, {encoding}, {index}, {strict_indexing}) ============================================================================== Lua module: vim.inspector *vim.inspector* -vim.inspect_pos({bufnr}, {row}, {col}, {filter}) *vim.inspect_pos()* +vim.inspect_pos({buf}, {row}, {col}, {filter}) *vim.inspect_pos()* Get all the items at a given buffer position. Can also be pretty-printed with `:Inspect!`. *:Inspect!* @@ -1532,7 +1532,7 @@ vim.inspect_pos({bufnr}, {row}, {col}, {filter}) *vim.inspect_pos()* Since: 0.9.0 Parameters: ~ - • {bufnr} (`integer?`) defaults to the current buffer + • {buf} (`integer?`) defaults to the current buffer • {row} (`integer?`) row to inspect, 0-based. Defaults to the row of the current cursor • {col} (`integer?`) col to inspect, 0-based. Defaults to the col of @@ -1559,7 +1559,7 @@ vim.inspect_pos({bufnr}, {row}, {col}, {filter}) *vim.inspect_pos()* • row: the row used to get the items • col: the col used to get the items -vim.show_pos({bufnr}, {row}, {col}, {filter}) *vim.show_pos()* +vim.show_pos({buf}, {row}, {col}, {filter}) *vim.show_pos()* Show all the items at a given buffer position. Can also be shown with `:Inspect`. *:Inspect* @@ -1573,7 +1573,7 @@ vim.show_pos({bufnr}, {row}, {col}, {filter}) *vim.show_pos()* Since: 0.9.0 Parameters: ~ - • {bufnr} (`integer?`) defaults to the current buffer + • {buf} (`integer?`) defaults to the current buffer • {row} (`integer?`) row to inspect, 0-based. Defaults to the row of the current cursor • {col} (`integer?`) col to inspect, 0-based. Defaults to the col of @@ -2923,11 +2923,11 @@ vim.hl.priorities *vim.hl.priorities* symbols or `on_yank` autocommands *vim.hl.range()* -vim.hl.range({bufnr}, {ns}, {higroup}, {start}, {finish}, {opts}) +vim.hl.range({buf}, {ns}, {higroup}, {start}, {finish}, {opts}) Apply highlight group to range of text. Parameters: ~ - • {bufnr} (`integer`) Buffer number to apply highlighting to + • {buf} (`integer`) Buffer number to apply highlighting to • {ns} (`integer`) Namespace to add highlight to • {higroup} (`string`) Highlight group to use for highlighting • {start} (`[integer,integer]|string`) Start of region as a (line, @@ -4609,14 +4609,14 @@ within a single line. *regex:match_line()* -regex:match_line({bufnr}, {line_idx}, {start}, {end_}) - Matches line at `line_idx` (zero-based) in buffer `bufnr`. Match is +regex:match_line({buf}, {line_idx}, {start}, {end_}) + Matches line at `line_idx` (zero-based) in buffer `buf`. Match is restricted to byte index range `start` and `end_` if given, otherwise see |regex:match_str()|. Returned byte indices are relative to `start` if given. Parameters: ~ - • {bufnr} (`integer`) + • {buf} (`integer`) • {line_idx} (`integer`) • {start} (`integer?`) • {end_} (`integer?`) @@ -5237,11 +5237,11 @@ vim.uri_encode({str}, {rfc}) *vim.uri_encode()* Return: ~ (`string`) encoded string -vim.uri_from_bufnr({bufnr}) *vim.uri_from_bufnr()* +vim.uri_from_bufnr({buf}) *vim.uri_from_bufnr()* Gets a URI from a bufnr. Parameters: ~ - • {bufnr} (`integer`) + • {buf} (`integer`) Return: ~ (`string`) URI diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index 0cce01eef6..494757742a 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -61,6 +61,7 @@ LSP • Values < 0 are now treated as `nil` instead of 0. • Values outside the range of `signatures[activeSignature].parameters` are now treated as `nil` instead of `#signatures[activeSignature].parameters` +• `client.attached_buffers[buf]` now stores `languageId` string (was boolean). LUA diff --git a/runtime/doc/treesitter.txt b/runtime/doc/treesitter.txt index 690f69986b..be51e3d99e 100644 --- a/runtime/doc/treesitter.txt +++ b/runtime/doc/treesitter.txt @@ -964,18 +964,17 @@ foldexpr({lnum}) *vim.treesitter.foldexpr()* Return: ~ (`string`) - *vim.treesitter.get_captures_at_cursor()* -get_captures_at_cursor({winnr}) +get_captures_at_cursor({win}) *vim.treesitter.get_captures_at_cursor()* Returns a list of highlight capture names under the cursor Parameters: ~ - • {winnr} (`integer?`) |window-ID| or 0 for current window (default) + • {win} (`integer?`) |window-ID| or 0 for current window (default) Return: ~ (`string[]`) List of capture names *vim.treesitter.get_captures_at_pos()* -get_captures_at_pos({bufnr}, {row}, {col}) +get_captures_at_pos({buf}, {row}, {col}) Returns a list of highlight captures at the given position Each capture is represented by a table containing the capture name as a @@ -983,9 +982,9 @@ get_captures_at_pos({bufnr}, {row}, {col}) `conceal`, ...; empty if none are defined), and the id of the capture. Parameters: ~ - • {bufnr} (`integer`) Buffer number (0 for current buffer) - • {row} (`integer`) Position row - • {col} (`integer`) Position column + • {buf} (`integer`) Buffer number (0 for current buffer) + • {row} (`integer`) Position row + • {col} (`integer`) Position column Return: ~ (`{capture: string, lang: string, metadata: vim.treesitter.query.TSMetadata, id: integer}[]`) @@ -1044,7 +1043,7 @@ get_node_text({node}, {source}, {opts}) Return: ~ (`string`) -get_parser({bufnr}, {lang}, {opts}) *vim.treesitter.get_parser()* +get_parser({buf}, {lang}, {opts}) *vim.treesitter.get_parser()* Returns the parser for a specific buffer and attaches it to the buffer If needed, this will create the parser. @@ -1052,11 +1051,11 @@ get_parser({bufnr}, {lang}, {opts}) *vim.treesitter.get_parser()* If no parser can be created, nil (and an error message) is returned. Parameters: ~ - • {bufnr} (`integer?`) Buffer the parser should be tied to (default: - current buffer) - • {lang} (`string?`) Language of this parser (default: from buffer - filetype) - • {opts} (`table?`) Options to pass to the created language tree + • {buf} (`integer?`) Buffer the parser should be tied to (default: + current buffer) + • {lang} (`string?`) Language of this parser (default: from buffer + filetype) + • {opts} (`table?`) Options to pass to the created language tree Return (multiple): ~ (`vim.treesitter.LanguageTree?`) object to use for parsing @@ -1156,7 +1155,7 @@ node_contains({node}, {range}) *vim.treesitter.node_contains()* Return: ~ (`boolean`) True if the {node} contains the {range} -start({bufnr}, {lang}) *vim.treesitter.start()* +start({buf}, {lang}) *vim.treesitter.start()* Starts treesitter highlighting for a buffer Can be used in an ftplugin or FileType autocommand. @@ -1178,17 +1177,17 @@ start({bufnr}, {lang}) *vim.treesitter.start()* < Parameters: ~ - • {bufnr} (`integer?`) Buffer to be highlighted (default: current - buffer) - • {lang} (`string?`) Language of the parser (default: from buffer - filetype) + • {buf} (`integer?`) Buffer to be highlighted (default: current + buffer) + • {lang} (`string?`) Language of the parser (default: from buffer + filetype) -stop({bufnr}) *vim.treesitter.stop()* +stop({buf}) *vim.treesitter.stop()* Stops treesitter highlighting for a buffer Parameters: ~ - • {bufnr} (`integer?`) Buffer to stop highlighting (default: current - buffer) + • {buf} (`integer?`) Buffer to stop highlighting (default: current + buffer) ============================================================================== diff --git a/runtime/lua/editorconfig.lua b/runtime/lua/editorconfig.lua index f786afd1ed..472874c1a9 100644 --- a/runtime/lua/editorconfig.lua +++ b/runtime/lua/editorconfig.lua @@ -308,15 +308,15 @@ M.properties = properties --- @private --- Configure the given buffer with options from an `.editorconfig` file ---- @param bufnr integer Buffer number to configure -function M.config(bufnr) - bufnr = bufnr or vim.api.nvim_get_current_buf() - if not vim.api.nvim_buf_is_valid(bufnr) then +--- @param buf integer Buffer number to configure +function M.config(buf) + buf = buf or vim.api.nvim_get_current_buf() + if not vim.api.nvim_buf_is_valid(buf) then return end - local path = vim.fs.normalize(vim.api.nvim_buf_get_name(bufnr)) - if vim.bo[bufnr].buftype ~= '' or not vim.bo[bufnr].modifiable or path == '' then + local path = vim.fs.normalize(vim.api.nvim_buf_get_name(buf)) + if vim.bo[buf].buftype ~= '' or not vim.bo[buf].modifiable or path == '' then return end @@ -339,7 +339,7 @@ function M.config(bufnr) local func = M.properties[opt] if func then --- @type boolean, string? - local ok, err = pcall(func, bufnr, val, opts) + local ok, err = pcall(func, buf, val, opts) if ok then applied[opt] = val else @@ -349,7 +349,7 @@ function M.config(bufnr) end end - vim.b[bufnr].editorconfig = applied + vim.b[buf].editorconfig = applied end return M diff --git a/runtime/lua/vim/_core/shared.lua b/runtime/lua/vim/_core/shared.lua index e5bd833c87..6df4c85ae7 100644 --- a/runtime/lua/vim/_core/shared.lua +++ b/runtime/lua/vim/_core/shared.lua @@ -1669,14 +1669,14 @@ function vim._with(context, f) return vim._with_c(context, callback) end ---- @param bufnr? integer +--- @param buf? integer --- @return integer -function vim._resolve_bufnr(bufnr) - if bufnr == nil or bufnr == 0 then +function vim._resolve_bufnr(buf) + if buf == nil or buf == 0 then return vim.api.nvim_get_current_buf() end - vim.validate('bufnr', bufnr, 'number') - return bufnr + vim.validate('buf', buf, 'number') + return buf end --- @generic T diff --git a/runtime/lua/vim/_inspector.lua b/runtime/lua/vim/_inspector.lua index c08523716c..a7417b9b0b 100644 --- a/runtime/lua/vim/_inspector.lua +++ b/runtime/lua/vim/_inspector.lua @@ -30,7 +30,7 @@ local defaults = { ---Can also be pretty-printed with `:Inspect!`. [:Inspect!]() --- ---@since 11 ----@param bufnr? integer defaults to the current buffer +---@param buf? integer defaults to the current buffer ---@param row? integer row to inspect, 0-based. Defaults to the row of the current cursor ---@param col? integer col to inspect, 0-based. Defaults to the col of the current cursor ---@param filter? vim._inspector.Filter Table with key-value pairs to filter the items @@ -42,27 +42,27 @@ local defaults = { --- - buffer: the buffer used to get the items --- - row: the row used to get the items --- - col: the col used to get the items -function vim.inspect_pos(bufnr, row, col, filter) +function vim.inspect_pos(buf, row, col, filter) filter = vim.tbl_deep_extend('force', defaults, filter or {}) - bufnr = bufnr or 0 + buf = buf or 0 if row == nil or col == nil then -- get the row/col from the first window displaying the buffer - local win = bufnr == 0 and vim.api.nvim_get_current_win() or vim.fn.bufwinid(bufnr) + local win = buf == 0 and vim.api.nvim_get_current_win() or vim.fn.bufwinid(buf) if win == -1 then error('row/col is required for buffers not visible in a window') end local cursor = vim.api.nvim_win_get_cursor(win) row, col = cursor[1] - 1, cursor[2] end - bufnr = vim._resolve_bufnr(bufnr) + buf = vim._resolve_bufnr(buf) local results = { treesitter = {}, --- @type table[] syntax = {}, --- @type table[] extmarks = {}, semantic_tokens = {}, - buffer = bufnr, + buffer = buf, row = row, col = col, } @@ -79,7 +79,7 @@ function vim.inspect_pos(bufnr, row, col, filter) -- treesitter if filter.treesitter then - for _, capture in pairs(vim.treesitter.get_captures_at_pos(bufnr, row, col)) do + for _, capture in pairs(vim.treesitter.get_captures_at_pos(buf, row, col)) do --- @diagnostic disable-next-line: inject-field capture.hl_group = '@' .. capture.capture .. '.' .. capture.lang results.treesitter[#results.treesitter + 1] = resolve_hl(capture) @@ -87,8 +87,8 @@ function vim.inspect_pos(bufnr, row, col, filter) end -- syntax - if filter.syntax and vim.api.nvim_buf_is_valid(bufnr) then - vim._with({ buf = bufnr }, function() + if filter.syntax and vim.api.nvim_buf_is_valid(buf) then + vim._with({ buf = buf }, function() for _, i1 in ipairs(vim.fn.synstack(row + 1, col + 1)) do results.syntax[#results.syntax + 1] = resolve_hl({ hl_group = vim.fn.synIDattr(i1, 'name') }) @@ -124,7 +124,7 @@ function vim.inspect_pos(bufnr, row, col, filter) end -- All overlapping extmarks at this position: - local extmarks = vim.api.nvim_buf_get_extmarks(bufnr, -1, { row, col }, { row, col }, { + local extmarks = vim.api.nvim_buf_get_extmarks(buf, -1, { row, col }, { row, col }, { details = true, overlap = true, }) @@ -159,12 +159,12 @@ end ---``` --- ---@since 11 ----@param bufnr? integer defaults to the current buffer +---@param buf? integer defaults to the current buffer ---@param row? integer row to inspect, 0-based. Defaults to the row of the current cursor ---@param col? integer col to inspect, 0-based. Defaults to the col of the current cursor ---@param filter? vim._inspector.Filter -function vim.show_pos(bufnr, row, col, filter) - local items = vim.inspect_pos(bufnr, row, col, filter) +function vim.show_pos(buf, row, col, filter) + local items = vim.inspect_pos(buf, row, col, filter) local lines = { {} } diff --git a/runtime/lua/vim/_meta/regex.lua b/runtime/lua/vim/_meta/regex.lua index 37521b178a..f488ca5313 100644 --- a/runtime/lua/vim/_meta/regex.lua +++ b/runtime/lua/vim/_meta/regex.lua @@ -27,13 +27,13 @@ local regex = {} -- luacheck: no unused --- @return integer? # match end (byte index), or `nil` if no match function regex:match_str(str) end ---- Matches line at `line_idx` (zero-based) in buffer `bufnr`. Match is restricted to byte index +--- Matches line at `line_idx` (zero-based) in buffer `buf`. Match is restricted to byte index --- range `start` and `end_` if given, otherwise see |regex:match_str()|. Returned byte indices are --- relative to `start` if given. ---- @param bufnr integer +--- @param buf integer --- @param line_idx integer --- @param start? integer --- @param end_? integer --- @return integer? # match start (byte index) relative to `start`, or `nil` if no match --- @return integer? # match end (byte index) relative to `start`, or `nil` if no match -function regex:match_line(bufnr, line_idx, start, end_) end +function regex:match_line(buf, line_idx, start, end_) end diff --git a/runtime/lua/vim/filetype.lua b/runtime/lua/vim/filetype.lua index 5bd0382470..01bb69c435 100644 --- a/runtime/lua/vim/filetype.lua +++ b/runtime/lua/vim/filetype.lua @@ -41,34 +41,34 @@ local function starsetf(ft, priority) end --- Get a line range from the buffer. ----@param bufnr integer The buffer to get the lines from +---@param buf integer The buffer to get the lines from ---@param start_lnum integer|nil The line number of the first line (inclusive, 1-based) ---@param end_lnum integer|nil The line number of the last line (inclusive, 1-based) ---@return string[] # Array of lines -function M._getlines(bufnr, start_lnum, end_lnum) - if not bufnr or bufnr < 0 then +function M._getlines(buf, start_lnum, end_lnum) + if not buf or buf < 0 then return {} end if start_lnum then - return api.nvim_buf_get_lines(bufnr, start_lnum - 1, end_lnum or start_lnum, false) + return api.nvim_buf_get_lines(buf, start_lnum - 1, end_lnum or start_lnum, false) end -- Return all lines - return api.nvim_buf_get_lines(bufnr, 0, -1, false) + return api.nvim_buf_get_lines(buf, 0, -1, false) end --- Get a single line from the buffer. ----@param bufnr integer The buffer to get the lines from +---@param buf integer The buffer to get the lines from ---@param start_lnum integer The line number of the first line (inclusive, 1-based) ---@return string -function M._getline(bufnr, start_lnum) - if not bufnr or bufnr < 0 then +function M._getline(buf, start_lnum) + if not buf or buf < 0 then return '' end -- Return a single line - return api.nvim_buf_get_lines(bufnr, start_lnum - 1, start_lnum, false)[1] or '' + return api.nvim_buf_get_lines(buf, start_lnum - 1, start_lnum, false)[1] or '' end --- Check whether a string matches any of the given Lua patterns. @@ -90,12 +90,12 @@ end --- Get the next non-whitespace line in the buffer. --- ----@param bufnr integer The buffer to get the line from +---@param buf integer The buffer to get the line from ---@param start_lnum integer The line number of the first line to start from (inclusive, 1-based) ---@return string|nil line The first non-blank line if found or `nil` otherwise ---@return integer|nil lnum The line number of the first non-blank line or `nil` -function M._nextnonblank(bufnr, start_lnum) - for off, line in ipairs(M._getlines(bufnr, start_lnum, -1)) do +function M._nextnonblank(buf, start_lnum) + for off, line in ipairs(M._getlines(buf, start_lnum, -1)) do if not line:find('^%s*$') then return line, start_lnum + off - 1 end diff --git a/runtime/lua/vim/hl.lua b/runtime/lua/vim/hl.lua index aeae11a123..3a3e3b4edb 100644 --- a/runtime/lua/vim/hl.lua +++ b/runtime/lua/vim/hl.lua @@ -38,7 +38,7 @@ M.priorities = { --- Apply highlight group to range of text. --- ----@param bufnr integer Buffer number to apply highlighting to +---@param buf integer Buffer number to apply highlighting to ---@param ns integer Namespace to add highlight to ---@param higroup string Highlight group to use for highlighting ---@param start [integer,integer]|string Start of region as a (line, column) tuple or string accepted by |getpos()| @@ -48,7 +48,7 @@ M.priorities = { --- highlight has left --- @return fun()? range_clear A function which allows clearing the highlight manually. --- nil is returned if timeout is not specified -function M.range(bufnr, ns, higroup, start, finish, opts) +function M.range(buf, ns, higroup, start, finish, opts) opts = opts or {} local regtype = opts.regtype or 'v' local inclusive = opts.inclusive or false @@ -59,20 +59,20 @@ function M.range(bufnr, ns, higroup, start, finish, opts) local pos1 = type(start) == 'string' and vim.fn.getpos(start) or { - bufnr, + buf, start[1] + 1, start[2] ~= -1 and start[2] ~= v_maxcol and start[2] + 1 or v_maxcol, 0, } local pos2 = type(finish) == 'string' and vim.fn.getpos(finish) or { - bufnr, + buf, finish[1] + 1, finish[2] ~= -1 and start[2] ~= v_maxcol and finish[2] + 1 or v_maxcol, 0, } - local buf_line_count = api.nvim_buf_line_count(bufnr) + local buf_line_count = api.nvim_buf_line_count(buf) pos1[2] = math.min(pos1[2], buf_line_count) pos2[2] = math.min(pos2[2], buf_line_count) @@ -80,7 +80,7 @@ function M.range(bufnr, ns, higroup, start, finish, opts) return end - vim._with({ buf = bufnr }, function() + vim._with({ buf = buf }, function() if pos1[3] ~= v_maxcol then local max_col1 = vim.fn.col({ pos1[2], '$' }) pos1[3] = math.min(pos1[3], max_col1) @@ -119,7 +119,7 @@ function M.range(bufnr, ns, higroup, start, finish, opts) local end_col = res[2][3] table.insert( extmarks, - api.nvim_buf_set_extmark(bufnr, ns, start_row, start_col, { + api.nvim_buf_set_extmark(buf, ns, start_row, start_col, { hl_group = higroup, end_row = end_row, end_col = end_col, @@ -130,11 +130,11 @@ function M.range(bufnr, ns, higroup, start, finish, opts) end local range_hl_clear = function() - if not api.nvim_buf_is_valid(bufnr) then + if not api.nvim_buf_is_valid(buf) then return end for _, mark in ipairs(extmarks) do - api.nvim_buf_del_extmark(bufnr, ns, mark) + api.nvim_buf_del_extmark(buf, ns, mark) end end diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 1869f1a106..cc6518aa01 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -531,9 +531,20 @@ local function lsp_enable_callback(bufnr) lsp.is_enabled(client.name) -- Check that the client is managed by vim.lsp.config before deciding to detach it! and lsp.config[client.name] - and not can_start(bufnr, lsp.config[client.name], false) then - lsp.buf_detach_client(bufnr, client.id) + if can_start(bufnr, lsp.config[client.name], false) then + -- When switch between lsp supported filetype (e.g. json to jsonc like #39498), + -- client should send `textDocument/didClose` + `textDocument/didOpen` with new language id + local new_language_id = client.get_language_id(bufnr, vim.bo[bufnr].filetype) + local old_language_id = client.attached_buffers[bufnr] ---@type string? + if old_language_id and old_language_id ~= new_language_id then + client:_text_document_did_close_handler(bufnr) + client.attached_buffers[bufnr] = new_language_id + client:_text_document_did_open_handler(bufnr) + end + else + lsp.buf_detach_client(bufnr, client.id) + end end end @@ -1002,7 +1013,7 @@ function lsp.buf_attach_client(bufnr, client_id) return true end - client.attached_buffers[bufnr] = true + client.attached_buffers[bufnr] = client.get_language_id(bufnr, vim.bo[bufnr].filetype) -- This is our first time attaching this client to this buffer. -- Send didOpen for the client if it is initialized. If it isn't initialized diff --git a/runtime/lua/vim/lsp/client.lua b/runtime/lua/vim/lsp/client.lua index 957364d019..7692759fb8 100644 --- a/runtime/lua/vim/lsp/client.lua +++ b/runtime/lua/vim/lsp/client.lua @@ -154,7 +154,8 @@ local all_clients = {} --- @class vim.lsp.Client --- ---- @field attached_buffers table +--- Each buffer's last used `languageId`. +--- @field attached_buffers table --- --- Capabilities provided by the client (editor or tool), at startup. --- @field capabilities lsp.ClientCapabilities @@ -1109,6 +1110,18 @@ function Client:exec_cmd(cmd, context, handler) self:request('workspace/executeCommand', params, handler, context.bufnr) end +--- Default handler for the 'textDocument/didClose' LSP notification. +--- +--- @param buf integer Number of the buffer, or 0 for current +function Client:_text_document_did_close_handler(buf) + if not self:supports_method('textDocument/didClose') then + return + end + local uri = vim.uri_from_bufnr(buf) + local params = { textDocument = { uri = uri } } + self:notify('textDocument/didClose', params) +end + --- Default handler for the 'textDocument/didOpen' LSP notification. --- --- @param bufnr integer Number of the buffer, or 0 for current @@ -1177,7 +1190,7 @@ function Client:on_attach(bufnr) end end) - self.attached_buffers[bufnr] = true + self.attached_buffers[bufnr] = self:_get_language_id(bufnr) end --- @private @@ -1368,11 +1381,7 @@ function Client:_on_detach(bufnr) changetracking.reset_buf(self, bufnr) - if self:supports_method('textDocument/didClose') then - local uri = vim.uri_from_bufnr(bufnr) - local params = { textDocument = { uri = uri } } - self:notify('textDocument/didClose', params) - end + self:_text_document_did_close_handler(bufnr) self.attached_buffers[bufnr] = nil diff --git a/runtime/lua/vim/snippet.lua b/runtime/lua/vim/snippet.lua index 6f9c4358c3..cd52927d3f 100644 --- a/runtime/lua/vim/snippet.lua +++ b/runtime/lua/vim/snippet.lua @@ -114,13 +114,13 @@ local Tabstop = {} --- --- @package --- @param index integer ---- @param bufnr integer +--- @param buf integer --- @param placement integer --- @param range Range4 --- @param choices? string[] --- @return vim.snippet.Tabstop -function Tabstop.new(index, bufnr, placement, range, choices) - local extmark_id = vim.api.nvim_buf_set_extmark(bufnr, snippet_ns, range[1], range[2], { +function Tabstop.new(index, buf, placement, range, choices) + local extmark_id = vim.api.nvim_buf_set_extmark(buf, snippet_ns, range[1], range[2], { right_gravity = true, end_right_gravity = false, end_line = range[3], @@ -130,7 +130,7 @@ function Tabstop.new(index, bufnr, placement, range, choices) local self = setmetatable({ extmark_id = extmark_id, - bufnr = bufnr, + bufnr = buf, index = index, placement = placement, choices = choices, @@ -216,17 +216,17 @@ local Session = {} --- Creates a new snippet session in the current buffer. --- --- @package ---- @param bufnr integer +--- @param buf integer --- @param snippet_extmark integer --- @param tabstop_data table --- @return vim.snippet.Session -function Session.new(bufnr, snippet_extmark, tabstop_data) +function Session.new(buf, snippet_extmark, tabstop_data) local self = setmetatable({ - bufnr = bufnr, + bufnr = buf, extmark_id = snippet_extmark, tabstops = {}, tabstop_placements = {}, - current_tabstop = Tabstop.new(0, bufnr, 0, { 0, 0, 0, 0 }), + current_tabstop = Tabstop.new(0, buf, 0, { 0, 0, 0, 0 }), tab_keymaps = { i = nil, s = nil }, shift_tab_keymaps = { i = nil, s = nil }, }, { __index = Session }) diff --git a/runtime/lua/vim/treesitter.lua b/runtime/lua/vim/treesitter.lua index 13657571ca..9fb4182d1b 100644 --- a/runtime/lua/vim/treesitter.lua +++ b/runtime/lua/vim/treesitter.lua @@ -26,23 +26,23 @@ M.minimum_language_version = vim._ts_get_minimum_language_version() --- --- It is not recommended to use this; use |get_parser()| instead. --- ----@param bufnr integer Buffer the parser will be tied to (0 for current buffer) +---@param buf integer Buffer the parser will be tied to (0 for current buffer) ---@param lang string Language of the parser ---@param opts (table|nil) Options to pass to the created language tree --- ---@return vim.treesitter.LanguageTree object to use for parsing -function M._create_parser(bufnr, lang, opts) - bufnr = vim._resolve_bufnr(bufnr) +function M._create_parser(buf, lang, opts) + buf = vim._resolve_bufnr(buf) - local self = LanguageTree.new(bufnr, lang, opts) + local self = LanguageTree.new(buf, lang, opts) local function bytes_cb(_, ...) self:_on_bytes(...) end local function detach_cb(_, ...) - if parsers[bufnr] == self then - parsers[bufnr] = nil + if parsers[buf] == self then + parsers[buf] = nil end self:_on_detach(...) end @@ -72,41 +72,41 @@ end --- --- If no parser can be created, nil (and an error message) is returned. --- ----@param bufnr (integer|nil) Buffer the parser should be tied to (default: current buffer) +---@param buf (integer|nil) Buffer the parser should be tied to (default: current buffer) ---@param lang (string|nil) Language of this parser (default: from buffer filetype) ---@param opts (table|nil) Options to pass to the created language tree --- ---@return vim.treesitter.LanguageTree? object to use for parsing ---@return string? error message, if applicable -function M.get_parser(bufnr, lang, opts) +function M.get_parser(buf, lang, opts) opts = opts or {} - bufnr = vim._resolve_bufnr(bufnr) + buf = vim._resolve_bufnr(buf) if not valid_lang(lang) then - lang = M.language.get_lang(vim.bo[bufnr].filetype) + lang = M.language.get_lang(vim.bo[buf].filetype) end if not valid_lang(lang) then - if not parsers[bufnr] then + if not parsers[buf] then return nil, - string.format('Parser not found for buffer %s: language could not be determined', bufnr) + string.format('Parser not found for buffer %s: language could not be determined', buf) end - elseif parsers[bufnr] == nil or parsers[bufnr]:lang() ~= lang then - if not api.nvim_buf_is_loaded(bufnr) then - return nil, string.format('Buffer %s must be loaded to create parser', bufnr) + elseif parsers[buf] == nil or parsers[buf]:lang() ~= lang then + if not api.nvim_buf_is_loaded(buf) then + return nil, string.format('Buffer %s must be loaded to create parser', buf) end - local parser = vim.F.npcall(M._create_parser, bufnr, lang, opts) + local parser = vim.F.npcall(M._create_parser, buf, lang, opts) if not parser then return nil, - string.format('Parser could not be created for buffer %s and language "%s"', bufnr, lang) + string.format('Parser could not be created for buffer %s and language "%s"', buf, lang) end - parsers[bufnr] = parser + parsers[buf] = parser end - parsers[bufnr]:register_cbs(opts.buf_attach_cbs) + parsers[buf]:register_cbs(opts.buf_attach_cbs) - return parsers[bufnr] + return parsers[buf] end --- Returns a string parser @@ -268,14 +268,14 @@ end --- language, a table of metadata (`priority`, `conceal`, ...; empty if none are defined), and the --- id of the capture. --- ----@param bufnr integer Buffer number (0 for current buffer) +---@param buf integer Buffer number (0 for current buffer) ---@param row integer Position row ---@param col integer Position column --- ---@return {capture: string, lang: string, metadata: vim.treesitter.query.TSMetadata, id: integer}[] -function M.get_captures_at_pos(bufnr, row, col) - bufnr = vim._resolve_bufnr(bufnr) - local buf_highlighter = M.highlighter.active[bufnr] +function M.get_captures_at_pos(buf, row, col) + buf = vim._resolve_bufnr(buf) + local buf_highlighter = M.highlighter.active[buf] if not buf_highlighter then return {} @@ -328,13 +328,13 @@ end --- Returns a list of highlight capture names under the cursor --- ----@param winnr (integer|nil): |window-ID| or 0 for current window (default) +---@param win (integer|nil): |window-ID| or 0 for current window (default) --- ---@return string[] List of capture names -function M.get_captures_at_cursor(winnr) - winnr = winnr or 0 - local bufnr = api.nvim_win_get_buf(winnr) - local cursor = api.nvim_win_get_cursor(winnr) +function M.get_captures_at_cursor(win) + win = win or 0 + local bufnr = api.nvim_win_get_buf(win) + local cursor = api.nvim_win_get_cursor(win) local data = M.get_captures_at_pos(bufnr, cursor[1] - 1, cursor[2]) @@ -434,30 +434,30 @@ end --- }) --- ``` --- ----@param bufnr integer? Buffer to be highlighted (default: current buffer) +---@param buf integer? Buffer to be highlighted (default: current buffer) ---@param lang string? Language of the parser (default: from buffer filetype) -function M.start(bufnr, lang) - bufnr = vim._resolve_bufnr(bufnr) +function M.start(buf, lang) + buf = vim._resolve_bufnr(buf) -- Ensure buffer is loaded. `:edit` over `bufload()` to show swapfile prompt. - if not api.nvim_buf_is_loaded(bufnr) then - if api.nvim_buf_get_name(bufnr) ~= '' then - pcall(api.nvim_buf_call, bufnr, vim.cmd.edit) + if not api.nvim_buf_is_loaded(buf) then + if api.nvim_buf_get_name(buf) ~= '' then + pcall(api.nvim_buf_call, buf, vim.cmd.edit) else - vim.fn.bufload(bufnr) + vim.fn.bufload(buf) end end - local parser = assert(M.get_parser(bufnr, lang)) + local parser = assert(M.get_parser(buf, lang)) M.highlighter.new(parser) end --- Stops treesitter highlighting for a buffer --- ----@param bufnr (integer|nil) Buffer to stop highlighting (default: current buffer) -function M.stop(bufnr) - bufnr = vim._resolve_bufnr(bufnr) +---@param buf (integer|nil) Buffer to stop highlighting (default: current buffer) +function M.stop(buf) + buf = vim._resolve_bufnr(buf) - if M.highlighter.active[bufnr] then - M.highlighter.active[bufnr]:destroy() + if M.highlighter.active[buf] then + M.highlighter.active[buf]:destroy() end end diff --git a/runtime/lua/vim/treesitter/_fold.lua b/runtime/lua/vim/treesitter/_fold.lua index 14d03e96cb..ce3288af65 100644 --- a/runtime/lua/vim/treesitter/_fold.lua +++ b/runtime/lua/vim/treesitter/_fold.lua @@ -26,12 +26,12 @@ local FoldInfo = {} FoldInfo.__index = FoldInfo ---@private ----@param bufnr integer -function FoldInfo.new(bufnr) +---@param buf integer +function FoldInfo.new(buf) return setmetatable({ levels0 = {}, levels = {}, - parser = ts.get_parser(bufnr, nil), + parser = ts.get_parser(buf, nil), }, FoldInfo) end diff --git a/runtime/lua/vim/treesitter/dev.lua b/runtime/lua/vim/treesitter/dev.lua index 853705bcba..a1dd988497 100644 --- a/runtime/lua/vim/treesitter/dev.lua +++ b/runtime/lua/vim/treesitter/dev.lua @@ -69,24 +69,20 @@ end --- Create a new treesitter view. --- ----@param bufnr integer Source buffer number +---@param buf integer Source buffer number ---@param lang string|nil Language of source buffer --- ---@return vim.treesitter.dev.TSTreeView|nil ---@return string|nil Error message, if any --- ---@package -function TSTreeView:new(bufnr, lang) - bufnr = bufnr or 0 - lang = lang or vim.treesitter.language.get_lang(vim.bo[bufnr].filetype) - local parser = vim.treesitter.get_parser(bufnr, lang) +function TSTreeView:new(buf, lang) + buf = buf or 0 + lang = lang or vim.treesitter.language.get_lang(vim.bo[buf].filetype) + local parser = vim.treesitter.get_parser(buf, lang) if not parser then return nil, - string.format( - 'Failed to create TSTreeView for buffer %s: no parser for lang "%s"', - bufnr, - lang - ) + string.format('Failed to create TSTreeView for buffer %s: no parser for lang "%s"', buf, lang) end -- For each child tree (injected language), find the root of the tree and locate the node within @@ -232,10 +228,10 @@ end --- --- Calling this function computes the text that is displayed for each node. --- ----@param bufnr integer Buffer number to write into. +---@param buf integer Buffer number to write into. ---@package -function TSTreeView:draw(bufnr) - vim.bo[bufnr].modifiable = true +function TSTreeView:draw(buf) + vim.bo[buf].modifiable = true local lines = {} ---@type string[] local lang_hl_marks = {} ---@type table[] @@ -284,18 +280,18 @@ function TSTreeView:draw(bufnr) lines[i] = line end - api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) + api.nvim_buf_set_lines(buf, 0, -1, false, lines) - api.nvim_buf_clear_namespace(bufnr, decor_ns, 0, -1) + api.nvim_buf_clear_namespace(buf, decor_ns, 0, -1) for i, m in ipairs(lang_hl_marks) do - api.nvim_buf_set_extmark(bufnr, decor_ns, i - 1, m.col, { + api.nvim_buf_set_extmark(buf, decor_ns, i - 1, m.col, { hl_group = 'Title', end_col = m.end_col, }) end - vim.bo[bufnr].modifiable = false + vim.bo[buf].modifiable = false end --- Get node {i} from this View. diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index 4da4ea3ee1..28a069f40d 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -1231,7 +1231,7 @@ function LanguageTree:_edit( end end ----@param bufnr integer +---@param buf integer ---@param changed_tick integer ---@param start_row integer ---@param start_col integer @@ -1243,7 +1243,7 @@ end ---@param new_col integer ---@param new_byte integer function LanguageTree:_on_bytes( - bufnr, + buf, changed_tick, start_row, start_col, @@ -1260,7 +1260,7 @@ function LanguageTree:_on_bytes( self:_log( 'on_bytes', - bufnr, + buf, changed_tick, start_row, start_col, @@ -1288,7 +1288,7 @@ function LanguageTree:_on_bytes( self:_do_callback( 'bytes', - bufnr, + buf, changed_tick, start_row, start_col, diff --git a/runtime/lua/vim/uri.lua b/runtime/lua/vim/uri.lua index 95a5183d73..51e8de7947 100644 --- a/runtime/lua/vim/uri.lua +++ b/runtime/lua/vim/uri.lua @@ -77,10 +77,10 @@ function M.uri_from_fname(path) end ---Gets a URI from a bufnr. ----@param bufnr integer +---@param buf integer ---@return string URI -function M.uri_from_bufnr(bufnr) - local fname = vim.api.nvim_buf_get_name(bufnr) +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? diff --git a/src/gen/lint.lua b/src/gen/lint.lua index 442e5c2b9a..de2db2b302 100644 --- a/src/gen/lint.lua +++ b/src/gen/lint.lua @@ -25,6 +25,8 @@ local banned_verbs = { disable = 'enable', exit = 'cancel', -- or "stop" -- format = 'fmt', + -- hide = '?', + -- show = '?', list = 'get', notify = 'print', -- or "echo" pretty = 'fmt', @@ -57,40 +59,10 @@ local legacy_names = { nvim_list_uis = true, nvim_list_wins = true, }, - ['runtime/lua/vim/diagnostic.lua'] = { - count = true, - get = true, - hide = true, - reset = true, - set = true, - show = true, - status = true, - }, - ['runtime/lua/editorconfig.lua'] = { - config = true, - }, ['runtime/lua/vim/uri.lua'] = { uri_from_bufnr = true, uri_to_bufnr = true, }, - ['runtime/lua/vim/snippet.lua'] = { - new = true, - }, - ['runtime/lua/vim/hl.lua'] = { - range = true, - }, - ['runtime/lua/vim/filetype.lua'] = { - _getline = true, - _getlines = true, - _nextnonblank = true, - }, - ['runtime/lua/vim/_meta/regex.lua'] = { - match_line = true, - }, - ['runtime/lua/vim/_inspector.lua'] = { - ['vim.inspect_pos'] = true, - ['vim.show_pos'] = true, - }, ['runtime/lua/vim/_core/shared.lua'] = { _ensure_list = true, _list_insert = true, @@ -100,97 +72,23 @@ local legacy_names = { tbl_contains = true, }, ['runtime/lua/vim/lsp.lua'] = { - _buf_get_full_text = true, - _buf_get_line_ending = true, - _set_defaults = true, - buf_attach_client = true, - buf_detach_client = true, - buf_is_attached = true, buf_notify = true, - buf_request = true, - buf_request_all = true, - buf_request_sync = true, - }, - ['runtime/lua/vim/lsp/_folding_range.lua'] = { - new = true, - }, - ['runtime/lua/vim/lsp/_changetracking.lua'] = { - _send_did_save = true, - flush = true, - init = true, - reset_buf = true, - send_changes = true, - }, - ['runtime/lua/vim/lsp/_capability.lua'] = { - new = true, - }, - ['runtime/lua/vim/lsp/semantic_tokens.lua'] = { - _start = true, - force_refresh = true, - get_at_pos = true, - highlight_token = true, - new = true, - }, - ['runtime/lua/vim/lsp/document_color.lua'] = { - new = true, - }, - ['runtime/lua/vim/lsp/diagnostic.lua'] = { - _enable = true, - _refresh = true, - get_line_diagnostics = true, - }, - ['runtime/lua/vim/lsp/completion.lua'] = { - enable = true, - request = true, - }, - ['runtime/lua/vim/lsp/codelens.lua'] = { - new = true, }, ['runtime/lua/vim/lsp/client.lua'] = { - _get_registrations = true, - _on_detach = true, _on_exit = true, _process_request = true, _process_static_registrations = true, _remove_workspace_folder = true, - _text_document_did_open_handler = true, - on_attach = true, - request = true, - request_sync = true, - supports_method = true, }, ['runtime/lua/vim/lsp/util.lua'] = { - _make_line_range_params = true, - apply_text_edits = true, - buf_clear_references = true, buf_highlight_references = true, - get_effective_tabstop = true, - make_given_range_params = true, make_position_params = true, - make_text_document_params = true, - symbols_to_items = true, }, ['runtime/lua/vim/lsp/rpc.lua'] = { _notify = true, }, ['runtime/lua/vim/treesitter.lua'] = { - _create_parser = true, - get_captures_at_cursor = true, - get_captures_at_pos = true, - get_parser = true, node_contains = true, - start = true, - stop = true, - }, - ['runtime/lua/vim/treesitter/languagetree.lua'] = { - _on_bytes = true, - }, - ['runtime/lua/vim/treesitter/dev.lua'] = { - draw = true, - new = true, - }, - ['runtime/lua/vim/treesitter/_fold.lua'] = { - new = true, }, ['runtime/lua/vim/treesitter/highlighter.lua'] = { for_each_highlight_state = true, @@ -232,7 +130,7 @@ local legacy_fields = { bufnr = true, }, ['TS.Heading'] = { - bufnr = true, + bufnr = true, -- Passed to setloclist(). }, ['vim.treesitter.get_node.Opts'] = { bufnr = true, @@ -282,14 +180,13 @@ function M.lint_names(source, api_funs, keysets, classes) local src_legacy = legacy_names[source] or {} for _, fun in ipairs(api_funs) do if fun.name and fun.params and not fun.deprecated and not fun.deprecated_since then - -- Positional parameter names. - if not src_legacy[fun.name] then - for _, p in ipairs(fun.params) do - local want_name = banned_nouns[p.name] - if want_name then - local msg = '%s: %s(): param "%s" should be renamed to "%s"' - errors[#errors + 1] = fmt(msg, source, fun.name, p.name, want_name) - end + -- Positional parameter names: always checked (no legacy allowed). + -- Exception: "bufnr" is allowed as a param name. + for _, p in ipairs(fun.params) do + local want_name = banned_nouns[p.name] + if want_name and p.name ~= 'bufnr' then + local msg = '%s: %s(): param "%s" should be renamed to "%s"' + errors[#errors + 1] = fmt(msg, source, fun.name, p.name, want_name) end end diff --git a/test/functional/plugin/lsp_spec.lua b/test/functional/plugin/lsp_spec.lua index 4c04793079..b9bbfba7c0 100644 --- a/test/functional/plugin/lsp_spec.lua +++ b/test/functional/plugin/lsp_spec.lua @@ -4526,6 +4526,52 @@ describe('LSP', function() eq({ 0, 'foo', 1, 'bar' }, count_clients()) end) + it('sends didClose and didOpen when languageId changes', function() + exec_lua(create_server_definition) + + local tmp1 = t.tmpname(true) + + exec_lua(function() + _G.server = _G._create_server({ + handlers = { + initialize = function(_, _, callback) + callback(nil, { capabilities = { textDocumentSync = { openClose = true } } }) + end, + }, + }) + vim.lsp.config('foo', { cmd = _G.server.cmd, filetypes = { 'foo', 'bar' } }) + vim.lsp.enable('foo') + vim.cmd.edit(tmp1) + end) + + local function test_messages() + local opens = 0 + local closes = 0 + local msgs = exec_lua([[ return _G.server.messages ]]) + local num_clients = exec_lua([[ return #vim.lsp.get_clients() ]]) + for _, msg in ipairs(msgs) do + opens = opens + (msg.method == 'textDocument/didOpen' and 1 or 0) + closes = closes + (msg.method == 'textDocument/didClose' and 1 or 0) + end + return { opens, 'did_open', closes, 'did_close', num_clients, 'clients' } + end + + -- No filetype on the buffer yet. + eq({ 0, 'did_open', 0, 'did_close', 0, 'clients' }, test_messages()) + + -- Set the filetype to 'foo', confirm didOpen is sent. + exec_lua([[vim.bo.filetype = 'foo']]) + retry(nil, 1000, function() + eq({ 1, 'did_open', 0, 'did_close', 1, 'clients' }, test_messages()) + end) + + -- Set to anohter lsp-supported filetype 'bar', confirm didClose and didOpen are sent. + exec_lua([[vim.bo.filetype = 'bar']]) + retry(nil, 1000, function() + eq({ 2, 'did_open', 1, 'did_close', 1, 'clients' }, test_messages()) + end) + end) + it('validates config on attach', function() local tmp1 = t.tmpname(true) exec_lua(function()