mirror of
https://github.com/neovim/neovim.git
synced 2025-09-06 11:28:22 +00:00
feat(lsp): support textDocument/foldingRange
(#31311)
* refactor(shared): extract `vim._list_insert` and `vim._list_remove` * feat(lsp): add `vim.lsp.foldexpr()` * docs(lsp): add a todo for state management * feat(lsp): add `vim.lsp.folding_range.foldclose()` * feat(lsp): schedule `foldclose()` if the buffer is not up-to-date * feat(lsp): add `vim.lsp.foldtext()` * feat(lsp): support multiple folding range providers * refactor(lsp): expose all folding related functions under `vim.lsp.*` * perf(lsp): add `lsp.MultiHandler` for do `foldupdate()` only once
This commit is contained in:
@@ -204,6 +204,7 @@ won't run if your server doesn't support them.
|
|||||||
- `'textDocument/diagnostic'`
|
- `'textDocument/diagnostic'`
|
||||||
- `'textDocument/documentHighlight'`
|
- `'textDocument/documentHighlight'`
|
||||||
- `'textDocument/documentSymbol'`
|
- `'textDocument/documentSymbol'`
|
||||||
|
- `'textDocument/foldingRange'`
|
||||||
- `'textDocument/formatting'`
|
- `'textDocument/formatting'`
|
||||||
- `'textDocument/hover'`
|
- `'textDocument/hover'`
|
||||||
- `'textDocument/implementation'`
|
- `'textDocument/implementation'`
|
||||||
@@ -697,6 +698,35 @@ commands *vim.lsp.commands*
|
|||||||
|
|
||||||
The second argument is the `ctx` of |lsp-handler|
|
The second argument is the `ctx` of |lsp-handler|
|
||||||
|
|
||||||
|
foldclose({kind}, {winid}) *vim.lsp.foldclose()*
|
||||||
|
Close all {kind} of folds in the the window with {winid}.
|
||||||
|
|
||||||
|
To automatically fold imports when opening a file, you can use an autocmd: >lua
|
||||||
|
vim.api.nvim_create_autocmd('LspNotify', {
|
||||||
|
callback = function(args)
|
||||||
|
if args.data.method == 'textDocument/didOpen' then
|
||||||
|
vim.lsp.foldclose('imports', vim.fn.bufwinid(args.buf))
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
<
|
||||||
|
|
||||||
|
Parameters: ~
|
||||||
|
• {kind} (`lsp.FoldingRangeKind`) Kind to close, one of "comment",
|
||||||
|
"imports" or "region".
|
||||||
|
• {winid} (`integer?`) Defaults to the current window.
|
||||||
|
|
||||||
|
foldexpr({lnum}) *vim.lsp.foldexpr()*
|
||||||
|
Provides an interface between the built-in client and a `foldexpr`
|
||||||
|
function.
|
||||||
|
|
||||||
|
Parameters: ~
|
||||||
|
• {lnum} (`integer`) line number
|
||||||
|
|
||||||
|
foldtext() *vim.lsp.foldtext()*
|
||||||
|
Provides a `foldtext` function that shows the `collapsedText` retrieved,
|
||||||
|
defaults to the first folded line if `collapsedText` is not provided.
|
||||||
|
|
||||||
formatexpr({opts}) *vim.lsp.formatexpr()*
|
formatexpr({opts}) *vim.lsp.formatexpr()*
|
||||||
Provides an interface between the built-in client and a `formatexpr`
|
Provides an interface between the built-in client and a `formatexpr`
|
||||||
function.
|
function.
|
||||||
|
@@ -230,6 +230,8 @@ LSP
|
|||||||
• |vim.lsp.buf.hover()| now highlights hover ranges using the
|
• |vim.lsp.buf.hover()| now highlights hover ranges using the
|
||||||
|hl-LspReferenceTarget| highlight group.
|
|hl-LspReferenceTarget| highlight group.
|
||||||
• Functions in |vim.lsp.Client| can now be called as methods.
|
• Functions in |vim.lsp.Client| can now be called as methods.
|
||||||
|
• Implemented LSP folding: |vim.lsp.foldexpr()|
|
||||||
|
https://microsoft.github.io/language-server-protocol/specification/#textDocument_foldingRange
|
||||||
|
|
||||||
LUA
|
LUA
|
||||||
|
|
||||||
|
@@ -3,6 +3,7 @@ local validate = vim.validate
|
|||||||
|
|
||||||
local lsp = vim._defer_require('vim.lsp', {
|
local lsp = vim._defer_require('vim.lsp', {
|
||||||
_changetracking = ..., --- @module 'vim.lsp._changetracking'
|
_changetracking = ..., --- @module 'vim.lsp._changetracking'
|
||||||
|
_folding_range = ..., --- @module 'vim.lsp._folding_range'
|
||||||
_snippet_grammar = ..., --- @module 'vim.lsp._snippet_grammar'
|
_snippet_grammar = ..., --- @module 'vim.lsp._snippet_grammar'
|
||||||
_tagfunc = ..., --- @module 'vim.lsp._tagfunc'
|
_tagfunc = ..., --- @module 'vim.lsp._tagfunc'
|
||||||
_watchfiles = ..., --- @module 'vim.lsp._watchfiles'
|
_watchfiles = ..., --- @module 'vim.lsp._watchfiles'
|
||||||
@@ -57,6 +58,7 @@ lsp._request_name_to_capability = {
|
|||||||
[ms.textDocument_documentHighlight] = { 'documentHighlightProvider' },
|
[ms.textDocument_documentHighlight] = { 'documentHighlightProvider' },
|
||||||
[ms.textDocument_documentLink] = { 'documentLinkProvider' },
|
[ms.textDocument_documentLink] = { 'documentLinkProvider' },
|
||||||
[ms.textDocument_documentSymbol] = { 'documentSymbolProvider' },
|
[ms.textDocument_documentSymbol] = { 'documentSymbolProvider' },
|
||||||
|
[ms.textDocument_foldingRange] = { 'foldingRangeProvider' },
|
||||||
[ms.textDocument_formatting] = { 'documentFormattingProvider' },
|
[ms.textDocument_formatting] = { 'documentFormattingProvider' },
|
||||||
[ms.textDocument_hover] = { 'hoverProvider' },
|
[ms.textDocument_hover] = { 'hoverProvider' },
|
||||||
[ms.textDocument_implementation] = { 'implementationProvider' },
|
[ms.textDocument_implementation] = { 'implementationProvider' },
|
||||||
@@ -1094,6 +1096,38 @@ function lsp.tagfunc(pattern, flags)
|
|||||||
return vim.lsp._tagfunc(pattern, flags)
|
return vim.lsp._tagfunc(pattern, flags)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Provides an interface between the built-in client and a `foldexpr` function.
|
||||||
|
---@param lnum integer line number
|
||||||
|
function lsp.foldexpr(lnum)
|
||||||
|
return vim.lsp._folding_range.foldexpr(lnum)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Close all {kind} of folds in the the window with {winid}.
|
||||||
|
---
|
||||||
|
--- To automatically fold imports when opening a file, you can use an autocmd:
|
||||||
|
---
|
||||||
|
--- ```lua
|
||||||
|
--- vim.api.nvim_create_autocmd('LspNotify', {
|
||||||
|
--- callback = function(args)
|
||||||
|
--- if args.data.method == 'textDocument/didOpen' then
|
||||||
|
--- vim.lsp.foldclose('imports', vim.fn.bufwinid(args.buf))
|
||||||
|
--- end
|
||||||
|
--- end,
|
||||||
|
--- })
|
||||||
|
--- ```
|
||||||
|
---
|
||||||
|
---@param kind lsp.FoldingRangeKind Kind to close, one of "comment", "imports" or "region".
|
||||||
|
---@param winid? integer Defaults to the current window.
|
||||||
|
function lsp.foldclose(kind, winid)
|
||||||
|
return vim.lsp._folding_range.foldclose(kind, winid)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Provides a `foldtext` function that shows the `collapsedText` retrieved,
|
||||||
|
--- defaults to the first folded line if `collapsedText` is not provided.
|
||||||
|
function lsp.foldtext()
|
||||||
|
return vim.lsp._folding_range.foldtext()
|
||||||
|
end
|
||||||
|
|
||||||
---Checks whether a client is stopped.
|
---Checks whether a client is stopped.
|
||||||
---
|
---
|
||||||
---@param client_id (integer)
|
---@param client_id (integer)
|
||||||
|
371
runtime/lua/vim/lsp/_folding_range.lua
Normal file
371
runtime/lua/vim/lsp/_folding_range.lua
Normal file
@@ -0,0 +1,371 @@
|
|||||||
|
local util = require('vim.lsp.util')
|
||||||
|
local log = require('vim.lsp.log')
|
||||||
|
local ms = require('vim.lsp.protocol').Methods
|
||||||
|
local api = vim.api
|
||||||
|
|
||||||
|
local M = {}
|
||||||
|
|
||||||
|
---@class (private) vim.lsp.folding_range.BufState
|
||||||
|
---
|
||||||
|
---@field version? integer
|
||||||
|
---
|
||||||
|
--- Never use this directly, `renew()` the cached foldinfo
|
||||||
|
--- then use on demand via `row_*` fields.
|
||||||
|
---
|
||||||
|
--- Index In the form of client_id -> ranges
|
||||||
|
---@field client_ranges table<integer, lsp.FoldingRange[]?>
|
||||||
|
---
|
||||||
|
--- Index in the form of row -> [foldlevel, mark]
|
||||||
|
---@field row_level table<integer, [integer, ">" | "<"?]?>
|
||||||
|
---
|
||||||
|
--- Index in the form of start_row -> kinds
|
||||||
|
---@field row_kinds table<integer, table<lsp.FoldingRangeKind, true?>?>>
|
||||||
|
---
|
||||||
|
--- Index in the form of start_row -> collapsed_text
|
||||||
|
---@field row_text table<integer, string?>
|
||||||
|
|
||||||
|
---@type table<integer, vim.lsp.folding_range.BufState?>
|
||||||
|
local bufstates = {}
|
||||||
|
|
||||||
|
--- Renew the cached foldinfo in the buffer.
|
||||||
|
---@param bufnr integer
|
||||||
|
local function renew(bufnr)
|
||||||
|
local bufstate = assert(bufstates[bufnr])
|
||||||
|
|
||||||
|
---@type table<integer, [integer, ">" | "<"?]?>
|
||||||
|
local row_level = {}
|
||||||
|
---@type table<integer, table<lsp.FoldingRangeKind, true?>?>>
|
||||||
|
local row_kinds = {}
|
||||||
|
---@type table<integer, string?>
|
||||||
|
local row_text = {}
|
||||||
|
|
||||||
|
for _, ranges in pairs(bufstate.client_ranges) do
|
||||||
|
for _, range in ipairs(ranges) do
|
||||||
|
local start_row = range.startLine
|
||||||
|
local end_row = range.endLine
|
||||||
|
-- Adding folds within a single line is not supported by Nvim.
|
||||||
|
if start_row ~= end_row then
|
||||||
|
row_text[start_row] = range.collapsedText
|
||||||
|
|
||||||
|
local kind = range.kind
|
||||||
|
if kind then
|
||||||
|
local kinds = row_kinds[start_row] or {}
|
||||||
|
kinds[kind] = true
|
||||||
|
row_kinds[start_row] = kinds
|
||||||
|
end
|
||||||
|
|
||||||
|
for row = start_row, end_row do
|
||||||
|
local level = row_level[row] or { 0 }
|
||||||
|
level[1] = level[1] + 1
|
||||||
|
row_level[row] = level
|
||||||
|
end
|
||||||
|
row_level[start_row][2] = '>'
|
||||||
|
row_level[end_row][2] = '<'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
bufstate.row_level = row_level
|
||||||
|
bufstate.row_kinds = row_kinds
|
||||||
|
bufstate.row_text = row_text
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Renew the cached foldinfo then force `foldexpr()` to be re-evaluated,
|
||||||
|
--- without opening folds.
|
||||||
|
---@param bufnr integer
|
||||||
|
local function foldupdate(bufnr)
|
||||||
|
renew(bufnr)
|
||||||
|
for _, winid in ipairs(vim.fn.win_findbuf(bufnr)) do
|
||||||
|
local wininfo = vim.fn.getwininfo(winid)[1]
|
||||||
|
if wininfo and wininfo.tabnr == vim.fn.tabpagenr() then
|
||||||
|
if vim.wo[winid].foldmethod == 'expr' then
|
||||||
|
vim._foldupdate(winid, 0, api.nvim_buf_line_count(bufnr))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Whether `foldupdate()` is scheduled for the buffer with `bufnr`.
|
||||||
|
---
|
||||||
|
--- Index in the form of bufnr -> true?
|
||||||
|
---@type table<integer, true?>
|
||||||
|
local scheduled_foldupdate = {}
|
||||||
|
|
||||||
|
--- Schedule `foldupdate()` after leaving insert mode.
|
||||||
|
---@param bufnr integer
|
||||||
|
local function schedule_foldupdate(bufnr)
|
||||||
|
if not scheduled_foldupdate[bufnr] then
|
||||||
|
scheduled_foldupdate[bufnr] = true
|
||||||
|
api.nvim_create_autocmd('InsertLeave', {
|
||||||
|
buffer = bufnr,
|
||||||
|
once = true,
|
||||||
|
callback = function()
|
||||||
|
foldupdate(bufnr)
|
||||||
|
scheduled_foldupdate[bufnr] = nil
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param results table<integer,{err: lsp.ResponseError?, result: lsp.FoldingRange[]?}>
|
||||||
|
---@type lsp.MultiHandler
|
||||||
|
local function multi_handler(results, ctx)
|
||||||
|
local bufnr = assert(ctx.bufnr)
|
||||||
|
-- Handling responses from outdated buffer only causes performance overhead.
|
||||||
|
if util.buf_versions[bufnr] ~= ctx.version then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local bufstate = assert(bufstates[bufnr])
|
||||||
|
for client_id, result in pairs(results) do
|
||||||
|
if result.err then
|
||||||
|
log.error(result.err)
|
||||||
|
else
|
||||||
|
bufstate.client_ranges[client_id] = result.result
|
||||||
|
end
|
||||||
|
end
|
||||||
|
bufstate.version = ctx.version
|
||||||
|
|
||||||
|
if api.nvim_get_mode().mode:match('^i') then
|
||||||
|
-- `foldUpdate()` is guarded in insert mode.
|
||||||
|
schedule_foldupdate(bufnr)
|
||||||
|
else
|
||||||
|
foldupdate(bufnr)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param result lsp.FoldingRange[]?
|
||||||
|
---@type lsp.Handler
|
||||||
|
local function handler(err, result, ctx)
|
||||||
|
multi_handler({ [ctx.client_id] = { err = err, result = result } }, ctx)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Request `textDocument/foldingRange` from the server.
|
||||||
|
--- `foldupdate()` is scheduled once after the request is completed.
|
||||||
|
---@param bufnr integer
|
||||||
|
---@param client? vim.lsp.Client The client whose server supports `foldingRange`.
|
||||||
|
local function request(bufnr, client)
|
||||||
|
---@type lsp.FoldingRangeParams
|
||||||
|
local params = { textDocument = util.make_text_document_params(bufnr) }
|
||||||
|
|
||||||
|
if client then
|
||||||
|
client:request(ms.textDocument_foldingRange, params, handler, bufnr)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if not next(vim.lsp.get_clients({ bufnr = bufnr, method = ms.textDocument_foldingRange })) then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
vim.lsp.buf_request_all(bufnr, ms.textDocument_foldingRange, params, multi_handler)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- NOTE:
|
||||||
|
-- `bufstate` and event hooks are interdependent:
|
||||||
|
-- * `bufstate` needs event hooks for correctness.
|
||||||
|
-- * event hooks require the previous `bufstate` for updates.
|
||||||
|
-- Since they are manually created and destroyed,
|
||||||
|
-- we ensure their lifecycles are always synchronized.
|
||||||
|
--
|
||||||
|
-- TODO(ofseed):
|
||||||
|
-- 1. Implement clearing `bufstate` and event hooks
|
||||||
|
-- when no clients in the buffer support the corresponding method.
|
||||||
|
-- 2. Then generalize this state management to other LSP modules.
|
||||||
|
local augroup_setup = api.nvim_create_augroup('vim_lsp_folding_range/setup', {})
|
||||||
|
|
||||||
|
--- Initialize `bufstate` and event hooks, then request folding ranges.
|
||||||
|
--- Manage their lifecycle within this function.
|
||||||
|
---@param bufnr integer
|
||||||
|
---@return vim.lsp.folding_range.BufState?
|
||||||
|
local function setup(bufnr)
|
||||||
|
if not api.nvim_buf_is_loaded(bufnr) then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Register the new `bufstate`.
|
||||||
|
bufstates[bufnr] = {
|
||||||
|
client_ranges = {},
|
||||||
|
row_level = {},
|
||||||
|
row_kinds = {},
|
||||||
|
row_text = {},
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Event hooks from `buf_attach` can't be removed externally.
|
||||||
|
-- Hooks and `bufstate` share the same lifecycle;
|
||||||
|
-- they should self-destroy if `bufstate == nil`.
|
||||||
|
api.nvim_buf_attach(bufnr, false, {
|
||||||
|
-- `on_detach` also runs on buffer reload (`:e`).
|
||||||
|
-- Ensure `bufstate` and hooks are cleared to avoid duplication or leftover states.
|
||||||
|
on_detach = function()
|
||||||
|
bufstates[bufnr] = nil
|
||||||
|
api.nvim_clear_autocmds({ buffer = bufnr, group = augroup_setup })
|
||||||
|
end,
|
||||||
|
-- Reset `bufstate` and request folding ranges.
|
||||||
|
on_reload = function()
|
||||||
|
bufstates[bufnr] = {
|
||||||
|
client_ranges = {},
|
||||||
|
row_level = {},
|
||||||
|
row_kinds = {},
|
||||||
|
row_text = {},
|
||||||
|
}
|
||||||
|
request(bufnr)
|
||||||
|
end,
|
||||||
|
--- Sync changed rows with their previous foldlevels before applying new ones.
|
||||||
|
on_bytes = function(_, _, _, start_row, _, _, old_row, _, _, new_row, _, _)
|
||||||
|
if bufstates[bufnr] == nil then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
local row_level = bufstates[bufnr].row_level
|
||||||
|
if next(row_level) == nil then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local row = new_row - old_row
|
||||||
|
if row > 0 then
|
||||||
|
vim._list_insert(row_level, start_row, start_row + math.abs(row) - 1, { -1 })
|
||||||
|
-- If the previous row ends a fold,
|
||||||
|
-- Nvim treats the first row after consecutive `-1`s as a new fold start,
|
||||||
|
-- which is not the desired behavior.
|
||||||
|
local prev_level = row_level[start_row - 1]
|
||||||
|
if prev_level and prev_level[2] == '<' then
|
||||||
|
row_level[start_row] = { prev_level[1] - 1 }
|
||||||
|
end
|
||||||
|
elseif row < 0 then
|
||||||
|
vim._list_remove(row_level, start_row, start_row + math.abs(row) - 1)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
api.nvim_create_autocmd('LspDetach', {
|
||||||
|
group = augroup_setup,
|
||||||
|
buffer = bufnr,
|
||||||
|
callback = function(args)
|
||||||
|
if not api.nvim_buf_is_loaded(bufnr) then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
---@type integer
|
||||||
|
local client_id = args.data.client_id
|
||||||
|
bufstates[bufnr].client_ranges[client_id] = nil
|
||||||
|
|
||||||
|
---@type vim.lsp.Client[]
|
||||||
|
local clients = vim
|
||||||
|
.iter(vim.lsp.get_clients({ bufnr = bufnr, method = ms.textDocument_foldingRange }))
|
||||||
|
---@param client vim.lsp.Client
|
||||||
|
:filter(function(client)
|
||||||
|
return client.id ~= client_id
|
||||||
|
end)
|
||||||
|
:totable()
|
||||||
|
if #clients == 0 then
|
||||||
|
bufstates[bufnr] = {
|
||||||
|
client_ranges = {},
|
||||||
|
row_level = {},
|
||||||
|
row_kinds = {},
|
||||||
|
row_text = {},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
foldupdate(bufnr)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
api.nvim_create_autocmd('LspAttach', {
|
||||||
|
group = augroup_setup,
|
||||||
|
buffer = bufnr,
|
||||||
|
callback = function(args)
|
||||||
|
local client = assert(vim.lsp.get_client_by_id(args.data.client_id))
|
||||||
|
request(bufnr, client)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
api.nvim_create_autocmd('LspNotify', {
|
||||||
|
group = augroup_setup,
|
||||||
|
buffer = bufnr,
|
||||||
|
callback = function(args)
|
||||||
|
local client = assert(vim.lsp.get_client_by_id(args.data.client_id))
|
||||||
|
if
|
||||||
|
client:supports_method(ms.textDocument_foldingRange, bufnr)
|
||||||
|
and (
|
||||||
|
args.data.method == ms.textDocument_didChange
|
||||||
|
or args.data.method == ms.textDocument_didOpen
|
||||||
|
)
|
||||||
|
then
|
||||||
|
request(bufnr, client)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
|
||||||
|
request(bufnr)
|
||||||
|
|
||||||
|
return bufstates[bufnr]
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param kind lsp.FoldingRangeKind
|
||||||
|
---@param winid integer
|
||||||
|
local function foldclose(kind, winid)
|
||||||
|
vim._with({ win = winid }, function()
|
||||||
|
local bufnr = api.nvim_win_get_buf(winid)
|
||||||
|
local row_kinds = bufstates[bufnr].row_kinds
|
||||||
|
-- Reverse traverse to ensure that the smallest ranges are closed first.
|
||||||
|
for row = api.nvim_buf_line_count(bufnr) - 1, 0, -1 do
|
||||||
|
local kinds = row_kinds[row]
|
||||||
|
if kinds and kinds[kind] then
|
||||||
|
vim.cmd(row + 1 .. 'foldclose')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param kind lsp.FoldingRangeKind
|
||||||
|
---@param winid? integer
|
||||||
|
function M.foldclose(kind, winid)
|
||||||
|
vim.validate('kind', kind, 'string')
|
||||||
|
vim.validate('winid', winid, 'number', true)
|
||||||
|
|
||||||
|
winid = winid or api.nvim_get_current_win()
|
||||||
|
local bufnr = api.nvim_win_get_buf(winid)
|
||||||
|
local bufstate = bufstates[bufnr]
|
||||||
|
if not bufstate then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if bufstate.version == util.buf_versions[bufnr] then
|
||||||
|
foldclose(kind, winid)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
-- Schedule `foldclose()` if the buffer is not up-to-date.
|
||||||
|
|
||||||
|
if not next(vim.lsp.get_clients({ bufnr = bufnr, method = ms.textDocument_foldingRange })) then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
---@type lsp.FoldingRangeParams
|
||||||
|
local params = { textDocument = util.make_text_document_params(bufnr) }
|
||||||
|
vim.lsp.buf_request_all(bufnr, ms.textDocument_foldingRange, params, function(...)
|
||||||
|
multi_handler(...)
|
||||||
|
foldclose(kind, winid)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
---@return string
|
||||||
|
function M.foldtext()
|
||||||
|
local bufnr = api.nvim_get_current_buf()
|
||||||
|
local lnum = vim.v.foldstart
|
||||||
|
local row = lnum - 1
|
||||||
|
local bufstate = bufstates[bufnr]
|
||||||
|
if bufstate and bufstate.row_text[row] then
|
||||||
|
return bufstate.row_text[row]
|
||||||
|
end
|
||||||
|
return vim.fn.getline(lnum)
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param lnum? integer
|
||||||
|
---@return string level
|
||||||
|
function M.foldexpr(lnum)
|
||||||
|
local bufnr = api.nvim_get_current_buf()
|
||||||
|
local bufstate = bufstates[bufnr] or setup(bufnr)
|
||||||
|
if not bufstate then
|
||||||
|
return '0'
|
||||||
|
end
|
||||||
|
|
||||||
|
local row = (lnum or vim.v.lnum) - 1
|
||||||
|
local level = bufstate.row_level[row]
|
||||||
|
return level and (level[2] or '') .. (level[1] or '0') or '0'
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
@@ -440,6 +440,13 @@ function protocol.make_client_capabilities()
|
|||||||
properties = { 'command' },
|
properties = { 'command' },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
foldingRange = {
|
||||||
|
dynamicRegistration = false,
|
||||||
|
lineFoldingOnly = true,
|
||||||
|
foldingRange = {
|
||||||
|
collapsedText = true,
|
||||||
|
},
|
||||||
|
},
|
||||||
formatting = {
|
formatting = {
|
||||||
dynamicRegistration = true,
|
dynamicRegistration = true,
|
||||||
},
|
},
|
||||||
|
@@ -737,6 +737,51 @@ function vim.list_slice(list, start, finish)
|
|||||||
return new_list
|
return new_list
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Efficiently insert items into the middle of a list.
|
||||||
|
---
|
||||||
|
--- Calling table.insert() in a loop will re-index the tail of the table on
|
||||||
|
--- every iteration, instead this function will re-index the table exactly
|
||||||
|
--- once.
|
||||||
|
---
|
||||||
|
--- Based on https://stackoverflow.com/questions/12394841/safely-remove-items-from-an-array-table-while-iterating/53038524#53038524
|
||||||
|
---
|
||||||
|
---@param t any[]
|
||||||
|
---@param first integer
|
||||||
|
---@param last integer
|
||||||
|
---@param v any
|
||||||
|
function vim._list_insert(t, first, last, v)
|
||||||
|
local n = #t
|
||||||
|
|
||||||
|
-- Shift table forward
|
||||||
|
for i = n - first, 0, -1 do
|
||||||
|
t[last + 1 + i] = t[first + i]
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Fill in new values
|
||||||
|
for i = first, last do
|
||||||
|
t[i] = v
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Efficiently remove items from middle of a list.
|
||||||
|
---
|
||||||
|
--- Calling table.remove() in a loop will re-index the tail of the table on
|
||||||
|
--- every iteration, instead this function will re-index the table exactly
|
||||||
|
--- once.
|
||||||
|
---
|
||||||
|
--- Based on https://stackoverflow.com/questions/12394841/safely-remove-items-from-an-array-table-while-iterating/53038524#53038524
|
||||||
|
---
|
||||||
|
---@param t any[]
|
||||||
|
---@param first integer
|
||||||
|
---@param last integer
|
||||||
|
function vim._list_remove(t, first, last)
|
||||||
|
local n = #t
|
||||||
|
for i = 0, n - first do
|
||||||
|
t[first + i] = t[last + 1 + i]
|
||||||
|
t[last + 1 + i] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
--- Trim whitespace (Lua pattern "%s") from both sides of a string.
|
--- Trim whitespace (Lua pattern "%s") from both sides of a string.
|
||||||
---
|
---
|
||||||
---@see |lua-patterns|
|
---@see |lua-patterns|
|
||||||
|
@@ -30,65 +30,20 @@ function FoldInfo.new()
|
|||||||
}, FoldInfo)
|
}, FoldInfo)
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Efficiently remove items from middle of a list a list.
|
|
||||||
---
|
|
||||||
--- Calling table.remove() in a loop will re-index the tail of the table on
|
|
||||||
--- every iteration, instead this function will re-index the table exactly
|
|
||||||
--- once.
|
|
||||||
---
|
|
||||||
--- Based on https://stackoverflow.com/questions/12394841/safely-remove-items-from-an-array-table-while-iterating/53038524#53038524
|
|
||||||
---
|
|
||||||
---@param t any[]
|
|
||||||
---@param first integer
|
|
||||||
---@param last integer
|
|
||||||
local function list_remove(t, first, last)
|
|
||||||
local n = #t
|
|
||||||
for i = 0, n - first do
|
|
||||||
t[first + i] = t[last + 1 + i]
|
|
||||||
t[last + 1 + i] = nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
---@package
|
---@package
|
||||||
---@param srow integer
|
---@param srow integer
|
||||||
---@param erow integer 0-indexed, exclusive
|
---@param erow integer 0-indexed, exclusive
|
||||||
function FoldInfo:remove_range(srow, erow)
|
function FoldInfo:remove_range(srow, erow)
|
||||||
list_remove(self.levels, srow + 1, erow)
|
vim._list_remove(self.levels, srow + 1, erow)
|
||||||
list_remove(self.levels0, srow + 1, erow)
|
vim._list_remove(self.levels0, srow + 1, erow)
|
||||||
end
|
|
||||||
|
|
||||||
--- Efficiently insert items into the middle of a list.
|
|
||||||
---
|
|
||||||
--- Calling table.insert() in a loop will re-index the tail of the table on
|
|
||||||
--- every iteration, instead this function will re-index the table exactly
|
|
||||||
--- once.
|
|
||||||
---
|
|
||||||
--- Based on https://stackoverflow.com/questions/12394841/safely-remove-items-from-an-array-table-while-iterating/53038524#53038524
|
|
||||||
---
|
|
||||||
---@param t any[]
|
|
||||||
---@param first integer
|
|
||||||
---@param last integer
|
|
||||||
---@param v any
|
|
||||||
local function list_insert(t, first, last, v)
|
|
||||||
local n = #t
|
|
||||||
|
|
||||||
-- Shift table forward
|
|
||||||
for i = n - first, 0, -1 do
|
|
||||||
t[last + 1 + i] = t[first + i]
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Fill in new values
|
|
||||||
for i = first, last do
|
|
||||||
t[i] = v
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
---@package
|
---@package
|
||||||
---@param srow integer
|
---@param srow integer
|
||||||
---@param erow integer 0-indexed, exclusive
|
---@param erow integer 0-indexed, exclusive
|
||||||
function FoldInfo:add_range(srow, erow)
|
function FoldInfo:add_range(srow, erow)
|
||||||
list_insert(self.levels, srow + 1, erow, -1)
|
vim._list_insert(self.levels, srow + 1, erow, -1)
|
||||||
list_insert(self.levels0, srow + 1, erow, -1)
|
vim._list_insert(self.levels0, srow + 1, erow, -1)
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param range Range2
|
---@param range Range2
|
||||||
|
@@ -274,6 +274,7 @@ local config = {
|
|||||||
'diagnostic.lua',
|
'diagnostic.lua',
|
||||||
'codelens.lua',
|
'codelens.lua',
|
||||||
'completion.lua',
|
'completion.lua',
|
||||||
|
'folding_range.lua',
|
||||||
'inlay_hint.lua',
|
'inlay_hint.lua',
|
||||||
'tagfunc.lua',
|
'tagfunc.lua',
|
||||||
'semantic_tokens.lua',
|
'semantic_tokens.lua',
|
||||||
|
647
test/functional/plugin/lsp/folding_range_spec.lua
Normal file
647
test/functional/plugin/lsp/folding_range_spec.lua
Normal file
@@ -0,0 +1,647 @@
|
|||||||
|
local t = require('test.testutil')
|
||||||
|
local n = require('test.functional.testnvim')()
|
||||||
|
local Screen = require('test.functional.ui.screen')
|
||||||
|
local t_lsp = require('test.functional.plugin.lsp.testutil')
|
||||||
|
|
||||||
|
local eq = t.eq
|
||||||
|
local tempname = t.tmpname
|
||||||
|
|
||||||
|
local clear_notrace = t_lsp.clear_notrace
|
||||||
|
local create_server_definition = t_lsp.create_server_definition
|
||||||
|
|
||||||
|
local api = n.api
|
||||||
|
local exec_lua = n.exec_lua
|
||||||
|
local insert = n.insert
|
||||||
|
local command = n.command
|
||||||
|
local feed = n.feed
|
||||||
|
|
||||||
|
describe('vim.lsp.folding_range', function()
|
||||||
|
local text = [[// foldLevel() {{{2
|
||||||
|
/// @return fold level at line number "lnum" in the current window.
|
||||||
|
static int foldLevel(linenr_T lnum)
|
||||||
|
{
|
||||||
|
// While updating the folds lines between invalid_top and invalid_bot have
|
||||||
|
// an undefined fold level. Otherwise update the folds first.
|
||||||
|
if (invalid_top == 0) {
|
||||||
|
checkupdate(curwin);
|
||||||
|
} else if (lnum == prev_lnum && prev_lnum_lvl >= 0) {
|
||||||
|
return prev_lnum_lvl;
|
||||||
|
} else if (lnum >= invalid_top && lnum <= invalid_bot) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return quickly when there is no folding at all in this window.
|
||||||
|
if (!hasAnyFolding(curwin)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return foldLevelWin(curwin, lnum);
|
||||||
|
}]]
|
||||||
|
|
||||||
|
local result = {
|
||||||
|
{
|
||||||
|
endLine = 19,
|
||||||
|
kind = 'region',
|
||||||
|
startCharacter = 1,
|
||||||
|
startLine = 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
endCharacter = 2,
|
||||||
|
endLine = 7,
|
||||||
|
kind = 'region',
|
||||||
|
startCharacter = 25,
|
||||||
|
startLine = 6,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
endCharacter = 2,
|
||||||
|
endLine = 9,
|
||||||
|
kind = 'region',
|
||||||
|
startCharacter = 55,
|
||||||
|
startLine = 8,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
endCharacter = 2,
|
||||||
|
endLine = 11,
|
||||||
|
kind = 'region',
|
||||||
|
startCharacter = 58,
|
||||||
|
startLine = 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
endCharacter = 2,
|
||||||
|
endLine = 16,
|
||||||
|
kind = 'region',
|
||||||
|
startCharacter = 31,
|
||||||
|
startLine = 15,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
endCharacter = 68,
|
||||||
|
endLine = 1,
|
||||||
|
kind = 'comment',
|
||||||
|
startCharacter = 2,
|
||||||
|
startLine = 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
endCharacter = 64,
|
||||||
|
endLine = 5,
|
||||||
|
kind = 'comment',
|
||||||
|
startCharacter = 4,
|
||||||
|
startLine = 4,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
local bufnr ---@type integer
|
||||||
|
local client_id ---@type integer
|
||||||
|
|
||||||
|
clear_notrace()
|
||||||
|
before_each(function()
|
||||||
|
clear_notrace()
|
||||||
|
|
||||||
|
exec_lua(create_server_definition)
|
||||||
|
bufnr = n.api.nvim_get_current_buf()
|
||||||
|
client_id = exec_lua(function()
|
||||||
|
_G.server = _G._create_server({
|
||||||
|
capabilities = {
|
||||||
|
foldingRangeProvider = true,
|
||||||
|
},
|
||||||
|
handlers = {
|
||||||
|
['textDocument/foldingRange'] = function(_, _, callback)
|
||||||
|
callback(nil, result)
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
vim.api.nvim_win_set_buf(0, bufnr)
|
||||||
|
|
||||||
|
return vim.lsp.start({ name = 'dummy', cmd = _G.server.cmd })
|
||||||
|
end)
|
||||||
|
command('set foldmethod=expr foldcolumn=1 foldlevel=999')
|
||||||
|
insert(text)
|
||||||
|
end)
|
||||||
|
after_each(function()
|
||||||
|
api.nvim_exec_autocmds('VimLeavePre', { modeline = false })
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe('setup()', function()
|
||||||
|
---@type integer
|
||||||
|
local bufnr_set_expr
|
||||||
|
---@type integer
|
||||||
|
local bufnr_never_set_expr
|
||||||
|
|
||||||
|
local function buf_autocmd_num(bufnr_to_check)
|
||||||
|
return exec_lua(function()
|
||||||
|
return #vim.api.nvim_get_autocmds({ buffer = bufnr_to_check, event = 'LspNotify' })
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
before_each(function()
|
||||||
|
command([[setlocal foldexpr=v:lua.vim.lsp.foldexpr()]])
|
||||||
|
exec_lua(function()
|
||||||
|
bufnr_set_expr = vim.api.nvim_create_buf(true, false)
|
||||||
|
vim.api.nvim_set_current_buf(bufnr_set_expr)
|
||||||
|
end)
|
||||||
|
insert(text)
|
||||||
|
command('write ' .. tempname(false))
|
||||||
|
command([[setlocal foldexpr=v:lua.vim.lsp.foldexpr()]])
|
||||||
|
exec_lua(function()
|
||||||
|
bufnr_never_set_expr = vim.api.nvim_create_buf(true, false)
|
||||||
|
vim.api.nvim_set_current_buf(bufnr_never_set_expr)
|
||||||
|
end)
|
||||||
|
insert(text)
|
||||||
|
api.nvim_win_set_buf(0, bufnr_set_expr)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('only create event hooks where foldexpr has been set', function()
|
||||||
|
eq(1, buf_autocmd_num(bufnr))
|
||||||
|
eq(1, buf_autocmd_num(bufnr_set_expr))
|
||||||
|
eq(0, buf_autocmd_num(bufnr_never_set_expr))
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('does not create duplicate event hooks after reloaded', function()
|
||||||
|
command('edit')
|
||||||
|
eq(1, buf_autocmd_num(bufnr_set_expr))
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('cleans up event hooks when buffer is unloaded', function()
|
||||||
|
command('bdelete')
|
||||||
|
eq(0, buf_autocmd_num(bufnr_set_expr))
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe('expr()', function()
|
||||||
|
--- @type test.functional.ui.screen
|
||||||
|
local screen
|
||||||
|
before_each(function()
|
||||||
|
screen = Screen.new(80, 45)
|
||||||
|
screen:set_default_attr_ids({
|
||||||
|
[1] = { background = Screen.colors.Grey, foreground = Screen.colors.DarkBlue },
|
||||||
|
[2] = { bold = true, foreground = Screen.colors.Blue1 },
|
||||||
|
[3] = { bold = true, reverse = true },
|
||||||
|
[4] = { reverse = true },
|
||||||
|
})
|
||||||
|
command([[set foldexpr=v:lua.vim.lsp.foldexpr()]])
|
||||||
|
command([[split]])
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('can compute fold levels', function()
|
||||||
|
---@type table<integer, string>
|
||||||
|
local foldlevels = {}
|
||||||
|
for i = 1, 21 do
|
||||||
|
foldlevels[i] = exec_lua('return vim.lsp.foldexpr(' .. i .. ')')
|
||||||
|
end
|
||||||
|
eq({
|
||||||
|
[1] = '>1',
|
||||||
|
[2] = '<1',
|
||||||
|
[3] = '0',
|
||||||
|
[4] = '>1',
|
||||||
|
[5] = '>2',
|
||||||
|
[6] = '<2',
|
||||||
|
[7] = '>2',
|
||||||
|
[8] = '<2',
|
||||||
|
[9] = '>2',
|
||||||
|
[10] = '<2',
|
||||||
|
[11] = '>2',
|
||||||
|
[12] = '<2',
|
||||||
|
[13] = '1',
|
||||||
|
[14] = '1',
|
||||||
|
[15] = '1',
|
||||||
|
[16] = '>2',
|
||||||
|
[17] = '<2',
|
||||||
|
[18] = '1',
|
||||||
|
[19] = '1',
|
||||||
|
[20] = '<1',
|
||||||
|
[21] = '0',
|
||||||
|
}, foldlevels)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('updates folds in all windows', function()
|
||||||
|
screen:expect({
|
||||||
|
grid = [[
|
||||||
|
{1:-}// foldLevel() {{{2 |
|
||||||
|
{1:│}/// @return fold level at line number "lnum" in the current window. |
|
||||||
|
{1: }static int foldLevel(linenr_T lnum) |
|
||||||
|
{1:-}{ |
|
||||||
|
{1:-} // While updating the folds lines between invalid_top and invalid_bot have |
|
||||||
|
{1:2} // an undefined fold level. Otherwise update the folds first. |
|
||||||
|
{1:-} if (invalid_top == 0) { |
|
||||||
|
{1:2} checkupdate(curwin); |
|
||||||
|
{1:-} } else if (lnum == prev_lnum && prev_lnum_lvl >= 0) { |
|
||||||
|
{1:2} return prev_lnum_lvl; |
|
||||||
|
{1:-} } else if (lnum >= invalid_top && lnum <= invalid_bot) { |
|
||||||
|
{1:2} return -1; |
|
||||||
|
{1:│} } |
|
||||||
|
{1:│} |
|
||||||
|
{1:│} // Return quickly when there is no folding at all in this window. |
|
||||||
|
{1:-} if (!hasAnyFolding(curwin)) { |
|
||||||
|
{1:2} return 0; |
|
||||||
|
{1:│} } |
|
||||||
|
{1:│} |
|
||||||
|
{1:│} return foldLevelWin(curwin, lnum); |
|
||||||
|
{1: }^} |
|
||||||
|
{3:[No Name] [+] }|
|
||||||
|
{1:-}// foldLevel() {{{2 |
|
||||||
|
{1:│}/// @return fold level at line number "lnum" in the current window. |
|
||||||
|
{1: }static int foldLevel(linenr_T lnum) |
|
||||||
|
{1:-}{ |
|
||||||
|
{1:-} // While updating the folds lines between invalid_top and invalid_bot have |
|
||||||
|
{1:2} // an undefined fold level. Otherwise update the folds first. |
|
||||||
|
{1:-} if (invalid_top == 0) { |
|
||||||
|
{1:2} checkupdate(curwin); |
|
||||||
|
{1:-} } else if (lnum == prev_lnum && prev_lnum_lvl >= 0) { |
|
||||||
|
{1:2} return prev_lnum_lvl; |
|
||||||
|
{1:-} } else if (lnum >= invalid_top && lnum <= invalid_bot) { |
|
||||||
|
{1:2} return -1; |
|
||||||
|
{1:│} } |
|
||||||
|
{1:│} |
|
||||||
|
{1:│} // Return quickly when there is no folding at all in this window. |
|
||||||
|
{1:-} if (!hasAnyFolding(curwin)) { |
|
||||||
|
{1:2} return 0; |
|
||||||
|
{1:│} } |
|
||||||
|
{1:│} |
|
||||||
|
{1:│} return foldLevelWin(curwin, lnum); |
|
||||||
|
{1: }} |
|
||||||
|
{4:[No Name] [+] }|
|
||||||
|
|
|
||||||
|
]],
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('persists wherever foldexpr is set', function()
|
||||||
|
command([[setlocal foldexpr=]])
|
||||||
|
feed('<C-w><C-w>zx')
|
||||||
|
screen:expect({
|
||||||
|
grid = [[
|
||||||
|
{1: }// foldLevel() {{{2 |
|
||||||
|
{1: }/// @return fold level at line number "lnum" in the current window. |
|
||||||
|
{1: }static int foldLevel(linenr_T lnum) |
|
||||||
|
{1: }{ |
|
||||||
|
{1: } // While updating the folds lines between invalid_top and invalid_bot have |
|
||||||
|
{1: } // an undefined fold level. Otherwise update the folds first. |
|
||||||
|
{1: } if (invalid_top == 0) { |
|
||||||
|
{1: } checkupdate(curwin); |
|
||||||
|
{1: } } else if (lnum == prev_lnum && prev_lnum_lvl >= 0) { |
|
||||||
|
{1: } return prev_lnum_lvl; |
|
||||||
|
{1: } } else if (lnum >= invalid_top && lnum <= invalid_bot) { |
|
||||||
|
{1: } return -1; |
|
||||||
|
{1: } } |
|
||||||
|
{1: } |
|
||||||
|
{1: } // Return quickly when there is no folding at all in this window. |
|
||||||
|
{1: } if (!hasAnyFolding(curwin)) { |
|
||||||
|
{1: } return 0; |
|
||||||
|
{1: } } |
|
||||||
|
{1: } |
|
||||||
|
{1: } return foldLevelWin(curwin, lnum); |
|
||||||
|
{1: }} |
|
||||||
|
{4:[No Name] [+] }|
|
||||||
|
{1:-}// foldLevel() {{{2 |
|
||||||
|
{1:│}/// @return fold level at line number "lnum" in the current window. |
|
||||||
|
{1: }static int foldLevel(linenr_T lnum) |
|
||||||
|
{1:-}{ |
|
||||||
|
{1:-} // While updating the folds lines between invalid_top and invalid_bot have |
|
||||||
|
{1:2} // an undefined fold level. Otherwise update the folds first. |
|
||||||
|
{1:-} if (invalid_top == 0) { |
|
||||||
|
{1:2} checkupdate(curwin); |
|
||||||
|
{1:-} } else if (lnum == prev_lnum && prev_lnum_lvl >= 0) { |
|
||||||
|
{1:2} return prev_lnum_lvl; |
|
||||||
|
{1:-} } else if (lnum >= invalid_top && lnum <= invalid_bot) { |
|
||||||
|
{1:2} return -1; |
|
||||||
|
{1:│} } |
|
||||||
|
{1:│} |
|
||||||
|
{1:│} // Return quickly when there is no folding at all in this window. |
|
||||||
|
{1:-} if (!hasAnyFolding(curwin)) { |
|
||||||
|
{1:2} return 0; |
|
||||||
|
{1:│} } |
|
||||||
|
{1:│} |
|
||||||
|
{1:│} return foldLevelWin(curwin, lnum); |
|
||||||
|
{1: }^} |
|
||||||
|
{3:[No Name] [+] }|
|
||||||
|
|
|
||||||
|
]],
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('synchronizes changed rows with their previous foldlevels', function()
|
||||||
|
command('1,2d')
|
||||||
|
screen:expect({
|
||||||
|
grid = [[
|
||||||
|
{1: }^static int foldLevel(linenr_T lnum) |
|
||||||
|
{1:-}{ |
|
||||||
|
{1:-} // While updating the folds lines between invalid_top and invalid_bot have |
|
||||||
|
{1:2} // an undefined fold level. Otherwise update the folds first. |
|
||||||
|
{1:-} if (invalid_top == 0) { |
|
||||||
|
{1:2} checkupdate(curwin); |
|
||||||
|
{1:-} } else if (lnum == prev_lnum && prev_lnum_lvl >= 0) { |
|
||||||
|
{1:2} return prev_lnum_lvl; |
|
||||||
|
{1:-} } else if (lnum >= invalid_top && lnum <= invalid_bot) { |
|
||||||
|
{1:2} return -1; |
|
||||||
|
{1:│} } |
|
||||||
|
{1:│} |
|
||||||
|
{1:│} // Return quickly when there is no folding at all in this window. |
|
||||||
|
{1:-} if (!hasAnyFolding(curwin)) { |
|
||||||
|
{1:2} return 0; |
|
||||||
|
{1:│} } |
|
||||||
|
{1:│} |
|
||||||
|
{1:│} return foldLevelWin(curwin, lnum); |
|
||||||
|
{1: }} |
|
||||||
|
{2:~ }|*2
|
||||||
|
{3:[No Name] [+] }|
|
||||||
|
{1: }static int foldLevel(linenr_T lnum) |
|
||||||
|
{1:-}{ |
|
||||||
|
{1:-} // While updating the folds lines between invalid_top and invalid_bot have |
|
||||||
|
{1:2} // an undefined fold level. Otherwise update the folds first. |
|
||||||
|
{1:-} if (invalid_top == 0) { |
|
||||||
|
{1:2} checkupdate(curwin); |
|
||||||
|
{1:-} } else if (lnum == prev_lnum && prev_lnum_lvl >= 0) { |
|
||||||
|
{1:2} return prev_lnum_lvl; |
|
||||||
|
{1:-} } else if (lnum >= invalid_top && lnum <= invalid_bot) { |
|
||||||
|
{1:2} return -1; |
|
||||||
|
{1:│} } |
|
||||||
|
{1:│} |
|
||||||
|
{1:│} // Return quickly when there is no folding at all in this window. |
|
||||||
|
{1:-} if (!hasAnyFolding(curwin)) { |
|
||||||
|
{1:2} return 0; |
|
||||||
|
{1:│} } |
|
||||||
|
{1:│} |
|
||||||
|
{1:│} return foldLevelWin(curwin, lnum); |
|
||||||
|
{1: }} |
|
||||||
|
{2:~ }|*2
|
||||||
|
{4:[No Name] [+] }|
|
||||||
|
|
|
||||||
|
]],
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('clears folds when sole client detaches', function()
|
||||||
|
exec_lua(function()
|
||||||
|
vim.lsp.buf_detach_client(bufnr, client_id)
|
||||||
|
end)
|
||||||
|
screen:expect({
|
||||||
|
grid = [[
|
||||||
|
{1: }// foldLevel() {{{2 |
|
||||||
|
{1: }/// @return fold level at line number "lnum" in the current window. |
|
||||||
|
{1: }static int foldLevel(linenr_T lnum) |
|
||||||
|
{1: }{ |
|
||||||
|
{1: } // While updating the folds lines between invalid_top and invalid_bot have |
|
||||||
|
{1: } // an undefined fold level. Otherwise update the folds first. |
|
||||||
|
{1: } if (invalid_top == 0) { |
|
||||||
|
{1: } checkupdate(curwin); |
|
||||||
|
{1: } } else if (lnum == prev_lnum && prev_lnum_lvl >= 0) { |
|
||||||
|
{1: } return prev_lnum_lvl; |
|
||||||
|
{1: } } else if (lnum >= invalid_top && lnum <= invalid_bot) { |
|
||||||
|
{1: } return -1; |
|
||||||
|
{1: } } |
|
||||||
|
{1: } |
|
||||||
|
{1: } // Return quickly when there is no folding at all in this window. |
|
||||||
|
{1: } if (!hasAnyFolding(curwin)) { |
|
||||||
|
{1: } return 0; |
|
||||||
|
{1: } } |
|
||||||
|
{1: } |
|
||||||
|
{1: } return foldLevelWin(curwin, lnum); |
|
||||||
|
{1: }^} |
|
||||||
|
{3:[No Name] [+] }|
|
||||||
|
{1: }// foldLevel() {{{2 |
|
||||||
|
{1: }/// @return fold level at line number "lnum" in the current window. |
|
||||||
|
{1: }static int foldLevel(linenr_T lnum) |
|
||||||
|
{1: }{ |
|
||||||
|
{1: } // While updating the folds lines between invalid_top and invalid_bot have |
|
||||||
|
{1: } // an undefined fold level. Otherwise update the folds first. |
|
||||||
|
{1: } if (invalid_top == 0) { |
|
||||||
|
{1: } checkupdate(curwin); |
|
||||||
|
{1: } } else if (lnum == prev_lnum && prev_lnum_lvl >= 0) { |
|
||||||
|
{1: } return prev_lnum_lvl; |
|
||||||
|
{1: } } else if (lnum >= invalid_top && lnum <= invalid_bot) { |
|
||||||
|
{1: } return -1; |
|
||||||
|
{1: } } |
|
||||||
|
{1: } |
|
||||||
|
{1: } // Return quickly when there is no folding at all in this window. |
|
||||||
|
{1: } if (!hasAnyFolding(curwin)) { |
|
||||||
|
{1: } return 0; |
|
||||||
|
{1: } } |
|
||||||
|
{1: } |
|
||||||
|
{1: } return foldLevelWin(curwin, lnum); |
|
||||||
|
{1: }} |
|
||||||
|
{4:[No Name] [+] }|
|
||||||
|
|
|
||||||
|
]],
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('remains valid after the client re-attaches.', function()
|
||||||
|
exec_lua(function()
|
||||||
|
vim.lsp.buf_detach_client(bufnr, client_id)
|
||||||
|
vim.lsp.buf_attach_client(bufnr, client_id)
|
||||||
|
end)
|
||||||
|
screen:expect({
|
||||||
|
grid = [[
|
||||||
|
{1:-}// foldLevel() {{{2 |
|
||||||
|
{1:│}/// @return fold level at line number "lnum" in the current window. |
|
||||||
|
{1: }static int foldLevel(linenr_T lnum) |
|
||||||
|
{1:-}{ |
|
||||||
|
{1:-} // While updating the folds lines between invalid_top and invalid_bot have |
|
||||||
|
{1:2} // an undefined fold level. Otherwise update the folds first. |
|
||||||
|
{1:-} if (invalid_top == 0) { |
|
||||||
|
{1:2} checkupdate(curwin); |
|
||||||
|
{1:-} } else if (lnum == prev_lnum && prev_lnum_lvl >= 0) { |
|
||||||
|
{1:2} return prev_lnum_lvl; |
|
||||||
|
{1:-} } else if (lnum >= invalid_top && lnum <= invalid_bot) { |
|
||||||
|
{1:2} return -1; |
|
||||||
|
{1:│} } |
|
||||||
|
{1:│} |
|
||||||
|
{1:│} // Return quickly when there is no folding at all in this window. |
|
||||||
|
{1:-} if (!hasAnyFolding(curwin)) { |
|
||||||
|
{1:2} return 0; |
|
||||||
|
{1:│} } |
|
||||||
|
{1:│} |
|
||||||
|
{1:│} return foldLevelWin(curwin, lnum); |
|
||||||
|
{1: }^} |
|
||||||
|
{3:[No Name] [+] }|
|
||||||
|
{1:-}// foldLevel() {{{2 |
|
||||||
|
{1:│}/// @return fold level at line number "lnum" in the current window. |
|
||||||
|
{1: }static int foldLevel(linenr_T lnum) |
|
||||||
|
{1:-}{ |
|
||||||
|
{1:-} // While updating the folds lines between invalid_top and invalid_bot have |
|
||||||
|
{1:2} // an undefined fold level. Otherwise update the folds first. |
|
||||||
|
{1:-} if (invalid_top == 0) { |
|
||||||
|
{1:2} checkupdate(curwin); |
|
||||||
|
{1:-} } else if (lnum == prev_lnum && prev_lnum_lvl >= 0) { |
|
||||||
|
{1:2} return prev_lnum_lvl; |
|
||||||
|
{1:-} } else if (lnum >= invalid_top && lnum <= invalid_bot) { |
|
||||||
|
{1:2} return -1; |
|
||||||
|
{1:│} } |
|
||||||
|
{1:│} |
|
||||||
|
{1:│} // Return quickly when there is no folding at all in this window. |
|
||||||
|
{1:-} if (!hasAnyFolding(curwin)) { |
|
||||||
|
{1:2} return 0; |
|
||||||
|
{1:│} } |
|
||||||
|
{1:│} |
|
||||||
|
{1:│} return foldLevelWin(curwin, lnum); |
|
||||||
|
{1: }} |
|
||||||
|
{4:[No Name] [+] }|
|
||||||
|
|
|
||||||
|
]],
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe('foldtext()', function()
|
||||||
|
--- @type test.functional.ui.screen
|
||||||
|
local screen
|
||||||
|
before_each(function()
|
||||||
|
screen = Screen.new(80, 23)
|
||||||
|
screen:set_default_attr_ids({
|
||||||
|
[1] = { background = Screen.colors.Grey, foreground = Screen.colors.DarkBlue },
|
||||||
|
[2] = { foreground = Screen.colors.DarkBlue, background = Screen.colors.LightGrey },
|
||||||
|
[3] = { bold = true, foreground = Screen.colors.Blue1 },
|
||||||
|
[4] = { bold = true, reverse = true },
|
||||||
|
[5] = { reverse = true },
|
||||||
|
})
|
||||||
|
command(
|
||||||
|
[[set foldexpr=v:lua.vim.lsp.foldexpr() foldtext=v:lua.vim.lsp.foldtext() foldlevel=1]]
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('shows the first folded line if `collapsedText` does not exist', function()
|
||||||
|
screen:expect({
|
||||||
|
grid = [[
|
||||||
|
{1:-}// foldLevel() {{{2 |
|
||||||
|
{1:│}/// @return fold level at line number "lnum" in the current window. |
|
||||||
|
{1: }static int foldLevel(linenr_T lnum) |
|
||||||
|
{1:-}{ |
|
||||||
|
{1:+}{2: // While updating the folds lines between invalid_top and invalid_bot have···}|
|
||||||
|
{1:+}{2: if (invalid_top == 0) {······················································}|
|
||||||
|
{1:+}{2: } else if (lnum == prev_lnum && prev_lnum_lvl >= 0) {························}|
|
||||||
|
{1:+}{2: } else if (lnum >= invalid_top && lnum <= invalid_bot) {·····················}|
|
||||||
|
{1:│} } |
|
||||||
|
{1:│} |
|
||||||
|
{1:│} // Return quickly when there is no folding at all in this window. |
|
||||||
|
{1:+}{2: if (!hasAnyFolding(curwin)) {················································}|
|
||||||
|
{1:│} } |
|
||||||
|
{1:│} |
|
||||||
|
{1:│} return foldLevelWin(curwin, lnum); |
|
||||||
|
{1: }^} |
|
||||||
|
{3:~ }|*6
|
||||||
|
|
|
||||||
|
]],
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe('foldclose()', function()
|
||||||
|
--- @type test.functional.ui.screen
|
||||||
|
local screen
|
||||||
|
before_each(function()
|
||||||
|
screen = Screen.new(80, 23)
|
||||||
|
screen:set_default_attr_ids({
|
||||||
|
[1] = { background = Screen.colors.Grey, foreground = Screen.colors.DarkBlue },
|
||||||
|
[2] = { foreground = Screen.colors.DarkBlue, background = Screen.colors.LightGrey },
|
||||||
|
[3] = { bold = true, foreground = Screen.colors.Blue1 },
|
||||||
|
[4] = { bold = true, reverse = true },
|
||||||
|
[5] = { reverse = true },
|
||||||
|
})
|
||||||
|
command([[set foldexpr=v:lua.vim.lsp.foldexpr()]])
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('closes all folds of one kind immediately', function()
|
||||||
|
exec_lua(function()
|
||||||
|
vim.lsp.foldclose('comment')
|
||||||
|
end)
|
||||||
|
screen:expect({
|
||||||
|
grid = [[
|
||||||
|
{1:+}{2:+-- 2 lines: foldLevel()······················································}|
|
||||||
|
{1: }static int foldLevel(linenr_T lnum) |
|
||||||
|
{1:-}{ |
|
||||||
|
{1:+}{2:+--- 2 lines: While updating the folds lines between invalid_top and invalid_b}|
|
||||||
|
{1:-} if (invalid_top == 0) { |
|
||||||
|
{1:2} checkupdate(curwin); |
|
||||||
|
{1:-} } else if (lnum == prev_lnum && prev_lnum_lvl >= 0) { |
|
||||||
|
{1:2} return prev_lnum_lvl; |
|
||||||
|
{1:-} } else if (lnum >= invalid_top && lnum <= invalid_bot) { |
|
||||||
|
{1:2} return -1; |
|
||||||
|
{1:│} } |
|
||||||
|
{1:│} |
|
||||||
|
{1:│} // Return quickly when there is no folding at all in this window. |
|
||||||
|
{1:-} if (!hasAnyFolding(curwin)) { |
|
||||||
|
{1:2} return 0; |
|
||||||
|
{1:│} } |
|
||||||
|
{1:│} |
|
||||||
|
{1:│} return foldLevelWin(curwin, lnum); |
|
||||||
|
{1: }^} |
|
||||||
|
{3:~ }|*3
|
||||||
|
|
|
||||||
|
]],
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('closes the smallest fold first', function()
|
||||||
|
exec_lua(function()
|
||||||
|
vim.lsp.foldclose('region')
|
||||||
|
end)
|
||||||
|
screen:expect({
|
||||||
|
grid = [[
|
||||||
|
{1:-}// foldLevel() {{{2 |
|
||||||
|
{1:│}/// @return fold level at line number "lnum" in the current window. |
|
||||||
|
{1: }static int foldLevel(linenr_T lnum) |
|
||||||
|
{1:+}{2:+-- 17 lines: {································································}|
|
||||||
|
{1: }^} |
|
||||||
|
{3:~ }|*17
|
||||||
|
|
|
||||||
|
]],
|
||||||
|
})
|
||||||
|
command('4foldopen')
|
||||||
|
screen:expect({
|
||||||
|
grid = [[
|
||||||
|
{1:-}// foldLevel() {{{2 |
|
||||||
|
{1:│}/// @return fold level at line number "lnum" in the current window. |
|
||||||
|
{1: }static int foldLevel(linenr_T lnum) |
|
||||||
|
{1:-}{ |
|
||||||
|
{1:-} // While updating the folds lines between invalid_top and invalid_bot have |
|
||||||
|
{1:2} // an undefined fold level. Otherwise update the folds first. |
|
||||||
|
{1:+}{2:+--- 2 lines: if (invalid_top == 0) {·········································}|
|
||||||
|
{1:+}{2:+--- 2 lines: } else if (lnum == prev_lnum && prev_lnum_lvl >= 0) {···········}|
|
||||||
|
{1:+}{2:+--- 2 lines: } else if (lnum >= invalid_top && lnum <= invalid_bot) {········}|
|
||||||
|
{1:│} } |
|
||||||
|
{1:│} |
|
||||||
|
{1:│} // Return quickly when there is no folding at all in this window. |
|
||||||
|
{1:+}{2:+--- 2 lines: if (!hasAnyFolding(curwin)) {···································}|
|
||||||
|
{1:│} } |
|
||||||
|
{1:│} |
|
||||||
|
{1:│} return foldLevelWin(curwin, lnum); |
|
||||||
|
{1: }^} |
|
||||||
|
{3:~ }|*5
|
||||||
|
|
|
||||||
|
]],
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('is defered when the buffer is not up-to-date', function()
|
||||||
|
exec_lua(function()
|
||||||
|
vim.lsp.foldclose('comment')
|
||||||
|
vim.lsp.util.buf_versions[bufnr] = 0
|
||||||
|
end)
|
||||||
|
screen:expect({
|
||||||
|
grid = [[
|
||||||
|
{1:+}{2:+-- 2 lines: foldLevel()······················································}|
|
||||||
|
{1: }static int foldLevel(linenr_T lnum) |
|
||||||
|
{1:-}{ |
|
||||||
|
{1:+}{2:+--- 2 lines: While updating the folds lines between invalid_top and invalid_b}|
|
||||||
|
{1:-} if (invalid_top == 0) { |
|
||||||
|
{1:2} checkupdate(curwin); |
|
||||||
|
{1:-} } else if (lnum == prev_lnum && prev_lnum_lvl >= 0) { |
|
||||||
|
{1:2} return prev_lnum_lvl; |
|
||||||
|
{1:-} } else if (lnum >= invalid_top && lnum <= invalid_bot) { |
|
||||||
|
{1:2} return -1; |
|
||||||
|
{1:│} } |
|
||||||
|
{1:│} |
|
||||||
|
{1:│} // Return quickly when there is no folding at all in this window. |
|
||||||
|
{1:-} if (!hasAnyFolding(curwin)) { |
|
||||||
|
{1:2} return 0; |
|
||||||
|
{1:│} } |
|
||||||
|
{1:│} |
|
||||||
|
{1:│} return foldLevelWin(curwin, lnum); |
|
||||||
|
{1: }^} |
|
||||||
|
{3:~ }|*3
|
||||||
|
|
|
||||||
|
]],
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end)
|
Reference in New Issue
Block a user