aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYi Ming <ofseed@foxmail.com>2024-11-29 20:40:32 +0800
committerGitHub <noreply@github.com>2024-11-29 12:40:32 +0000
commita1e313ded6e4c46c58012639e5c0c6d0b009d52a (patch)
tree891bcc418369607060b599f4b224b41a46fc86e7
parentc867a4f5e335a1e62e699c01056f9553c0ce151a (diff)
downloadrneovim-a1e313ded6e4c46c58012639e5c0c6d0b009d52a.tar.gz
rneovim-a1e313ded6e4c46c58012639e5c0c6d0b009d52a.tar.bz2
rneovim-a1e313ded6e4c46c58012639e5c0c6d0b009d52a.zip
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
-rw-r--r--runtime/doc/lsp.txt30
-rw-r--r--runtime/doc/news.txt2
-rw-r--r--runtime/lua/vim/lsp.lua34
-rw-r--r--runtime/lua/vim/lsp/_folding_range.lua371
-rw-r--r--runtime/lua/vim/lsp/protocol.lua7
-rw-r--r--runtime/lua/vim/shared.lua45
-rw-r--r--runtime/lua/vim/treesitter/_fold.lua53
-rwxr-xr-xscripts/gen_vimdoc.lua1
-rw-r--r--test/functional/plugin/lsp/folding_range_spec.lua647
9 files changed, 1141 insertions, 49 deletions
diff --git a/runtime/doc/lsp.txt b/runtime/doc/lsp.txt
index 1693ff5e4f..f7157df0f2 100644
--- a/runtime/doc/lsp.txt
+++ b/runtime/doc/lsp.txt
@@ -204,6 +204,7 @@ won't run if your server doesn't support them.
- `'textDocument/diagnostic'`
- `'textDocument/documentHighlight'`
- `'textDocument/documentSymbol'`
+- `'textDocument/foldingRange'`
- `'textDocument/formatting'`
- `'textDocument/hover'`
- `'textDocument/implementation'`
@@ -697,6 +698,35 @@ commands *vim.lsp.commands*
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()*
Provides an interface between the built-in client and a `formatexpr`
function.
diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt
index 98782bfd15..ad3f2c0a6a 100644
--- a/runtime/doc/news.txt
+++ b/runtime/doc/news.txt
@@ -230,6 +230,8 @@ LSP
• |vim.lsp.buf.hover()| now highlights hover ranges using the
|hl-LspReferenceTarget| highlight group.
• 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
diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua
index 6d29c9e4df..a3791e15c3 100644
--- a/runtime/lua/vim/lsp.lua
+++ b/runtime/lua/vim/lsp.lua
@@ -3,6 +3,7 @@ local validate = vim.validate
local lsp = vim._defer_require('vim.lsp', {
_changetracking = ..., --- @module 'vim.lsp._changetracking'
+ _folding_range = ..., --- @module 'vim.lsp._folding_range'
_snippet_grammar = ..., --- @module 'vim.lsp._snippet_grammar'
_tagfunc = ..., --- @module 'vim.lsp._tagfunc'
_watchfiles = ..., --- @module 'vim.lsp._watchfiles'
@@ -57,6 +58,7 @@ lsp._request_name_to_capability = {
[ms.textDocument_documentHighlight] = { 'documentHighlightProvider' },
[ms.textDocument_documentLink] = { 'documentLinkProvider' },
[ms.textDocument_documentSymbol] = { 'documentSymbolProvider' },
+ [ms.textDocument_foldingRange] = { 'foldingRangeProvider' },
[ms.textDocument_formatting] = { 'documentFormattingProvider' },
[ms.textDocument_hover] = { 'hoverProvider' },
[ms.textDocument_implementation] = { 'implementationProvider' },
@@ -1094,6 +1096,38 @@ function lsp.tagfunc(pattern, flags)
return vim.lsp._tagfunc(pattern, flags)
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.
---
---@param client_id (integer)
diff --git a/runtime/lua/vim/lsp/_folding_range.lua b/runtime/lua/vim/lsp/_folding_range.lua
new file mode 100644
index 0000000000..6a445017a3
--- /dev/null
+++ b/runtime/lua/vim/lsp/_folding_range.lua
@@ -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
diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua
index 3d29dad90a..cfd47d8f7c 100644
--- a/runtime/lua/vim/lsp/protocol.lua
+++ b/runtime/lua/vim/lsp/protocol.lua
@@ -440,6 +440,13 @@ function protocol.make_client_capabilities()
properties = { 'command' },
},
},
+ foldingRange = {
+ dynamicRegistration = false,
+ lineFoldingOnly = true,
+ foldingRange = {
+ collapsedText = true,
+ },
+ },
formatting = {
dynamicRegistration = true,
},
diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua
index 4f2373b182..2e8edea22a 100644
--- a/runtime/lua/vim/shared.lua
+++ b/runtime/lua/vim/shared.lua
@@ -737,6 +737,51 @@ function vim.list_slice(list, start, finish)
return new_list
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.
---
---@see |lua-patterns|
diff --git a/runtime/lua/vim/treesitter/_fold.lua b/runtime/lua/vim/treesitter/_fold.lua
index 7237d2e7d4..0cb5b497c7 100644
--- a/runtime/lua/vim/treesitter/_fold.lua
+++ b/runtime/lua/vim/treesitter/_fold.lua
@@ -30,65 +30,20 @@ function FoldInfo.new()
}, FoldInfo)
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
---@param srow integer
---@param erow integer 0-indexed, exclusive
function FoldInfo:remove_range(srow, erow)
- list_remove(self.levels, srow + 1, erow)
- 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
+ vim._list_remove(self.levels, srow + 1, erow)
+ vim._list_remove(self.levels0, srow + 1, erow)
end
---@package
---@param srow integer
---@param erow integer 0-indexed, exclusive
function FoldInfo:add_range(srow, erow)
- list_insert(self.levels, srow + 1, erow, -1)
- list_insert(self.levels0, srow + 1, erow, -1)
+ vim._list_insert(self.levels, srow + 1, erow, -1)
+ vim._list_insert(self.levels0, srow + 1, erow, -1)
end
---@param range Range2
diff --git a/scripts/gen_vimdoc.lua b/scripts/gen_vimdoc.lua
index 1125021bdc..3f870c561f 100755
--- a/scripts/gen_vimdoc.lua
+++ b/scripts/gen_vimdoc.lua
@@ -274,6 +274,7 @@ local config = {
'diagnostic.lua',
'codelens.lua',
'completion.lua',
+ 'folding_range.lua',
'inlay_hint.lua',
'tagfunc.lua',
'semantic_tokens.lua',
diff --git a/test/functional/plugin/lsp/folding_range_spec.lua b/test/functional/plugin/lsp/folding_range_spec.lua
new file mode 100644
index 0000000000..7e68a598d2
--- /dev/null
+++ b/test/functional/plugin/lsp/folding_range_spec.lua
@@ -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)