1

Regenerate nvim config

This commit is contained in:
2024-06-02 03:29:20 +02:00
parent 75eea0c030
commit ef2e28883d
5576 changed files with 604886 additions and 503 deletions

View File

@ -0,0 +1,196 @@
local api = vim.api
---Export methods to the users, `require('ufo').method(...)`
---@class Ufo
local M = {}
---Peek the folded line under cursor, any motions in the normal window will close the floating window.
---@param enter? boolean enter the floating window, default value is false
---@param nextLineIncluded? boolean include the next line of last line of closed fold, default is true
---@return number? winid return the winid if successful, otherwise return nil
function M.peekFoldedLinesUnderCursor(enter, nextLineIncluded)
return require('ufo.preview'):peekFoldedLinesUnderCursor(enter, nextLineIncluded)
end
---Go to previous start fold. Neovim can't go to previous start fold directly, it's an extra motion.
function M.goPreviousStartFold()
require('ufo.action').goPreviousStartFold()
end
---Go to previous closed fold. It's an extra motion.
function M.goPreviousClosedFold()
require('ufo.action').goPreviousClosedFold()
end
---Go to next closed fold. It's an extra motion.
function M.goNextClosedFold()
return require('ufo.action').goNextClosedFold()
end
---Close all folds but keep foldlevel
function M.closeAllFolds()
return M.closeFoldsWith(0)
end
---Open all folds but keep foldlevel
function M.openAllFolds()
return require('ufo.action').openFoldsExceptKinds()
end
---Close the folds with a higher level,
---Like execute `set foldlevel=level` but keep foldlevel
---@param level? number fold level, `v:count` by default
function M.closeFoldsWith(level)
return require('ufo.action').closeFolds(level or vim.v.count)
end
---Open folds except specified kinds
---@param kinds? UfoFoldingRangeKind[] kind in ranges, get default kinds from `config.close_fold_kinds_for_ft`
function M.openFoldsExceptKinds(kinds)
if not kinds then
local c = require('ufo.config')
if c.close_fold_kinds and not vim.tbl_isempty(c.close_fold_kinds) then
kinds = c.close_fold_kinds
else
kinds = c.close_fold_kinds_for_ft[vim.bo.ft] or c.close_fold_kinds_for_ft.default
end
end
return require('ufo.action').openFoldsExceptKinds(kinds)
end
---Inspect ufo information by bufnr
---@param bufnr? number buffer number, current buffer by default
function M.inspect(bufnr)
bufnr = bufnr or api.nvim_get_current_buf()
local msg = require('ufo.main').inspectBuf(bufnr)
if not msg then
vim.notify(('Buffer %d has not been attached.'):format(bufnr), vim.log.levels.ERROR)
else
vim.notify(table.concat(msg, '\n'), vim.log.levels.INFO)
end
end
---Enable ufo
function M.enable()
require('ufo.main').enable()
end
---Disable ufo
function M.disable()
require('ufo.main').disable()
end
---Check whether the buffer has been attached
---@param bufnr? number buffer number, current buffer by default
---@return boolean
function M.hasAttached(bufnr)
return require('ufo.main').hasAttached(bufnr)
end
---Attach bufnr to enable all features
---@param bufnr? number buffer number, current buffer by default
function M.attach(bufnr)
require('ufo.main').attach(bufnr)
end
---Detach bufnr to disable all features
---@param bufnr? number buffer number, current buffer by default
function M.detach(bufnr)
require('ufo.main').detach(bufnr)
end
---Enable to get folds and update them at once
---@param bufnr? number buffer number, current buffer by default
---@return string|'start'|'pending'|'stop' status
function M.enableFold(bufnr)
return require('ufo.main').enableFold(bufnr)
end
---Disable to get folds
---@param bufnr? number buffer number, current buffer by default
---@return string|'start'|'pending'|'stop' status
function M.disableFold(bufnr)
return require('ufo.main').disableFold(bufnr)
end
---Get foldingRange from the ufo internal providers by name
---@param bufnr number
---@param providerName string|'lsp'|'treesitter'|'indent'
---@return UfoFoldingRange[]|Promise
function M.getFolds(bufnr, providerName)
if type(bufnr) == 'string' and type(providerName) == 'number' then
--TODO signature is changed (swap parameters), notify deprecated in next released
---@deprecated
---@diagnostic disable-next-line: cast-local-type
bufnr, providerName = providerName, bufnr
end
local func = require('ufo.provider'):getFunction(providerName)
return func(bufnr)
end
---Apply foldingRange at once.
---ufo always apply folds asynchronously, this function can apply folds synchronously.
---Note: Get ranges from 'lsp' provider is asynchronous.
---@param bufnr number
---@param ranges UfoFoldingRange[]
---@return number winid return the winid if successful, otherwise return -1
function M.applyFolds(bufnr, ranges)
vim.validate({bufnr = {bufnr, 'number', true}, ranges = {ranges, 'table'}})
return require('ufo.fold').apply(bufnr, ranges, true)
end
---Setup configuration and enable ufo
---@param opts? UfoConfig
function M.setup(opts)
opts = opts or {}
M._config = opts
M.enable()
end
---------------------------------------setFoldVirtTextHandler---------------------------------------
---@class UfoFoldVirtTextHandlerContext
---@field bufnr number buffer for closed fold
---@field winid number window for closed fold
---@field text string text for the first line of closed fold
---@field get_fold_virt_text fun(lnum: number): UfoExtmarkVirtTextChunk[] a function to get virtual text by lnum
---@class UfoExtmarkVirtTextChunk
---@field [1] string text
---@field [2] string|number highlight
---Set a fold virtual text handler for a buffer, will override global handler if it's existed.
---Ufo actually uses a virtual text with \`nvim_buf_set_extmark\` to overlap the first line of closed fold
---run \`:h nvim_buf_set_extmark | call search('virt_text')\` for detail.
---Return `{}` will not render folded line but only keep a extmark for providers.
---@diagnostic disable: undefined-doc-param
---Detail for handler function:
---@param virtText UfoExtmarkVirtTextChunk[] contained text and highlight captured by Ufo, export to caller
---@param lnum number first line of closed fold, like \`v:foldstart\` in foldtext()
---@param endLnum number last line of closed fold, like \`v:foldend\` in foldtext()
---@param width number text area width, exclude foldcolumn, signcolumn and numberwidth
---@param truncate fun(str: string, width: number): string truncate the str to become specific width,
---return width of string is equal or less than width (2nd argument).
---For example: '1': 1 cell, '你': 2 cells, '2': 1 cell, '好': 2 cells
---truncate('1你2好', 1) return '1'
---truncate('1你2好', 2) return '1'
---truncate('1你2好', 3) return '1你'
---truncate('1你2好', 4) return '1你2'
---truncate('1你2好', 5) return '1你2'
---truncate('1你2好', 6) return '1你2好'
---truncate('1你2好', 7) return '1你2好'
---@param ctx UfoFoldVirtTextHandlerContext the context used by ufo, export to caller
---@alias UfoFoldVirtTextHandler fun(virtText: UfoExtmarkVirtTextChunk[], lnum: number, endLnum: number, width: number, truncate: fun(str: string, width: number), ctx: UfoFoldVirtTextHandlerContext): UfoExtmarkVirtTextChunk[]
---@param bufnr number
---@param handler UfoFoldVirtTextHandler
function M.setFoldVirtTextHandler(bufnr, handler)
vim.validate({bufnr = {bufnr, 'number', true}, handler = {handler, 'function'}})
require('ufo.decorator'):setVirtTextHandler(bufnr, handler)
end
---@diagnostic disable: undefined-doc-param
---------------------------------------setFoldVirtTextHandler---------------------------------------
return M

View File

@ -0,0 +1,196 @@
local api = vim.api
local cmd = vim.cmd
local fn = vim.fn
local utils = require('ufo.utils')
local fold = require('ufo.fold')
local M = {}
function M.goPreviousStartFold()
local function getCurLnum()
return api.nvim_win_get_cursor(0)[1]
end
local cnt = vim.v.count1
local view = utils.saveView(0)
local curLnum = getCurLnum()
cmd('norm! m`')
local previousLnum
local previousLnumList = {}
while cnt > 0 do
cmd([[keepj norm! zk]])
local tLnum = getCurLnum()
cmd([[keepj norm! [z]])
if tLnum == getCurLnum() then
local foldStartLnum = utils.foldClosed(0, tLnum)
if foldStartLnum > 0 then
cmd(('keepj norm! %dgg'):format(foldStartLnum))
end
end
local nextLnum = getCurLnum()
while curLnum > nextLnum do
tLnum = nextLnum
table.insert(previousLnumList, nextLnum)
cmd([[keepj norm! zj]])
nextLnum = getCurLnum()
if nextLnum == tLnum then
break
end
end
if #previousLnumList == 0 then
break
end
if #previousLnumList < cnt then
cnt = cnt - #previousLnumList
curLnum = previousLnumList[1]
previousLnum = curLnum
cmd(('keepj norm! %dgg'):format(curLnum))
previousLnumList = {}
else
while cnt > 0 do
previousLnum = table.remove(previousLnumList)
cnt = cnt - 1
end
end
end
utils.restView(0, view)
if previousLnum then
cmd(('norm! %dgg_'):format(previousLnum))
end
end
function M.goPreviousClosedFold()
local count = vim.v.count1
local curLnum = api.nvim_win_get_cursor(0)[1]
local cnt = 0
local lnum
for i = curLnum - 1, 1, -1 do
if utils.foldClosed(0, i) == i then
cnt = cnt + 1
lnum = i
if cnt == count then
break
end
end
end
if lnum then
cmd('norm! m`')
api.nvim_win_set_cursor(0, {lnum, 0})
end
end
function M.goNextClosedFold()
local count = vim.v.count1
local curLnum = api.nvim_win_get_cursor(0)[1]
local lineCount = api.nvim_buf_line_count(0)
local cnt = 0
local lnum
for i = curLnum + 1, lineCount do
if utils.foldClosed(0, i) == i then
cnt = cnt + 1
lnum = i
if cnt == count then
break
end
end
end
if lnum then
cmd('norm! m`')
api.nvim_win_set_cursor(0, {lnum, 0})
end
end
function M.closeFolds(level)
cmd('silent! %foldclose!')
local curBufnr = api.nvim_get_current_buf()
local fb = fold.get(curBufnr)
if not fb then
return
end
for _, range in ipairs(fb.foldRanges) do
fb:closeFold(range.startLine + 1, range.endLine + 1)
end
if level == 0 then
return
end
local lineCount = fb:lineCount()
local stack = {}
local lastLevel = 0
local lastEndLnum = -1
local lnum = 1
while lnum <= lineCount do
local l = fn.foldlevel(lnum)
if lastLevel < l or l > 0 and lnum == lastEndLnum + 1 then
local endLnum = utils.foldClosedEnd(0, lnum)
table.insert(stack, {endLnum, false})
if l <= level then
local cmds = {}
for i = #stack, 1, -1 do
local opened = stack[i][2]
if opened then
break
end
stack[i][2] = true
table.insert(cmds, lnum .. 'foldopen')
fb:openFold(lnum)
end
if #cmds > 0 then
cmd(table.concat(cmds, '|'))
-- A line may contain multiple folds, make sure lnum is opened.
while lnum == utils.foldClosed(0, lnum) do
cmd(lnum .. 'foldopen')
end
end
else
--TODO
-- (#184)
--`%foldclose!` doesn't close all folds for window
--endLnum may return -1, look like upstream issue.
if lnum < endLnum then
lnum = endLnum
end
end
end
lastLevel = l
lnum = lnum + 1
while #stack > 0 do
local endLnum = stack[#stack][1]
if lnum <= endLnum then
break
end
table.remove(stack)
lastEndLnum = math.max(lastEndLnum, endLnum)
end
end
end
function M.openFoldsExceptKinds(kinds)
cmd('silent! %foldopen!')
local curBufnr = api.nvim_get_current_buf()
local fb = fold.get(curBufnr)
if not fb or type(kinds) ~= 'table' or #kinds == 0 then
return
end
local res = {}
for _, range in ipairs(fb.foldRanges) do
if range.kind and vim.tbl_contains(kinds, range.kind) then
local startLnum, endLnum = range.startLine + 1, range.endLine + 1
fb:closeFold(startLnum, endLnum)
table.insert(res, {startLnum, endLnum})
end
end
table.sort(res, function(a, b)
return a[1] == b[1] and a[2] < b[2] or a[1] > b[1]
end)
local cmds = {}
for _, range in ipairs(res) do
table.insert(cmds, range[1] .. 'foldclose')
end
if #cmds > 0 then
cmd(table.concat(cmds, '|'))
end
end
return M

View File

@ -0,0 +1,113 @@
local api = vim.api
local buffer = require('ufo.model.buffer')
local event = require('ufo.lib.event')
local disposable = require('ufo.lib.disposable')
local promise = require('promise')
local utils = require('ufo.utils')
---@class UfoBufferManager
---@field initialized boolean
---@field buffers UfoBuffer[]
---@field disposables UfoDisposable[]
local BufferManager = {}
local function attach(self, bufnr)
if not self.buffers[bufnr] and not self.bufDetachSet[bufnr] then
local buf = buffer:new(bufnr)
self.buffers[bufnr] = buf
if not buf:attach() then
self.buffers[bufnr] = nil
end
end
end
function BufferManager:initialize()
if self.initialized then
return self
end
self.initialized = true
self.buffers = {}
self.bufDetachSet = {}
self.disposables = {}
table.insert(self.disposables, disposable:create(function()
for _, b in pairs(self.buffers) do
b:dispose()
end
self.initialized = false
self.buffers = {}
self.bufDetachSet = {}
end))
---@diagnostic disable-next-line: unused-local
event:on('BufWinEnter', function(bufnr, winid)
attach(self, bufnr or api.nvim_get_current_buf())
end, self.disposables)
event:on('BufDetach', function(bufnr)
local b = self.buffers[bufnr]
if b then
b:dispose()
self.buffers[bufnr] = nil
end
end, self.disposables)
event:on('BufTypeChanged', function(bufnr, new, old)
local b = self.buffers[bufnr]
if b and old ~= new then
if new == 'terminal' or new == 'prompt' then
event:emit('BufDetach', bufnr)
else
b.bt = new
end
end
end, self.disposables)
event:on('FileTypeChanged', function(bufnr, new, old)
local b = self.buffers[bufnr]
if b and old ~= new then
b.ft = new
end
end, self.disposables)
event:on('SyntaxChanged', function(bufnr, new, old)
local b = self.buffers[bufnr]
if b and old ~= new then
b._syntax = new
end
end, self.disposables)
for _, winid in ipairs(api.nvim_tabpage_list_wins(0)) do
local bufnr = api.nvim_win_get_buf(winid)
if utils.isBufLoaded(bufnr) then
attach(self, bufnr)
else
-- the first buffer is unloaded while firing `BufWinEnter`
promise.resolve():thenCall(function()
if utils.isBufLoaded(bufnr) then
attach(self, bufnr)
end
end)
end
end
return self
end
---
---@param bufnr number
---@return UfoBuffer
function BufferManager:get(bufnr)
return self.buffers[bufnr]
end
function BufferManager:dispose()
disposable.disposeAll(self.disposables)
self.disposables = {}
end
function BufferManager:attach(bufnr)
self.bufDetachSet[bufnr] = nil
attach(self, bufnr)
end
function BufferManager:detach(bufnr)
self.bufDetachSet[bufnr] = true
event:emit('BufDetach', bufnr)
end
return BufferManager

View File

@ -0,0 +1,88 @@
---@class UfoConfig
---@field provider_selector? function
---@field open_fold_hl_timeout number
---@field close_fold_kinds_for_ft table<string, UfoFoldingRangeKind[]>
---@field fold_virt_text_handler? UfoFoldVirtTextHandler A global virtual text handler, reference to `ufo.setFoldVirtTextHandler`
---@field enable_get_fold_virt_text boolean
---@field preview table
local def = {
open_fold_hl_timeout = 400,
provider_selector = nil,
close_fold_kinds_for_ft = {default = {}},
fold_virt_text_handler = nil,
enable_get_fold_virt_text = false,
preview = {
win_config = {
border = 'rounded',
winblend = 12,
winhighlight = 'Normal:Normal',
maxheight = 20
},
mappings = {
scrollB = '',
scrollF = '',
scrollU = '',
scrollD = '',
scrollE = '<C-E>',
scrollY = '<C-Y>',
jumpTop = '',
jumpBot = '',
close = 'q',
switch = '<Tab>',
trace = '<CR>'
}
}
}
---@type UfoConfig
local Config = {}
---@alias UfoProviderEnum
---| 'lsp'
---| 'treesitter'
---| 'indent'
---
---@param bufnr number
---@param filetype string file type
---@param buftype string buffer type
---@return UfoProviderEnum|string[]|function|nil
---return a string type use ufo providers
---return a string in a table like a string type
---return empty string '' will disable any providers
---return `nil` will use default value {'lsp', 'indent'}
---@diagnostic disable-next-line: unused-function, unused-local
function Config.provider_selector(bufnr, filetype, buftype) end
local function init()
local ufo = require('ufo')
---@type UfoConfig
Config = vim.tbl_deep_extend('keep', ufo._config or {}, def)
vim.validate({
open_fold_hl_timeout = {Config.open_fold_hl_timeout, 'number'},
provider_selector = {Config.provider_selector, 'function', true},
close_fold_kinds_for_ft = {Config.close_fold_kinds_for_ft, 'table'},
fold_virt_text_handler = {Config.fold_virt_text_handler, 'function', true},
preview_mappings = {Config.preview.mappings, 'table'}
})
local preview = Config.preview
for msg, key in pairs(preview.mappings) do
if key == '' then
preview.mappings[msg] = nil
end
end
if Config.close_fold_kinds and not vim.tbl_isempty(Config.close_fold_kinds) then
vim.notify('Option `close_fold_kinds` in `nvim-ufo` is deprecated, use `close_fold_kinds_for_ft` instead.',
vim.log.levels.WARN)
if not Config.close_fold_kinds_for_ft.default then
Config.close_fold_kinds_for_ft.default = Config.close_fold_kinds
end
end
ufo._config = nil
end
init()
return Config

View File

@ -0,0 +1,390 @@
local api = vim.api
local fn = vim.fn
local cmd = vim.cmd
local utils = require('ufo.utils')
local config = require('ufo.config')
local log = require('ufo.lib.log')
local disposable = require('ufo.lib.disposable')
local event = require('ufo.lib.event')
local window = require('ufo.model.window')
local fold = require('ufo.fold')
local render = require('ufo.render')
---@class UfoDecorator
---@field initialized boolean
---@field ns number
---@field hlNs number
---@field virtTextHandler? UfoFoldVirtTextHandler[]
---@field enableFoldEndVirtText boolean
---@field openFoldHlTimeout number
---@field openFoldHlEnabled boolean
---@field curWinid number
---@field lastWinid number
---@field virtTextHandlers table<number, function>
---@field winSessions table<number, UfoWindow>
---@field disposables UfoDisposable[]
local Decorator = {}
local collection
local bufnrSet
local namespaces
local handlerErrorMsg
---@diagnostic disable-next-line: unused-local
local function onStart(name, tick)
collection = {}
bufnrSet = {}
namespaces = {}
end
---@diagnostic disable-next-line: unused-local
local function onWin(name, winid, bufnr, topRow, botRow)
local fb = fold.get(bufnr)
if bufnrSet[bufnr] or not fb or fb.foldedLineCount == 0 and not vim.wo[winid].foldenable then
collection[winid] = nil
return false
end
local self = Decorator
local wses = self.winSessions[winid]
wses:onWin(bufnr, fb)
collection[winid] = {
winid = winid,
bufnr = bufnr,
rows = {}
}
bufnrSet[bufnr] = winid
end
---@diagnostic disable-next-line: unused-local
local function onLine(name, winid, bufnr, row)
table.insert(collection[winid].rows, row)
end
---@diagnostic disable-next-line: unused-local
local function onEnd(name, tick)
local needRedraw = false
local self = Decorator
self.curWinid = api.nvim_get_current_win()
for winid, data in pairs(collection or {}) do
if #data.rows > 0 then
local wses = self.winSessions[winid]
local fb = wses.foldbuffer
local foldedPairs = wses.foldedPairs
if self.curWinid == winid and not next(foldedPairs) then
foldedPairs = self:computeFoldedPairs(data.rows)
end
local shared
for _, row in ipairs(data.rows) do
local lnum = row + 1
if not foldedPairs[lnum] and fb:lineIsClosed(lnum) then
if shared == nil then
local _, winids = utils.getWinByBuf(fb.bufnr)
shared = winids ~= nil
end
self:highlightOpenFold(fb, winid, lnum, shared)
local didOpen = fb:openFold(lnum)
if not shared then
needRedraw = didOpen or needRedraw
end
end
end
local cursor = wses:cursor()
local curLnum = cursor[1]
if needRedraw then
fb:syncFoldedLines(winid)
end
local curFoldStart, curFoldEnd = 0, 0
for fs, fe in pairs(foldedPairs) do
local _, didClose = self:getVirtTextAndCloseFold(winid, fs, fe)
if not utils.has10() then
needRedraw = needRedraw or didClose
end
if curFoldStart == 0 and fs <= curLnum and curLnum <= fe then
curFoldStart, curFoldEnd = fs, fe
end
end
if not needRedraw then
local lastCurLnum = wses.lastCurLnum
local lastCurFoldStart, lastCurFoldEnd = wses.lastCurFoldStart, wses.lastCurFoldEnd
if lastCurFoldStart ~= curFoldStart and
lastCurFoldStart < lastCurLnum and lastCurLnum <= lastCurFoldEnd and
lastCurFoldStart < curLnum and curLnum <= lastCurFoldEnd then
log.debug('Curosr under the stale fold range, should open fold.')
needRedraw = fb:openFold(lastCurFoldStart) or needRedraw
end
end
local didHighlight = false
if curLnum == curFoldStart then
didHighlight = wses:setCursorFoldedLineHighlight()
else
didHighlight = wses:clearCursorFoldedLineHighlight()
end
needRedraw = needRedraw or didHighlight
wses.lastCurFoldStart, wses.lastCurFoldEnd = curFoldStart, curFoldEnd
wses.lastCurLnum = curLnum
end
end
if needRedraw then
log.debug('Need redraw.')
cmd('redraw')
end
self.lastWinid = self.curWinid
end
local function silentOnEnd(...)
local ok, msg = pcall(onEnd, ...)
if not ok and (type(msg) ~= 'string' or not msg:match('Keyboard interrupt\n')) then
error(msg, 0)
end
end
function Decorator:resetCurosrFoldedLineHighlightByBuf(bufnr)
-- TODO
-- exit cmd window will throw E315: ml_get: invalid lnum: 1
if api.nvim_buf_line_count(bufnr) == 0 then
return
end
local id, winids = utils.getWinByBuf(bufnr)
if id == -1 then
return
end
for _, winid in ipairs(winids or {id}) do
local wses = self.winSessions[winid]
wses:clearCursorFoldedLineHighlight()
end
end
function Decorator:highlightOpenFold(fb, winid, lnum, shared)
if self.openFoldHlEnabled and winid == self.lastWinid and api.nvim_get_mode().mode ~= 'c' then
local endLnum
if not shared then
local fl = fb:foldedLine(lnum)
local _
_, endLnum = fl:range()
if endLnum == 0 then
return
end
else
endLnum = utils.foldClosedEnd(winid, lnum)
if endLnum < 0 then
return
end
end
render.highlightLinesWithTimeout(shared and winid or fb.bufnr, 'UfoFoldedBg', self.hlNs,
lnum, endLnum, self.openFoldHlTimeout, shared)
end
end
function Decorator:computeFoldedPairs(rows)
local lastRow = rows[1]
local res = {}
for i = 2, #rows do
local lnum = lastRow + 1
local curRow = rows[i]
if curRow > lnum and utils.foldClosed(0, lnum) == lnum then
res[lnum] = curRow
end
lastRow = curRow
end
local lnum = lastRow + 1
if utils.foldClosed(0, lnum) == lnum then
res[lnum] = utils.foldClosedEnd(0, lnum)
end
return res
end
function Decorator:getVirtTextAndCloseFold(winid, lnum, endLnum, doRender)
local didClose = false
local wses = self.winSessions[winid]
if not wses then
return {}, didClose
end
local bufnr, fb = wses.bufnr, wses.foldbuffer
if endLnum then
wses.foldedPairs[lnum] = endLnum
end
local width = wses:textWidth()
local ok, res = true, wses.foldedTextMaps[lnum]
local fl = fb:foldedLine(lnum)
local rendered = false
if fl then
if not res and not fl:widthChanged(width) then
res = fl.virtText
end
rendered = fl:hasRendered()
end
if not res or not rendered then
if not endLnum then
endLnum = wses:foldEndLnum(lnum)
end
local text = fb:lines(lnum)[1]
if not res then
local handler = self:getVirtTextHandler(bufnr)
local virtText
local syntax = fb:syntax() ~= ''
local concealLevel = wses:concealLevel()
if not next(namespaces) then
for _, ns in pairs(api.nvim_get_namespaces()) do
if self.ns ~= ns then
table.insert(namespaces, ns)
end
end
end
virtText = render.captureVirtText(bufnr, text, lnum, syntax, namespaces, concealLevel)
local getFoldVirtText
if self.enableGetFoldVirtText then
getFoldVirtText = function(l)
local t = type(l)
assert(t == 'number', 'expected a number, got ' .. t)
assert(lnum <= l and l <= endLnum,
('expected lnum range from %d to %d, got %d'):format(lnum, endLnum, l))
local line = fb:lines(l)[1]
return render.captureVirtText(bufnr, line, l, syntax, namespaces, concealLevel)
end
end
ok, res = pcall(handler, virtText, lnum, endLnum, width, utils.truncateStrByWidth, {
bufnr = bufnr,
winid = winid,
text = text,
get_fold_kind = function(l)
l = l == nil and lnum or l
local t = type(l)
assert(t == 'number', 'expected a number, got ' .. t)
return fb:lineKind(winid, l)
end,
get_fold_virt_text = getFoldVirtText
})
wses.foldedTextMaps[lnum] = res
end
if doRender == nil then
doRender = true
end
if ok then
if bufnrSet[bufnr] == winid then
if doRender then
log.debug('Window:', winid, 'need add/update folded lnum:', lnum)
didClose = true
else
log.debug('Window:', winid, 'will add/update folded lnum:', lnum)
end
fb:closeFold(lnum, endLnum, text, res, width, doRender)
end
else
fb:closeFold(lnum, endLnum, text, {{handlerErrorMsg, 'Error'}}, width, doRender)
log.error(res)
end
end
return res, didClose
end
---@diagnostic disable-next-line: unused-local
function Decorator.defaultVirtTextHandler(virtText, lnum, endLnum, width, truncate, ctx)
local newVirtText = {}
local suffix = ''
local sufWidth = fn.strdisplaywidth(suffix)
local targetWidth = width - sufWidth
local curWidth = 0
for _, chunk in ipairs(virtText) do
local chunkText = chunk[1]
local chunkWidth = fn.strdisplaywidth(chunkText)
if targetWidth > curWidth + chunkWidth then
table.insert(newVirtText, chunk)
else
chunkText = truncate(chunkText, targetWidth - curWidth)
local hlGroup = chunk[2]
table.insert(newVirtText, {chunkText, hlGroup})
chunkWidth = fn.strdisplaywidth(chunkText)
-- str width returned from truncate() may less than 2nd argument, need padding
if curWidth + chunkWidth < targetWidth then
suffix = suffix .. (' '):rep(targetWidth - curWidth - chunkWidth)
end
break
end
curWidth = curWidth + chunkWidth
end
table.insert(newVirtText, {suffix, 'UfoFoldedEllipsis'})
return newVirtText
end
function Decorator:setVirtTextHandler(bufnr, handler)
bufnr = bufnr or api.nvim_get_current_buf()
self.virtTextHandlers[bufnr] = handler
end
---
---@param bufnr number
---@return UfoFoldVirtTextHandler
function Decorator:getVirtTextHandler(bufnr)
bufnr = bufnr or api.nvim_get_current_buf()
return self.virtTextHandlers[bufnr]
end
---
---@param namespace number
---@return UfoDecorator
function Decorator:initialize(namespace)
if self.initialized then
return self
end
self.initialized = true
api.nvim_set_decoration_provider(namespace, {
on_start = onStart,
on_win = onWin,
on_line = onLine,
on_end = silentOnEnd
})
self.ns = namespace
self.hlNs = self.hlNs or api.nvim_create_namespace('')
self.disposables = {}
table.insert(self.disposables, disposable:create(function()
self.initialized = false
api.nvim_set_decoration_provider(namespace, {})
for bufnr in ipairs(fold.buffers()) do
self:resetCurosrFoldedLineHighlightByBuf(bufnr)
end
end))
self.enableGetFoldVirtText = config.enable_get_fold_virt_text
self.openFoldHlTimeout = config.open_fold_hl_timeout
self.openFoldHlEnabled = self.openFoldHlTimeout > 0
event:on('setOpenFoldHl', function(val)
if type(val) == 'boolean' then
self.openFoldHlEnabled = val
else
self.openFoldHlEnabled = self.openFoldHlTimeout > 0
end
end, self.disposables)
local virtTextHandler = config.fold_virt_text_handler or self.defaultVirtTextHandler
self.virtTextHandlers = setmetatable({}, {
__index = function(tbl, bufnr)
rawset(tbl, bufnr, virtTextHandler)
return virtTextHandler
end
})
handlerErrorMsg = ([[!Error in user's handler, check out `%s`]]):format(log.path)
self.winSessions = setmetatable({}, {
__index = function(tbl, winid)
local o = window:new(winid)
rawset(tbl, winid, o)
return o
end
})
event:on('WinClosed', function(winid)
self.winSessions[winid] = nil
end, self.disposables)
event:on('BufDetach', function(bufnr)
self:resetCurosrFoldedLineHighlightByBuf(bufnr)
self.virtTextHandlers[bufnr] = nil
end, self.disposables)
return self
end
function Decorator:dispose()
disposable.disposeAll(self.disposables)
self.disposables = {}
end
return Decorator

View File

@ -0,0 +1,107 @@
local cmd = vim.cmd
local utils = require('ufo.utils')
local function convertToFoldRanges(ranges, rowPairs)
-- just check the last range only to filter out same ranges
local lastStartLine, lastEndLine
local foldRanges = {}
local minLines = vim.wo.foldminlines
for _, r in ipairs(ranges) do
local startLine, endLine = r.startLine, r.endLine
if not rowPairs[startLine] and endLine - startLine >= minLines and
(lastStartLine ~= startLine or lastEndLine ~= endLine) then
lastStartLine, lastEndLine = startLine, endLine
table.insert(foldRanges, {startLine + 1, endLine + 1})
end
end
return foldRanges
end
---@type UfoFoldDriverNonFFI|UfoFoldDriverFFI
local FoldDriver
---@class UfoFoldDriverFFI
local FoldDriverFFI = {}
function FoldDriverFFI:new(wffi)
local o = setmetatable({}, self)
self.__index = self
self._wffi = wffi
return o
end
---
---@param winid number
---@param ranges UfoFoldingRange
---@param rowPairs table<number, number>
function FoldDriverFFI:createFolds(winid, ranges, rowPairs)
utils.winCall(winid, function()
local wo = vim.wo
local level = wo.foldlevel
self._wffi.clearFolds(winid)
local foldRanges = convertToFoldRanges(ranges, rowPairs)
self._wffi.createFolds(winid, foldRanges)
wo.foldmethod = 'manual'
wo.foldenable = true
wo.foldlevel = level
foldRanges = {}
for row, endRow in pairs(rowPairs) do
table.insert(foldRanges, {row + 1, endRow + 1})
end
self._wffi.createFolds(winid, foldRanges)
end)
end
---@class UfoFoldDriverNonFFI
local FoldDriverNonFFI = {}
function FoldDriverNonFFI:new()
local o = setmetatable({}, self)
self.__index = self
return o
end
---
---@param winid number
---@param ranges UfoFoldingRange
---@param rowPairs table<number, number>
function FoldDriverNonFFI:createFolds(winid, ranges, rowPairs)
utils.winCall(winid, function()
local level = vim.wo.foldlevel
local cmds = {}
local foldRanges = convertToFoldRanges(ranges, rowPairs)
for _, r in ipairs(foldRanges) do
table.insert(cmds, ('%d,%d:fold'):format(r[1], r[2]))
end
local view = utils.saveView(0)
cmd('norm! zE')
utils.restView(0, view)
table.insert(cmds, 'setl foldmethod=manual')
table.insert(cmds, 'setl foldenable')
table.insert(cmds, 'setl foldlevel=' .. level)
foldRanges = {}
for row, endRow in pairs(rowPairs) do
table.insert(foldRanges, {row + 1, endRow + 1})
end
table.sort(foldRanges, function(a, b)
return a[1] == b[1] and a[2] < b[2] or a[1] > b[1]
end)
for _, r in ipairs(foldRanges) do
table.insert(cmds, ('%d,%dfold'):format(r[1], r[2]))
end
cmd(table.concat(cmds, '|'))
end)
end
local function init()
if jit ~= nil then
FoldDriver = FoldDriverFFI:new(require('ufo.wffi'))
else
FoldDriver = FoldDriverNonFFI:new()
end
end
init()
return FoldDriver

View File

@ -0,0 +1,229 @@
local api = vim.api
local cmd = vim.cmd
local config = require('ufo.config')
local promise = require('promise')
local async = require('async')
local utils = require('ufo.utils')
local provider = require('ufo.provider')
local log = require('ufo.lib.log')
local event = require('ufo.lib.event')
local manager = require('ufo.fold.manager')
local disposable = require('ufo.lib.disposable')
---@class UfoFold
---@field initialized boolean
---@field disposables UfoDisposable[]
local Fold = {}
local updateFoldDebounced
---@param bufnr number
---@return Promise
local function tryUpdateFold(bufnr)
return async(function()
local winid = utils.getWinByBuf(bufnr)
if not utils.isWinValid(winid) then
return
end
-- some plugins may change foldmethod to diff
await(utils.wait(50))
if not utils.isWinValid(winid) or utils.isDiffOrMarkerFold(winid) then
return
end
await(Fold.update(bufnr))
end)
end
local function setFoldText(bufnr)
api.nvim_buf_call(bufnr, function()
cmd([[
setl foldtext=v:lua.require'ufo.main'.foldtext()
setl fillchars+=fold:\ ]])
end)
end
function Fold.update(bufnr)
bufnr = bufnr or api.nvim_get_current_buf()
local fb = manager:get(bufnr)
if not fb then
return promise.resolve()
end
if manager:isFoldMethodsDisabled(fb) then
if not pcall(fb.getRangesFromExtmarks, fb) then
fb:resetFoldedLines(true)
end
return promise.resolve()
end
if fb.status == 'pending' and manager:applyFoldRanges(bufnr) ~= -1 then
return promise.resolve()
end
local changedtick, ft, bt = fb:changedtick(), fb:filetype(), fb:buftype()
fb:acquireRequest()
local function dispose(resolved)
---@diagnostic disable-next-line: redefined-local
local fb = manager:get(bufnr)
if not fb then
return false
end
fb:releaseRequest()
local ok = ft == fb:filetype() and bt == fb:buftype()
if ok then
if resolved then
ok = changedtick == fb:changedtick()
end
end
local requested = fb:requested()
if not ok and not requested then
log.debug('update fold again for bufnr:', bufnr)
updateFoldDebounced(bufnr)
end
return ok and not requested
end
log.info('providers:', fb.providers)
return provider:requestFoldingRange(fb.providers, bufnr):thenCall(function(res)
if not dispose(true) then
return
end
local selected, ranges = res[1], res[2]
fb.selectedProvider = type(selected) == 'string' and selected or 'external'
log.info('selected provider:', fb.selectedProvider)
if not ranges or #ranges == 0 or not utils.isBufLoaded(bufnr) then
return
end
fb.status = manager:applyFoldRanges(bufnr, ranges) == -1 and 'pending' or 'start'
end, function(err)
if not dispose(false) then
return
end
return promise.reject(err)
end)
end
---
---@param bufnr number
function Fold.get(bufnr)
return manager:get(bufnr)
end
function Fold.buffers()
return manager.buffers
end
function Fold.apply(bufnr, ranges, manual)
return manager:applyFoldRanges(bufnr, ranges, manual)
end
function Fold.attach(bufnr)
bufnr = bufnr or api.nvim_get_current_buf()
if not manager:attach(bufnr) then
return
end
log.debug('attach bufnr:', bufnr)
setFoldText(bufnr)
tryUpdateFold(bufnr)
end
function Fold.setStatus(bufnr, status)
local fb = manager:get(bufnr)
local old = ''
if fb then
old = fb.status
fb.status = status
end
return old
end
updateFoldDebounced = (function()
local lastBufnr
local debounced = require('ufo.lib.debounce')(Fold.update, 300)
return function(bufnr, flush, onlyPending)
bufnr = bufnr or api.nvim_get_current_buf()
local fb = manager:get(bufnr)
if not fb or utils.mode() ~= 'n' or
onlyPending and fb.status ~= 'pending' or fb.status == 'stop' then
return
end
if lastBufnr ~= bufnr then
debounced:flush()
end
lastBufnr = bufnr
debounced(bufnr)
if flush then
debounced:flush()
end
end
end)()
local function handleDiffMode(winid, new, old)
if old ~= new and new == 0 then
local bufnr = api.nvim_win_get_buf(winid)
local fb = manager:get(bufnr)
if fb then
fb:resetFoldedLines(true)
-- TODO
-- buffer go back normal mode from diff mode will disable `foldenable` if the foldmethod was
-- `manual` before entering diff mode. Unfortunately, foldmethod will always be `manual` if
-- enable ufo, `foldenable` will be disabled.
-- `set foldenable` forcedly, feel free to open an issue if ufo is evil.
promise.resolve():thenCall(function()
if utils.isWinValid(winid) and vim.wo[winid].foldmethod == 'manual' then
utils.winCall(winid, function()
cmd('silent! %foldopen!')
end)
vim.wo[winid].foldenable = true
end
end)
tryUpdateFold(bufnr)
end
end
end
---
---@param ns number
---@return UfoFold
function Fold:initialize(ns)
if self.initialized then
return self
end
self.initialized = true
self.disposables = {}
table.insert(self.disposables, disposable:create(function()
self.initialized = false
end))
event:on('BufWinEnter', function(bufnr)
bufnr = bufnr or api.nvim_get_current_buf()
local fb = manager:get(bufnr)
if not fb then
return
end
setFoldText(bufnr)
updateFoldDebounced(bufnr, true, true)
end, self.disposables)
event:on('BufWritePost', function(bufnr)
updateFoldDebounced(bufnr, true)
end, self.disposables)
event:on('TextChanged', updateFoldDebounced, self.disposables)
event:on('ModeChangedToNormal', function(bufnr, oldMode)
local onlyPending = oldMode ~= 'i' and oldMode ~= 't'
updateFoldDebounced(bufnr, true, onlyPending)
end, self.disposables)
event:on('BufAttach', Fold.attach, self.disposables)
event:on('DiffModeChanged', handleDiffMode, self.disposables)
table.insert(self.disposables, manager:initialize(ns, config.provider_selector,
config.close_fold_kinds_for_ft))
return self
end
function Fold:dispose()
disposable.disposeAll(self.disposables)
self.disposables = {}
end
return Fold

View File

@ -0,0 +1,211 @@
local buffer = require('ufo.model.foldbuffer')
local event = require('ufo.lib.event')
local disposable = require('ufo.lib.disposable')
local bufmanager = require('ufo.bufmanager')
local utils = require('ufo.utils')
local driver = require('ufo.fold.driver')
local log = require('ufo.lib.log')
---@class UfoFoldBufferManager
---@field initialized boolean
---@field buffers UfoFoldBuffer[]
---@field providerSelector function
---@field closeKindsMap table<string,UfoFoldingRangeKind[]>
---@field disposables UfoDisposable[]
local FoldBufferManager = {}
---
---@param namespace number
---@param selector function
---@param closeKindsMap <table, string,UfoFoldingRangeKind[]>
---@return UfoFoldBufferManager
function FoldBufferManager:initialize(namespace, selector, closeKindsMap)
if self.initialized then
return self
end
self.ns = namespace
self.providerSelector = selector
self.closeKindsMap = closeKindsMap
self.buffers = {}
self.initialized = true
self.disposables = {}
table.insert(self.disposables, disposable:create(function()
for _, fb in pairs(self.buffers) do
fb:dispose()
end
self.buffers = {}
self.initialized = false
end))
event:on('BufDetach', function(bufnr)
local fb = self:get(bufnr)
if fb then
fb:dispose()
end
self.buffers[bufnr] = nil
end, self.disposables)
event:on('BufReload', function(bufnr)
local fb = self:get(bufnr)
if fb then
fb:dispose()
end
end, self.disposables)
local function optChanged(bufnr, new, old)
if old ~= new then
local fb = self:get(bufnr)
if fb then
fb.providers = nil
end
end
end
event:on('BufTypeChanged', optChanged, self.disposables)
event:on('FileTypeChanged', optChanged, self.disposables)
event:on('BufLinesChanged', function(bufnr, _, firstLine, lastLine, lastLineUpdated)
local fb = self:get(bufnr)
if fb then
fb:handleFoldedLinesChanged(firstLine, lastLine, lastLineUpdated)
end
end, self.disposables)
self.providerSelector = selector
return self
end
---
---@param bufnr number
---@return boolean
function FoldBufferManager:attach(bufnr)
local fb = self:get(bufnr)
if not fb then
self.buffers[bufnr] = buffer:new(bufmanager:get(bufnr), self.ns)
end
return not fb
end
---
---@param bufnr number
---@return UfoFoldBuffer
function FoldBufferManager:get(bufnr)
return self.buffers[bufnr]
end
function FoldBufferManager:dispose()
disposable.disposeAll(self.disposables)
self.disposables = {}
end
function FoldBufferManager:parseBufferProviders(fb, selector)
if not utils.isBufLoaded(fb.bufnr) then
return
end
if not selector then
fb.providers = {'lsp', 'indent'}
return
end
local res
local providers = selector(fb.bufnr, fb:filetype(), fb:buftype())
local t = type(providers)
if t == 'nil' then
res = {'lsp', 'indent'}
elseif t == 'string' or t == 'function' then
res = {providers}
elseif t == 'table' then
res = {}
for _, m in ipairs(providers) do
if #res == 2 then
error('Return value of `provider_selector` only supports {`main`, `fallback`} ' ..
[[combo, don't add providers more than two into return table.]])
end
table.insert(res, m)
end
else
res = {''}
end
fb.providers = res
end
function FoldBufferManager:isFoldMethodsDisabled(fb)
if not fb.providers then
self:parseBufferProviders(fb, self.providerSelector)
end
return not fb.providers or fb.providers[1] == ''
end
function FoldBufferManager:getRowPairsByScanning(fb, winid)
local rowPairs = {}
for _, range in ipairs(fb:scanFoldedRanges(winid)) do
local row, endRow = range[1], range[2]
rowPairs[row] = endRow
end
return rowPairs
end
---
---@param bufnr number
---@param ranges? UfoFoldingRange[]
---@param manual? boolean
---@return number
function FoldBufferManager:applyFoldRanges(bufnr, ranges, manual)
local fb = self:get(bufnr)
if not fb then
return -1
end
local changedtick = fb:changedtick()
if ranges then
fb.foldRanges = ranges
fb.version = changedtick
elseif changedtick ~= fb.version then
return -1
end
local winid, windows = utils.getWinByBuf(bufnr)
if winid == -1 or not utils.isWinValid(winid) then
return -1
elseif not vim.wo[winid].foldenable or utils.isDiffOrMarkerFold(winid) then
return -1
elseif utils.mode() ~= 'n' then
return -1
end
local rowPairs = {}
local isFirstApply = not fb.scanned
if not manual and not fb.scanned or windows then
rowPairs = self:getRowPairsByScanning(fb, winid)
local kinds = self.closeKindsMap[fb:filetype()] or self.closeKindsMap.default
for _, range in ipairs(fb.foldRanges) do
if range.kind and vim.tbl_contains(kinds, range.kind) then
local startLine, endLine = range.startLine, range.endLine
rowPairs[startLine] = endLine
fb:closeFold(startLine + 1, endLine + 1)
end
end
fb.scanned = true
else
local ok, res = pcall(function()
for _, range in ipairs(fb:getRangesFromExtmarks()) do
local row, endRow = range[1], range[2]
rowPairs[row] = endRow
end
end)
if not ok then
log.info(res)
fb:resetFoldedLines(true)
rowPairs = self:getRowPairsByScanning(fb, winid)
end
end
local view, wrow
-- topline may changed after applying folds, restore topline to save our eyes
if isFirstApply and not vim.tbl_isempty(rowPairs) then
view = utils.saveView(winid)
wrow = utils.wrow(winid)
end
log.info('apply fold ranges:', fb.foldRanges)
log.info('apply fold rowPairs:', rowPairs)
driver:createFolds(winid, fb.foldRanges, rowPairs)
if view and utils.wrow(winid) ~= wrow then
view.topline, view.topfill = utils.evaluateTopline(winid, view.lnum, wrow)
utils.restView(winid, view)
end
return winid
end
return FoldBufferManager

View File

@ -0,0 +1,116 @@
local cmd = vim.cmd
local api = vim.api
local fn = vim.fn
local event = require('ufo.lib.event')
local disposable = require('ufo.lib.disposable')
---@class UfoHighlight
local Highlight = {}
local initialized
---@type table<number|string, table>
local hlGroups
---@type table<string, string>
local signNames
local function resetHighlightGroup()
local termguicolors = vim.o.termguicolors
hlGroups = setmetatable({}, {
__index = function(tbl, k)
local ok, hl
if type(k) == 'number' then
ok, hl = pcall(api.nvim_get_hl_by_id, k, termguicolors)
else
ok, hl = pcall(api.nvim_get_hl_by_name, k, termguicolors)
end
if not ok or hl[vim.type_idx] == vim.types.dictionary then
hl = {}
end
rawset(tbl, k, hl)
return hl
end
})
local ok, hl = pcall(api.nvim_get_hl_by_name, 'Folded', termguicolors)
if ok and hl.background then
if termguicolors then
cmd(('hi default UfoFoldedBg guibg=#%x'):format(hl.background))
else
cmd(('hi default UfoFoldedBg ctermbg=%d'):format(hl.background))
end
else
cmd('hi default link UfoFoldedBg Visual')
end
ok, hl = pcall(api.nvim_get_hl_by_name, 'Normal', termguicolors)
if ok and hl.foreground then
if termguicolors then
cmd(('hi default UfoFoldedFg guifg=#%x'):format(hl.foreground))
else
cmd(('hi default UfoFoldedFg ctermfg=%d'):format(hl.foreground))
end
else
cmd('hi default UfoFoldedFg ctermfg=None guifg=None')
end
cmd([[
hi default link UfoPreviewSbar PmenuSbar
hi default link UfoPreviewThumb PmenuThumb
hi default link UfoPreviewWinBar UfoFoldedBg
hi default link UfoPreviewCursorLine Visual
hi default link UfoFoldedEllipsis Comment
hi default link UfoCursorFoldedLine CursorLine
]])
end
local function resetSignGroup()
signNames = setmetatable({}, {
__index = function(tbl, k)
assert(fn.sign_define(k, {linehl = k}) == 0,
'Define sign name ' .. k .. 'failed')
rawset(tbl, k, k)
return k
end
})
return disposable:create(function()
for _, name in pairs(signNames) do
pcall(fn.sign_undefine, name)
end
end)
end
function Highlight.hlGroups()
if not initialized then
Highlight:initialize()
end
return hlGroups
end
function Highlight.signNames()
if not initialized then
Highlight:initialize()
end
return signNames
end
---
---@return UfoHighlight
function Highlight:initialize()
if initialized then
return self
end
self.disposables = {}
event:on('ColorScheme', resetHighlightGroup, self.disposables)
resetHighlightGroup()
table.insert(self.disposables, resetSignGroup())
initialized = true
return self
end
function Highlight:dispose()
disposable.disposeAll(self.disposables)
self.disposables = {}
initialized = false
end
return Highlight

View File

@ -0,0 +1,78 @@
local uv = vim.loop
---@class UfoDebounce
---@field timer userdata
---@field fn function
---@field args table
---@field wait number
---@field leading? boolean
---@overload fun(fn: function, wait: number, leading?: boolean): UfoDebounce
local Debounce = {}
---
---@param fn function
---@param wait number
---@param leading? boolean
---@return UfoDebounce
function Debounce:new(fn, wait, leading)
vim.validate({
fn = {fn, 'function'},
wait = {wait, 'number'},
leading = {leading, 'boolean', true}
})
local o = setmetatable({}, self)
o.timer = nil
o.fn = vim.schedule_wrap(fn)
o.args = nil
o.wait = wait
o.leading = leading
return o
end
function Debounce:call(...)
local timer = self.timer
self.args = {...}
if not timer then
---@type userdata
timer = uv.new_timer()
self.timer = timer
local wait = self.wait
timer:start(wait, wait, self.leading and function()
self:cancel()
end or function()
self:flush()
end)
if self.leading then
self.fn(...)
end
else
timer:again()
end
end
function Debounce:cancel()
local timer = self.timer
if timer then
if timer:has_ref() then
timer:stop()
if not timer:is_closing() then
timer:close()
end
end
self.timer = nil
end
end
function Debounce:flush()
if self.timer then
self:cancel()
self.fn(unpack(self.args))
end
end
Debounce.__index = Debounce
Debounce.__call = Debounce.call
return setmetatable(Debounce, {
__call = Debounce.new
})

View File

@ -0,0 +1,36 @@
---@class UfoDisposable
---@field func fun()
local Disposable = {}
---
---@param disposables UfoDisposable[]
function Disposable.disposeAll(disposables)
for _, item in ipairs(disposables) do
if item.dispose then
item:dispose()
end
end
end
---
---@param func fun()
---@return UfoDisposable
function Disposable:new(func)
local o = setmetatable({}, self)
self.__index = self
o.func = func
return o
end
---
---@param func fun()
---@return UfoDisposable
function Disposable:create(func)
return self:new(func)
end
function Disposable:dispose()
self.func()
end
return Disposable

View File

@ -0,0 +1,58 @@
local disposable = require('ufo.lib.disposable')
local log = require('ufo.lib.log')
---@class UfoEvent
local Event = {
_collection = {}
}
---@param name string
---@param listener function
function Event:off(name, listener)
local listeners = self._collection[name]
if not listeners then
return
end
for i = 1, #listeners do
if listeners[i] == listener then
table.remove(listeners, i)
break
end
end
if #listeners == 0 then
self._collection[name] = nil
end
end
---@param name string
---@param listener function
---@param disposables? UfoDisposable[]
---@return UfoDisposable
function Event:on(name, listener, disposables)
if not self._collection[name] then
self._collection[name] = {}
end
table.insert(self._collection[name], listener)
local d = disposable:create(function()
self:off(name, listener)
end)
if type(disposables) == 'table' then
table.insert(disposables, d)
end
return d
end
---@param name string
---@vararg any
function Event:emit(name, ...)
local listeners = self._collection[name]
if not listeners then
return
end
log.trace('event:', name, 'listeners:', listeners, 'args:', ...)
for _, listener in ipairs(listeners) do
listener(...)
end
end
return Event

View File

@ -0,0 +1,106 @@
--- Singleton
---@class UfoLog
---@field trace fun(...)
---@field debug fun(...)
---@field info fun(...)
---@field warn fun(...)
---@field error fun(...)
---@field path string
local Log = {}
local fn = vim.fn
local uv = vim.loop
---@type table<string, number>
local levelMap
local levelNr
local defaultLevel
local function getLevelNr(level)
local nr
if type(level) == 'number' then
nr = level
elseif type(level) == 'string' then
nr = levelMap[level:upper()]
else
nr = defaultLevel
end
return nr
end
---
---@param l number|string
function Log.setLevel(l)
levelNr = getLevelNr(l)
end
---
---@param l number|string
---@return boolean
function Log.isEnabled(l)
return getLevelNr(l) >= levelNr
end
---
---@return string|'trace'|'debug'|'info'|'warn'|'error'
function Log.level()
for l, nr in pairs(levelMap) do
if nr == levelNr then
return l
end
end
return 'UNDEFINED'
end
local function inspect(v)
local s
local t = type(v)
if t == 'nil' then
s = 'nil'
elseif t ~= 'string' then
s = vim.inspect(v)
else
s = tostring(v)
end
return s
end
local function pathSep()
return uv.os_uname().sysname == 'Windows_NT' and [[\]] or '/'
end
local function init()
local logDir = fn.stdpath('cache')
Log.path = table.concat({logDir, 'ufo.log'}, pathSep())
local logDateFmt = '%y-%m-%d %T'
fn.mkdir(logDir, 'p')
levelMap = {TRACE = 0, DEBUG = 1, INFO = 2, WARN = 3, ERROR = 4}
defaultLevel = 3
Log.setLevel(vim.env.UFO_LOG)
for l in pairs(levelMap) do
Log[l:lower()] = function(...)
local argc = select('#', ...)
if argc == 0 or levelMap[l] < levelNr then
return
end
local msgTbl = {}
for i = 1, argc do
local arg = select(i, ...)
table.insert(msgTbl, inspect(arg))
end
local msg = table.concat(msgTbl, ' ')
local info = debug.getinfo(2, 'Sl')
local linfo = info.short_src:match('[^/]*$') .. ':' .. info.currentline
local fp = assert(io.open(Log.path, 'a+'))
local str = string.format('[%s] [%s] %s : %s\n', os.date(logDateFmt), l, linfo, msg)
fp:write(str)
fp:close()
end
end
end
init()
return Log

View File

@ -0,0 +1,204 @@
local M = {}
local cmd = vim.cmd
local api = vim.api
local event = require('ufo.lib.event')
local utils = require('ufo.utils')
local provider = require('ufo.provider')
local fold = require('ufo.fold')
local decorator = require('ufo.decorator')
local highlight = require('ufo.highlight')
local preview = require('ufo.preview')
local disposable = require('ufo.lib.disposable')
local bufmanager = require('ufo.bufmanager')
local enabled
---@type UfoDisposable[]
local disposables = {}
local function createEvents()
local gid = api.nvim_create_augroup('Ufo', {})
api.nvim_create_autocmd({'BufWinEnter', 'TextChanged', 'BufWritePost'}, {
group = gid,
callback = function(ev)
event:emit(ev.event, ev.buf)
end
})
api.nvim_create_autocmd('WinClosed', {
group = gid,
callback = function(ev)
event:emit(ev.event, tonumber(ev.file))
end
})
api.nvim_create_autocmd('ModeChanged', {
group = gid,
pattern = '*:n',
callback = function(ev)
local previousMode = ev.match:match('(%a):')
event:emit('ModeChangedToNormal', ev.buf, previousMode)
end
})
api.nvim_create_autocmd('ColorScheme', {
group = gid,
callback = function(ev)
event:emit(ev.event)
end
})
api.nvim_create_autocmd('OptionSet', {
group = gid,
pattern = {'buftype', 'filetype', 'syntax', 'diff'},
callback = function(ev)
local bufnr = api.nvim_get_current_buf()
local match = ev.match
local e
if match == 'buftype' then
e = 'BufTypeChanged'
elseif match == 'filetype' then
e = 'FileTypeChanged'
elseif match == 'syntax' then
e = 'SyntaxChanged'
elseif match == 'diff' then
event:emit('DiffModeChanged', api.nvim_get_current_win(), vim.v.option_new, vim.v.option_old)
return
else
error([[Didn't match any events!]])
end
event:emit(e, bufnr, vim.v.option_new, vim.v.option_old)
end
})
return disposable:create(function()
api.nvim_del_augroup_by_id(gid)
end)
end
local function createCommand()
cmd([[
com! UfoEnable lua require('ufo').enable()
com! UfoDisable lua require('ufo').disable()
com! UfoInspect lua require('ufo').inspect()
com! UfoAttach lua require('ufo').attach()
com! UfoDetach lua require('ufo').detach()
com! UfoEnableFold lua require('ufo').enableFold()
com! UfoDisableFold lua require('ufo').disableFold()
]])
end
function M.enable()
if enabled then
return false
end
local ns = api.nvim_create_namespace('ufo')
createCommand()
disposables = {}
table.insert(disposables, createEvents())
table.insert(disposables, highlight:initialize())
table.insert(disposables, provider:initialize())
table.insert(disposables, decorator:initialize(ns))
table.insert(disposables, fold:initialize(ns))
table.insert(disposables, preview:initialize(ns))
table.insert(disposables, bufmanager:initialize())
enabled = true
return true
end
function M.disable()
if not enabled then
return false
end
disposable.disposeAll(disposables)
enabled = false
return true
end
function M.inspectBuf(bufnr)
bufnr = bufnr or api.nvim_get_current_buf()
local fb = fold.get(bufnr)
if not fb then
return
end
local msg = {}
table.insert(msg, 'Buffer: ' .. bufnr)
table.insert(msg, 'Fold Status: ' .. fb.status)
local main = fb.providers[1]
table.insert(msg, 'Main provider: ' .. (type(main) == 'function' and 'external' or main))
if fb.providers[2] then
table.insert(msg, 'Fallback provider: ' .. fb.providers[2])
end
table.insert(msg, 'Selected provider: ' .. (fb.selectedProvider or 'nil'))
local winid = utils.getWinByBuf(bufnr)
local curKind
local curStartLine, curEndLine = 0, 0
local kindSet = {}
local lnum = api.nvim_win_get_cursor(winid)[1]
for _, range in ipairs(fb.foldRanges) do
local sl, el = range.startLine, range.endLine
if curStartLine < sl and sl < lnum and lnum <= el + 1 then
curStartLine, curEndLine = sl, el
curKind = range.kind
end
if range.kind then
kindSet[range.kind] = true
end
end
local kinds = {}
for kind in pairs(kindSet) do
table.insert(kinds, kind)
end
table.insert(msg, 'Fold kinds: ' .. table.concat(kinds, ', '))
if curStartLine ~= 0 or curEndLine ~= 0 then
table.insert(msg, ('Cursor range: [%d, %d]'):format(curStartLine + 1, curEndLine + 1))
end
if curKind then
table.insert(msg, 'Cursor kind: ' .. curKind)
end
return msg
end
function M.hasAttached(bufnr)
bufnr = bufnr or api.nvim_get_current_buf()
local buf = bufmanager:get(bufnr)
return buf and buf.attached
end
function M.attach(bufnr)
bufnr = bufnr or api.nvim_get_current_buf()
bufmanager:attach(bufnr)
end
function M.detach(bufnr)
bufnr = bufnr or api.nvim_get_current_buf()
bufmanager:detach(bufnr)
end
function M.enableFold(bufnr)
bufnr = bufnr or api.nvim_get_current_buf()
local old = fold.setStatus(bufnr, 'start')
fold.update(bufnr)
return old
end
function M.disableFold(bufnr)
bufnr = bufnr or api.nvim_get_current_buf()
return fold.setStatus(bufnr, 'stop')
end
function M.foldtext()
local fs, fe = vim.v.foldstart, vim.v.foldend
local winid = api.nvim_get_current_win()
local virtText = decorator:getVirtTextAndCloseFold(winid, fs, fe, false)
if utils.has10() then
return virtText
end
local text
if next(virtText) then
text = ''
for _, chunk in ipairs(virtText) do
text = text .. chunk[1]
end
text = utils.expandTab(text, vim.bo.ts)
end
return text or utils.expandTab(api.nvim_buf_get_lines(0, fs - 1, fs, true)[1], vim.bo.ts)
end
return M

View File

@ -0,0 +1,246 @@
local event = require('ufo.lib.event')
local api = vim.api
---@class UfoBuffer
---@field bufnr number
---@field attached boolean
---@field bt string
---@field ft string
---@field _syntax string
---@field _lines table<number, string|boolean> A list of string or boolean
local Buffer = {}
function Buffer:new(bufnr)
local o = setmetatable({}, self)
self.__index = self
o.bufnr = bufnr
o:reload()
return o
end
function Buffer:reload()
self._changedtick = api.nvim_buf_get_changedtick(self.bufnr)
self._lines = {}
for _ = 1, api.nvim_buf_line_count(self.bufnr) do
table.insert(self._lines, false)
end
end
function Buffer:dispose()
self.attached = false
self.bt = nil
self.ft = nil
end
function Buffer:attach()
local bt = self:buftype()
if bt == 'terminal' or bt == 'prompt' then
return false
end
---@diagnostic disable: redefined-local, unused-local
self.attached = api.nvim_buf_attach(self.bufnr, false, {
on_lines = function(name, bufnr, changedtick, firstLine, lastLine,
lastLineUpdated, byteCount)
if not self.attached then
return true
end
if firstLine == lastLine and lastLine == lastLineUpdated and byteCount == 0 then
return
end
-- TODO upstream bug
-- set foldmethod=expr && change lines from floating window will fire `on_lines`,
-- skip if changedtick is unchanged
if self._changedtick == changedtick then
return
end
self._changedtick = changedtick
self._lines = self:handleLinesChanged(self._lines, firstLine, lastLine, lastLineUpdated)
event:emit('BufLinesChanged', bufnr, changedtick, firstLine, lastLine,
lastLineUpdated, byteCount)
end,
on_changedtick = function(name, bufnr, changedtick)
self._changedtick = changedtick
end,
on_detach = function(name, bufnr)
event:emit('BufDetach', bufnr)
end,
on_reload = function(name, bufnr)
self:reload()
event:emit('BufReload', bufnr)
end
})
---@diagnostic enable: redefined-local, unused-local
if self.attached then
event:emit('BufAttach', self.bufnr)
end
return self.attached
end
---lower is less than or equal to lnum
---@param lnum number
---@param endLnum number
---@return table[]
function Buffer:buildMissingHunk(lnum, endLnum)
local hunks = {}
local s, e
local cnt = 0
for i = lnum, endLnum do
if not self._lines[i] then
cnt = cnt + 1
if not s then
s = i
end
e = i
elseif e then
table.insert(hunks, {s, e})
s, e = nil, nil
end
end
if e then
table.insert(hunks, {s, e})
end
-- scan backward
if #hunks > 0 then
local firstHunk = hunks[1]
local fhLnum = firstHunk[1]
if fhLnum == lnum then
local i = lnum - 1
while i > 0 do
if self._lines[i] then
break
end
i = i - 1
end
fhLnum = i + 1
cnt = cnt + lnum - fhLnum
firstHunk[1] = fhLnum
lnum = fhLnum
end
end
if cnt > (endLnum - lnum) / 4 and #hunks > 2 then
hunks = {{lnum, endLnum}}
end
return hunks
end
function Buffer:sliceLines(lines, firstLine, lastLine, lastLineUpdated)
local newLines = {}
for i = 1, firstLine do
table.insert(newLines, lines[i])
end
for _ = firstLine + 1, lastLineUpdated do
table.insert(newLines, false)
end
for i = lastLine + 1, #lines do
table.insert(newLines, lines[i])
end
return newLines
end
---
---@param lines any[]
---@param firstLine number
---@param lastLine number
---@param lastLineUpdated number
---@return any[]
function Buffer:handleLinesChanged(lines, firstLine, lastLine, lastLineUpdated)
local delta = lastLineUpdated - lastLine
if delta == 0 then
for i = firstLine + 1, lastLine do
lines[i] = false
end
elseif delta > 0 then
if #lines > 800 and delta > 10 then
lines = self:sliceLines(lines, firstLine, lastLine, lastLineUpdated)
else
for i = firstLine + 1, lastLine do
lines[i] = false
end
for i = firstLine + 1, firstLine + delta do
table.insert(lines, i, false)
end
end
else
if #lines > 800 and -delta > 10 then
lines = self:sliceLines(lines, firstLine, lastLine, lastLineUpdated)
else
for i = lastLine, lastLineUpdated + 1, -1 do
table.remove(lines, i)
end
for i = firstLine + 1, lastLineUpdated do
lines[i] = false
end
end
if #lines == 0 then
lines = {false}
end
end
return lines
end
---
---@return number
function Buffer:changedtick()
return self._changedtick
end
---
---@return string
function Buffer:filetype()
if self.attached and not self.ft then
self.ft = vim.bo[self.bufnr].ft
end
return self.ft
end
---
---@return string
function Buffer:buftype()
if self.attached and not self.bt then
self.bt = vim.bo[self.bufnr].bt
end
return self.bt
end
function Buffer:syntax()
if self.attached and not self.syntax then
self._syntax = vim.bo[self.bufnr].syntax
end
return self._syntax
end
---
---@return number
function Buffer:lineCount()
if self:buftype() == 'quickfix' then
self:reload()
end
return #self._lines
end
---@param lnum number
---@param endLnum? number
---@return string[]
function Buffer:lines(lnum, endLnum)
local lineCount = self:lineCount()
assert(lineCount >= lnum, 'index out of bounds')
local res = {}
endLnum = endLnum and endLnum or lnum
if endLnum < 0 then
endLnum = lineCount + endLnum + 1
end
for _, hunk in ipairs(self:buildMissingHunk(lnum, endLnum)) do
local hs, he = hunk[1], hunk[2]
local lines = api.nvim_buf_get_lines(self.bufnr, hs - 1, he, true)
for i = hs, he do
self._lines[i] = lines[i - hs + 1]
end
end
for i = lnum, endLnum do
table.insert(res, self._lines[i])
end
return res
end
return Buffer

View File

@ -0,0 +1,260 @@
local api = vim.api
local cmd = vim.cmd
local utils = require('ufo.utils')
local buffer = require('ufo.model.buffer')
local foldedline = require('ufo.model.foldedline')
---@class UfoFoldBuffer
---@field bufnr number
---@field buf UfoBuffer
---@field ns number
---@field status string|'start'|'pending'|'stop'
---@field version number
---@field requestCount number
---@field foldRanges UfoFoldingRange[]
---@field foldedLines table<number, UfoFoldedLine|boolean> A list of UfoFoldedLine or boolean
---@field foldedLineCount number
---@field providers table
---@field scanned boolean
---@field selectedProvider string
local FoldBuffer = setmetatable({}, buffer)
FoldBuffer.__index = FoldBuffer
---@param buf UfoBuffer
---@return UfoFoldBuffer
function FoldBuffer:new(buf, ns)
local o = setmetatable({}, self)
self.__index = self
o.bufnr = buf.bufnr
o.buf = buf
o.ns = ns
o:reset()
return o
end
function FoldBuffer:dispose()
self:resetFoldedLines(true)
self:reset()
end
function FoldBuffer:changedtick()
return self.buf:changedtick()
end
function FoldBuffer:filetype()
return self.buf:filetype()
end
function FoldBuffer:buftype()
return self.buf:buftype()
end
function FoldBuffer:syntax()
return self.buf:syntax()
end
function FoldBuffer:lineCount()
return self.buf:lineCount()
end
---
---@param lnum number
---@param endLnum? number
---@return string[]
function FoldBuffer:lines(lnum, endLnum)
return self.buf:lines(lnum, endLnum)
end
function FoldBuffer:reset()
self.status = 'start'
self.providers = nil
self.selectedProvider = nil
self.version = 0
self.requestCount = 0
self.foldRanges = {}
self:resetFoldedLines()
self.scanned = false
end
function FoldBuffer:resetFoldedLines(clear)
self.foldedLines = {}
self.foldedLineCount = 0
for _ = 1, self:lineCount() do
table.insert(self.foldedLines, false)
end
if clear then
pcall(api.nvim_buf_clear_namespace, self.bufnr, self.ns, 0, -1)
end
end
function FoldBuffer:foldedLine(lnum)
local fl = self.foldedLines[lnum]
if not fl then
return
end
return fl
end
---
---@param winid number
---@param lnum number 1-index
---@return UfoFoldingRangeKind|''
function FoldBuffer:lineKind(winid, lnum)
if utils.isDiffOrMarkerFold(winid) then
return ''
end
local row = lnum - 1
for _, range in ipairs(self.foldRanges) do
if row >= range.startLine and row <= range.endLine then
return range.kind
end
end
return ''
end
function FoldBuffer:handleFoldedLinesChanged(first, last, lastUpdated)
if self.foldedLineCount == 0 then
return
end
local didOpen = false
for i = first + 1, last do
didOpen = self:openFold(i) or didOpen
end
if didOpen and lastUpdated > first then
local winid = utils.getWinByBuf(self.bufnr)
if winid ~= -1 then
utils.winCall(winid, function()
cmd(('sil! %d,%dfoldopen!'):format(first + 1, lastUpdated))
end)
end
end
self.foldedLines = self.buf:handleLinesChanged(self.foldedLines, first, last, lastUpdated)
end
function FoldBuffer:acquireRequest()
self.requestCount = self.requestCount + 1
end
function FoldBuffer:releaseRequest()
if self.requestCount > 0 then
self.requestCount = self.requestCount - 1
end
end
function FoldBuffer:requested()
return self.requestCount > 0
end
---
---@param lnum number
---@return boolean
function FoldBuffer:lineIsClosed(lnum)
return self:foldedLine(lnum) ~= nil
end
---
---@param winid number
function FoldBuffer:syncFoldedLines(winid)
for lnum, fl in ipairs(self.foldedLines) do
if fl and utils.foldClosed(winid, lnum) == -1 then
self:openFold(lnum)
end
end
end
function FoldBuffer:getRangesFromExtmarks()
local res = {}
if self.foldedLineCount == 0 then
return res
end
local marks = api.nvim_buf_get_extmarks(self.bufnr, self.ns, 0, -1, {details = true})
for _, m in ipairs(marks) do
local row, endRow = m[2], m[4].end_row
-- extmark may give backward range
if row > endRow then
error(('expected forward range, got row: %d, endRow: %d'):format(row, endRow))
end
table.insert(res, {row, endRow})
end
return res
end
---
---@param lnum number
---@return boolean
function FoldBuffer:openFold(lnum)
local folded = false
local fl = self.foldedLines[lnum]
if fl then
folded = self.foldedLines[lnum] ~= nil
fl:deleteExtmark()
self.foldedLineCount = self.foldedLineCount - 1
self.foldedLines[lnum] = false
end
return folded
end
---
---@param lnum number
---@param endLnum number
---@param text? string
---@param virtText? string
---@param width? number
---@param doRender? boolean
---@return boolean
function FoldBuffer:closeFold(lnum, endLnum, text, virtText, width, doRender)
local lineCount = self:lineCount()
endLnum = math.min(endLnum, lineCount)
if endLnum < lnum then
return false
end
local fl = self.foldedLines[lnum]
if fl then
if width and fl:widthChanged(width) then
fl.width = width
end
if text and fl:textChanged(text) then
fl.text = text
end
if not width and not text then
return false
end
else
if self.foldedLineCount == 0 and lineCount ~= #self.foldedLines then
self:resetFoldedLines()
end
fl = foldedline:new(self.bufnr, self.ns, text, width)
self.foldedLineCount = self.foldedLineCount + 1
self.foldedLines[lnum] = fl
end
fl:updateVirtText(lnum, endLnum, virtText, doRender)
return true
end
function FoldBuffer:scanFoldedRanges(winid, s, e)
local res = {}
local stack = {}
s, e = s or 1, e or self:lineCount()
utils.winCall(winid, function()
for i = s, e do
local skip = false
while #stack > 0 and i >= stack[#stack] do
local endLnum = table.remove(stack)
cmd(endLnum .. 'foldclose')
skip = true
end
if not skip then
local endLnum = utils.foldClosedEnd(winid, i)
if endLnum ~= -1 then
table.insert(stack, endLnum)
table.insert(res, {i - 1, endLnum - 1})
cmd(i .. 'foldopen')
end
end
end
end)
return res
end
return FoldBuffer

View File

@ -0,0 +1,77 @@
local utils = require('ufo.utils')
local api = vim.api
---@class UfoFoldedLine
---@field id number
---@field bufnr number
---@field ns number
---@field rendered boolean
---@field text? string
---@field width? number
---@field virtText? UfoExtmarkVirtTextChunk[]
local FoldedLine = {}
function FoldedLine:new(bufnr, ns, text, width)
local o = setmetatable({}, self)
self.__index = self
o.id = nil
o.bufnr = bufnr
o.ns = ns
o.text = text
o.width = width
o.rendered = false
o.virtText = nil
return o
end
---
---@param width number
---@return boolean
function FoldedLine:widthChanged(width)
return self.width ~= width
end
function FoldedLine:textChanged(text)
return self.text ~= text
end
function FoldedLine:hasRendered()
return self.rendered == true
end
function FoldedLine:deleteExtmark()
if self.id then
api.nvim_buf_del_extmark(self.bufnr, self.ns, self.id)
end
end
function FoldedLine:updateVirtText(lnum, endLnum, virtText, doRender)
if doRender then
local opts = {
id = self.id,
end_row = endLnum - 1,
end_col = 0,
priority = 10,
hl_mode = 'combine'
}
if not utils.has10() then
opts.virt_text = virtText
opts.virt_text_win_col = 0
end
self.id = api.nvim_buf_set_extmark(self.bufnr, self.ns, lnum - 1, 0, opts)
end
self.rendered = doRender
self.virtText = virtText
end
function FoldedLine:range()
if not self.id then
return 0, 0
end
local mark = api.nvim_buf_get_extmark_by_id(self.bufnr, self.ns, self.id, {details = true})
local row, details = mark[1], mark[3]
local endRow = details.end_row
return row + 1, endRow + 1
end
return FoldedLine

View File

@ -0,0 +1,35 @@
---@alias UfoFoldingRangeKind
---| 'comment'
---| 'imports'
---| 'region'
---@class UfoFoldingRange
---@field startLine number
---@field startCharacter? number
---@field endLine number
---@field endCharacter? number
---@field kind? UfoFoldingRangeKind
local FoldingRange = {}
function FoldingRange.new(startLine, endLine, startCharacter, endCharacter, kind)
local o = {}
o.startLine = startLine
o.endLine = endLine
o.startCharacter = startCharacter
o.endCharacter = endCharacter
o.kind = kind
return o
end
---
---@param ranges UfoFoldingRange
function FoldingRange.sortRanges(ranges)
if jit then
return
end
table.sort(ranges, function(a, b)
return a.startLine == b.startLine and a.endLine < b.endLine or a.startLine > b.startLine
end)
end
return FoldingRange

View File

@ -0,0 +1,119 @@
local api = vim.api
local fn = vim.fn
local utils = require('ufo.utils')
local LSize
---
---@class UfoLineSizeBase
---@field winid number
---@field foldenable boolean
---@field foldClosePairs table<number, number[]>
---@field sizes table<number, number>
local LBase = {}
---
---@param sizes table<number, number>
---@return UfoLineSizeBase
function LBase:new(winid, sizes)
local o = setmetatable({}, self)
self.__index = self
o.winid = winid
o.foldenable = vim.wo[winid].foldenable
o.foldClosePairs = {}
o.sizes = sizes
return o
end
---
---@param lnum number
---@return number
function LBase:size(lnum)
return self.sizes[lnum]
end
---
---@class UfoLineSizeFFI : UfoLineSizeBase
---@field private _wffi UfoWffi
local LFFI = setmetatable({}, {__index = LBase})
---
---@return UfoLineSizeFFI
function LFFI:new(winid)
local super = LBase:new(winid, setmetatable({}, {
__index = function(t, i)
local v = self._wffi.plinesWin(winid, i)
rawset(t, i, v)
return v
end
}))
local o = setmetatable(super, self)
self.__index = self
return o
end
---
---@param lnum number
---@param winheight boolean
---@return number
function LFFI:nofillSize(lnum, winheight)
winheight = winheight or true
return self._wffi.plinesWinNofill(self.winid, lnum, winheight)
end
---
---@param lnum number
---@return number
function LFFI:fillSize(lnum)
return self:size(lnum) - self:nofillSize(lnum, true)
end
---
---@class UfoLineSizeNonFFI : UfoLineSizeBase
---@field perLineWidth number
local LNonFFI = setmetatable({}, {__index = LBase})
---
---@return UfoLineSizeNonFFI
function LNonFFI:new(winid)
local wrap = vim.wo[winid].wrap
local perLineWidth = api.nvim_win_get_width(winid) - utils.textoff(winid)
local super = LBase:new(winid, setmetatable({}, {
__index = function(t, i)
local v
if wrap then
v = math.ceil(math.max(fn.virtcol({i, '$'}) - 1, 1) / perLineWidth)
else
v = 1
end
rawset(t, i, v)
return v
end
}))
local o = setmetatable(super, self)
o.perLineWidth = perLineWidth
self.__index = self
return o
end
LNonFFI.nofillSize = LNonFFI.size
---
---@param _ any
---@return number
function LNonFFI.fillSize(_)
return 0
end
local function init()
if jit ~= nil then
LFFI._wffi = require('ufo.wffi')
LSize = LFFI
else
LSize = LNonFFI
end
end
init()
return LSize

View File

@ -0,0 +1,111 @@
local utils = require('ufo.utils')
local api = vim.api
local cmd = vim.cmd
---@class UfoWindow
---@field winid number
---@field bufnr number
---@field lastBufnr number
---@field foldbuffer UfoFoldBuffer
---@field lastCurLnum number
---@field lastCurFoldStart number
---@field lastCurFoldEnd number
---@field isCurFoldHighlighted boolean
---@field foldedPairs table<number,number>
---@field foldedTextMaps table<number, table>
---@field _cursor number[]
---@field _width number
---@field _concealLevel boolean
local Window = {}
Window.__index = Window
function Window:new(winid)
local o = self == Window and setmetatable({}, self) or self
o.winid = winid
o.bufnr = 0
o.lastCurLnum = -1
o.lastCurFoldStart = 0
o.lastCurFoldEnd = 0
o.isCurFoldHighlighted = false
return o
end
--- Must invoke in on_win cycle
---@param bufnr number
---@param fb UfoFoldBuffer
function Window:onWin(bufnr, fb)
self.lastBufnr = self.bufnr
self.bufnr = bufnr
self.foldbuffer = fb
self.foldedPairs = {}
self.foldedTextMaps = {}
self._cursor = nil
self._width = nil
self._concealLevel = nil
end
function Window:cursor()
if not self._cursor then
self._cursor = api.nvim_win_get_cursor(self.winid)
end
return self._cursor
end
function Window:textWidth()
if not self._width then
local textoff = utils.textoff(self.winid)
self._width = api.nvim_win_get_width(self.winid) - textoff
end
return self._width
end
function Window:concealLevel()
if not self._concealLevel then
self._concealLevel = vim.wo[self.winid].conceallevel
end
return self._concealLevel
end
function Window:foldEndLnum(fs)
local fe = self.foldedPairs[fs]
if not fe then
fe = utils.foldClosedEnd(self.winid, fs)
self.foldedPairs[fs] = fe
end
return fe
end
function Window:setCursorFoldedLineHighlight()
local res = false
if not self.isCurFoldHighlighted then
-- TODO
-- Upstream bug: Error in decoration provider (UNKNOWN PLUGIN).end
require('promise').resolve():thenCall(function()
utils.winCall(self.winid, function()
-- TODO
-- Upstream bug: `setl winhl` change curswant
local view = utils.saveView(0)
cmd('setl winhl+=CursorLine:UfoCursorFoldedLine')
utils.restView(0, view)
end)
end)
self.isCurFoldHighlighted = true
res = true
end
return res
end
function Window:clearCursorFoldedLineHighlight()
local res = false
if self.isCurFoldHighlighted or self.lastBufnr ~= 0 and self.lastBufnr ~= self.bufnr then
utils.winCall(self.winid, function()
cmd('setl winhl-=CursorLine:UfoCursorFoldedLine')
end)
self.isCurFoldHighlighted = false
res = true
end
return res
end
return Window

View File

@ -0,0 +1,247 @@
local api = vim.api
local fn = vim.fn
local cmd = vim.cmd
local utils = require('ufo.utils')
--- Singleton
---@class UfoPreviewFloatWin
---@field config table
---@field ns number
---@field winid number
---@field bufnr number
---@field bufferName string
---@field width number
---@field height number
---@field anchor string|'SW'|'NW'
---@field winblend number
---@field border string|'none'|'single'|'double'|'rounded'|'solid'|'shadow'|string[]
---@field lineCount number
---@field showScrollBar boolean
---@field topline number
---@field virtText UfoExtmarkVirtTextChunk[]
local FloatWin = {}
local defaultBorder = {
none = {'', '', '', '', '', '', '', ''},
single = {'', '', '', '', '', '', '', ''},
double = {'', '', '', '', '', '', '', ''},
rounded = {'', '', '', '', '', '', '', ''},
solid = {' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '},
shadow = {'', '', {' ', 'FloatShadowThrough'}, {' ', 'FloatShadow'},
{' ', 'FloatShadow'}, {' ', 'FloatShadow'}, {' ', 'FloatShadowThrough'}, ''},
}
local function borderHasLine(border, index)
local s = border[index]
if type(s) == 'string' then
return s ~= ''
else
return s[1] ~= ''
end
end
function FloatWin:borderHasUpLine()
return borderHasLine(self.border, 2)
end
function FloatWin:borderHasRightLine()
return borderHasLine(self.border, 4)
end
function FloatWin:borderHasBottomLine()
return borderHasLine(self.border, 6)
end
function FloatWin:borderHasLeftLine()
return borderHasLine(self.border, 8)
end
function FloatWin:build(winid, height, border, isAbove)
local winfo = utils.getWinInfo(winid)
local aboveLine = utils.winCall(winid, fn.winline) - 1
local belowLine = winfo.height - aboveLine
border = type(border) == 'string' and defaultBorder[border] or border
self.border = vim.deepcopy(border)
if fn.screencol() == 1 then
self.border[1], self.border[7], self.border[8] = '', '', ''
end
local row, col = 0, 0
if isAbove then
if aboveLine < height and belowLine > aboveLine then
self.height = math.min(height, belowLine)
row = self.height - aboveLine
if self:borderHasBottomLine() then
row = row + 1
end
if self:borderHasUpLine() then
row = row + 1
end
else
self.height = math.min(height, aboveLine)
row = 1
end
else
if belowLine < height and belowLine < aboveLine then
self.height = math.min(height, aboveLine)
row = belowLine - self.height
else
if self:borderHasUpLine() and fn.screenrow() == 1 and aboveLine == 0 then
self.border[1], self.border[2], self.border[3] = '', '', ''
end
self.height = math.min(height, belowLine)
row = 0
end
if self:borderHasUpLine() then
row = row - 1
end
end
self.width = winfo.width - winfo.textoff
if self:borderHasLeftLine() then
col = col - 1
end
if self:borderHasRightLine() then
self.width = self.width - 1
end
local anchor = isAbove and 'SW' or 'NW'
return {
border = self.border,
relative = 'cursor',
focusable = true,
width = self.width,
height = self.height,
anchor = anchor,
row = row,
col = col,
noautocmd = true,
zindex = 51
}
end
function FloatWin:validate()
return utils.isWinValid(rawget(self, 'winid'))
end
function FloatWin.getConfig()
local config = api.nvim_win_get_config(FloatWin.winid)
local row, col = config.row, config.col
-- row and col are a table value converted from the floating-point
if type(row) == 'table' then
---@diagnostic disable-next-line: need-check-nil, inject-field
config.row, config.col = tonumber(row[vim.val_idx]), tonumber(col[vim.val_idx])
end
return config
end
function FloatWin:open(wopts, enter)
if enter == nil then
enter = false
end
self.winid = api.nvim_open_win(self:getBufnr(), enter, wopts)
return self.winid
end
function FloatWin:close()
if self:validate() then
api.nvim_win_close(self.winid, true)
end
rawset(self, 'winid', nil)
end
function FloatWin:call(executor)
utils.winCall(self.winid, executor)
end
function FloatWin:getBufnr()
if utils.isBufLoaded(rawget(self, 'bufnr')) then
return self.bufnr
end
local bufnr = fn.bufnr('^' .. self.bufferName .. '$')
if bufnr > 0 then
self.bufnr = bufnr
else
self.bufnr = api.nvim_create_buf(false, true)
api.nvim_buf_set_name(self.bufnr, self.bufferName)
vim.bo[self.bufnr].bufhidden = 'hide'
end
return self.bufnr
end
function FloatWin:setContent(text)
vim.bo[self.bufnr].modifiable = true
api.nvim_buf_set_lines(self.bufnr, 0, -1, true, text)
vim.bo[self.bufnr].modifiable = false
self.lineCount = #text
self.showScrollBar = self.lineCount > self.height
api.nvim_win_set_cursor(self.winid, {1, 0})
cmd('norm! ze')
end
---
---@param winid number
---@param targetHeight number
---@param enter boolean
---@param isAbove boolean
---@param postHandle? fun()
---@return number
function FloatWin:display(winid, targetHeight, enter, isAbove, postHandle)
local height = math.min(self.config.maxheight, targetHeight)
local wopts = self:build(winid, height, self.config.border, isAbove)
if self:validate() then
wopts.noautocmd = nil
api.nvim_win_set_config(self.winid, wopts)
if enter == true then
api.nvim_set_current_win(self.winid)
end
else
self:open(wopts, enter)
self.winblend = self.config.winblend
local wo = vim.wo[self.winid]
wo.wrap = false
wo.spell, wo.list = false, true
wo.nu, wo.rnu = false, false
wo.fen, wo.fdm, wo.fdc = false, 'manual', '0'
wo.cursorline = enter == true
wo.signcolumn, wo.colorcolumn = 'no', ''
if wo.so == 0 then
wo.so = 1
end
wo.winhl = self.config.winhighlight
wo.winblend = self.winblend
end
if type(postHandle) == 'function' then
postHandle()
end
return self.winid
end
function FloatWin:refreshTopline()
self.topline = fn.line('w0', self.winid)
end
function FloatWin:initialize(ns, config)
self.ns = ns
local border = config.border
local tBorder = type(border)
if tBorder == 'string' then
if not defaultBorder[border] then
error(([[border string must be one of {%s}]])
:format(table.concat(vim.tbl_keys(defaultBorder), ',')))
end
elseif tBorder == 'table' then
assert(#border == 8, 'only support 8 chars for the border')
else
error('error border config')
end
self.bufferName = 'UfoPreviewFloatWin'
self.config = config
return self
end
function FloatWin:dispose()
self:close()
pcall(api.nvim_buf_delete, self.bufnr, {force = true})
self.bufnr = nil
end
return FloatWin

View File

@ -0,0 +1,364 @@
local api = vim.api
local cmd = vim.cmd
local fn = vim.fn
local promise = require('promise')
local render = require('ufo.render')
local utils = require('ufo.utils')
local floatwin = require('ufo.preview.floatwin')
local scrollbar = require('ufo.preview.scrollbar')
local winbar = require('ufo.preview.winbar')
local keymap = require('ufo.preview.keymap')
local event = require('ufo.lib.event')
local disposable = require('ufo.lib.disposable')
local config = require('ufo.config')
local fold = require('ufo.fold')
local highlight = require('ufo.highlight')
---@class UfoPreview
---@field initialized boolean
---@field disposables UfoDisposable[]
---@field detachDisposables UfoDisposable[]
---@field ns number
---@field winid number
---@field bufnr number
---@field lnum number
---@field col number
---@field topline number
---@field foldedLnum number
---@field foldedEndLnum number
---@field isAbove boolean
---@field cursorSignName string
---@field cursorSignId number
---@field keyMessages table<string, string>
local Preview = {}
function Preview:trace(bufnr)
local fb = fold.get(self.bufnr)
if not fb then
return
end
local fWinConfig = floatwin.getConfig()
local wrow = fWinConfig.row
if fWinConfig.anchor == 'SW' then
wrow = wrow - fWinConfig.height
if wrow < 0 then
wrow = floatwin:borderHasUpLine() and 1 or 0
else
if floatwin:borderHasBottomLine() then
wrow = wrow - 1
end
end
else
if floatwin:borderHasUpLine() then
wrow = wrow + 1
end
end
local fLnum, fWrow, col
if bufnr == self.bufnr then
fLnum, fWrow = floatwin.topline, 0
-- did scroll, do trace base on 2nd line
if fLnum > 1 then
fLnum = fLnum + 1
fWrow = 1
end
else
local floatCursor = api.nvim_win_get_cursor(floatwin.winid)
fLnum = floatCursor[1]
fWrow = fLnum - floatwin.topline
col = floatCursor[2]
end
local cursor = api.nvim_win_get_cursor(self.winid)
api.nvim_set_current_win(self.winid)
local lnum = utils.foldClosed(0, cursor[1]) + fLnum - 1
local lineSize = fWrow + wrow
cmd('norm! m`zO')
fb:syncFoldedLines(self.winid)
if bufnr == self.bufnr then
local s
s, col = fb:lines(lnum)[1]:find('^%s+%S')
col = s and col - 1 or 0
end
local topline, topfill = utils.evaluateTopline(self.winid, lnum, lineSize)
utils.restView(0, {
lnum = lnum,
col = col,
topline = topline,
topfill = topfill,
curswant = utils.curswant(self.bufnr, lnum, col + 1)
})
end
function Preview:winCall(executor)
local res = false
if self.validate() then
floatwin:call(executor)
res = true
end
return res
end
function Preview:scroll(char, toTopLeft)
if self:winCall(function()
local ctrlTbl = {B = 0x02, D = 0x04, E = 0x05, F = 0x06, U = 0x15, Y = 0x19}
cmd(('norm! %c%s'):format(ctrlTbl[char], toTopLeft and 'H_' or ''))
end) then
self:viewChanged()
end
end
function Preview:jumpView(toBottom)
if self:winCall(function()
cmd(('norm! %s'):format(toBottom and 'GH_' or 'gg'))
end) then
self:viewChanged()
end
end
function Preview:toggleCursor()
local bufnr = api.nvim_get_current_buf()
local floatBufnr = floatwin:getBufnr()
if self.bufnr == bufnr and self.lnum - self.foldedLnum > 0 then
self.cursorSignId = fn.sign_place(self.cursorSignId or 0, 'UfoPreview',
self.cursorSignName, floatBufnr, {lnum = self.lnum - self.foldedLnum + 1, priority = 1})
elseif self.cursorSignId then
pcall(fn.sign_unplace, 'UfoPreview', {buffer = floatBufnr})
self.cursorSignId = nil
end
end
local function onBufRemap(bufnr, str)
local self = Preview
local isNormalBuf = bufnr == self.bufnr
if str == 'switch' then
if isNormalBuf then
api.nvim_set_current_win(floatwin.winid)
vim.wo.cul = true
else
vim.wo.cul = false
api.nvim_set_current_win(self.winid)
end
self:toggleCursor()
elseif str == 'trace' or str == '2click' then
self:trace(bufnr)
elseif str == 'close' then
self:close()
elseif str == 'jumpTop' then
self:jumpView(false)
elseif str == 'jumpBot' then
self:jumpView(true)
elseif str == 'scrollB' then
self:scroll('B', isNormalBuf)
elseif str == 'scrollF' then
self:scroll('F', isNormalBuf)
elseif str == 'scrollU' then
self:scroll('U', isNormalBuf)
elseif str == 'scrollD' then
self:scroll('D', isNormalBuf)
elseif str == 'scrollE' then
self:scroll('E', isNormalBuf)
elseif str == 'scrollY' then
self:scroll('Y', isNormalBuf)
elseif str == 'wheelUp' or str == 'wheelDown' then
promise.resolve():thenCall(function()
self:viewChanged()
end)
elseif str == 'onKey' then
promise.resolve():thenCall(function()
Preview:afterKey()
end)
end
end
function Preview:attach(bufnr, winid, foldedLnum, foldedEndLnum, isAbove)
self:detach()
local disposables = {}
event:on('WinClosed', function()
promise.resolve():thenCall(function()
if not self.validate() then
self:detach()
self.close()
end
end)
end, disposables)
event:on('onBufRemap', onBufRemap, disposables)
event:emit('setOpenFoldHl', false)
table.insert(disposables, disposable:create(function()
event:emit('setOpenFoldHl')
end))
local view = utils.saveView(winid)
self.winid = winid
self.bufnr = bufnr
self.lnum = view.lnum
self.col = view.col
self.topline = view.topline
self.foldedLnum = foldedLnum
self.foldedEndLnum = foldedEndLnum
self.isAbove = isAbove
local floatBufnr = floatwin:getBufnr()
vim.bo[floatBufnr].iskeyword = vim.bo[bufnr].iskeyword
vim.bo[floatBufnr].tabstop = vim.bo[bufnr].tabstop
vim.bo[floatBufnr].shiftwidth = vim.bo[bufnr].shiftwidth
table.insert(disposables, disposable:create(function()
self.winid = nil
self.bufnr = nil
self.lnum = nil
self.col = nil
self.topline = nil
self.foldedLnum = nil
self.foldedEndLnum = nil
self.isAbove = nil
self.cursorSignId = nil
self.detachDisposables = nil
if utils.isBufLoaded(floatBufnr) then
api.nvim_buf_clear_namespace(floatBufnr, self.ns, 0, -1)
pcall(fn.sign_unplace, 'UfoPreview', {buffer = floatBufnr})
if floatwin:validate() then
fn.clearmatches(floatwin.winid)
end
pcall(api.nvim_buf_call, floatBufnr, function()
cmd('setl iskeyword<')
cmd('setl topstop<')
cmd('setl shiftwidth<')
end)
end
end))
table.insert(disposables, keymap:attach(bufnr, floatBufnr, self.ns, self.keyMessages, {
trace = self.keyMessages.trace,
switch = self.keyMessages.switch,
close = self.keyMessages.close,
['2click'] = '<2-LeftMouse>'
}))
self.detachDisposables = disposables
end
function Preview:detach()
if self.detachDisposables then
disposable.disposeAll(self.detachDisposables)
end
end
function Preview:viewChanged()
floatwin:refreshTopline()
scrollbar:update()
winbar:update()
end
function Preview:display(enter, handler)
local height = self.foldedEndLnum - self.foldedLnum + 1
floatwin:display(self.winid, height, enter, self.isAbove, handler)
scrollbar:display()
winbar:display()
end
---
---@param enter? boolean
---@param nextLineIncluded? boolean
---@return number? floatWinId
function Preview:peekFoldedLinesUnderCursor(enter, nextLineIncluded)
local bufnr = api.nvim_get_current_buf()
local fb = fold.get(bufnr)
if not fb then
-- buffer is detached
return
end
local oLnum, oCol = unpack(api.nvim_win_get_cursor(0))
local lnum = utils.foldClosed(0, oLnum)
local fl = fb.foldedLines[lnum]
if lnum == -1 or not fl then
return
end
local winid = api.nvim_get_current_win()
local endLnum = utils.foldClosedEnd(0, lnum)
local kind = fb:lineKind(winid, lnum)
local isAbove = kind == 'comment'
if not isAbove and nextLineIncluded ~= false then
endLnum = fb:lineCount() == endLnum and endLnum or (endLnum + 1)
end
self:attach(bufnr, winid, lnum, endLnum, isAbove)
floatwin.virtText = fl.virtText
local text = fb:lines(lnum, endLnum)
self:display(enter, function()
floatwin:setContent(text)
api.nvim_win_set_cursor(floatwin.winid, {oLnum - lnum + 1, oCol})
if oLnum > lnum then
floatwin:call(utils.zz)
end
floatwin:refreshTopline()
end)
self:toggleCursor()
render.mapHighlightLimitByRange(bufnr, floatwin:getBufnr(),
{lnum - 1, 0}, {endLnum - 1, #text[endLnum - lnum + 1]}, text, self.ns)
render.mapMatchByLnum(winid, floatwin.winid, lnum, endLnum)
vim.wo[floatwin.winid].listchars = vim.wo[winid].listchars
return floatwin.winid
end
function Preview.validate()
local res = floatwin:validate()
if floatwin.showScrollBar then
res = res and scrollbar:validate()
end
return res
end
function Preview.close()
floatwin:close()
scrollbar:close()
winbar:close()
end
function Preview.floatWinid()
return floatwin.winid
end
function Preview:afterKey()
local winid = api.nvim_get_current_win()
if floatwin.winid == winid then
self:viewChanged()
return
end
if winid == self.winid then
local view = utils.saveView(winid)
if self.lnum ~= view.lnum or
self.col ~= view.col then
self.close()
elseif self.foldedLnum ~= utils.foldClosed(self.winid, self.foldedLnum) then
self.close()
elseif self.topline ~= view.topline then
if floatwin:validate() then
self:display(false)
self.topline = view.topline
end
end
else
self.close()
end
end
function Preview:initialize(namespace)
if self.initialized then
return
end
self.initialized = true
local conf = vim.deepcopy(config.preview)
self.keyMessages = conf.mappings
self.disposables = {}
table.insert(self.disposables, disposable:create(function()
self.initialized = false
end))
table.insert(self.disposables, floatwin:initialize(namespace, conf.win_config))
table.insert(self.disposables, scrollbar:initialize())
table.insert(self.disposables, winbar:initialize())
self.ns = namespace
self.cursorSignName = highlight.signNames()['UfoPreviewCursorLine']
return self
end
function Preview:dispose()
disposable.disposeAll(self.disposables)
self.disposables = {}
end
return Preview

View File

@ -0,0 +1,97 @@
local event = require('ufo.lib.event')
local utils = require('ufo.utils')
local api = vim.api
---@class UfoPreviewKeymap
---@field ns number
---@field bufnr number
---@field keyMessages table
---@field keyMapsBackup table
local Keymap = {
keyBackup = {}
}
local function setKeymaps(bufnr, keyMessages)
local opts = {noremap = true, nowait = true}
local rhsFmt = [[<Cmd>lua require('ufo.lib.event'):emit('onBufRemap', %d, %q)<CR>]]
for msg, key in pairs(keyMessages) do
local lhs = key
local rhs = rhsFmt:format(bufnr, msg)
api.nvim_buf_set_keymap(bufnr, 'n', lhs, rhs, opts)
end
end
function Keymap:setKeymaps()
setKeymaps(self.bufnr, self.keyMessages)
end
function Keymap:restoreKeymaps()
if utils.isBufLoaded(self.bufnr) then
for _, key in pairs(self.keyMessages) do
pcall(api.nvim_buf_del_keymap, self.bufnr, 'n', key)
end
for _, k in ipairs(self.keyBackup) do
api.nvim_buf_set_keymap(self.bufnr, 'n', k.lhs, k.rhs, k.opts)
end
end
self.keyBackup = {}
end
function Keymap:saveKeymaps()
local keys = {}
for _, v in pairs(self.keyMessages) do
if v:match('^<.*>$') then
v = v:upper()
end
keys[v] = true
end
for _, k in ipairs(api.nvim_buf_get_keymap(self.bufnr, 'n')) do
if keys[k.lhs] then
local opts = {
callback = k.callback,
expr = k.expr == 1,
noremap = k.noremap == 1,
nowait = k.nowait == 1,
silent = k.silent == 1
}
table.insert(self.keyBackup, {lhs = k.lhs, rhs = k.rhs or '', opts = opts})
end
end
end
---
---@param bufnr number
---@param namespace number
---@param keyMessages table
---@param floatKeyMessages table
---@return UfoPreviewKeymap
function Keymap:attach(bufnr, floatBufnr, namespace, keyMessages, floatKeyMessages)
self.bufnr = bufnr
self.ns = namespace
self.keyMessages = keyMessages
self:saveKeymaps()
self:setKeymaps()
setKeymaps(floatBufnr, floatKeyMessages)
vim.on_key(function(char)
local b1, b2, b3 = char:byte(1, -1)
-- 0x80, 0xfd, 0x4b <ScrollWheelUp>
-- 0x80, 0xfd, 0x4c <ScrollWheelDown>
if b1 == 0x80 and b2 == 0xfd then
if b3 == 0x4b then
event:emit('onBufRemap', bufnr, 'wheelUp')
elseif b3 == 0x4c then
event:emit('onBufRemap', bufnr, 'wheelDown')
end
end
event:emit('onBufRemap', bufnr, 'onKey')
end, namespace)
return self
end
function Keymap:dispose()
vim.on_key(nil, self.ns)
self:restoreKeymaps()
end
return Keymap

View File

@ -0,0 +1,108 @@
local api = vim.api
local extmark = require('ufo.render.extmark')
local FloatWin = require('ufo.preview.floatwin')
--- Singleton
---@class UfoPreviewScrollBar : UfoPreviewFloatWin
---@field winid number
---@field bufnr number
---@field bufferName string
local ScrollBar = setmetatable({}, {__index = FloatWin})
function ScrollBar:build()
local config = FloatWin.getConfig()
local row, col, height = config.row, config.col + config.width, config.height
local anchor, zindex = config.anchor, config.zindex
if anchor == 'NW' then
row = self:borderHasUpLine() and row + 1 or row
else
row = (self:borderHasBottomLine() and row - 1 or row) - height
row = math.max(row, self:borderHasUpLine() and 1 or 0)
end
return vim.tbl_extend('force', config, {
anchor = 'NW',
width = 1,
row = row,
col = self:borderHasLeftLine() and col + 1 or col,
style = 'minimal',
noautocmd = true,
focusable = false,
border = 'none',
zindex = zindex + 2
})
end
function ScrollBar:floatWinid()
return FloatWin.winid
end
function ScrollBar:update()
if not self.showScrollBar then
self.winid = nil
return
end
local barSize = math.ceil(self.height * self.height / self.lineCount)
if barSize == self.height and barSize < self.lineCount then
barSize = self.height - 1
end
local barPos = math.ceil(self.height * self.topline / self.lineCount)
local size = barPos + barSize - 1
if size == self.height then
if self.topline + self.height - 1 < self.lineCount then
barPos = barPos - 1
end
elseif size > self.height then
barPos = self.height - barSize + 1
end
if self:borderHasRightLine() then
local wopts = self:build()
wopts.height = math.max(1, barSize)
wopts.row = wopts.row + barPos - 1
wopts.noautocmd = nil
api.nvim_win_set_config(self.winid, wopts)
vim.wo[self.winid].winhl = 'Normal:UfoPreviewThumb'
else
api.nvim_buf_clear_namespace(self.bufnr, self.ns, 0, -1)
for i = 1, self.height do
if i >= barPos and i < barPos + barSize then
extmark.setHighlight(self.bufnr, self.ns, i - 1, 0, i - 1, 1, 'UfoPreviewThumb')
end
end
end
end
function ScrollBar:display()
if not self.showScrollBar then
self:close()
return
end
local wopts = self:build()
if self:validate() then
wopts.noautocmd = nil
api.nvim_win_set_config(self.winid, wopts)
else
ScrollBar:open(wopts)
local wo = vim.wo[self.winid]
wo.winhl = 'Normal:UfoPreviewSbar'
wo.winblend = self.winblend
end
local lines = {}
for _ = 1, self.height do
table.insert(lines, ' ')
end
vim.bo[self.bufnr].modifiable = true
api.nvim_buf_set_lines(self.bufnr, 0, -1, true, lines)
vim.bo[self.bufnr].modifiable = false
self:update()
return self.winid
end
function ScrollBar:initialize()
self.bufferName = 'UfoPreviewScrollBar'
return self
end
return ScrollBar

View File

@ -0,0 +1,80 @@
local api = vim.api
local render = require('ufo.render')
local FloatWin = require('ufo.preview.floatwin')
--- Singleton
---@class UfoPreviewWinBar : UfoPreviewFloatWin
---@field winid number
---@field bufnr number
---@field bufferName string
---@field virtTextId number
---@field virtText UfoExtmarkVirtTextChunk[]
local WinBar = setmetatable({}, {__index = FloatWin})
function WinBar:build()
local config = FloatWin.getConfig()
local row, col, height = config.row, config.col, config.height
local anchor, zindex = config.anchor, config.zindex
if anchor == 'NW' then
row = self:borderHasUpLine() and row + 1 or row
else
row = (self:borderHasBottomLine() and row - 1 or row) - height
row = math.max(row, self:borderHasUpLine() and 1 or 0)
end
return vim.tbl_extend('force', config, {
anchor = 'NW',
height = 1,
row = row,
col = self:borderHasLeftLine() and col + 1 or col,
style = 'minimal',
noautocmd = true,
focusable = false,
border = 'none',
zindex = zindex + 1
})
end
function WinBar:floatWinid()
return FloatWin.winid
end
function WinBar:update()
if self.topline == 1 then
self:close()
return
end
if not self:validate() then
self:display()
end
self.virtTextId = render.setVirtText(self.bufnr, self.ns, 0, 0, self.virtText, {
id = self.virtTextId
})
end
function WinBar:display()
if self.topline == 1 then
self:close()
return
end
local wopts = self:build()
if self:validate() then
wopts.noautocmd = nil
api.nvim_win_set_config(self.winid, wopts)
else
WinBar:open(wopts)
local wo = vim.wo[self.winid]
wo.winhl = 'Normal:UfoPreviewWinBar'
wo.winblend = self.winblend
end
self:update()
return self.winid
end
function WinBar:initialize()
self.bufferName = 'UfoPreviewWinBar'
self.virtTextId = nil
return self
end
return WinBar

View File

@ -0,0 +1,69 @@
local foldingrange = require('ufo.model.foldingrange')
local bufmanager = require('ufo.bufmanager')
local Indent = {}
function Indent.getFolds(bufnr)
local buf = bufmanager:get(bufnr)
if not buf then
return
end
local lines = buf:lines(1, -1)
local ts = vim.bo[bufnr].ts
local sw = vim.bo[bufnr].sw
sw = sw == 0 and ts or sw
local levels = {}
for _, line in ipairs(lines) do
local level = -1
local n = 0
for col = 1, #line do
-- compare byte is slightly faster than a char in the string
local b = line:byte(col, col)
if b == 0x20 then
-- ' '
n = n + 1
elseif b == 0x09 then
-- '\t'
n = n + (ts - (n % ts))
else
level = math.ceil(n / sw)
break
end
end
table.insert(levels, level)
end
local ranges = {}
local stack = {}
local function pop(curLevel, lastLnum)
while #stack > 0 do
local data = stack[#stack]
local level, lnum = data[1], data[2]
if level >= curLevel then
table.insert(ranges, foldingrange.new(lnum - 1, lastLnum - 1))
table.remove(stack)
else
break
end
end
end
local lastLnum = 1
local lastLevel = levels[1]
for i, level in ipairs(levels) do
if level >= 0 then
if level > 0 and level > lastLevel then
table.insert(stack, {lastLevel, lastLnum})
elseif level < lastLevel then
pop(level, lastLnum)
end
lastLevel = level
lastLnum = i
end
end
pop(0, lastLnum)
return ranges
end
return Indent

View File

@ -0,0 +1,82 @@
local uv = vim.loop
local promise = require('promise')
local log = require('ufo.lib.log')
---@class Provider UfoProvider
---@field modulePathPrefix string
---@field innerProviders string[]
---@field modules table
local Provider = {
modulePathPrefix = 'ufo.provider.',
innerProviders = {'lsp', 'treesitter', 'indent'}
}
local function needFallback(reason)
return type(reason) == 'string' and reason:match('UfoFallbackException')
end
function Provider:getFunction(m)
return type(m) == 'string' and self.modules[m].getFolds or m
end
---
---@param providers table
---@param bufnr number
---@return Promise
function Provider:requestFoldingRange(providers, bufnr)
local main, fallback = providers[1], providers[2]
local mainFunc = self:getFunction(main)
local s
if log.isEnabled('debug') then
s = uv.hrtime()
end
local p = promise(function(resolve)
resolve(mainFunc(bufnr))
end):thenCall(function(value)
return {main, value}
end, function(reason)
if needFallback(reason) then
local fallbackFunc = self:getFunction(fallback)
if fallbackFunc then
return {fallback, fallbackFunc(bufnr)}
else
return {main, nil}
end
else
return promise.reject(reason)
end
end)
if log.isEnabled('debug') then
p = p:finally(function()
log.debug(('requestFoldingRange(%s, %d) has elapsed: %dms')
:format(vim.inspect(providers, {indent = '', newline = ' '}),
bufnr, (uv.hrtime() - s) / 1e6))
end)
end
return p
end
function Provider:initialize()
self.modules = setmetatable({}, {
__index = function(t, k)
local ok, res = pcall(require, self.modulePathPrefix .. k)
assert(ok, ([[Can't find a module in `%s%s`]]):format(self.modulePathPrefix, k))
rawset(t, k, res)
return res
end
})
return self
end
function Provider:dispose()
for _, name in ipairs(self.innerProviders) do
local module = _G.package.loaded[self.modulePathPrefix .. name]
if module and module.dispose then
module:dispose()
end
end
end
return Provider

View File

@ -0,0 +1,68 @@
local fn = vim.fn
local promise = require('promise')
---@class UfoLspCocClient
---@field initialized boolean
---@field enabled boolean
local CocClient = {
initialized = false,
enabled = false,
}
---@param action string
---@vararg any
---@return Promise
function CocClient.action(action, ...)
local args = {...}
return promise(function(resolve, reject)
table.insert(args, function(err, res)
if err ~= vim.NIL then
if type(err) == 'string' and
(err:match('service not started') or err:match('Plugin not ready')) then
resolve()
else
reject(err)
end
else
if res == vim.NIL then
res = nil
end
resolve(res)
end
end)
fn.CocActionAsync(action, unpack(args))
end)
end
---@param name string
---@vararg any
---@return Promise
function CocClient.runCommand(name, ...)
return CocClient.action('runCommand', name, ...)
end
---
---@param bufnr number
---@param kind? string|'comment'|'imports'|'region'
---@return Promise
function CocClient.requestFoldingRange(bufnr, kind)
if not CocClient.initialized or not CocClient.enabled then
return promise.reject('UfoFallbackException')
end
return CocClient.runCommand('ufo.foldingRange', bufnr, kind)
end
function CocClient.handleInitNotify()
if not CocClient.initialized then
CocClient.initialized = true
end
CocClient.enabled = true
end
function CocClient.handleDisposeNotify()
CocClient.initialized = false
CocClient.enabled = false
end
return CocClient

View File

@ -0,0 +1,18 @@
local promise = require('promise')
---@class UfoLspFastFailure
---@field initialized boolean
local FastFailure = {
initialized = false
}
---
---@param bufnr number
---@param kind? string|'comment'|'imports'|'region'
---@return Promise
---@diagnostic disable-next-line: unused-local
function FastFailure.requestFoldingRange(bufnr, kind)
return promise.reject('UfoFallbackException')
end
return FastFailure

View File

@ -0,0 +1,114 @@
local uv = vim.loop
local promise = require('promise')
local utils = require('ufo.utils')
local log = require('ufo.lib.log')
local bufmanager = require('ufo.bufmanager')
---@class UfoLSPProviderContext
---@field timestamp number
---@field count number
---@class UfoLSPProvider
---@field provider table
---@field hasProviders table<string, boolean>
---@field providerContext table<string, UfoLSPProviderContext>
local LSP = {
hasProviders = {},
providerContext = {}
}
function LSP:hasInitialized()
return self.provider and self.provider.initialized
end
function LSP:initialize()
return utils.wait(1500):thenCall(function()
local cocInitlized = vim.g.coc_service_initialized
local module
if _G.package.loaded['vim.lsp'] and (not cocInitlized or cocInitlized ~= 1) then
module = 'nvim'
elseif cocInitlized and cocInitlized == 1 then
module = 'coc'
else
module = 'fastfailure'
end
log.debug(('using %s as a lsp provider'):format(module))
self.provider = require('ufo.provider.lsp.' .. module)
end)
end
function LSP:request(bufnr)
local buf = bufmanager:get(bufnr)
if not buf then
return promise.resolve()
end
local bt = buf:buftype()
if bt ~= '' and bt ~= 'acwrite' then
return bt == 'nofile' and promise.reject('UfoFallbackException') or promise.resolve()
end
local ft = buf:filetype()
local hasProvider = self.hasProviders[ft]
local firstCheckFt = false
if hasProvider == nil then
local context = self.providerContext[ft]
if not context then
firstCheckFt = true
self.providerContext[ft] = {timestamp = uv.hrtime(), count = 0}
else
-- after 120 seconds and count is equal or greater than 5
if uv.hrtime() - context.timestamp > 1.2e11 and context.count >= 5 then
self.hasProviders[ft] = false
hasProvider = false
self.providerContext[ft] = nil
end
end
end
local provider = self.provider
if provider.initialized and hasProvider ~= false then
local p
if firstCheckFt then
-- wait for the server, is 500ms enough?
p = utils.wait(500):thenCall(function()
return provider.requestFoldingRange(bufnr)
end)
else
p = provider.requestFoldingRange(bufnr)
end
if hasProvider == nil then
p = p:thenCall(function(value)
self.hasProviders[ft] = true
self.providerContext[ft] = nil
return value
end, function(reason)
local context = self.providerContext[ft]
if context then
self.providerContext[ft].count = context.count + 1
end
return promise.reject(reason)
end)
end
return p
else
return promise.reject('UfoFallbackException')
end
end
function LSP.getFolds(bufnr)
local self = LSP
if not self:hasInitialized() then
return self:initialize():thenCall(function()
return self:request(bufnr)
end)
end
return self:request(bufnr)
end
function LSP:dispose()
self.provider = nil
self.hasProviders = {}
self.providerContext = {}
end
return LSP

View File

@ -0,0 +1,95 @@
local util = require('vim.lsp.util')
local promise = require('promise')
local utils = require('ufo.utils')
local async = require('async')
local log = require('ufo.lib.log')
local foldingrange = require('ufo.model.foldingrange')
---@class UfoLspNvimClient
---@field initialized boolean
local NvimClient = {
initialized = true
}
local errorCodes = {
-- Defined by JSON RPC
ParseError = -32700,
InvalidRequest = -32600,
MethodNotFound = -32601,
InvalidParams = -32602,
InternalError = -32603,
serverErrorStart = -32099,
serverErrorEnd = -32000,
ServerNotInitialized = -32002,
UnknownErrorCode = -32001,
-- Defined by the protocol.
RequestCancelled = -32800,
RequestFailed = -32803,
ContentModified = -32801,
}
local vimLspGetClients = vim.lsp.get_clients and vim.lsp.get_clients or vim.lsp.get_active_clients
function NvimClient.request(client, method, params, bufnr)
return promise(function(resolve, reject)
client.request(method, params, function(err, res)
if err then
log.error('Received error in callback', err)
log.error('Client:', client)
log.error('All clients:', vimLspGetClients({bufnr = bufnr}))
local code = err.code
if code == errorCodes.RequestCancelled or code == errorCodes.ContentModified or code == errorCodes.RequestFailed then
reject('UfoFallbackException')
else
reject(err)
end
else
resolve(res)
end
end, bufnr)
end)
end
local function getClients(bufnr)
local clients = vimLspGetClients({bufnr = bufnr})
return vim.tbl_filter(function(client)
if vim.tbl_get(client.server_capabilities, 'foldingRangeProvider') then
return true
else
return false
end
end, clients)
end
function NvimClient.requestFoldingRange(bufnr, kind)
return async(function()
if not utils.isBufLoaded(bufnr) then
return
end
local clients = getClients(bufnr)
if #clients == 0 then
await(utils.wait(500))
clients = getClients(bufnr)
end
-- TODO
-- How to get the highest priority for the client?
local client = clients[1]
if not client then
error('UfoFallbackException')
end
local params = {textDocument = util.make_text_document_params(bufnr)}
return NvimClient.request(client, 'textDocument/foldingRange', params, bufnr)
:thenCall(function(ranges)
if not ranges then
return {}
end
ranges = vim.tbl_filter(function(o)
return (not kind or kind == o.kind) and o.startLine < o.endLine
end, ranges)
foldingrange.sortRanges(ranges)
return ranges
end)
end)
end
return NvimClient

View File

@ -0,0 +1,193 @@
local bufmanager = require('ufo.bufmanager')
local foldingrange = require('ufo.model.foldingrange')
---@class UfoTreesitterProvider
---@field hasProviders table<string, boolean>
local Treesitter = {
hasProviders = {}
}
---@diagnostic disable: deprecated
---@return vim.treesitter.LanguageTree|nil parser for the buffer, or nil if parser is not available
local function getParser(bufnr, lang)
local ok, parser = pcall(vim.treesitter.get_parser, bufnr, lang)
if not ok then
return nil
end
return parser
end
local get_query = assert(vim.treesitter.query.get or vim.treesitter.query.get_query)
local get_query_files = assert(vim.treesitter.query.get_files or vim.treesitter.query.get_query_files)
---@diagnostic enable: deprecated
-- Backward compatibility for the dummy directive (#make-range!),
-- which no longer exists in nvim-treesitter v1.0+
if not vim.tbl_contains(vim.treesitter.query.list_directives(), "make-range!") then
vim.treesitter.query.add_directive("make-range!", function() end, {})
end
local MetaNode = {}
MetaNode.__index = MetaNode
function MetaNode:new(range)
local o = self == MetaNode and setmetatable({}, self) or self
o.value = range
return o
end
function MetaNode:range()
local range = self.value
return range[1], range[2], range[3], range[4]
end
--- Return a meta node that represents a range between two nodes, i.e., (#make-range!),
--- that is similar to the legacy TSRange.from_node() from nvim-treesitter.
function MetaNode.from_nodes(start_node, end_node)
local start_pos = { start_node:start() }
local end_pos = { end_node:end_() }
return MetaNode:new({
[1] = start_pos[1],
[2] = start_pos[2],
[3] = end_pos[1],
[4] = end_pos[2],
})
end
local function prepareQuery(bufnr, parser, root, rootLang, queryName)
if not root then
local firstTree = parser:trees()[1]
if firstTree then
root = firstTree:root()
else
return
end
end
local range = {root:range()}
if not rootLang then
local langTree = parser:language_for_range(range)
if langTree then
rootLang = langTree:lang()
else
return
end
end
return get_query(rootLang, queryName), {
root = root,
source = bufnr,
start = range[1],
-- The end row is exclusive so we need to add 1 to it.
stop = range[3] + 1,
}
end
local function iterFoldMatches(bufnr, parser, root, rootLang)
local q, p = prepareQuery(bufnr, parser, root, rootLang, 'folds')
if not q then
return function() end
end
---@diagnostic disable-next-line: need-check-nil
local iter = q:iter_matches(p.root, p.source, p.start, p.stop)
return function()
local pattern, match, metadata = iter()
local matches = {}
if pattern == nil then
return pattern
end
-- Extract capture names from each match
for id, node in pairs(match) do
local m = metadata[id]
if m and m.range then
node = MetaNode:new(m.range)
end
table.insert(matches, node)
end
-- Add some predicates for testing
local preds = q.info.patterns[pattern]
if preds then
for _, pred in pairs(preds) do
if pred[1] == 'make-range!' and type(pred[2]) == 'string' and #pred == 4 then
local node = MetaNode.from_nodes(match[pred[3]], match[pred[4]])
table.insert(matches, node)
end
end
end
return matches
end
end
local function getFoldMatches(res, bufnr, parser, root, lang)
for matches in iterFoldMatches(bufnr, parser, root, lang) do
for _, node in ipairs(matches) do
table.insert(res, node)
end
end
return res
end
local function getCpatureMatchesRecursively(bufnr, parser)
local noQuery = true
local res = {}
parser:for_each_tree(function(tree, langTree)
local lang = langTree:lang()
local has_folds = #get_query_files(lang, 'folds', nil) > 0
if has_folds then
noQuery = false
getFoldMatches(res, bufnr, parser, tree:root(), lang)
end
end)
return not noQuery, res
end
function Treesitter.getFolds(bufnr)
local buf = bufmanager:get(bufnr)
if not buf then
return
end
local bt = buf:buftype()
if bt ~= '' and bt ~= 'acwrite' then
if bt == 'nofile' then
error('UfoFallbackException')
end
return
end
local self = Treesitter
local ft = buf:filetype()
if self.hasProviders[ft] == false then
error('UfoFallbackException')
end
local parser = getParser(bufnr)
if not parser then
self.hasProviders[ft] = false
error('UfoFallbackException')
end
local ranges = {}
local ok, matches = getCpatureMatchesRecursively(bufnr, parser)
if not ok then
self.hasProviders[ft] = false
error('UfoFallbackException')
end
for _, node in ipairs(matches) do
local start, _, stop, stop_col = node:range()
if stop_col == 0 then
stop = stop - 1
end
if stop > start then
table.insert(ranges, foldingrange.new(start, stop))
end
end
foldingrange.sortRanges(ranges)
return ranges
end
function Treesitter:dispose()
self.hasProviders = {}
end
return Treesitter

View File

@ -0,0 +1,63 @@
local api = vim.api
local M = {}
---
---@param bufnr number
---@param startRange number[]
---@param endRange number[]
---@param namespaces number[]
---@return table, table
function M.getHighlightsAndInlayByRange(bufnr, startRange, endRange, namespaces)
local hlRes, inlayRes = {}, {}
local endRow, endCol = endRange[1], endRange[2]
for _, ns in pairs(namespaces) do
local marks = api.nvim_buf_get_extmarks(bufnr, ns, startRange, endRange, {details = true})
for _, m in ipairs(marks) do
local sr, sc, details = m[2], m[3], m[4]
local er = details.end_row or sr
local ec = details.end_col or (sc + 1)
local hlGroup = details.hl_group
local priority = details.priority
local conceal = details.conceal
local virtTextPos = details.virt_text_pos
if hlGroup then
if er > endRow then
er, ec = endRow, endCol
elseif er == endRow and ec > endCol then
er = endCol
end
table.insert(hlRes, {sr, sc, er, ec, hlGroup, priority, conceal})
end
if virtTextPos == 'inline' then
table.insert(inlayRes, {sr, sc, details.virt_text, priority})
end
end
end
return hlRes, inlayRes
end
function M.setHighlight(bufnr, ns, row, col, endRow, endCol, hlGroup, priority)
return api.nvim_buf_set_extmark(bufnr, ns, row, col, {
end_row = endRow,
end_col = endCol,
hl_group = hlGroup,
priority = priority
})
end
function M.setVirtText(bufnr, ns, row, col, virtText, opts)
opts = opts or {}
local textPos = opts.virt_text_pos
local winCol = not textPos and col == 0 and 0 or nil
return api.nvim_buf_set_extmark(bufnr, ns, row, col, {
id = opts.id,
virt_text = virtText,
virt_text_win_col = winCol,
virt_text_pos = textPos or 'eol',
priority = opts.priority or 10,
hl_mode = opts.hl_mode or 'combine'
})
end
return M

View File

@ -0,0 +1,297 @@
local api = vim.api
local fn = vim.fn
local highlight = require('ufo.highlight')
local extmark = require('ufo.render.extmark')
local treesitter = require('ufo.render.treesitter')
local match = require('ufo.render.match')
local utils = require('ufo.utils')
local M = {}
local function fillSlots(marks, len, concealLevel)
local res = {}
local prioritySlots = {}
local hlGroups = highlight.hlGroups()
local concealEabnled = concealLevel > 0
for _, m in ipairs(marks) do
---@type 'string'|'number'
local hlGroup = m[5]
local cchar = m[7]
local isConcealSlot = concealEabnled and cchar
if isConcealSlot or hlGroup and hlGroups[hlGroup].foreground then
local col, endCol, priority = m[2], m[4], m[6]
if endCol == -1 then
endCol = len
end
if isConcealSlot and concealLevel == 3 then
cchar = ''
end
local e = isConcealSlot and {col + 1, cchar, hlGroup} or hlGroup
for i = col + 1, endCol do
local oPriority = prioritySlots[i]
local oType = type(res[i])
if oType == 'nil' then
res[i], prioritySlots[i] = e, priority
elseif oType == 'string' or oType == 'number' then
if isConcealSlot or oPriority <= priority then
res[i], prioritySlots[i] = e, priority
end
else
if isConcealSlot and oPriority <= priority then
res[i], prioritySlots[i] = e, priority
end
end
end
end
end
return res
end
local function handleSyntaxSlot(slotData, slotLen, bufnr, lnum, syntax, concealEnabled)
if not syntax and not concealEnabled then
return
end
api.nvim_buf_call(bufnr, function()
local lastConcealId = -1
local lastConcealCol = 0
for i = 1, slotLen do
if concealEnabled then
local concealed = fn.synconcealed(lnum, i)
if concealed[1] == 1 then
local cchar, concealId = concealed[2], concealed[3]
if concealId ~= lastConcealId then
if type(slotData[i]) ~= 'table' or slotData[i][1] ~= i then
slotData[i] = {i, cchar, 'Conceal'}
end
lastConcealCol = i
lastConcealId = concealId
else
slotData[i] = {lastConcealCol, cchar, 'Conceal'}
end
end
end
if syntax and not slotData[i] then
local hlGroupId = fn.synID(lnum, i, true)
if hlGroupId ~= 0 then
slotData[i] = hlGroupId
end
end
end
end)
end
-- 1-indexed
local function syntaxToRowHighlightRange(res, lnum, startCol, endCol)
local lastIndex = 1
local lastHlId
for c = startCol, endCol do
local hlId = fn.synID(lnum, c, true)
if lastHlId and lastHlId ~= hlId then
table.insert(res, {lnum, lastIndex, c - 1, lastHlId})
lastIndex = c
end
lastHlId = hlId
end
table.insert(res, {lnum, lastIndex, endCol, lastHlId})
end
local function mapHighlightMarkers(bufnr, startRow, marks, hlGroups, ns)
for _, m in ipairs(marks) do
local hlGroup = m[5]
if next(hlGroups[hlGroup]) then
local sr, sc = m[1] - startRow, m[2]
local er, ec = m[3] - startRow, m[4]
extmark.setHighlight(bufnr, ns, sr, sc, er, ec, hlGroup, m[6])
end
end
end
local function mapInlayMarkers(bufnr, startRow, marks, ns)
for _, m in ipairs(marks) do
local sr, sc = m[1] - startRow, m[2]
extmark.setVirtText(bufnr, ns, sr, sc, m[3], {
priority = m[4],
virt_text_pos = 'inline'
})
end
end
function M.mapHighlightLimitByRange(srcBufnr, dstBufnr, startRange, endRange, text, ns)
local startRow, startCol = startRange[1], startRange[1]
local endRow, endCol = endRange[1], endRange[2]
local nss = {}
for _, namespace in pairs(api.nvim_get_namespaces()) do
if ns ~= namespace then
table.insert(nss, namespace)
end
end
local hlGroups = highlight.hlGroups()
local hlMarks, inlayMarks = extmark.getHighlightsAndInlayByRange(srcBufnr, startRange, endRange, nss)
mapHighlightMarkers(dstBufnr, startRow, hlMarks, hlGroups, ns)
hlMarks = treesitter.getHighlightsByRange(srcBufnr, startRange, endRange, hlGroups)
mapHighlightMarkers(dstBufnr, startRow, hlMarks, hlGroups, ns)
if vim.bo[srcBufnr].syntax ~= '' then
api.nvim_buf_call(srcBufnr, function()
local res = {}
local lnum, endLnum = startRow + 1, endRow + 1
if lnum == endLnum then
syntaxToRowHighlightRange(res, lnum, startCol + 1, endCol)
else
for l = lnum, endLnum - 1 do
syntaxToRowHighlightRange(res, l, 1, #text[l - lnum + 1])
end
syntaxToRowHighlightRange(res, endLnum, 1, endCol)
end
for _, r in ipairs(res) do
local row = r[1] - lnum
extmark.setHighlight(dstBufnr, ns, row, r[2] - 1, row, r[3], r[4], 1)
end
end)
end
mapInlayMarkers(dstBufnr, startRow, inlayMarks, ns)
end
function M.mapMatchByLnum(srcWinid, dstWinid, lnum, endLnum)
local res = match.mapHighlightsByLnum(srcWinid, lnum, endLnum)
if not vim.tbl_isempty(res) then
fn.setmatches(res, dstWinid)
end
end
function M.setVirtText(bufnr, ns, row, col, virtText, opts)
return extmark.setVirtText(bufnr, ns, row, col, virtText, opts)
end
function M.captureVirtText(bufnr, text, lnum, syntax, namespaces, concealLevel)
local len = #text
if len == 0 then
return {{'', 'UfoFoldedFg'}}
end
local extMarks, inlayMarks = extmark.getHighlightsAndInlayByRange(bufnr, {lnum - 1, 0}, {lnum - 1, len}, namespaces)
local tsMarks = treesitter.getHighlightsByRange(bufnr, {lnum - 1, 0}, {lnum - 1, len})
local marks = {}
for _, m in ipairs(extMarks) do
table.insert(marks, m)
end
for _, m in ipairs(tsMarks) do
table.insert(marks, m)
end
table.sort(inlayMarks, function(a, b)
local aCol, bCol, aPriority, bPriority = a[2], b[2], a[4], b[4]
if aCol == bCol then
return aPriority > bPriority
else
return aCol > bCol
end
end)
local sData = fillSlots(marks, len, concealLevel)
handleSyntaxSlot(sData, len, bufnr, lnum, syntax, concealLevel > 0)
local virtText = {}
local inlayMark = table.remove(inlayMarks)
local shouldInsertChunk = true
for i = 1, len do
local e = sData[i] or 'UfoFoldedFg'
local eType = type(e)
if eType == 'table' then
local startCol, cchar, hlGroup = e[1], e[2], e[3]
if startCol == i then
table.insert(virtText, {cchar, hlGroup})
end
shouldInsertChunk = true
else
local lastChunk = virtText[#virtText] or {}
if shouldInsertChunk or e ~= lastChunk[2] then
table.insert(virtText, {{i, i}, e})
shouldInsertChunk = false
else
lastChunk[1][2] = i
end
end
-- insert inlay hints
while inlayMark and inlayMark[2] == i do
for _, chunk in ipairs(inlayMark[3]) do
table.insert(virtText, chunk)
end
inlayMark = table.remove(inlayMarks)
shouldInsertChunk = true
end
end
for _, chunk in ipairs(virtText) do
local e1, e2 = chunk[1], chunk[2]
if type(e1) == 'table' then
local sc, ec = e1[1], e1[2]
chunk[1] = text:sub(sc, ec)
end
if e2 == 'Normal' then
chunk[2] = 'UfoFoldedFg'
end
end
return virtText
end
---Prefer use nvim_buf_set_extmark rather than matchaddpos, only use matchaddpos if buffer is shared
---with multiple windows in current tabpage.
---Check out https://github.com/neovim/neovim/issues/20208 for detail.
---@param handle number
---@param hlGroup string
---@param ns number
---@param start number
---@param finish number
---@param delay? number
---@param shared? boolean
---@return Promise
function M.highlightLinesWithTimeout(handle, hlGroup, ns, start, finish, delay, shared)
vim.validate({
handle = {handle, 'number'},
hlGroup = {hlGroup, 'string'},
ns = {ns, 'number'},
start = {start, 'number'},
finish = {finish, 'number'},
delay = {delay, 'number', true},
shared = {shared, 'boolean', true},
})
local ids = {}
local onFulfilled
if shared then
local prior = 10
local l = {}
for i = start, finish do
table.insert(l, {i})
if i % 8 == 0 then
table.insert(ids, fn.matchaddpos(hlGroup, l, prior))
l = {}
end
end
if #l > 0 then
table.insert(ids, fn.matchaddpos(hlGroup, l, prior))
end
onFulfilled = function()
for _, id in ipairs(ids) do
pcall(fn.matchdelete, id, handle)
end
end
else
local o = {hl_group = hlGroup}
for i = start, finish do
local row, col = i - 1, 0
o.end_row = i
o.end_col = 0
table.insert(ids, api.nvim_buf_set_extmark(handle, ns, row, col, o))
end
onFulfilled = function()
for _, id in ipairs(ids) do
pcall(api.nvim_buf_del_extmark, handle, ns, id)
end
end
end
return utils.wait(delay or 300):thenCall(onFulfilled)
end
return M

View File

@ -0,0 +1,49 @@
local fn = vim.fn
local M = {}
---
---@param winid number
---@param lnum number
---@param endLnum number
---@return table
function M.mapHighlightsByLnum(winid, lnum, endLnum)
local res = {}
for _, m in pairs(fn.getmatches(winid)) do
if m.pattern then
table.insert(res, m)
else
local added = false
local function add(match)
if not added then
table.insert(res, match)
added = true
end
end
for i = 1, 8 do
local k = 'pos' .. i
local p = m[k]
local pType = type(p)
if pType == 'nil' then
break
end
if pType == 'number' then
if p >= lnum and p <= endLnum then
m[k] = p - lnum + 1
add(m)
end
else
local l = p[1]
if l >= lnum and l <= endLnum then
m[k][1] = l - lnum + 1
add(m)
end
end
end
end
end
return res
end
return M

View File

@ -0,0 +1,77 @@
local highlighter = require('vim.treesitter.highlighter')
local M = {}
---
---@param bufnr number
---@param startRange number
---@param endRange number
---@param hlGroups? table<number|string, table>
---@return table
function M.getHighlightsByRange(bufnr, startRange, endRange, hlGroups)
local data = highlighter.active[bufnr]
if not data then
return {}
end
local res = {}
local row, col = startRange[1], startRange[2]
local endRow, endCol = endRange[1], endRange[2]
data.tree:for_each_tree(function(tstree, tree)
if not tstree then
return
end
local root = tstree:root()
local rootStartRow, _, rootEndRow, _ = root:range()
if rootEndRow < row or rootStartRow > endRow then
return
end
local query = data:get_query(tree:lang())
-- Some injected languages may not have highlight queries.
if not query:query() then
return
end
local iter = query:query():iter_captures(root, data.bufnr, row, endRow + 1)
-- Record the last range and priority
local lsr, lsc, ler, lec, lpriority, last
for capture, node, metadata in iter do
if not capture then
break
end
local hlId = assert((function()
if query.get_hl_from_capture then -- nvim 0.10+ #26675
return query:get_hl_from_capture(capture)
else
return query.hl_cache[capture]
end
end)())
local priority = tonumber(metadata.priority) or 100
local conceal = metadata.conceal
local sr, sc, er, ec = node:range()
if row <= er and endRow >= sr then
if sr < row or sr == row and sc < col then
sr, sc = row, col
end
if er > endRow or er == endRow and ec > endCol then
er, ec = endRow, endCol
end
if hlGroups then
-- Overlap highlighting if range is equal to last's
if lsr == sr and lsc == sc and ler == er and lec == ec then
if hlGroups[hlId].foreground and lpriority <= priority then
last[5], last[6], last[7] = hlId, priority, conceal
end
else
last = {sr, sc, er, ec, hlId, priority, conceal}
table.insert(res, last)
end
lsr, lsc, ler, lec, lpriority = sr, sc, er, ec, priority
else
table.insert(res, {sr, sc, er, ec, hlId, priority, conceal})
end
end
end
end)
return res
end
return M

View File

@ -0,0 +1,344 @@
---@class UfoUtils
local M = {}
local api = vim.api
local fn = vim.fn
local cmd = vim.cmd
local uv = vim.loop
---
---@return fun(): boolean
M.has10 = (function()
local has10
return function()
if has10 == nil then
has10 = fn.has('nvim-0.10') == 1
end
return has10
end
end)()
---
---@return fun(): boolean
M.has08 = (function()
local has08
return function()
if has08 == nil then
has08 = fn.has('nvim-0.8') == 1
end
return has08
end
end)()
---@return fun(): boolean
M.isWindows = (function()
local isWin
return function()
if isWin == nil then
isWin = uv.os_uname().sysname == 'Windows_NT'
end
return isWin
end
end)()
---
---@return string
function M.mode()
return api.nvim_get_mode().mode
end
---
---@param bufnr number
---@return number, number[]?
function M.getWinByBuf(bufnr)
local curBufnr
if not bufnr then
curBufnr = api.nvim_get_current_buf()
bufnr = curBufnr
end
local winids = {}
for _, winid in ipairs(api.nvim_list_wins()) do
if bufnr == api.nvim_win_get_buf(winid) then
table.insert(winids, winid)
end
end
if #winids == 0 then
return -1
elseif #winids == 1 then
return winids[1]
else
if not curBufnr then
curBufnr = api.nvim_get_current_buf()
end
local winid = curBufnr == bufnr and api.nvim_get_current_win() or winids[1]
return winid, winids
end
end
---
---@param winid number
---@param f fun(): any
---@return any
function M.winCall(winid, f)
if winid == 0 or winid == api.nvim_get_current_win() then
return f()
else
return api.nvim_win_call(winid, f)
end
end
---
---@param winid number
---@param lnum number
---@return number
function M.foldClosed(winid, lnum)
return M.winCall(winid, function()
return fn.foldclosed(lnum)
end)
end
---
---@param winid number
---@param lnum number
---@return number
function M.foldClosedEnd(winid, lnum)
return M.winCall(winid, function()
return fn.foldclosedend(lnum)
end)
end
---
---@param str string
---@param ts number
---@param start? number
---@return string
function M.expandTab(str, ts, start)
start = start or 1
local new = str:sub(1, start - 1)
local pad = ' '
local ti = start - 1
local i = start
while true do
i = str:find('\t', i, true)
if not i then
if ti == 0 then
new = str
else
new = new .. str:sub(ti + 1)
end
break
end
if ti + 1 == i then
new = new .. pad:rep(ts)
else
local append = str:sub(ti + 1, i - 1)
new = new .. append .. pad:rep(ts - api.nvim_strwidth(append) % ts)
end
ti = i
i = i + 1
end
return new
end
---@param ms number
---@return Promise
function M.wait(ms)
return require('promise')(function(resolve)
local timer = uv.new_timer()
timer:start(ms, 0, function()
timer:close()
resolve()
end)
end)
end
---
---@param callback function
---@param ms number
---@return userdata
function M.setTimeout(callback, ms)
---@type userdata
local timer = uv.new_timer()
timer:start(ms, 0, function()
timer:close()
callback()
end)
return timer
end
function M.zz()
local lnum1, lcount = api.nvim_win_get_cursor(0)[1], api.nvim_buf_line_count(0)
local zb = 'keepj norm! %dzb'
if lnum1 == lcount then
cmd(zb:format(lnum1))
return
end
cmd('norm! zvzz')
lnum1 = api.nvim_win_get_cursor(0)[1]
cmd('norm! L')
local lnum2 = api.nvim_win_get_cursor(0)[1]
if lnum2 + fn.getwinvar(0, '&scrolloff') >= lcount then
cmd(zb:format(lnum2))
end
if lnum1 ~= lnum2 then
cmd('keepj norm! ``')
end
end
---
---@param bufnr number
---@param name? string
---@param off? number
---@return boolean
function M.isUnNameBuf(bufnr, name, off)
name = name or api.nvim_buf_get_name(bufnr)
off = off or api.nvim_buf_get_offset(bufnr, 1)
return name == '' and off <= 0
end
---
---@param winid number
---@return boolean
function M.isDiffOrMarkerFold(winid)
local method = vim.wo[winid].foldmethod
return method == 'diff' or method == 'marker'
end
---
---@param winid number
---@return table<string, number>
function M.getWinInfo(winid)
local winfos = fn.getwininfo(winid)
assert(type(winfos) == 'table' and #winfos == 1,
'`getwininfo` expected 1 table with single element.')
return winfos[1]
end
---@param str string
---@param targetWidth number
---@return string
function M.truncateStrByWidth(str, targetWidth)
-- str in `strdisplaywidth` need to be converted from Lua to VimScript
-- If a Lua string contains a NUL byte, it will be converted to a |Blob|.
str = str:gsub('%z', '^@')
if fn.strdisplaywidth(str) <= targetWidth then
return str
end
local width = 0
local byteOff = 0
while true do
local part = fn.strpart(str, byteOff, 1, true)
width = width + fn.strdisplaywidth(part)
if width > targetWidth then
break
end
byteOff = byteOff + #part
end
return str:sub(1, byteOff)
end
---
---@param winid number
---@return number
function M.textoff(winid)
return M.getWinInfo(winid).textoff
end
---
---@param bufnr number
---@param lnum number 1-indexed
---@param col number 1-indexed
---@return number 0-indexed
function M.curswant(bufnr, lnum, col)
if col == 0 then
return 0
end
local text = api.nvim_buf_get_lines(bufnr, lnum - 1, lnum, true)[1]
text = M.expandTab(text:sub(1, col), vim.bo[bufnr].ts)
return #text - 1
end
---
---@param winid number
---@return boolean
function M.isWinValid(winid)
return type(winid) == 'number' and winid > 0 and api.nvim_win_is_valid(winid)
end
---
---@param bufnr number
---@return boolean
function M.isBufLoaded(bufnr)
return type(bufnr) == 'number' and bufnr > 0 and api.nvim_buf_is_loaded(bufnr)
end
---
---@param winid number
---@param line number
---@param lsizes number
---@return number, number
function M.evaluateTopline(winid, line, lsizes)
local log = require('ufo.lib.log')
local topline
local iStart = M.foldClosed(winid, line)
iStart = iStart == -1 and line or iStart
local lsizeSum = 0
local i = iStart - 1
local lsizeObj = require('ufo.model.linesize'):new(winid)
local len = lsizes - lsizeObj:fillSize(line)
log.info('winid:', winid, 'line:', line, 'lsizes:', lsizes, 'len:', len)
local size
while lsizeSum < len and i > 0 do
local lnum = M.foldClosed(winid, i)
log.info('lnum:', lnum, 'i:', i)
if lnum == -1 then
size = lsizeObj:size(i)
else
size = 1
i = lnum
end
lsizeSum = lsizeSum + size
log.info('size:', size, 'lsizeSum:', lsizeSum)
topline = i
i = i - 1
end
if not topline then
topline = iStart
end
-- extraOff lines is need to be showed near the topline
local topfill = lsizeObj:fillSize(topline)
local extraOff = lsizeSum - len
if extraOff > 0 then
if topfill < extraOff then
topline = topline + 1
else
topfill = topfill - extraOff
end
end
log.info('topline:', topline, 'topfill:', topfill)
return topline, topfill
end
---
---@param winid number
---@return table
function M.saveView(winid)
return M.winCall(winid, fn.winsaveview)
end
---
---@param winid number
---@param view table
function M.restView(winid, view)
M.winCall(winid, function()
fn.winrestview(view)
end)
end
---
---@param winid number
---@return number
function M.wrow(winid)
return M.winCall(winid, fn.winline) - 1
end
return M

View File

@ -0,0 +1,95 @@
---@diagnostic disable: undefined-field
---@class UfoWffi
local M = {}
local utils
local C
local ffi
local CPos_T
local function findWin(winid)
local err = ffi.new('Error')
return C.find_window_by_handle(winid, err)
end
---
---@param winid number
---@param lnum number
---@return number
function M.plinesWin(winid, lnum)
local wp = findWin(winid)
return C.plines_win(wp, lnum, true)
end
---
---@param winid number
---@param lnum number
---@param winheight boolean
---@return number
function M.plinesWinNofill(winid, lnum, winheight)
local wp = findWin(winid)
return C.plines_win_nofill(wp, lnum, winheight)
end
---
---@param winid number
function M.clearFolds(winid)
local wp = findWin(winid)
C.clearFolding(wp)
C.changed_window_setting(wp)
end
---
---@param winid number
---@param ranges table<number, number[]> list of line range
function M.createFolds(winid, ranges)
local wp = findWin(winid)
local s, e = CPos_T(), CPos_T()
for _, p in ipairs(ranges) do
s.lnum = p[1]
e.lnum = p[2]
C.foldCreate(wp, s, e)
end
end
local function init()
ffi = require('ffi')
setmetatable(M, {__index = ffi})
C = ffi.C
utils = require('ufo.utils')
if utils.has08() then
ffi.cdef([[
typedef int32_t linenr_T;
]])
else
ffi.cdef([[
typedef long linenr_T;
]])
end
ffi.cdef([[
typedef struct window_S win_T;
typedef int colnr_T;
typedef struct {} Error;
win_T *find_window_by_handle(int window, Error *err);
typedef struct {
linenr_T lnum;
colnr_T col;
colnr_T coladd;
} pos_T;
void clearFolding(win_T *win);
void changed_window_setting(win_T *wp);
void foldCreate(win_T *wp, pos_T start, pos_T end);
int plines_win(win_T *wp, linenr_T lnum, bool winheight);
int plines_win_nofill(win_T *wp, linenr_T lnum, bool winheight);
]])
CPos_T = ffi.typeof('pos_T')
end
init()
return M