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,22 @@
[*]
indent_style = space
indent_size = 4
tab_width = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
[*.lua]
align_continuous_assign_statement = false
space_around_table_field_list = false
align_call_args = false
quote_style = single
[*.json,*.jsonc]
indent_style = tab
[*.js]
indent_style = tab
[{Makefile,**.mk}]
indent_style = tab

View File

@ -0,0 +1,65 @@
name: Bug Report
description: File a bug report
labels: [bug]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
- type: input
attributes:
label: 'Neovim version (nvim -v | head -n1)'
placeholder: 'NVIM v0.7.0'
validations:
required: true
- type: input
attributes:
label: 'Operating system/version'
placeholder: 'macOS 11.5'
validations:
required: true
- type: textarea
attributes:
label: 'How to reproduce the issue'
description: |
How do you trigger this bug? Please walk us through it step by step.
Log path: `~/.cache/nvim/ufo.log`
value: |
`cat mini.lua`
```lua
-- Use Vim packages install the plugin, also work with some plugins manager such as packer.nvim
vim.o.packpath = '~/.local/share/nvim/site'
vim.cmd('packadd promise-async')
vim.cmd('packadd nvim-ufo')
-- Setting
vim.o.foldcolumn = '1'
vim.o.foldlevel = 99
vim.o.foldlevelstart = -1
vim.o.foldenable = true
local ufo = require('ufo')
ufo.setup()
vim.keymap.set('n', 'zR', ufo.openAllFolds)
vim.keymap.set('n', 'zM', ufo.closeAllFolds)
```
`nvim --clean +'so mini.lua'`
1.
2.
3.
...
validations:
required: true
- type: textarea
attributes:
label: 'Expected behavior'
description: 'Describe the behavior you expect. May include logs, images, or videos.'
validations:
required: true
- type: textarea
attributes:
label: 'Actual behavior'
validations:
required: true

View File

@ -0,0 +1 @@
blank_issues_enabled: false

View File

@ -0,0 +1,23 @@
name: Feature request
description: Request an enhancement for nvim-ufo
labels: [enhancement]
body:
- type: markdown
attributes:
value: |
Before requesting: search [existing issues](https://github.com/kevinhwang91/nvim-ufo/labels/enhancement).
- type: textarea
attributes:
label: "Feature description"
validations:
required: true
- type: textarea
attributes:
label: "Describe the solution you'd like"
validations:
required: true
- type: textarea
attributes:
label: "Additional context"
validations:
required: false

View File

@ -0,0 +1,31 @@
---
name: Push to Luarocks
on:
push:
tags:
- '*'
release:
types:
- created
tags:
- '*'
workflow_dispatch:
jobs:
luarocks-upload:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0 # Required to count the commits
- name: Get Version
run: echo "LUAROCKS_VERSION=$(git describe --abbrev=0 --tags)" >> $GITHUB_ENV
- name: LuaRocks Upload
uses: nvim-neorocks/luarocks-tag-release@v5
env:
LUAROCKS_API_KEY: ${{ secrets.LUAROCKS_API_KEY }}
with:
version: ${{ env.LUAROCKS_VERSION }}
dependencies: |
promise-async

View File

@ -0,0 +1,2 @@
doc/tags
build

View File

@ -0,0 +1,29 @@
{
"$schema": "https://raw.githubusercontent.com/sumneko/vscode-lua/master/setting/schema.json",
"completion.callSnippet": "Replace",
"completion.displayContext": 50,
"completion.keywordSnippet": "Disable",
"completion.postfix": ".",
"diagnostics.libraryFiles": "Disable",
"diagnostics.disable": [
"different-requires",
"param-type-mismatch",
"assign-type-mismatch"
],
"diagnostics.globals": [
"jit",
"it",
"describe",
"before_each",
"after_each",
"setup",
"teardown"
],
"runtime.version": "LuaJIT",
"type.castNumberToInteger": true,
"type.weakUnionCheck": true,
"workspace.library": [
"$VIM/runtime/lua",
"$VIM/site/pack/packer/start/promise-async/typings"
]
}

View File

@ -0,0 +1,180 @@
# Changelog
## [1.4.0] - 2024-04-03
### 🚀 Features
- *(preview)* Add `jumpTop` and `jumpBot` keymap actions (#109)
- *(highlight)* Add `UfoCursorFoldedLine` (#103)
- *(render)* Support inlay (#155)
- *(render)* Add support for concealed characters (#153) (#156)
- *(api)* Add cursor range and kind information for `UfoInspect`
- *(config)* [**breaking**] Use `close_fold_kinds_for_ft` instead `close_fold_kinds`
- *(decorator)* Export fold kind in `fold_virt_text_handler` (#207)
- *(build)* Luarocks support (#211)
### 🐛 Bug Fixes
- *(preview)* Respect `tabstop` and `shiftwidth` opts
- *(provider)* Respect 'tabstop' and 'shiftwidth' for indent
- *(decorator)* Reset winhl after detach
- *(decorator)* Keep last winid field
- *(driver)* Respect `foldminlines` (#108)
- *(decorator)* Buffer may be changed in a window
- *(decorator)* `setl winhl` erase hl of `nvim_win_set_hl_ns` (#111)
- *(preview)* Dispose preview window even if buffer is wiped out
- *(buffer)* Quickfix buftype can't detect line changed
- *(decorator)* Open fold should redraw at once (#132)
- *(treesitter)* Support `#make-range!` (#139)
- *(preview)* Window height should more than zero
- *(fold)* Refresh fb table in closure function
- Throw UfoFallbackException on RequestFailed (#159)
- *(render)* Join text for default hlgroup (#163)
- *(render)* Skip error return by `synID`
- *(fold)* Sync extmarks with foldedLines (#167)
- *(treesitter)* Use metadata.range prefer (#169)
- *(window)* Clear win highlight if buf changed
- *(decorator)* Ignore redraw request for closing fold (#176)
- *(decorator)* Ignore redundant redraw (#180,#181)
- *(fold)* Scan win folds if one buffer in multiple window
- *(decorator)* Correct bufnrSet logic
- *(window)* Don't clear winhl during first render (#183)
- *(render)* Replace `Normal` highlight with `UfoFoldedFg`
- *(action)* Check endLnum to avoid infinite loop (#184)
- *(decorator)* Highlight open fold for multiple windows correctly (#187)
- *(decorator)* Erase extmark even in multiple windows
- *(decorator)* Narrow the fold range for stale
- *(treesitter)* Fix errors when getting hlId on nvim 0.10.x (#188)
- *(model)* Use private field to avoid inherit (#186)
- *(fold)* Don't make scan flag if manual invoke (#192)
- *(window)* Upstream bug, `set winhl` change curswant (#194)
- *(preview)* Nightly change `nvim_win_get_config` return val
- *(wffi)* `changed_window_setting` signature changed
- *(decorator)* Keep silent for `Keyboard interrupt` error (#202)
- *(decorator)* Correct capture condition
- *(fold)* Return correct winid
### ⚡ Performance
- *(decorator)* Skip rendering of horizontal movement
- *(decorator)* `set winhl` will redraw all lines
## [1.3.0] - 2023-01-05
### Features
#### Provider
- Use fallback if `buftype == 'nofile'`
- Inspect current fold kinds
### Bug Fixes
#### Preview
- Respect target buffer opts
- Stick to top left corner while scrolling in normal window
- Fix wrong row for upward display
#### Fold
- Window maybe changed before set opts
- Improve leaving diff mode behavior
#### Miscellaneous
- Substitute NUL byte for VimScript func
- Catch coc.nvim `Plugin not ready` error and resolve
### Documentation
- Explain `fold_virt_text_handler` (#98)
- Make capabilities for all available lsp servers & remove "other_fields" (#100)
## [1.2.0] - 2022-10-09
### Features
#### Fold
- Add `close_fold_kinds` option
- Make the window display upward if `kind == 'comment'` (#73)
#### API
- Add `applyFolds`
- Add `openFoldsExceptKinds` (#64)
#### Preview
- Support highlighting with `:match`
- Show virtual winbar if preview is scrolled and export `UfoPreviewWinbar` highlight group
- Highlight cursor line for preview and export `UfoPreviewCursorLine` highlight group
#### Decorator
- Hint error for users' virtTextHandler (#79)
- Add `enable_get_fold_virt_text` option to get virt texts of all folded lines (#74)
### !Breaking
- `enable_fold_end_virt_text` option is deprecated, use `enable_get_fold_virt_text` instead
- The signature of `peekFoldedLinesUnderCursor` API is changed
### Bug Fixes
#### Fold
- Handle multiple windows with same buffers
- `set foldenable` forecdly after leaving diff mode
- Restore topline after first applying folds to keep eyes comfortable
- EndLnum may exceed buffer line count because of the asynchronization
#### API
- Action should work after detach (#75)
#### Preview
- Dispose previous resources before a new attach
- Scroll bar reaches the bottom until the end of the line is visible
#### Provider
- Need more time to wait for the server
- Better bypass strategy, must reach the timeout and a certain number of requests
- Lsp provider always returns Promise object
- Validate buffer after requesting folds
- Dispose all providers properly
#### Decorator
- Stop highlighting after opening folds during incsearch
- Keep refreshing even if nofoldenable
#### Render
- Limit the end of range
- Treesitter extmarks may be overlapped, filter invalid extmarks out
## [1.1.0] - 2022-08-13
### Bug Fixes
- Reset foldlines if extmark range is backward
- Unexpected fired `on_lines` at nvim_buf_attach
- Fix `winsaveview()` for scanning fold ranges
- Always open folds if text content in range (#60)
- Scroll bar shouldn't be filled fully if it's scrollable
- Drop coc.nvim cancellation
- Filter out last same ranges
- Assert `provider_selector` return value (#61)
### Features
- Add `closeFoldsWith` API (#62)
- Truncate top border for preview if possible
## [1.0.0] - 2022-07-24
First release with 1.0.0 version.

View File

@ -0,0 +1,29 @@
BSD 3-Clause License
Copyright (c) 2022-2023, kevinhwang91
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -0,0 +1,18 @@
SHELL := /bin/bash
DEPS ?= build
LUA_LS ?= $(DEPS)/lua-language-server
LINT_LEVEL ?= Information
all:
lint:
@rm -rf $(LUA_LS)
@mkdir -p $(LUA_LS)
@VIM=$(HOME)/.local/share/nvim lua-language-server --check $(PWD) --checklevel=$(LINT_LEVEL) --logpath=$(LUA_LS)
@[[ -f $(LUA_LS)/check.json ]] && { cat $(LUA_LS)/check.json 2>/dev/null; exit 1; } || true
clean:
rm -rf $(DEPS)
.PHONY: all deps clean lint

View File

@ -0,0 +1,385 @@
# nvim-ufo
The goal of nvim-ufo is to make Neovim's fold look modern and keep high performance.
<https://user-images.githubusercontent.com/17562139/173796287-9842fb3a-37c2-47fb-8968-6e7600c0fcef.mp4>
> [setup foldcolumn like demo](https://github.com/kevinhwang91/nvim-ufo/issues/4)
---
## Table of contents
- [Table of contents](#table-of-contents)
- [Features](#features)
- [Quickstart](#quickstart)
- [Requirements](#requirements)
- [Installation](#installation)
- [Minimal configuration](#minimal-configuration)
- [Usage](#usage)
- [Documentation](#documentation)
- [How does nvim-ufo get the folds?](#how-does-nvim-ufo-get-the-folds)
- [Setup and description](#setup-and-description)
- [Preview function table](#preview-function-table)
- [Commands](#commands)
- [API](#api)
- [Highlight groups](#highlight-groups)
- [Advanced configuration](#advanced-configuration)
- [Customize configuration](#customize-configuration)
- [Customize fold text](#customize-fold-text)
- [Feedback](#feedback)
- [License](#license)
## Features
- Penetrate color for folded lines like other modern editors/IDEs
- Never block Neovim
- Adding folds high accuracy with Folding Range in LSP
- Support fallback and customize strategy for fold provider
- Peek folded line and jump the desired location with less redraw
## Quickstart
### Requirements
- [Neovim](https://github.com/neovim/neovim) 0.7.2 or later
- [coc.nvim](https://github.com/neoclide/coc.nvim) (optional)
### Installation
Install with [Packer.nvim](https://github.com/wbthomason/packer.nvim):
```lua
use {'kevinhwang91/nvim-ufo', requires = 'kevinhwang91/promise-async'}
```
### Minimal configuration
```lua
use {'kevinhwang91/nvim-ufo', requires = 'kevinhwang91/promise-async'}
vim.o.foldcolumn = '1' -- '0' is not bad
vim.o.foldlevel = 99 -- Using ufo provider need a large value, feel free to decrease the value
vim.o.foldlevelstart = 99
vim.o.foldenable = true
-- Using ufo provider need remap `zR` and `zM`. If Neovim is 0.6.1, remap yourself
vim.keymap.set('n', 'zR', require('ufo').openAllFolds)
vim.keymap.set('n', 'zM', require('ufo').closeAllFolds)
-- Option 1: coc.nvim as LSP client
use {'neoclide/coc.nvim', branch = 'master', run = 'yarn install --frozen-lockfile'}
require('ufo').setup()
--
-- Option 2: nvim lsp as LSP client
-- Tell the server the capability of foldingRange,
-- Neovim hasn't added foldingRange to default capabilities, users must add it manually
local capabilities = vim.lsp.protocol.make_client_capabilities()
capabilities.textDocument.foldingRange = {
dynamicRegistration = false,
lineFoldingOnly = true
}
local language_servers = require("lspconfig").util.available_servers() -- or list servers manually like {'gopls', 'clangd'}
for _, ls in ipairs(language_servers) do
require('lspconfig')[ls].setup({
capabilities = capabilities
-- you can add other fields for setting up lsp server in this table
})
end
require('ufo').setup()
--
-- Option 3: treesitter as a main provider instead
-- (Note: the `nvim-treesitter` plugin is *not* needed.)
-- ufo uses the same query files for folding (queries/<lang>/folds.scm)
-- performance and stability are better than `foldmethod=nvim_treesitter#foldexpr()`
require('ufo').setup({
provider_selector = function(bufnr, filetype, buftype)
return {'treesitter', 'indent'}
end
})
--
-- Option 4: disable all providers for all buffers
-- Not recommend, AFAIK, the ufo's providers are the best performance in Neovim
require('ufo').setup({
provider_selector = function(bufnr, filetype, buftype)
return ''
end
})
```
### Usage
Use fold as usual.
Using a provider of ufo, must set a large value for `foldlevel`, this is the limitation of
`foldmethod=manual`. A small value may close fold automatically if the fold ranges updated.
After running `zR` and `zM` normal commands will change the `foldlevel`, ufo provide the APIs
`openAllFolds`/`closeAllFolds` to open/close all folds but keep `foldlevel` value, need to remap
them.
Like `zR` and `zM`, if you used `zr` and `zm` before, please use `closeFoldsWith` API to close folds
like `set foldlevel=n` but keep `foldlevel` value.
## Documentation
### How does nvim-ufo get the folds?
If ufo detect `foldmethod` option is not `diff` or `marker`, it will request the providers to get
the folds, the request strategy is formed by the main and the fallback. The default value of main is
`lsp` and the default value of fallback is `indent` which implemented by ufo.
For example, Changing the text in a buffer will request the providers for folds.
> `foldmethod` option will finally become `manual` if ufo is working.
### Setup and description
```lua
{
open_fold_hl_timeout = {
description = [[Time in millisecond between the range to be highlgihted and to be cleared
while opening the folded line, `0` value will disable the highlight]],
default = 400
},
provider_selector = {
description = [[A function as a selector for fold providers. For now, there are
'lsp' and 'treesitter' as main provider, 'indent' as fallback provider]],
default = nil
},
close_fold_kinds_for_ft = {
description = [[After the buffer is displayed (opened for the first time), close the
folds whose range with `kind` field is included in this option. For now,
'lsp' provider's standardized kinds are 'comment', 'imports' and 'region'.
This option is a table with filetype as key and fold kinds as value. Use a
default value if value of filetype is absent.
Run `UfoInspect` for details if your provider has extended the kinds.]],
default = {default = {}}
},
fold_virt_text_handler = {
description = [[A function customize fold virt text, see ### Customize fold text]],
default = nil
},
enable_get_fold_virt_text = {
description = [[Enable a function with `lnum` as a parameter to capture the virtual text
for the folded lines and export the function to `get_fold_virt_text` field of
ctx table as 6th parameter in `fold_virt_text_handler`]],
default = false
},
preview = {
description = [[Configure the options for preview window and remap the keys for current
buffer and preview buffer if the preview window is displayed.
Never worry about the users's keymaps are overridden by ufo, ufo will save
them and restore them if preview window is closed.]],
win_config = {
border = {
description = [[The border for preview window,
`:h nvim_open_win() | call search('border:')`]],
default = 'rounded',
},
winblend = {
description = [[The winblend for preview window, `:h winblend`]],
default = 12,
},
winhighlight = {
description = [[The winhighlight for preview window, `:h winhighlight`]],
default = 'Normal:Normal',
},
maxheight = {
description = [[The max height of preview window]],
default = 20,
}
},
mappings = {
description = [[The table for {function = key}]],
default = [[see ###Preview function table for detail]],
}
}
}
```
`:h ufo` may help you to get the all default configuration.
### Preview function table
<!-- markdownlint-disable MD013 -->
| Function | Action | Def Key |
| -------- | ---------------------------------------------------------------------------------------------- | ------- |
| scrollB | Type `CTRL-B` in preview window | |
| scrollF | Type `CTRL-F` in preview window | |
| scrollU | Type `CTRL-U` in preview window | |
| scrollD | Type `CTRL-D` in preview window | |
| scrollE | Type `CTRL-E` in preview window | `<C-E>` |
| scrollY | Type `CTRL-Y` in preview window | `<C-Y>` |
| jumpTop | Jump to top region in preview window | |
| jumpBot | Jump to bottom region in preview window | |
| close | In normal window: Close preview window<br>In preview window: Close preview window | `q` |
| switch | In normal window: Go to preview window<br>In preview window: Go to normal window | `<Tab>` |
| trace | In normal window: Trace code based on topline<br>In preview window: Trace code based on cursor | `<CR>` |
<!-- markdownlint-enable MD013-->
Additional mouse supported:
1. `<ScrollWheelUp>` and `<ScrollWheelDown>`: Scroll preview window.
2. `<2-LeftMouse>`: Same as `trace` action in preview window.
> `trace` action will open all fold for the folded lines
### Commands
| Command | Description |
| -------------- | -------------------------------------------------------------- |
| UfoEnable | Enable ufo |
| UfoDisable | Disable ufo |
| UfoInspect | Inspect current buffer information |
| UfoAttach | Attach current buffer to enable all features |
| UfoDetach | Detach current buffer to disable all features |
| UfoEnableFold | Enable to get folds and update them at once for current buffer |
| UfoDisableFold | Disable to get folds for current buffer |
### API
[ufo.lua](./lua/ufo.lua)
### Highlight groups
```vim
" hi default UfoFoldedFg guifg=Normal.foreground
" hi default UfoFoldedBg guibg=Folded.background
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
```
- `UfoFoldedFg`: Foreground for raw text of folded line.
- `UfoFoldedBg`: Background of folded line.
- `UfoPreviewSbar`: Scroll bar of preview window, only take effect if the border is missing right
horizontal line, like `border = 'none'`.
- `UfoPreviewCursorLine`: Highlight current line in preview window if it isn't the start of folded
lines.
- `UfoPreviewWinBar`: Virtual winBar of preview window.
- `UfoPreviewThumb`: Thumb of preview window.
- `UfoFoldedEllipsis`: Ellipsis at the end of folded line, invalid if `fold_virt_text_handler` is
set.
- `UfoCursorFoldedLine`: Highlight the folded line under the cursor
## Advanced configuration
Configuration can be found at [example.lua](./doc/example.lua)
### Customize configuration
```lua
local ftMap = {
vim = 'indent',
python = {'indent'},
git = ''
}
require('ufo').setup({
open_fold_hl_timeout = 150,
close_fold_kinds_for_ft = {
default = {'imports', 'comment'},
json = {'array'},
c = {'comment', 'region'}
},
preview = {
win_config = {
border = {'', '─', '', '', '', '─', '', ''},
winhighlight = 'Normal:Folded',
winblend = 0
},
mappings = {
scrollU = '<C-u>',
scrollD = '<C-d>',
jumpTop = '[',
jumpBot = ']'
}
},
provider_selector = function(bufnr, filetype, buftype)
-- if you prefer treesitter provider rather than lsp,
-- return ftMap[filetype] or {'treesitter', 'indent'}
return ftMap[filetype]
-- refer to ./doc/example.lua for detail
end
})
vim.keymap.set('n', 'zR', require('ufo').openAllFolds)
vim.keymap.set('n', 'zM', require('ufo').closeAllFolds)
vim.keymap.set('n', 'zr', require('ufo').openFoldsExceptKinds)
vim.keymap.set('n', 'zm', require('ufo').closeFoldsWith) -- closeAllFolds == closeFoldsWith(0)
vim.keymap.set('n', 'K', function()
local winid = require('ufo').peekFoldedLinesUnderCursor()
if not winid then
-- choose one of coc.nvim and nvim lsp
vim.fn.CocActionAsync('definitionHover') -- coc.nvim
vim.lsp.buf.hover()
end
end)
```
### Customize fold text
Adding number suffix of folded lines instead of the default ellipsis, here is the example:
<p align="center">
<img width="864px" src=https://user-images.githubusercontent.com/17562139/174121926-e90a962d-9fc9-428a-bd53-274ed392c68d.png>
</p>
```lua
local handler = function(virtText, lnum, endLnum, width, truncate)
local newVirtText = {}
local suffix = (' 󰁂 %d '):format(endLnum - lnum)
local sufWidth = vim.fn.strdisplaywidth(suffix)
local targetWidth = width - sufWidth
local curWidth = 0
for _, chunk in ipairs(virtText) do
local chunkText = chunk[1]
local chunkWidth = vim.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 = vim.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, 'MoreMsg'})
return newVirtText
end
-- global handler
-- `handler` is the 2nd parameter of `setFoldVirtTextHandler`,
-- check out `./lua/ufo.lua` and search `setFoldVirtTextHandler` for detail.
require('ufo').setup({
fold_virt_text_handler = handler
})
-- buffer scope handler
-- will override global handler if it is existed
-- local bufnr = vim.api.nvim_get_current_buf()
-- require('ufo').setFoldVirtTextHandler(bufnr, handler)
```
## Feedback
- If you get an issue or come up with an awesome idea, don't hesitate to open an issue in github.
- If you think this plugin is useful or cool, consider rewarding it a star.
## License
The project is licensed under a BSD-3-clause license. See [LICENSE](./LICENSE) file for details.

View File

@ -0,0 +1,48 @@
const { languages, commands, workspace, wait, CancellationTokenSource, Disposable } = require('coc.nvim')
exports.activate = async context => {
let { logger, subscriptions } = context
let nvim = workspace.nvim
subscriptions.push(commands.registerCommand('ufo.foldingRange', async (bufnr, kind) => {
let doc = workspace.getDocument(bufnr)
if (!doc || !doc.attached) {
await wait(50)
doc = workspace.getDocument(bufnr)
if (!doc || !doc.attached) {
return
}
}
let { textDocument } = doc
// TODO
// no way to check whether server supports `textDocument/foldingRange`
// or server will call `client/registerCapability`
if (!languages.hasProvider('foldingRange', textDocument)) {
await wait(500)
if (!languages.hasProvider('foldingRange', textDocument)) {
throw 'UfoFallbackException'
}
}
await doc.synchronize()
let tokenSource = new CancellationTokenSource()
let { token } = tokenSource
let ranges = await languages.provideFoldingRanges(textDocument, {}, token)
if (!ranges || !ranges.length) {
return []
}
ranges = ranges.filter(o => (!kind || kind == o.kind) && o.startLine < o.endLine)
.sort((a, b) => {
if (b.startLine == a.startLine) {
return a.endLine - b.endLine
} else {
return b.startLine - a.startLine
}
})
return ranges
}))
subscriptions.push(Disposable.create(async () => {
await nvim.lua(`require('ufo.provider.lsp.coc').handleDisposeNotify(...)`, [])
}))
await nvim.lua(`require('ufo.provider.lsp.coc').handleInitNotify(...)`, [])
}

View File

@ -0,0 +1,186 @@
---@diagnostic disable: unused-local, unused-function, undefined-field
-----------------------------------------providerSelector-------------------------------------------
local function selectProviderWithFt()
local ftMap = {
vim = 'indent',
python = {'indent'},
git = ''
}
require('ufo').setup({
provider_selector = function(bufnr, filetype, buftype)
-- return a table with string elements: 1st is name of main provider, 2nd is fallback
-- return a string type: use ufo inner providers
-- return a string in a table: like a string type above
-- return empty string '': disable any providers
-- return `nil`: use default value {'lsp', 'indent'}
-- return a function: it will be involved and expected return `UfoFoldingRange[]|Promise`
-- if you prefer treesitter provider rather than lsp,
-- return ftMap[filetype] or {'treesitter', 'indent'}
return ftMap[filetype]
end
})
end
-- lsp->treesitter->indent
local function selectProviderWithChainByDefault()
local ftMap = {
vim = 'indent',
python = {'indent'},
git = ''
}
---@param bufnr number
---@return Promise
local function customizeSelector(bufnr)
local function handleFallbackException(err, providerName)
if type(err) == 'string' and err:match('UfoFallbackException') then
return require('ufo').getFolds(bufnr, providerName)
else
return require('promise').reject(err)
end
end
return require('ufo').getFolds(bufnr, 'lsp'):catch(function(err)
return handleFallbackException(err, 'treesitter')
end):catch(function(err)
return handleFallbackException(err, 'indent')
end)
end
require('ufo').setup({
provider_selector = function(bufnr, filetype, buftype)
return ftMap[filetype] or customizeSelector
end
})
end
local function selectProviderWithFunction()
---@param bufnr number
---@return UfoFoldingRange[]
local function customizeSelector(bufnr)
local res = {}
table.insert(res, {startLine = 1, endLine = 3})
table.insert(res, {startLine = 5, endLine = 10})
return res
end
local ftMap = {
vim = 'indent',
python = {'indent'},
git = customizeSelector
}
require('ufo').setup({
provider_selector = function(bufnr, filetype, buftype)
return ftMap[filetype]
end
})
end
-----------------------------------------providerSelector-------------------------------------------
------------------------------------------enhanceAction---------------------------------------------
local function peekOrHover()
local winid = require('ufo').peekFoldedLinesUnderCursor()
if winid then
local bufnr = vim.api.nvim_win_get_buf(winid)
local keys = {'a', 'i', 'o', 'A', 'I', 'O', 'gd', 'gr'}
for _, k in ipairs(keys) do
-- Add a prefix key to fire `trace` action,
-- if Neovim is 0.8.0 before, remap yourself
vim.keymap.set('n', k, '<CR>' .. k, {noremap = false, buffer = bufnr})
end
else
-- coc.nvim
vim.fn.CocActionAsync('definitionHover')
-- nvimlsp
vim.lsp.buf.hover()
end
end
local function goPreviousClosedAndPeek()
require('ufo').goPreviousClosedFold()
require('ufo').peekFoldedLinesUnderCursor()
end
local function goNextClosedAndPeek()
require('ufo').goNextClosedFold()
require('ufo').peekFoldedLinesUnderCursor()
end
local function applyFoldsAndThenCloseAllFolds(providerName)
require('async')(function()
local bufnr = vim.api.nvim_get_current_buf()
-- make sure buffer is attached
require('ufo').attach(bufnr)
-- getFolds return Promise if providerName == 'lsp'
local ranges = await(require('ufo').getFolds(bufnr, providerName))
if not vim.tbl_isempty(ranges) then
local ok = require('ufo').applyFolds(bufnr, ranges)
if ok then
require('ufo').closeAllFolds()
end
end
end)
end
------------------------------------------enhanceAction---------------------------------------------
---------------------------------------setFoldVirtTextHandler---------------------------------------
local handler = function(virtText, lnum, endLnum, width, truncate)
local newVirtText = {}
local suffix = (' 󰁂 %d '):format(endLnum - lnum)
local sufWidth = vim.fn.strdisplaywidth(suffix)
local targetWidth = width - sufWidth
local curWidth = 0
for _, chunk in ipairs(virtText) do
local chunkText = chunk[1]
local chunkWidth = vim.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 = vim.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, 'MoreMsg'})
return newVirtText
end
local function customizeFoldText()
-- global handler
require('ufo').setup({
fold_virt_text_handler = handler
})
end
local function customizeBufFoldText()
-- buffer scope handler
-- will override global handler if it is existed
local bufnr = vim.api.nvim_get_current_buf()
require('ufo').setFoldVirtTextHandler(bufnr, handler)
end
local function inspectVirtTextForFoldedLines()
require('ufo').setup({
enable_get_fold_virt_text = true,
fold_virt_text_handler = function(virtText, lnum, endLnum, width, truncate, ctx)
for i = lnum, endLnum do
print('lnum: ', i, ', virtText: ', vim.inspect(ctx.get_fold_virt_text(i)))
end
return virtText
end
})
end
---------------------------------------setFoldVirtTextHandler---------------------------------------

View File

@ -0,0 +1,17 @@
*ufo.txt*
*nvimufo.txt*
*nvim-ufo.txt*
type `gf` or `CTRL-W gf` under the PATH value :)
`URL`: https://github.com/kevinhwang91/nvim-ufo
`PATH`: ../README.md
`PATH`: ../lua/ufo/config.lua |nvim-ufo-default-config|
`PATH`: ../lua/ufo.lua |nvim-ufo-api|
`PATH`: ../doc/example.lua |nvim-ufo-example|
vim:ft=help

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

View File

@ -0,0 +1,16 @@
{
"name": "coc-ufo",
"version": "0.0.1",
"description": "handle folding ranges in coc.nvim",
"author": "kevinhwang91 <kevin.hwang@live.com>",
"license": "BSD 3-Clause",
"main": "coc-extension/index.js",
"type": "commonjs",
"engines": {
"coc": "^0.0.82"
},
"activationEvents": [
"*"
],
"contributes": {}
}