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>
This commit is contained in:
Justin M. Keyes
2026-04-30 09:09:55 -04:00
committed by GitHub
parent d147d0434d
commit 4b424a06c5
20 changed files with 243 additions and 283 deletions

View File

@@ -1706,7 +1706,8 @@ Lua module: vim.lsp.client *lsp-client*
*vim.lsp.Client*
Fields: ~
• {attached_buffers} (`table<integer,true>`)
• {attached_buffers} (`table<integer,string>`) Each buffer's last
used `languageId`.
• {cancel_request} (`fun(self: vim.lsp.Client, id: integer): boolean`)
See |Client:cancel_request()|.
• {capabilities} (`lsp.ClientCapabilities`) Capabilities

View File

@@ -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

View File

@@ -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

View File

@@ -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)
==============================================================================

View File

@@ -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

View File

@@ -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

View File

@@ -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 = { {} }

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -154,7 +154,8 @@ local all_clients = {}
--- @class vim.lsp.Client
---
--- @field attached_buffers table<integer,true>
--- Each buffer's last used `languageId`.
--- @field attached_buffers table<integer,string>
---
--- 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

View File

@@ -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<integer, { placement: integer, range: Range4, choices?: string[] }[]>
--- @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 })

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -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,

View File

@@ -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?

View File

@@ -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

View File

@@ -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()