diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
deleted file mode 100644
index 10aee26..0000000
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ /dev/null
@@ -1,43 +0,0 @@
----
-name: Bug report
-about: Create a report to help us improve
-title: ''
-labels: ''
-assignees: ''
----
-
-
-
-- `nvim --version`:
-- Operating system/version:
-
-**Describe the bug**
-A clear and concise description of what the bug is.
-
-**To Reproduce using `nvim -u mini.vim`**
-
-Example:
-`cat mini.vim`
-
-```vim
-" use your plugin manager, here is `vim-plug`
-call plug#begin('~/.config/nvim/plugged')
-Plug 'kevinhwang91/nvim-bqf'
-Plug 'junegunn/fzf', { 'do': { -> fzf#install() } }
-call plug#end()
-```
-
-Steps to reproduce the behavior:
-
-1.
-2.
-3.
-
-**Expected behavior**
-A clear and concise description of what you expected to happen.
-
-**Screenshots**
-If applicable, add screenshots to help explain your problem.
-
-**Additional context**
-Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
new file mode 100644
index 0000000..8d81f57
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -0,0 +1,43 @@
+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.5.1'
+ 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.'
+ value: |
+ 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
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000..3ba13e0
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1 @@
+blank_issues_enabled: false
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
deleted file mode 100644
index bbcbbe7..0000000
--- a/.github/ISSUE_TEMPLATE/feature_request.md
+++ /dev/null
@@ -1,20 +0,0 @@
----
-name: Feature request
-about: Suggest an idea for this project
-title: ''
-labels: ''
-assignees: ''
-
----
-
-**Is your feature request related to a problem? Please describe.**
-A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
-
-**Describe the solution you'd like**
-A clear and concise description of what you want to happen.
-
-**Describe alternatives you've considered**
-A clear and concise description of any alternative solutions or features you've considered.
-
-**Additional context**
-Add any other context or screenshots about the feature request here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
new file mode 100644
index 0000000..c5a1fe2
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -0,0 +1,23 @@
+name: Feature request
+description: Request an enhancement for Neovim
+labels: [enhancement]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Before requesting: search [existing issues](https://github.com/kevinhwang91/nvim-bqf/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
diff --git a/.vim/coc-settings.json b/.vim/coc-settings.json
deleted file mode 100644
index 9cfbf4a..0000000
--- a/.vim/coc-settings.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- "languageserver": {
- "lua": {
- "settings": {
- "Lua": {
- "runtime": {
- "version": "LuaJIT"
- }
- }
- }
- }
- }
-}
diff --git a/README.md b/README.md
index c5071be..a339b73 100644
--- a/README.md
+++ b/README.md
@@ -2,9 +2,7 @@
The goal of nvim-bqf is to make Neovim's quickfix window better.
-
-
-
+
---
@@ -13,7 +11,7 @@ uncomfortable? Are you constantly jumping between the edit window and the quick
use quickfix window to refactor because of lacking a sustainable preview window? Do you think
quickfix window lacks a fuzzy search function? At present, nvim-bqf can solve the above problems.
-You really don't need any search and replace plugins, because nvim-bqf with the built-in function of
+You really don't need any search replace plugins, because nvim-bqf with the built-in function of
the quickfix window allows you to easily search and replace targets.
So why not nvim-bqf?
@@ -28,8 +26,9 @@ So why not nvim-bqf?
* [Installation](#installation)
* [Minimal configuration](#minimal-configuration)
* [Usage](#usage)
- * [filter with signs](#filter-with-signs)
- * [fzf mode](#fzf-mode)
+ * [Filter with signs](#filter-with-signs)
+ * [Fzf mode](#fzf-mode)
+ * [Filter items with signs demo](#filter-items-with-signs-demo)
* [Search and replace demo](#search-and-replace-demo)
* [Documentation](#documentation)
* [Setup and description](#setup-and-description)
@@ -39,7 +38,7 @@ So why not nvim-bqf?
* [Quickfix context](#quickfix-context)
* [Why use an additional context?](#why-use-an-additional-context?)
* [Supported keys](#supported-keys)
- * [Simple vimscript tests for understanding](#simple-vimscript-tests-for-understanding)
+ * [Simple lua tests for understanding](#simple-lua-tests-for-understanding)
* [Highlight groups](#highlight-groups)
* [Advanced configuration](#advanced-configuration)
* [Customize configuration](#customize-configuration)
@@ -52,56 +51,55 @@ So why not nvim-bqf?
- Toggle quickfix window with magic window keep your eyes comfortable
- Extend built-in context of quickfix to build an eye friendly highlighting at preview
- Support convenient actions inside quickfix window, see [Function table](#function-table) below
-- Support built-in buffer for preview perfectly
-- Fast start time compare with others lua plugins, which almost only spend time on `lua require`
+- Optimize the buffer preview under treesitter to get extreme performance
- Using signs to filter the items of quickfix window
- Integrate [fzf](https://github.com/junegunn/fzf) as a picker/filter in quickfix window
## TODO
-- [ ] Provide statusline for information
- [ ] Find a better way to list history and switch to one
-- [ ] Provide some useful functions to users
-- [ ] Add tests
- [ ] Use context field to override the existed configuration
+- [ ] Add tests
## Quickstart
### Requirements
-- Neovim [nightly](https://github.com/neovim/neovim#install-from-source)
+- [Neovim](https://github.com/neovim/neovim) 0.5 or later
- [fzf](https://github.com/junegunn/fzf) (optional, 0.24.0 later)
-> Preview with fzf needs a pipe command, Windows can't be supported. It must be stated that
+> Preview with fzf needs a pipe, Windows can't be supported. It must be stated that
> I'm not working under Windows.
### Installation
-Install nvim-bqf with your favorite plugin manager! For instance: [Vim-plug](https://github.com/junegunn/vim-plug):
+Install with [Packer.nvim](https://github.com/wbthomason/packer.nvim):
-```vim
-Plug 'kevinhwang91/nvim-bqf'
+```lua
+use {'kevinhwang91/nvim-bqf'}
```
-> The default branch is main, please upgrade vim-plug if you encounter any installation issues.
-
### Minimal configuration
-```vim
-Plug 'kevinhwang91/nvim-bqf'
+```lua
+use {'kevinhwang91/nvim-bqf', ft = 'qf'}
-" if you install fzf as system package like `pacman -S fzf` in ArchLinux,
-" please comment next line
-Plug 'junegunn/fzf', { 'do': { -> fzf#install() } }
+-- optional
+use {'junegunn/fzf', run = function()
+ vim.fn['fzf#install']()
+end
+}
-" highly recommended
-Plug 'nvim-treesitter/nvim-treesitter', {'do': ':TSUpdate'}
+-- optional, highly recommended
+use {'nvim-treesitter/nvim-treesitter', run = ':TSUpdate'}
```
The nvim-bqf's preview builds upon the buffers. I highly recommended to use
-[nvim-treesitter](https://github.com/nvim-treesitter/nvim-treesitter) to do syntax to the buffer,
+[nvim-treesitter](https://github.com/nvim-treesitter/nvim-treesitter) to do syntax for the buffer,
because vim's syntax is very lagging and is extremely bad for the user experience in large files.
+> nvim-bqf has optimized the preview performance for treesitter
+
### Usage
1. If you are familiar with quickfix, use quickfix as usual.
@@ -129,22 +127,16 @@ fzf also support `ctrl-q` to toggle items' sign.
#### Filter items with signs demo
-Key input sequence: `jznzf^^zf`
-
-
-
-
+
-> input `^^` in fzf will find all signed items, `ctrl-o` in fzf mode has bind `toggle-all`
+> input `^^` in fzf prompt will find all signed items, `ctrl-o` in fzf mode has bind `toggle-all`
#### Search and replace demo
-Using external grep-like program to search `qftool` and replace it to `mytool`,
-but filter `fzf.lua` file.
+Using external grep-like program to search `display` and replace it to `show`,
+but exclude `session.lua` file.
-
-
-
+
> Demonstrating batch undo just show that quickfix has this feature
@@ -153,7 +145,7 @@ but filter `fzf.lua` file.
### Setup and description
```lua
-root = {
+{
auto_enable = {
description = [[enable nvim-bqf in quickfix window automatically]],
default = true
@@ -196,6 +188,11 @@ root = {
wrap = {
description = [[wrap the line, `:h wrap` for detail]],
default = false
+ },
+ should_preview_cb = {
+ description = [[a callback function to decide whether to preview while switching buffer,
+ with a bufnr parameter]],
+ default = nil
}
},
func_map = {
@@ -284,6 +281,9 @@ Vim grant users an ability to stuff a context to quickfix, please run `:help qui
#### Why use an additional context?
+**Neovim nightly version has supported position range, use the builtin range if highlight
+context doesn't exist.**
+
nvim-bqf will use the context to implement missing features of quickfix. If you are familiar with
quickfix, you know quickfix only contains `lnum` and `col` to locate a position in an item, but
lacks of range. To get better highlighting experience, nvim-bqf processeds the vim regrex pattern
@@ -292,13 +292,7 @@ context additionally.
The context's format that can be processed by nvim-bqf is:
-```vim
-" vimscript
-let context = {'context': {'bqf': {}}}
-```
-
```lua
--- lua
local context = {context = {bqf = {}}}
```
@@ -316,57 +310,69 @@ context = {
},
lsp_ranges_hl = {
description = [[a list of lsp range. The length of list is equal to the items',
- and each element corresponds one to one]],
- type = 'list in vimscript | table in lua'
+ pairwise correspondence each other]],
+ type = 'table'
}
-
}
}
```
-#### Simple vimscript tests for understanding
+#### Simple lua tests for understanding
-```vim
-function s:create_qf()
- enew
- let bufnr = bufnr('%')
- for i in range(1, 3)
- call setline(i, i .. ' | ' .. strftime("%F"))
- endfor
-
- call setqflist([{'bufnr': bufnr, 'lnum': 1, 'col': 5},
- \ {'bufnr': bufnr, 'lnum': 2, 'col': 10},
- \ {'bufnr': bufnr, 'lnum': 3, 'col': 13}])
-endfunction
-
-function! Test_bqf_pattern()
- call s:create_qf()
- call setqflist([], 'r', {'context': {'bqf': {'pattern_hl': '\d\+'}},
- \ 'title': 'pattern_hl'})
- cwindow
-endfunc
-
-function! Test_bqf_lsp_ranges()
- call s:create_qf()
- let lsp_ranges = []
- call add(lsp_ranges, {
- \ 'start': {'line': 0, 'character': 4},
- \ 'end': {'line': 0, 'character': 8}
- \ })
- call add(lsp_ranges, {
- \ 'start': {'line': 1, 'character': 9},
- \ 'end': {'line': 1, 'character': 11}
- \ })
- call add(lsp_ranges, {
- \ 'start': {'line': 2, 'character': 12},
- \ 'end': {'line': 2, 'character': 14}
- \ })
- call setqflist([], 'r', {'context': {'bqf': {'lsp_ranges_hl': lsp_ranges}},
- \ 'title': 'lsp_ranges_hl'})
- cwindow
-endfunc
-
-" Save me, source me. Run `call Test_bqf_pattern()` and `call Test_bqf_lsp_ranges()`
+```lua
+local cmd = vim.cmd
+local api = vim.api
+local fn = vim.fn
+
+local function create_qf()
+ cmd('enew')
+ local bufnr = api.nvim_get_current_buf()
+ local lines = {}
+ for i = 1, 3 do
+ table.insert(lines, ('%d | %s'):format(i, fn.strftime('%F')))
+ end
+ api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
+ fn.setqflist({
+ {bufnr = bufnr, lnum = 1, col = 5}, {bufnr = bufnr, lnum = 2, col = 10},
+ {bufnr = bufnr, lnum = 3, col = 13}
+ })
+end
+
+function _G.bqf_pattern()
+ create_qf()
+ fn.setqflist({}, 'r', {context = {bqf = {pattern_hl = [[\d\+]]}}, title = 'pattern_hl'})
+ cmd('cw')
+end
+
+function _G.bqf_lsp_ranges()
+ create_qf()
+ local lsp_ranges = {}
+ table.insert(lsp_ranges,
+ {start = {line = 0, character = 4}, ['end'] = {line = 0, character = 8}})
+ table.insert(lsp_ranges,
+ {start = {line = 1, character = 9}, ['end'] = {line = 1, character = 11}})
+ table.insert(lsp_ranges,
+ {start = {line = 2, character = 12}, ['end'] = {line = 2, character = 14}})
+ fn.setqflist({}, 'r', {context = {bqf = {lsp_ranges_hl = lsp_ranges}}, title = 'lsp_ranges_hl'})
+ cmd('cw')
+end
+
+function _G.qf_ranges()
+ if fn.has('nvim-0.6') == 1 then
+ create_qf()
+ local items = fn.getqflist()
+ local it1, it2, it3 = items[1], items[2], items[3]
+ it1.end_lnum, it1.end_col = it1.lnum, it1.col + 4
+ it2.end_lnum, it2.end_col = it2.lnum, it2.col + 2
+ it3.end_lnum, it3.end_col = it3.lnum, it3.col + 2
+ fn.setqflist({}, 'r', {items = items, title = 'qf_ranges_hl'})
+ cmd('cw')
+ else
+ error([[couldn't support quickfix ranges highlight before Neovim nightly]])
+ end
+end
+
+-- Save and source me(`so %`). Run `lua bqf_pattern()`, `lua bqf_lsp_ranges()` and `lua qf_ranges()`
```
nvim-bqf actually works with context in
@@ -385,33 +391,37 @@ hi default BqfSign ctermfg=14 guifg=Cyan
- `BqfPreviewFloat`: highlight floating window
- `BqfPreviewBorder`: highlight border of floating window
- `BqfPreviewCursor`: highlight the cursor format `[lnum, col]` in preview window
-- `BqfPreviewRange`: highlight the range format `[lnum, col, range]`, `pattern_hl` and `lsp_ranges_hl`
+- `BqfPreviewRange`: highlight the range format `[lnum, col, range]`,
+ which is produced by `pattern_hl`, `lsp_ranges_hl` and quickfix range
- `BqfSign`: highlight the sign in quickfix window
## Advanced configuration
### Customize configuration
-```vim
-call plug#begin('~/.config/nvim/plugged')
-
-Plug 'kevinhwang91/nvim-bqf'
-
-Plug 'junegunn/fzf', { 'do': { -> fzf#install() } }
-
-call plug#end()
-
-hi BqfPreviewBorder guifg=#50a14f ctermfg=71
-hi link BqfPreviewRange Search
+```lua
+vim.cmd([[
+ hi BqfPreviewBorder guifg=#50a14f ctermfg=71
+ hi link BqfPreviewRange Search
+]])
-lua < 100 * 1024 then
+ ret = false
+ end
+ return ret
+ end
},
func_map = {
vsplit = '',
@@ -425,83 +435,105 @@ require('bqf').setup({
}
}
})
-EOF
```
### Integrate with other plugins
-```vim
-call plug#begin('~/.config/nvim/plugged')
-
-Plug 'kevinhwang91/nvim-bqf'
-
-Plug 'junegunn/fzf', { 'do': { -> fzf#install() } }
-
-Plug 'neoclide/coc.nvim'
-
-" :h CocLocationsChange for detail
-let g:coc_enable_locationlist = 0
-aug Coc
- au!
- au User CocLocationsChange ++nested call Coc_qf_jump2loc(g:coc_jump_locations)
-aug END
-
-" if you use coc-fzf, you should disable its CocLocationsChange event make
-" bqf work for (coc-references)
-" au VimEnter * au! CocFzfLocation User CocLocationsChange
-nmap gr (coc-references)
-nnoremap qd call Coc_qf_diagnostic()
-
-function! Coc_qf_diagnostic() abort
- let diagnostic_list = CocAction('diagnosticList')
- let items = []
- let loc_ranges = []
- for d in diagnostic_list
- let text = printf('[%s%s] %s', (empty(d.source) ? 'coc.nvim' : d.source),
- \ (d.code ? ' ' . d.code : ''), split(d.message, '\n')[0])
- let item = {'filename': d.file, 'lnum': d.lnum, 'col': d.col, 'text': text, 'type':
- \ d.severity[0]}
- call add(loc_ranges, d.location.range)
- call add(items, item)
- endfor
- call setqflist([], ' ', {'title': 'CocDiagnosticList', 'items': items,
- \ 'context': {'bqf': {'lsp_ranges_hl': loc_ranges}}})
- botright copen
-endfunction
-
-function! Coc_qf_jump2loc(locs) abort
- let loc_ranges = map(deepcopy(a:locs), 'v:val.range')
- call setloclist(0, [], ' ', {'title': 'CocLocationList', 'items': a:locs,
- \ 'context': {'bqf': {'lsp_ranges_hl': loc_ranges}}})
- let winid = getloclist(0, {'winid': 0}).winid
- if winid == 0
- aboveleft lwindow
+```lua
+local fn = vim.fn
+local cmd = vim.cmd
+local api = vim.api
+
+cmd([[
+ packadd nvim-bqf
+ packadd fzf
+ packadd nvim-treesitter
+ packadd vim-grepper
+ packadd coc.nvim
+]])
+
+vim.g.grepper = {tools = {'rg', 'grep'}, open = 0, quickfix = 1, searchreg = 1, highlight = 0}
+cmd(([[
+ aug Grepper
+ au!
+ au User Grepper ++nested %s | %s
+ aug END
+]]):format([[call setqflist([], 'r', {'context': {'bqf': {'pattern_hl': '\%#' . getreg('/')}}})]],
+ 'bo cope'))
+
+-- try `gsiw` under word
+cmd([[
+ nmap gs (GrepperOperator)
+ xmap gs (GrepperOperator)
+]])
+
+-- if you use coc-fzf, you should disable its CocLocationsChange event
+-- to make bqf work for (coc-references)
+
+-- vim.schedule(function()
+-- cmd('au! CocFzfLocation User CocLocationsChange')
+-- end)
+vim.g.coc_enable_locationlist = 0
+cmd([[
+ aug Coc
+ au!
+ au User CocLocationsChange ++nested lua _G.jump2loc()
+ aug END
+]])
+
+cmd([[
+ nmap gr (coc-references)
+ nnoremap qd lua _G.diagnostic()
+]])
+
+-- just use `_G` prefix as a global function for a demo
+-- please use module instead in reality
+function _G.jump2loc(locs)
+ locs = locs or vim.g.coc_jump_locations
+ local loc_ranges = vim.tbl_map(function(val)
+ return val.range
+ end, locs)
+ fn.setloclist(0, {}, ' ', {
+ title = 'CocLocationList',
+ items = locs,
+ context = {bqf = {lsp_ranges_hl = loc_ranges}}
+ })
+ local winid = fn.getloclist(0, {winid = 0}).winid
+ if winid == 0 then
+ cmd('abo lw')
else
- call win_gotoid(winid)
- endif
-endfunction
-
-Plug 'mhinz/vim-grepper'
-
-aug Grepper
- au!
- au User Grepper call setqflist([], 'r',
- \ {'context': {'bqf': {'pattern_hl': histget('/')}}}) |
- \ botright copen
-aug END
-
-let g:grepper = {
- \ 'open': 0,
- \ 'quickfix': 1,
- \ 'searchreg': 1,
- \ 'highlight': 0,
- \ }
-
-" try `gsiw` under word
-nmap gs (GrepperOperator)
-xmap gs (GrepperOperator)
-
-call plug#end()
+ api.nvim_set_current_win(winid)
+ end
+end
+
+function _G.diagnostic()
+ fn.CocActionAsync('diagnosticList', '', function(err, res)
+ if err == vim.NIL then
+ local items, loc_ranges = {}, {}
+ for _, d in ipairs(res) do
+ local text = ('[%s%s] %s'):format((d.source == '' and 'coc.nvim' or d.source),
+ (d.code == vim.NIL and '' or ' ' .. d.code), d.message:match('([^\n]+)\n*'))
+ local item = {
+ filename = d.file,
+ lnum = d.lnum,
+ col = d.col,
+ text = text,
+ type = d.severity
+ }
+ table.insert(loc_ranges, d.location.range)
+ table.insert(items, item)
+ end
+ fn.setqflist({}, ' ', {
+ title = 'CocDiagnosticList',
+ items = items,
+ context = {bqf = {lsp_ranges_hl = loc_ranges}}
+ })
+
+ cmd('bo cope')
+ end
+ end)
+end
+
```
## Feedback
diff --git a/lua/bqf/config.lua b/lua/bqf/config.lua
index c2ccb2b..b68e972 100644
--- a/lua/bqf/config.lua
+++ b/lua/bqf/config.lua
@@ -1,6 +1,6 @@
local config = {}
-local function setup()
+local function init()
local bqf = require('bqf')
vim.validate({config = {bqf._config, 'table', true}})
config = vim.tbl_deep_extend('keep', bqf._config or {}, {
@@ -57,6 +57,6 @@ local function setup()
bqf._config = nil
end
-setup()
+init()
return config
diff --git a/lua/bqf/filter/base.lua b/lua/bqf/filter/base.lua
index 8d7dcc6..df88363 100644
--- a/lua/bqf/filter/base.lua
+++ b/lua/bqf/filter/base.lua
@@ -1,20 +1,27 @@
local M = {}
local api = vim.api
-local qftool = require('bqf.qftool')
+local qfs = require('bqf.qfwin.session')
-function M.filter_list(qf_winid, co_wrap)
- local qf_all = qftool.getall(qf_winid)
- local items = qf_all.items
- if not co_wrap or #items < 2 then
+function M.filter_list(qwinid, co_wrap)
+ if not co_wrap then
return
end
- local context, title = qf_all.context, qf_all.title
+
+ local qs = qfs.get(qwinid)
+ local qlist = qs:list()
+ local qinfo = qlist:get_qflist({size = 0, title = 0})
+ local size = qinfo.size
+ if size < 2 then
+ return
+ end
+ local context = qlist:get_context()
+ local title = qinfo.title
local lsp_ranges, new_items = {}, {}
for i, item in co_wrap do
table.insert(new_items, item)
- if qf_all.lsp_ranges_hl then
- table.insert(lsp_ranges, qf_all.lsp_ranges_hl[i])
+ if type(context.lsp_ranges_hl) == 'table' then
+ table.insert(lsp_ranges, context.lsp_ranges_hl[i])
end
end
@@ -23,21 +30,23 @@ function M.filter_list(qf_winid, co_wrap)
end
if #lsp_ranges > 0 then
- context.bqf.lsp_ranges_hl = lsp_ranges
+ context.lsp_ranges_hl = lsp_ranges
end
title = '*' .. title
- qftool.set({nr = '$', context = context, title = title, items = new_items}, qf_winid)
+ qlist:new_qflist({nr = '$', context = context, title = title, items = new_items})
end
function M.run(reverse)
- local qf_winid = api.nvim_get_current_win()
- local qf_all = qftool.getall(qf_winid)
- local items, signs = qf_all.items, qf_all.signs or {}
+ local qwinid = api.nvim_get_current_win()
+ local qs = qfs.get(qwinid)
+ local qlist = qs:list()
+ local signs = qlist:get_sign():list()
if reverse and vim.tbl_isempty(signs) then
return
end
- M.filter_list(qf_winid, coroutine.wrap(function()
+ M.filter_list(qwinid, coroutine.wrap(function()
+ local items = qlist:get_items()
if reverse then
for i in ipairs(items) do
if not signs[i] then
@@ -47,7 +56,7 @@ function M.run(reverse)
else
local k_signs = vim.tbl_keys(signs)
table.sort(k_signs)
- for _, i in pairs(k_signs) do
+ for _, i in ipairs(k_signs) do
coroutine.yield(i, items[i])
end
end
diff --git a/lua/bqf/filter/fzf.lua b/lua/bqf/filter/fzf.lua
index 498d603..5324f9b 100644
--- a/lua/bqf/filter/fzf.lua
+++ b/lua/bqf/filter/fzf.lua
@@ -4,45 +4,40 @@ local fn = vim.fn
local cmd = vim.cmd
local uv = vim.loop
-local preview, jump, supply, qftool, base, config, sign
+local phandler, qhandler, base, config, qfs
local utils = require('bqf.utils')
local log = require('bqf.log')
local action_for, extra_opts, has_tail, is_windows
+local version
local headless
-local function setup()
- if #api.nvim_list_uis() == 0 then
- headless = {}
- return
+local function get_version()
+ local exe = fn['fzf#exec']()
+ local msg_tbl = fn.systemlist({exe, '--version'})
+ local sh_error = vim.v.shell_error
+ local ver
+ if sh_error == 0 and type(msg_tbl) == 'table' and #msg_tbl > 0 then
+ ver = msg_tbl[1]:match('[0-9.]+')
+ else
+ ver = ''
end
- assert(vim.g.loaded_fzf or fn.exists('*fzf#run') == 1,
- 'fzf#run function not found. You also need Vim plugin from the main fzf repository')
-
- preview = require('bqf.preview')
- jump = require('bqf.jump')
- supply = require('bqf.supply')
- qftool = require('bqf.qftool')
- base = require('bqf.filter.base')
- config = require('bqf.config')
- sign = require('bqf.sign')
-
- local fzf_conf = config.filter.fzf
- action_for, extra_opts = fzf_conf.action_for, fzf_conf.extra_opts
- vim.validate({action_for = {action_for, 'table'}, extra_opts = {extra_opts, 'table'}})
- has_tail = fn.executable('tail') == 1
- is_windows = uv.os_uname().sysname == 'Windows_NT'
+ return ver
+end
- if not has_tail then
- -- also need echo :)
- api.nvim_err_writeln([[preview need 'tail' command]])
+local function compare_version(a, b)
+ local asecs = vim.split(a, '%.')
+ local bsecs = vim.split(b, '%.')
+ for i = 1, math.max(#asecs, #bsecs) do
+ local n1 = tonumber(asecs[i]) or -1
+ local n2 = tonumber(bsecs[i]) or -1
+ if n1 < n2 then
+ return -1
+ elseif n1 > n2 then
+ return 1
+ end
end
-
- cmd([[
- aug BqfFilterFzf
- au!
- aug END
- ]])
+ return 0
end
local function export4headless(bufnr, signs, fname)
@@ -53,7 +48,7 @@ local function export4headless(bufnr, signs, fname)
fd:close()
end
-local function source_list(qf_winid, signs)
+local function source_list(qwinid, signs)
local ret = {}
local function hl_ansi(name, str)
if not name then
@@ -71,39 +66,45 @@ local function source_list(qf_winid, signs)
end
})
- local bufnr = qf_winid and api.nvim_win_get_buf(qf_winid) or 0
- local padding = (' '):rep(headless and headless.padding_nr or utils.gutter_size(qf_winid) - 4)
+ local bufnr = qwinid and api.nvim_win_get_buf(qwinid) or 0
+ local padding = (' '):rep(headless and headless.padding_nr or utils.gutter_size(qwinid) - 4)
local sign_ansi = hl_ansi('BqfSign', '^')
local line_fmt = headless and '%d\t%s%s %s\n' or '%d\t%s%s %s'
local is_keyword = utils.gen_is_keyword(bufnr)
local start = headless and 3 or 1
+ local ts = vim.bo[bufnr].ts
for i, line in pairs(api.nvim_buf_get_lines(bufnr, 0, -1, false)) do
local signed = ' '
if headless then
if line:byte() == 0 then
signed = sign_ansi
end
+ line = utils.expandtab(line, ts, 3)
else
if signs[i] then
signed = sign_ansi
end
+ line = utils.expandtab(line, ts)
end
+
local line_sect = {}
- local last_hl_id = -1
+ local last_hl_id = 0
local last_is_kw = false
local last_col = start
local j = start
while j <= #line do
local byte = line:byte(j)
local is_kw = is_keyword(byte)
- if last_is_kw == false or is_kw ~= last_is_kw then
- if utils.is_special(byte) then
+ if not (last_is_kw and is_kw and
+ (byte >= 97 and byte <= 122 or byte >= 65 and byte <= 90)) then
+ -- TODO the filter is not good enough
+ if byte <= 32 then
last_is_kw = false
else
local hl_id = fn.synID(i, j, true)
- if j > start and hl_id ~= last_hl_id then
+ if j > start and hl_id > 0 and hl_id ~= last_hl_id then
table.insert(line_sect,
hl_id2ansi[last_hl_id]:format(line:sub(last_col, j - 1)))
last_col = j
@@ -113,7 +114,7 @@ local function source_list(qf_winid, signs)
end
j = j + 1
end
- local hl_fmt = last_hl_id and hl_id2ansi[last_hl_id] or '%s'
+ local hl_fmt = last_hl_id > 0 and hl_id2ansi[last_hl_id] or '%s'
table.insert(line_sect, hl_fmt:format(line:sub(last_col, #line):gsub('%c*$', '')))
local processed_line = line_fmt:format(i, padding, signed, table.concat(line_sect, ''))
if headless then
@@ -125,44 +126,64 @@ local function source_list(qf_winid, signs)
return ret
end
-local function source_cmd(qf_winid, signs)
- local fname = fn.fnameescape(fn.tempname())
- local bufnr = api.nvim_win_get_buf(qf_winid)
- export4headless(bufnr, signs, fname)
- local cmds = {vim.v.progpath, '--clean -n --headless'}
- local function append_cmd(str)
- table.insert(cmds, '-c')
- table.insert(cmds, ('%q'):format(str))
- end
- append_cmd(('e ++enc=utf8 %s'):format(fname))
+local function source_cmd(qwinid, signs)
+ local tname = fn.tempname()
+ local qfname = fn.fnameescape(tname)
+ local sfname = fn.fnameescape(tname .. '.lua')
+
+ local bufnr = api.nvim_win_get_buf(qwinid)
+ export4headless(bufnr, signs, qfname)
+ -- keep spawn process away from inheriting $NVIM_LISTEN_ADDRESS to call server_init()
+ -- look like widnows can't clear env in cmdline
+ local no_listen_env = is_windows and '' or 'NVIM_LISTEN_ADDRESS='
+ local cmds = {no_listen_env, vim.v.progpath, '--clean -n --headless', '-c', ('so %q'):format(sfname)}
+ local script = {'vim.cmd([['}
+
+ local fd = assert(io.open(sfname, 'w'))
+
+ local fenc = vim.bo[bufnr].fenc
+ table.insert(script, ('e %s %s'):format(fenc ~= '' and '++enc=' .. fenc or '', qfname))
+
+ local ansi_tbl = {[('BqfSign'):upper()] = utils.render_str('^', 'BqfSign')}
- local ansi_tbl = {['BqfSign'] = utils.render_str('^', 'BqfSign')}
for _, name in ipairs(utils.syntax_list(bufnr)) do
name = name:upper()
ansi_tbl[name] = utils.render_str('%s', name)
end
+
for _, path in ipairs(api.nvim_get_runtime_file('syntax/qf.vim', true)) do
- append_cmd(('sil! so %s'):format(fn.fnameescape(path)))
+ table.insert(script, ('sil! so %s'):format(fn.fnameescape(path)))
end
+
local bqf_path = vim.tbl_filter(function(p)
return p:match('nvim%-bqf$')
end, api.nvim_list_runtime_paths())[1]
- assert(bqf_path, [[can't find nvim-bqf's runtime path]])
- append_cmd(('set rtp+=%s'):format(fn.fnameescape(bqf_path)))
+ assert(bqf_path, [[Can't find nvim-bqf's runtime path]])
+
+ table.insert(script, ('set ts=%d'):format(vim.bo[bufnr].ts))
+
+ table.insert(script, ('set rtp+=%s'):format(fn.fnameescape(bqf_path)))
if not log.is_enabled('debug') then
- append_cmd(([[sil! call delete('%s')]]):format(fname))
+ table.insert(script, ([[sil! call delete('%s')]]):format(qfname))
+ table.insert(script, ([[sil! call delete('%s')]]):format(sfname))
+ table.insert(script, ']])')
else
- append_cmd([[sil! lua require('bqf.log').set_level('debug')]])
+ table.insert(script, ']])')
+ table.insert(script, [[require('bqf.log').set_level('debug')]])
end
- append_cmd(([[sil! lua require('bqf.filter.fzf').headless_run(%s, %d)]]):format(
- vim.inspect(ansi_tbl), utils.gutter_size(qf_winid) - 4))
- append_cmd('q!')
- local c_out = table.concat(cmds, ' ')
-
- log.debug('tmp_fname:', fname)
- log.debug('cmd_out:', c_out)
- return c_out
+
+ table.insert(script,
+ ([[require('bqf.filter.fzf').headless_run(%s, %d)]]):format(
+ vim.inspect(ansi_tbl, {newline = ''}), utils.gutter_size(qwinid) - 4))
+
+ fd:write(table.concat(script, '\n'))
+ fd:close()
+
+ local cout = table.concat(cmds, ' ')
+
+ log.debug('cmd_out:', cout)
+ return cout
end
local function set_qf_cursor(winid, lnum)
@@ -170,7 +191,7 @@ local function set_qf_cursor(winid, lnum)
api.nvim_win_set_cursor(winid, {lnum, col})
end
-local function handler(qf_winid, ret)
+local function handler(qwinid, ret)
local key = table.remove(ret, 1)
local selected_index = vim.tbl_map(function(e)
return tonumber(e:match('%d+'))
@@ -182,57 +203,61 @@ local function handler(qf_winid, ret)
if #selected_index == 1 then
idx = selected_index[1]
if action == 'tabedit' then
- set_qf_cursor(qf_winid, idx)
- jump.tabedit(false, qf_winid, idx)
+ set_qf_cursor(qwinid, idx)
+ qhandler.tabedit(false, qwinid, idx)
elseif action == 'split' then
- jump.split(false, qf_winid, idx)
+ qhandler.split(false, qwinid, idx)
elseif action == 'vsplit' then
- jump.split(true, qf_winid, idx)
+ qhandler.split(true, qwinid, idx)
elseif not action then
- jump.open(true, qf_winid, idx)
+ qhandler.open(true, qwinid, idx)
end
+ return
end
+ local qs = qfs.get(qwinid)
if action == 'signtoggle' then
+ local sign = qs:list():get_sign()
for _, i in ipairs(selected_index) do
- sign.toggle(0, i, api.nvim_win_get_buf(qf_winid))
+ sign:toggle(i, api.nvim_win_get_buf(qwinid))
end
- set_qf_cursor(qf_winid, selected_index[1])
+ set_qf_cursor(qwinid, selected_index[1])
else
- if #selected_index == 1 then
- return
- end
- local qf_all = qftool.getall(qf_winid)
- base.filter_list(qf_winid, coroutine.wrap(function()
+ local items = qs:list():get_items()
+ base.filter_list(qwinid, coroutine.wrap(function()
for _, i in ipairs(selected_index) do
- coroutine.yield(i, qf_all.items[i])
+ coroutine.yield(i, items[i])
end
end))
end
end
-local function create_job(qf_winid, tmpfile)
+local function new_job(qwinid, tmpfile)
io.open(tmpfile, 'w'):close()
local stdout = uv.new_pipe(false)
local handle, pid
handle, pid = uv.spawn('tail', {args = {'-f', tmpfile}, stdio = {nil, stdout}}, function()
- stdout:close()
handle:close()
os.remove(tmpfile)
end)
- stdout:read_start(function(_, data)
- if data and data ~= '' then
- local tbl_data = vim.split(data, ',')
- local idx
- while #tbl_data > 0 and not idx do
- idx = tonumber(table.remove(tbl_data))
- end
- if idx and idx > 0 then
- vim.schedule(function()
- set_qf_cursor(qf_winid, idx)
- preview.open(qf_winid, idx)
- end)
+ stdout:read_start(function(err, data)
+ assert(not err, err)
+ if data then
+ if data ~= '' then
+ local tbl_data = vim.split(data, ',')
+ local idx
+ while #tbl_data > 0 and not idx do
+ idx = tonumber(table.remove(tbl_data))
+ end
+ if idx and idx > 0 then
+ vim.schedule(function()
+ set_qf_cursor(qwinid, idx)
+ phandler.open(qwinid, idx)
+ end)
+ end
end
+ else
+ stdout:close()
end
end)
return pid
@@ -241,24 +266,40 @@ end
function M.headless_run(hl_ansi, padding_nr)
log.debug('hl_ansi:', hl_ansi)
log.debug('padding_nr:', padding_nr)
- log.debug(headless)
if headless then
headless.hl_ansi, headless.padding_nr = hl_ansi, padding_nr
source_list()
+ cmd('q!')
end
end
-function M.prepare(qf_winid, pid)
- local line_count = api.nvim_buf_line_count(api.nvim_win_get_buf(qf_winid))
+function M.prepare(qwinid, pid, size)
+ local line_count = api.nvim_buf_line_count(0)
api.nvim_win_set_config(0, {
relative = 'win',
- win = qf_winid,
- width = api.nvim_win_get_width(qf_winid),
- height = math.min(api.nvim_win_get_height(qf_winid) + 1, line_count + 1),
+ win = qwinid,
+ width = api.nvim_win_get_width(qwinid),
+ height = math.min(api.nvim_win_get_height(qwinid) + 1, line_count + 1),
row = 0,
col = 0
})
+ -- keep fzf term away from dithering
+ if vim.o.termguicolors then
+ vim.wo.winbl = 100
+ local stl
+ local ok, msg = pcall(api.nvim_win_get_option, qwinid, 'stl')
+ if ok then
+ stl = msg
+ end
+ local winid = api.nvim_get_current_win()
+ vim.wo[qwinid].stl = '%#Normal#'
+ vim.defer_fn(function()
+ pcall(api.nvim_win_set_option, qwinid, 'stl', stl)
+ pcall(api.nvim_win_set_option, winid, 'winbl', 0)
+ end, size > 1000 and 100 or 50)
+ end
+
if pid then
cmd('aug BqfFilterFzf')
cmd(('au BufWipeout %s'):format(('lua vim.loop.kill(%d, 15)'):format(pid)))
@@ -267,51 +308,99 @@ function M.prepare(qf_winid, pid)
end
function M.run()
- local qf_winid = api.nvim_get_current_win()
- local qf_type = qftool.type()
- local prompt = qf_type == 'loc' and ' Location> ' or ' Quickfix> '
- local qf_all = qftool.getall(qf_winid)
- local items, signs = qf_all.items, qf_all.signs or {}
- if #items < 2 then
+ local qwinid = api.nvim_get_current_win()
+ local qlist = qfs.get(qwinid):list()
+ local prompt = qlist.type == 'loc' and ' Location> ' or ' Quickfix> '
+ local size = qlist:get_qflist({size = 0}).size
+ if size < 2 then
return
end
-- greater than 1000 items is worth using headless as stream to improve user experience
- -- look like widnows can't spawn process :(
- local source = #items > 1000 and not is_windows and source_cmd or source_list
+ local source = size > 1000 and source_cmd or source_list
local expect_keys = table.concat(vim.tbl_keys(action_for), ',')
+
+ local base_opt = {}
+ if compare_version(version, '0.25.0') >= 0 then
+ base_opt = {'--color', 'gutter:-1'}
+ end
+ vim.list_extend(base_opt, {
+ '--multi', '--ansi', '--with-nth', '2..', '--delimiter', '\t', '--header-lines', 0,
+ '--tiebreak', 'index', '--info', 'inline', '--prompt', prompt, '--no-border', '--layout',
+ 'reverse-list', '--expect', expect_keys
+ })
local opts = {
- source = source(qf_winid, signs),
+ source = source(qwinid, qlist:get_sign():list()),
['sink*'] = nil,
- options = supply.tbl_concat({
- '--multi', '--ansi', '--tabstop', vim.bo.ts, '--with-nth', '2..', '--delimiter', '\t',
- '--header-lines', 0, '--tiebreak', 'index', '--info', 'inline', '--prompt', prompt,
- '--no-border', '--layout', 'reverse-list', '--expect', expect_keys
- }, extra_opts),
- window = {width = 2, height = 2, xoffset = 1, yoffset = 0, border = 'none'}
+ options = vim.list_extend(base_opt, extra_opts),
+ window = {
+ width = api.nvim_win_get_width(qwinid),
+ height = api.nvim_win_get_height(qwinid) + 1,
+ xoffset = 0,
+ yoffset = 0,
+ border = 'none'
+ }
}
local pid
if has_tail then
- if preview.auto_enabled() then
+ if phandler.auto_enabled() then
local tmpfile = fn.tempname()
- supply.tbl_concat(opts.options,
+ vim.list_extend(opts.options,
{'--preview-window', 0, '--preview', 'echo -n {1}, >> ' .. tmpfile})
- pid = create_job(qf_winid, tmpfile)
- preview.keep_preview()
+ pid = new_job(qwinid, tmpfile)
+ phandler.keep_preview()
end
end
cmd(('au BqfFilterFzf FileType fzf ++once %s'):format(
- ([[lua require('bqf.filter.fzf').prepare(%d, %s)]]):format(qf_winid, tostring(pid))))
+ ([[lua require('bqf.filter.fzf').prepare(%d, %s, %d)]]):format(qwinid, tostring(pid), size)))
-- TODO lua can't translate nested table data to vimscript
local fzf_wrap = fn['fzf#wrap'](opts)
fzf_wrap['sink*'] = function(ret)
- return handler(qf_winid, ret)
+ return handler(qwinid, ret)
end
fn['fzf#run'](fzf_wrap)
end
-setup()
+local function init()
+ if #api.nvim_list_uis() == 0 then
+ headless = {}
+ return
+ end
+ assert(vim.g.loaded_fzf or fn.exists('*fzf#run') == 1,
+ 'fzf#run function not found. You also need Vim plugin from the main fzf repository')
+ version = get_version()
+
+ config = require('bqf.config')
+
+ local fzf_conf = config.filter.fzf
+ action_for, extra_opts = fzf_conf.action_for, fzf_conf.extra_opts
+ vim.validate({
+ action_for = {action_for, 'table'},
+ extra_opts = {extra_opts, 'table'},
+ version = {version, 'string', 'version string'}
+ })
+
+ phandler = require('bqf.previewer.handler')
+ qhandler = require('bqf.qfwin.handler')
+ base = require('bqf.filter.base')
+ qfs = require('bqf.qfwin.session')
+ has_tail = fn.executable('tail') == 1
+ is_windows = uv.os_uname().sysname == 'Windows_NT'
+
+ if not has_tail then
+ -- also need echo :)
+ api.nvim_echo({{[[preview need 'tail' command]], 'WarningMsg'}}, true, {})
+ end
+
+ cmd([[
+ aug BqfFilterFzf
+ au!
+ aug END
+ ]])
+end
+
+init()
return M
diff --git a/lua/bqf/floatwin.lua b/lua/bqf/floatwin.lua
deleted file mode 100644
index ed1c347..0000000
--- a/lua/bqf/floatwin.lua
+++ /dev/null
@@ -1,195 +0,0 @@
-local M = {}
-local api = vim.api
-local fn = vim.fn
-local cmd = vim.cmd
-
-local border_chars, win_height, win_vheight
-
-local preview_winid = -1
-local border_winid = -1
-
-local qfpos = require('bqf.qfpos')
-
-local function get_opts(qf_winid, file_winid)
- local rel_pos, abs_pos = unpack(qfpos.get_pos(qf_winid, file_winid))
-
- local qf_info = fn.getwininfo(qf_winid)[1]
- local opts = {relative = 'win', win = qf_winid, focusable = false, style = 'minimal'}
- local width, height, col, row, anchor
- if rel_pos == 'above' or rel_pos == 'below' or abs_pos == 'top' or abs_pos == 'bottom' then
- local row_pos = qf_info.winrow
- width = qf_info.width - 2
- col = 1
- if rel_pos == 'above' or abs_pos == 'top' then
- anchor = 'NW'
- height = math.min(win_height, vim.o.lines - 4 - row_pos - qf_info.height)
- row = qf_info.height + 2
- else
- anchor = 'SW'
- height = math.min(win_height, row_pos - 4)
- row = -2
- end
- elseif rel_pos == 'left' or rel_pos == 'right' or abs_pos == 'left_far' or abs_pos ==
- 'right_far' then
- if abs_pos == 'left_far' then
- width = vim.o.columns - fn.win_screenpos(2)[2] - 1
- elseif abs_pos == 'right_far' then
- width = qf_info.wincol - 4
- else
- width = api.nvim_win_get_width(file_winid) - 2
- end
- height = math.min(win_vheight, qf_info.height - 2)
- local winline = fn.winline()
- row = height >= winline and 1 or winline - height - 1
- if rel_pos == 'left' or abs_pos == 'left_far' then
- anchor = 'NW'
- col = qf_info.width + 2
- else
- anchor = 'NE'
- col = -2
- end
- else
- return {}, {}
- end
-
- if width < 1 or height < 1 then
- return {}, {}
- end
-
- local preview_opts = vim.tbl_extend('force', opts, {
- anchor = anchor,
- width = width,
- height = height,
- col = col,
- row = row
- })
- local border_opts = vim.tbl_extend('force', opts, {
- anchor = anchor,
- width = width + 2,
- height = height + 2,
- col = anchor:match('W') and col - 1 or col + 1,
- row = anchor:match('N') and row - 1 or row + 1
- })
- return preview_opts, border_opts
-end
-
-local function update_border_buf(border_opts, border_buf)
- local width, height = border_opts.width, border_opts.height
- local top = border_chars[5] .. border_chars[3]:rep(width - 2) .. border_chars[6]
- local mid = border_chars[1] .. (' '):rep(width - 2) .. border_chars[2]
- local bot = border_chars[7] .. border_chars[4]:rep(width - 2) .. border_chars[8]
- local lines = {top}
- for _ = 1, height - 2 do
- table.insert(lines, mid)
- end
- table.insert(lines, bot)
- if not border_buf then
- border_buf = api.nvim_create_buf(false, true)
- -- run nvim with `-M` will reset modifiable's default value to false
- vim.bo[border_buf].modifiable = true
- vim.bo[border_buf].bufhidden = 'wipe'
- end
- api.nvim_buf_set_lines(border_buf, 0, -1, 1, lines)
- return border_buf
-end
-
-function M.set_win_height(p_hei, p_vhei)
- win_height, win_vheight = p_hei, p_vhei
-end
-
-function M.update_scrollbar()
- local buf = api.nvim_win_get_buf(preview_winid)
- local border_buf = api.nvim_win_get_buf(border_winid)
- local line_count = api.nvim_buf_line_count(buf)
-
- local win_info = fn.getwininfo(preview_winid)[1]
- local topline, height = win_info.topline, win_info.height
-
- local bar_size = math.min(height, math.ceil(height * height / line_count))
-
- local bar_pos = math.ceil(height * topline / line_count)
- if bar_pos + bar_size > height then
- bar_pos = height - bar_size + 1
- end
-
- local lines = api.nvim_buf_get_lines(border_buf, 1, -2, true)
- for i = 1, #lines do
- local bar_char
- if i >= bar_pos and i < bar_pos + bar_size then
- bar_char = border_chars[#border_chars]
- else
- bar_char = border_chars[2]
- end
- local line = lines[i]
- lines[i] = fn.strcharpart(line, 0, fn.strwidth(line) - 1) .. bar_char
- end
- api.nvim_buf_set_lines(border_buf, 1, -2, 0, lines)
-end
-
-function M.update_title(title)
- local border_buf = api.nvim_win_get_buf(border_winid)
- local top = api.nvim_buf_get_lines(border_buf, 0, 1, 0)[1]
- local prefix = fn.strcharpart(top, 0, 3)
- local suffix = fn.strcharpart(top, fn.strwidth(title) + 3, fn.strwidth(top))
- title = ('%s%s%s'):format(prefix, title, suffix)
- api.nvim_buf_set_lines(border_buf, 0, 1, 1, {title})
-end
-
-function M.validate_window()
- return preview_winid > 0 and api.nvim_win_is_valid(preview_winid) and border_winid > 0 and
- api.nvim_win_is_valid(border_winid)
-end
-
-function M.winid()
- return preview_winid, border_winid
-end
-
-function M.close()
- if M.validate_window() then
- api.nvim_win_close(preview_winid, true)
- api.nvim_win_close(border_winid, true)
- end
-end
-
-function M.setup(opts)
- vim.validate({opts = {opts, 'table'}})
- border_chars = opts.border_chars
- win_height = tonumber(opts.win_height)
- win_vheight = tonumber(opts.win_vheight or win_height)
- vim.validate({
- border_chars = {
- border_chars, function(chars)
- return type(chars) == 'table' and #chars == 9
- end, 'a table with 9 chars'
- },
- win_height = {win_height, 'number'},
- win_vheight = {win_vheight, 'number'}
- })
- cmd('hi default link BqfPreviewFloat Normal')
- cmd('hi default link BqfPreviewBorder Normal')
-end
-
-function M.open(bufnr, qf_winid, file_winid)
- local preview_opts, border_opts = get_opts(qf_winid, file_winid)
- if vim.tbl_isempty(preview_opts) or vim.tbl_isempty(border_opts) then
- return -1, -1
- end
-
- local border_buf
- if M.validate_window() then
- border_buf = api.nvim_win_get_buf(border_winid)
- update_border_buf(border_opts, border_buf)
- api.nvim_win_set_config(border_winid, border_opts)
- api.nvim_win_set_config(preview_winid, preview_opts)
- else
- border_buf = update_border_buf(border_opts)
- preview_winid = api.nvim_open_win(border_buf, false, preview_opts)
- border_winid = api.nvim_open_win(border_buf, false, border_opts)
- end
- cmd(('noa call nvim_win_set_buf(%d, %d)'):format(preview_winid, bufnr))
- api.nvim_win_set_option(border_winid, 'winhighlight', 'Normal:BqfPreviewBorder')
- api.nvim_win_set_option(preview_winid, 'winhighlight', 'Normal:BqfPreviewFloat')
- return preview_winid, border_winid
-end
-
-return M
diff --git a/lua/bqf/jump.lua b/lua/bqf/jump.lua
deleted file mode 100644
index f95e8f0..0000000
--- a/lua/bqf/jump.lua
+++ /dev/null
@@ -1,141 +0,0 @@
-local M = {}
-local api = vim.api
-local fn = vim.fn
-local cmd = vim.cmd
-
-local qftool = require('bqf.qftool')
-local utils = require('bqf.utils')
-
-local function set_opts_around(winid, func, ...)
- local opts = {
- wrap = vim.wo[winid].wrap,
- cursorline = vim.wo[winid].cursorline,
- number = vim.wo[winid].number,
- relativenumber = vim.wo[winid].relativenumber,
- signcolumn = vim.wo[winid].signcolumn,
- foldcolumn = vim.wo[winid].foldcolumn,
- list = vim.wo[winid].list,
- colorcolumn = vim.wo[winid].colorcolumn,
- winhighlight = vim.wo[winid].winhighlight,
- foldenable = vim.wo[winid].foldenable
- }
- func(...)
- for opt, val in pairs(opts) do
- vim.wo[opt] = val
- end
-end
-
-local function validate_size(qf_winid)
- local valid = qftool.get({size = 0}, qf_winid).size > 0
- if not valid then
- api.nvim_err_writeln('E42: No Errors')
- end
- return valid
-end
-
-local function qf_info(qf_winid, idx)
- idx = idx or api.nvim_win_get_cursor(qf_winid)[1]
- local qf_type = qftool.type(qf_winid)
- local file_winid = qftool.filewinid(qf_winid)
- return idx, qf_type, file_winid
-end
-
-function M.open(close, qf_winid, idx0)
- qf_winid = qf_winid or api.nvim_get_current_win()
- if validate_size(qf_winid) then
- local idx, qf_type, file_winid = qf_info(qf_winid, idx0)
-
- if file_winid and not api.nvim_win_is_valid(file_winid) then
- api.nvim_feedkeys(api.nvim_replace_termcodes('', true, false, true), 'n', true)
- else
-
- local suffix = qf_type == 'loc' and 'll' or 'cc'
- local file_w_info = fn.getwininfo(file_winid)[1]
- local topline, botline = file_w_info.topline, file_w_info.botline
-
- local last_bufnr = api.nvim_win_get_buf(file_winid)
- api.nvim_set_current_win(file_winid)
- if close then
- api.nvim_win_close(qf_winid, true)
- end
-
- set_opts_around(file_winid, function()
- cmd(([[sil exe '%d%s']]):format(idx, suffix))
- end)
-
- if vim.wo.foldenable and vim.o.fdo:match('quickfix') then
- cmd('norm! zv')
- end
-
- if last_bufnr ~= api.nvim_get_current_buf() then
- utils.zz()
- else
- local lnum = api.nvim_win_get_cursor(0)[1]
- if lnum < topline or lnum > botline then
- utils.zz()
- end
- end
- end
- end
-end
-
-function M.split(vertical, qf_winid, idx0)
- qf_winid = qf_winid or api.nvim_get_current_win()
- if validate_size(qf_winid) then
- local idx, qf_type, file_winid = qf_info(qf_winid, idx0)
- if qf_type == 'loc' then
- qftool.update({idx = idx}, qf_winid)
- end
- local suffix = qf_type == 'loc' and 'll' or 'cc'
- api.nvim_set_current_win(file_winid)
- api.nvim_win_close(qf_winid, true)
-
- local bufname = api.nvim_buf_get_name(api.nvim_win_get_buf(file_winid))
- set_opts_around(file_winid, function()
- if bufname == '' then
- cmd(([[sil exe '%d%s']]):format(idx, suffix))
- else
- cmd(('%ssp'):format(vertical and 'v' or ''))
- cmd(([[sil exe '%d%s']]):format(idx, suffix))
- end
- end)
- utils.zz()
- end
-end
-
-function M.tabedit(stay, qf_winid, idx0)
- qf_winid = qf_winid or api.nvim_get_current_win()
-
- if validate_size(qf_winid) then
- local idx, qf_type, file_winid = qf_info(qf_winid, idx0)
- if qf_type == 'loc' then
- qftool.update({idx = idx}, qf_winid)
- end
- local suffix = qf_type == 'loc' and 'll' or 'cc'
-
- api.nvim_set_current_win(file_winid)
- local bufname = api.nvim_buf_get_name(api.nvim_win_get_buf(file_winid))
- set_opts_around(file_winid, function()
- if bufname == '' then
- cmd(([[sil exe '%d%s']]):format(idx, suffix))
- else
- cmd(('%s tabedit'):format(stay and 'noa' or ''))
- cmd(([[%s sil exe '%d%s']]):format(stay and 'noa' or '', idx, suffix))
- end
- end)
-
- utils.zz()
- local abufnr = fn.bufnr('#')
- if api.nvim_buf_is_valid(abufnr) and api.nvim_buf_get_name(abufnr) == '' then
- cmd('noa bw #')
- end
-
- api.nvim_set_current_win(qf_winid)
-
- if bufname ~= '' and not stay then
- cmd('tabn')
- end
- end
-end
-
-return M
diff --git a/lua/bqf/keymap.lua b/lua/bqf/keymap.lua
index f3ff219..2cde626 100644
--- a/lua/bqf/keymap.lua
+++ b/lua/bqf/keymap.lua
@@ -6,42 +6,37 @@ local config = require('bqf.config')
local func_map
local action_funcref = {
- open = {mode = 'n', module = 'jump', funcref = 'open(false)'},
- openc = {mode = 'n', module = 'jump', funcref = 'open(true)'},
- split = {mode = 'n', module = 'jump', funcref = 'split(false)'},
- vsplit = {mode = 'n', module = 'jump', funcref = 'split(true)'},
- tab = {mode = 'n', module = 'jump', funcref = 'tabedit(false)'},
- tabb = {mode = 'n', module = 'jump', funcref = 'tabedit(true)'},
- ptogglemode = {mode = 'n', module = 'preview', funcref = 'toggle_mode()'},
- ptoggleitem = {mode = 'n', module = 'preview', funcref = 'toggle_item()'},
- ptoggleauto = {mode = 'n', module = 'preview', funcref = 'toggle()'},
- pscrollup = {mode = 'n', module = 'preview', funcref = 'scroll(-1)'},
- pscrolldown = {mode = 'n', module = 'preview', funcref = 'scroll(1)'},
- pscrollorig = {mode = 'n', module = 'preview', funcref = 'scroll(0)'},
- prevfile = {mode = 'n', module = 'qftool', funcref = 'file(false)'},
- nextfile = {mode = 'n', module = 'qftool', funcref = 'file(true)'},
- prevhist = {mode = 'n', module = 'qftool', funcref = 'history(false)'},
- nexthist = {mode = 'n', module = 'qftool', funcref = 'history(true)'},
- stoggleup = {mode = 'n', module = 'sign', funcref = 'toggle(-1)'},
- stoggledown = {mode = 'n', module = 'sign', funcref = 'toggle(1)'},
- stogglevm = {mode = 'x', module = 'sign', funcref = 'vm_toggle()'},
- stogglebuf = {mode = 'n', module = 'sign', funcref = 'toggle_buf()'},
- sclear = {mode = 'n', module = 'sign', funcref = 'clear()'},
+ ptogglemode = {mode = 'n', module = 'previewer.handler', funcref = 'toggle_mode()'},
+ ptoggleitem = {mode = 'n', module = 'previewer.handler', funcref = 'toggle_item()'},
+ ptoggleauto = {mode = 'n', module = 'previewer.handler', funcref = 'toggle()'},
+ pscrollup = {mode = 'n', module = 'previewer.handler', funcref = 'scroll(-1)'},
+ pscrolldown = {mode = 'n', module = 'previewer.handler', funcref = 'scroll(1)'},
+ pscrollorig = {mode = 'n', module = 'previewer.handler', funcref = 'scroll(0)'},
+ open = {mode = 'n', module = 'qfwin.handler', funcref = 'open(false)'},
+ openc = {mode = 'n', module = 'qfwin.handler', funcref = 'open(true)'},
+ split = {mode = 'n', module = 'qfwin.handler', funcref = 'split(false)'},
+ vsplit = {mode = 'n', module = 'qfwin.handler', funcref = 'split(true)'},
+ tab = {mode = 'n', module = 'qfwin.handler', funcref = 'tabedit(false)'},
+ tabb = {mode = 'n', module = 'qfwin.handler', funcref = 'tabedit(true)'},
+ prevfile = {mode = '', module = 'qfwin.handler', funcref = 'nav_file(false)'},
+ nextfile = {mode = '', module = 'qfwin.handler', funcref = 'nav_file(true)'},
+ prevhist = {mode = 'n', module = 'qfwin.handler', funcref = 'nav_history(false)'},
+ nexthist = {mode = 'n', module = 'qfwin.handler', funcref = 'nav_history(true)'},
+ stoggleup = {mode = 'n', module = 'qfwin.handler', funcref = 'sign_toggle(-1)'},
+ stoggledown = {mode = 'n', module = 'qfwin.handler', funcref = 'sign_toggle(1)'},
+ stogglevm = {mode = 'x', module = 'qfwin.handler', funcref = 'sign_vm_toggle()'},
+ stogglebuf = {mode = 'n', module = 'qfwin.handler', funcref = 'sign_toggle_buf()'},
+ sclear = {mode = 'n', module = 'qfwin.handler', funcref = 'sign_clear()'},
filter = {mode = 'n', module = 'filter.base', funcref = 'run()'},
filterr = {mode = 'n', module = 'filter.base', funcref = 'run(true)'},
fzffilter = {mode = 'n', module = 'filter.fzf', funcref = 'run()'}
}
-local function setup()
- func_map = config.func_map
- vim.validate({func_map = {func_map, 'table'}})
-end
-
local function funcref_str(tbl_func)
return ([[lua require('bqf.%s').%s]]):format(tbl_func.module, tbl_func.funcref)
end
-function M.buf_map()
+function M.initialize()
for action, keymap in pairs(func_map) do
local tbl_func = action_funcref[action]
if tbl_func and not vim.tbl_isempty(tbl_func) and keymap ~= '' then
@@ -50,8 +45,8 @@ function M.buf_map()
end
end
-function M.buf_unmap()
- local do_unmap = function(maparg)
+function M.dispose()
+ local function do_unmap(maparg)
if maparg.rhs:match([[lua require%('bqf%..*'%)]]) then
api.nvim_buf_del_keymap(0, maparg.mode, maparg.lhs)
end
@@ -66,6 +61,11 @@ function M.buf_unmap()
end
end
-setup()
+local function init()
+ func_map = config.func_map
+ vim.validate({func_map = {func_map, 'table'}})
+end
+
+init()
return M
diff --git a/lua/bqf/layout.lua b/lua/bqf/layout.lua
index d729b21..e4bf826 100644
--- a/lua/bqf/layout.lua
+++ b/lua/bqf/layout.lua
@@ -3,71 +3,62 @@ local api = vim.api
local fn = vim.fn
local cmd = vim.cmd
-local auto_resize_height
-local magic_window
-
-local qfs = require('bqf.qfsession')
-local qfpos = require('bqf.qfpos')
-local qftool = require('bqf.qftool')
+local qfs = require('bqf.qfwin.session')
+local wpos = require('bqf.wpos')
local config = require('bqf.config')
-local wmagic = require('bqf.magicwin')
-local utils = require('bqf.utils')
-
-local function setup()
- magic_window = config.magic_window
- auto_resize_height = config.auto_resize_height
-end
-local function store_fwin_opts(qf_winid, file_winid)
- local fwin_o = vim.wo[file_winid]
- qfs[qf_winid].fwin_opts = {
- wrap = fwin_o.wrap,
- cursorline = fwin_o.cursorline,
- number = fwin_o.number,
- relativenumber = fwin_o.relativenumber,
- signcolumn = fwin_o.signcolumn,
- foldcolumn = fwin_o.foldcolumn,
- list = fwin_o.list,
- colorcolumn = fwin_o.colorcolumn,
- winhighlight = fwin_o.winhighlight,
- foldenable = fwin_o.foldenable
- }
-end
+local auto_resize_height
+local POS
-local function fix_default_qf(qf_winid, file_winid, qf_type, qf_pos)
- local qf_win = fn.win_id2win(qf_winid)
+local function fix_default_qf(qwinid, pwinid, qf_type, qf_pos)
+ local qf_win = fn.win_id2win(qwinid)
if qf_type == 'qf' and fn.winnr('$') == qf_win then
- if qf_pos[1] == 'unknown' and qf_pos[2] == 'unknown' then
+ if qf_pos[1] == POS.UNKNOWN and qf_pos[2] == POS.UNKNOWN then
local above_winid = fn.win_getid(fn.winnr('k'))
- local hei = api.nvim_win_get_height(above_winid)
+ local heifix_tbl = {}
+ for _, winid in ipairs(wpos.find_bottom_wins()) do
+ if winid ~= qwinid then
+ heifix_tbl[winid] = vim.wo[winid].winfixheight
+ vim.wo[winid].winfixheight = true
+ end
+ end
+ local above_hei = api.nvim_win_get_height(above_winid)
cmd('winc J')
- api.nvim_win_set_height(above_winid, hei)
- qf_pos = qfpos.get_pos(qf_winid, file_winid)
+ for winid, value in pairs(heifix_tbl) do
+ vim.wo[winid].winfixheight = value
+ end
+ api.nvim_win_set_height(above_winid, above_hei)
+ qf_pos = wpos.get_pos(qwinid, pwinid)
end
end
return qf_pos
end
-local function adjust_width(qf_winid, file_winid, qf_pos)
- local qf_wid = api.nvim_win_get_width(qf_winid)
+local function adjust_width(qwinid, pwinid, qf_pos)
+ local qf_wid = api.nvim_win_get_width(qwinid)
if vim.o.winwidth > qf_wid then
- if qf_pos[1] == 'right' then
- local width = api.nvim_win_get_width(file_winid) - (vim.o.winwidth - qf_wid)
- api.nvim_win_set_width(file_winid, width)
+ if qf_pos[1] == POS.RIGHT then
+ local width = api.nvim_win_get_width(pwinid) - (vim.o.winwidth - qf_wid)
+ api.nvim_win_set_width(pwinid, width)
else
- api.nvim_win_set_width(qf_winid, vim.o.winwidth)
+ api.nvim_win_set_width(qwinid, vim.o.winwidth)
end
end
end
-local function adjust_height(qf_winid, file_winid, qf_pos)
- local size = math.max(qftool.get({size = 0}).size, 1)
- local qf_hei = api.nvim_win_get_height(qf_winid)
+local function adjust_height(qwinid, pwinid, qf_pos)
+ local qlist = qfs.get(qwinid):list()
+ local size = math.max(qlist:get_qflist({size = 0}).size, 1)
+ local qf_hei = api.nvim_win_get_height(qwinid)
local inc_hei = 0
- qfs[qf_winid].init_height = qfs[qf_winid].init_height or qf_hei
- if qf_hei < qfs[qf_winid].init_height then
- inc_hei = qfs[qf_winid].init_height - qf_hei
- qf_hei = qfs[qf_winid].init_height
+ local ok, init_height = pcall(api.nvim_win_get_var, qwinid, 'init_height')
+ if not ok then
+ init_height = qf_hei
+ api.nvim_win_set_var(qwinid, 'init_height', init_height)
+ end
+ if qf_hei < init_height then
+ inc_hei = init_height - qf_hei
+ qf_hei = init_height
end
if size < qf_hei then
@@ -79,119 +70,24 @@ local function adjust_height(qf_winid, file_winid, qf_pos)
end
local rel_pos, abs_pos = unpack(qf_pos)
- if rel_pos == 'above' or abs_pos == 'top' or abs_pos == 'bottom' then
- api.nvim_win_set_height(qf_winid, api.nvim_win_get_height(qf_winid) + inc_hei)
- elseif rel_pos == 'below' then
- vim.wo[qf_winid].winfixheight = false
- api.nvim_win_set_height(file_winid, api.nvim_win_get_height(file_winid) - inc_hei)
- vim.wo[qf_winid].winfixheight = true
+ if rel_pos == POS.ABOVE or abs_pos == POS.TOP or abs_pos == POS.BOTTOM then
+ api.nvim_win_set_height(qwinid, api.nvim_win_get_height(qwinid) + inc_hei)
+ elseif rel_pos == POS.BELOW then
+ vim.wo[qwinid].winfixheight = false
+ api.nvim_win_set_height(pwinid, api.nvim_win_get_height(pwinid) - inc_hei)
+ vim.wo[qwinid].winfixheight = true
end
end
-local function update_allfixhei(wfh)
- local holder = qfs.holder()
- local cur_tab_wins = api.nvim_tabpage_list_wins(0)
- for winid in pairs(holder) do
- if winid and vim.tbl_contains(cur_tab_wins, winid) then
- vim.wo[winid].winfixheight = wfh
- end
- end
-end
-
-function M.init(qf_winid, file_winid, qf_type)
- local qf_pos = qfpos.get_pos(qf_winid, file_winid)
- qf_pos = fix_default_qf(qf_winid, file_winid, qf_type, qf_pos)
- adjust_width(qf_winid, file_winid, qf_pos)
+function M.initialize(qwinid)
+ local qs = qfs.get(qwinid)
+ local qlist = qs:list()
+ local pwinid = qs:pwinid()
+ local qf_pos = wpos.get_pos(qwinid, pwinid)
+ qf_pos = fix_default_qf(qwinid, pwinid, qlist.qf_type, qf_pos)
+ adjust_width(qwinid, pwinid, qf_pos)
if auto_resize_height then
- adjust_height(qf_winid, file_winid, qf_pos)
- end
-
- if magic_window then
- update_allfixhei(false)
- wmagic.revert_enter_adjacent_wins(qf_winid, file_winid, qf_pos)
- update_allfixhei(true)
- end
- -- store file winodw's options for subsequent use
- store_fwin_opts(qf_winid, file_winid)
-end
-
-function M.restore_fwin_opts()
- local opts = vim.b.bqf_fwin_opts
- vim.b.bqf_fwin_opts = nil
- if not opts or vim.tbl_isempty(opts) then
- return
- end
-
- for opt, val in pairs(opts) do
- if vim.wo[opt] ~= val then
- vim.wo[opt] = val
- end
- end
-end
-
-function M.close_win(qf_winid)
- if qf_winid < 0 or not api.nvim_win_is_valid(qf_winid) then
- return
- end
-
- local file_winid = qftool.filewinid(qf_winid)
- local qf_pos = qfpos.get_pos(qf_winid, file_winid)
- local qf_win = fn.win_id2win(qf_winid)
- local qf_win_j, qf_win_l
- utils.win_execute(qf_winid, function()
- qf_win_j, qf_win_l = fn.winnr('j'), fn.winnr('l')
- end)
-
- local qf_hei, qf_wid, f_hei, f_wid
- local rel_pos = qf_pos[1]
- if rel_pos == 'right' and qf_win_l ~= qf_win then
- qf_wid, f_wid = api.nvim_win_get_width(qf_winid), api.nvim_win_get_width(file_winid)
- elseif rel_pos == 'below' and qf_win_j ~= qf_win then
- qf_hei, f_hei = api.nvim_win_get_height(qf_winid), api.nvim_win_get_height(file_winid)
- end
-
- local wmagic_defer_cb
- if magic_window then
- update_allfixhei(false)
- wmagic_defer_cb = wmagic.revert_close_adjacent_wins(qf_winid, file_winid, qf_pos)
- update_allfixhei(true)
- end
-
- local cur_winid = api.nvim_get_current_win()
-
- if vim.o.equalalways and fn.winnr('$') > 2 then
- -- close quickfix window in other tab or floating window can prevent nvim make windows equal
- -- after closing quickfix window, but in other tab can't run
- -- 'win_enter_ext(wp, false, true, false, true, true)' which triggers 'WinEnter', 'BufEnter'
- -- and 'CursorMoved' events. Search 'do_autocmd_winclosed' in src/nvim/window.c for details.
- local scratch = api.nvim_create_buf(false, true)
- api.nvim_open_win(scratch, true, {
- relative = 'win',
- width = 1,
- height = 1,
- row = 0,
- col = 0,
- style = 'minimal'
- })
- api.nvim_win_close(qf_winid, false)
- cmd(('noa bw %d'):format(scratch))
- else
- api.nvim_win_close(qf_winid, false)
- end
-
- if api.nvim_win_is_valid(file_winid) and cur_winid == qf_winid then
- -- current window is a quickfix window, go back file window
- api.nvim_set_current_win(file_winid)
- end
-
- if rel_pos == 'right' and qf_win_l ~= qf_win then
- api.nvim_win_set_width(file_winid, qf_wid + f_wid + 1)
- elseif rel_pos == 'below' and qf_win_j ~= qf_win then
- api.nvim_win_set_height(file_winid, qf_hei + f_hei + 1)
- end
-
- if wmagic_defer_cb then
- wmagic_defer_cb()
+ adjust_height(qwinid, pwinid, qf_pos)
end
end
@@ -201,6 +97,11 @@ function M.valid_qf_win()
win_l and win_k == win_l)
end
-setup()
+local function init()
+ auto_resize_height = config.auto_resize_height
+ POS = wpos.POS
+end
+
+init()
return M
diff --git a/lua/bqf/log.lua b/lua/bqf/log.lua
index 593ab86..26782a3 100644
--- a/lua/bqf/log.lua
+++ b/lua/bqf/log.lua
@@ -54,7 +54,7 @@ local function path_sep()
return vim.loop.os_uname().sysname == 'Windows' and [[\]] or '/'
end
-local function setup()
+local function init()
local log_dir = fn.stdpath('cache')
fn.mkdir(log_dir, 'p')
levels = {TRACE = 0, DEBUG = 1, INFO = 2, WARN = 3, ERROR = 4}
@@ -87,6 +87,6 @@ local function setup()
end
end
-setup()
+init()
return M
diff --git a/lua/bqf/magicwin.lua b/lua/bqf/magicwin.lua
deleted file mode 100644
index a3ef282..0000000
--- a/lua/bqf/magicwin.lua
+++ /dev/null
@@ -1,426 +0,0 @@
-local M = {}
-local api = vim.api
-local fn = vim.fn
-local cmd = vim.cmd
-local uv = vim.loop
-
-local qfs = require('bqf.qfsession')
-local qfpos = require('bqf.qfpos')
-local utils = require('bqf.utils')
-local log = require('bqf.log')
-
--- Code in this file relates to source code
--- https://github.com/neovim/neovim/blob/master/src/nvim/window.c
--- bfraction: before scroll_to_fraction, afraction: after scroll_to_fraction
--- bfraction = bwrow / bheight and afraction = awrow / aheight
--- bfraction = afraction ==> bwrow / bheight = awrow / aheight
-
--- FRACTION_MULT = 16384L
--- wp->w_fraction = ((long)wp->w_wrow * FRACTION_MULT + FRACTION_MULT / 2) / (long)wp->w_height_inner;
-local function cal_fraction(wrow, height)
- return math.floor((wrow * 16384 + 8192) / height)
-end
-
--- wp->w_wrow = ((long)wp->w_fraction * (long)height - 1L) / FRACTION_MULT;
-local function cal_wrow(fraction, height)
- return math.floor((fraction * height - 1) / 16384)
-end
-
--- Check out 'void scroll_to_fraction(win_T *wp, int prev_height)' in winodw.c for more details.
-local function evaluate_sline(fraction, height, lnum, lines_size)
- local sline = cal_wrow(fraction, height)
- local i = sline
- for j = lnum - 1, math.max(1, lnum - sline), -1 do
- i = i - lines_size[j]
- if i <= 0 then
- if i < 0 then
- sline = sline - lines_size[j] - i
- end
- break
- end
- end
- return sline
-end
-
-local function do_filter(tbl_info, height, lnum, lines_size)
- local t = {}
- for _, info in ipairs(tbl_info) do
- local sline = evaluate_sline(info, height, lnum, lines_size)
- if fn.winline() - 1 == sline then
- table.insert(t, info)
- end
- end
- return t
-end
-
--- If the lnum hasn't been changed, even if the window is resized, the fraction is still a constant.
--- And we can use this feature to find out the possible fraction with changing window height.
--- Check out 'void scroll_to_fraction(win_T *wp, int prev_height)' in winodw.c for more details.
-local function filter_fraction(tbl_info, lnum, lines_size, max_hei)
- local asc
- local height = api.nvim_win_get_height(0)
- local h = height
- local min_hei = math.max(vim.o.wmh, 1)
- if max_hei then
- asc = true
- else
- asc = false
- max_hei = vim.o.lines
- end
-
- while #tbl_info > 1 do
- if h <= min_hei or h > max_hei then
- break
- end
- h = asc and h + 1 or h - 1
- api.nvim_win_set_height(0, h)
- if asc and api.nvim_win_get_height(0) ~= h then
- break
- end
- tbl_info = do_filter(tbl_info, h, lnum, lines_size)
- end
- api.nvim_win_set_height(0, height)
- return tbl_info
-end
-
-local function evaluate_fraction(winid, lnum, awrow, aheight, bheight, lbwrow, lfraction)
- -- s_bwrow: the minimum bwrow value
- -- Below formula we can derive from the known conditions
- local s_bwrow = math.ceil(awrow * bheight / aheight - 0.5)
- if s_bwrow < 0 or bheight == aheight then
- return
- end
- -- e_bwrow: the maximum bwrow value
- -- There are not enough conditions to derive e_bwrow, so we have to figure it out by guessing,
- -- and confirm the range of bwrow.
- -- It seems that 10 as a minimum and 1.2 as a scale is good to balance performance and accuracy
- local e_bwrow = math.max(10, math.ceil(awrow * 1.2 * bheight / aheight - 0.25))
-
- local per_l_wid = api.nvim_win_get_width(winid) - utils.gutter_size(winid)
-
- local e_fraction = cal_fraction(e_bwrow, bheight)
- local e_sline = cal_wrow(e_fraction, aheight)
-
- if lbwrow then
- e_sline = math.max(cal_wrow(lfraction, aheight), e_sline)
- end
-
- local lines_size = {}
- local wrap = vim.wo[winid].wrap
- -- use 9 as additional compensation
- for i = math.max(1, lnum - e_sline - 9), lnum - 1 do
- lines_size[i] = wrap and math.ceil(math.max(fn.virtcol({i, '$'}) - 1, 1) / per_l_wid) or 1
- end
-
- if lbwrow and awrow == evaluate_sline(lfraction, aheight, lnum, lines_size) then
- return lfraction
- end
-
- local t_frac = {}
- for bw = s_bwrow, e_bwrow do
- table.insert(t_frac, cal_fraction(bw, bheight))
- end
- log.debug('before t_frac', t_frac)
-
- t_frac = filter_fraction(t_frac, lnum, lines_size)
-
- log.debug('first t_frac:', t_frac)
-
- t_frac = filter_fraction(t_frac, lnum, lines_size, aheight + 9)
-
- log.debug('second t_frac:', t_frac)
-
- if #t_frac > 0 then
- return t_frac[1]
- end
-end
-
-local function resetview(topline, lnum, col, curswant)
- fn.winrestview({topline = topline, lnum = lnum, col = col, curswant = curswant})
- -- topline seemly can't be changed sometimes without winline()
- fn.winline()
-end
-
-local function tune_line(winid, topline, lsizes)
- if not vim.wo[winid].wrap or lsizes == 0 then
- return lsizes
- end
-
- log.debug('lsizes:', lsizes)
-
- local i_start, i_end, i_inc, should_continue
- local len
- local foldenable = vim.wo[winid].foldenable
- local folded_other_lnum
- local neg_one_func = function()
- return -1
- end
-
- if lsizes > 0 then
- i_start, i_end, i_inc = topline - 1, math.max(1, topline - lsizes), -1
- should_continue = function(iter)
- return iter >= i_end
- end
- len = lsizes
- folded_other_lnum = foldenable and fn.foldclosed or neg_one_func
- else
- i_start, i_end, i_inc = topline, topline - lsizes - 1, 1
- should_continue = function(iter)
- return iter <= i_end
- end
- len = -lsizes
- folded_other_lnum = foldenable and fn.foldclosedend or neg_one_func
- end
- log.debug(i_start, i_end, i_inc, len)
-
- return utils.win_execute(winid, function()
- local per_l_wid = api.nvim_win_get_width(winid) - utils.gutter_size(winid)
- local loff, lsize_sum = 0, 0
- local i = i_start
- while should_continue(i) do
- log.debug('=====================================================')
- log.debug('i:', i, 'i_end:', i_end)
- local fo_lnum = folded_other_lnum(i)
- if fo_lnum == -1 then
- local per_l_size = math.ceil(math.max(fn.virtcol({i, '$'}) - 1, 1) / per_l_wid)
- log.debug('lsize_sum:', lsize_sum, 'per_l_size:', per_l_size, 'lnum:', i)
- lsize_sum = lsize_sum + per_l_size
- loff = loff + 1
- else
- log.debug('fo_lnum:', fo_lnum)
- lsize_sum = lsize_sum + 1
- loff = loff + math.abs(fo_lnum - i) + 1
- i_end = i_end + fo_lnum - i
- i = fo_lnum
- end
- log.debug('loff:', loff)
- log.debug('=====================================================')
- i = i + i_inc
- if lsize_sum >= len then
- break
- end
- end
- loff = lsizes > 0 and loff or -loff
- log.debug('line_offset:', loff)
- return loff
- end)
-end
-
-local function register_winenter()
- if fn.exists('#BqfMagicWin#WinEnter') == 0 then
- cmd(('au BqfMagicWin WinEnter * %s'):format(
- ([[lua require('bqf.magicwin').clear_winview()]])))
- end
-end
-
-local function unregister_winenter()
- -- TODO multiple quickfix windows map multiple file windows!!!!
- cmd('sil! au! BqfMagicWin WinEnter')
-end
-
-local function do_enter_revert(qf_winid, winid, qf_pos)
- log.debug('do_enter_revert start')
- -- TODO upstream bug
- -- local f_win_so = vim.wo[winid].scrolloff
- -- return a big number like '1.4014575443238e+14' if window option is absent
- -- Use getwinvar to workaround
- local f_win_so = fn.getwinvar(winid, '&scrolloff')
- if f_win_so ~= 0 then
- -- turn off scrolloff and then show us true wrow
- vim.wo[winid].scrolloff = 0
- cmd(('au BqfMagicWin WinLeave * ++once %s'):format(
- ([[lua vim.wo[%d].scrolloff = %d]]):format(winid, f_win_so)))
- end
-
- utils.win_execute(winid, function()
- local qf_hei, win_hei = api.nvim_win_get_height(qf_winid), api.nvim_win_get_height(winid)
- local wv = fn.winsaveview()
- local topline, lnum = wv.topline, wv.lnum
- local line_count = api.nvim_buf_line_count(0)
-
- -- qf winodw height might be changed by user adds new qf items or navigates history
- -- we need a cache to store previous state
- qfs[qf_winid].magicwin = qfs[qf_winid].magicwin or {}
- local mgw = qfs[qf_winid].magicwin[winid] or {}
- local def_hei = qf_hei + win_hei + 1
- local bheight, aheight = mgw.aheight or def_hei, win_hei
- local lbwrow, lfraction = mgw.bwrow, mgw.fraction
- local bwrow, fraction, delta_lsize
-
- local awrow = fn.winline() - 1
-
- if topline == 1 and line_count <= win_hei then
- delta_lsize = 0
- else
- if f_win_so >= awrow and awrow > 0 and win_hei > 1 then
- -- get the true wrow
- cmd('resize -1 | resize +1')
- awrow = fn.winline() - 1
- topline = fn.line('w0')
- end
-
- log.debug('awrow:', awrow, 'aheight:', aheight, 'bheight:', bheight)
- log.debug('lbwrow:', lbwrow, 'lfraction:', lfraction)
-
- fraction = evaluate_fraction(winid, lnum, awrow, aheight, bheight, lbwrow, lfraction)
- if not fraction then
- return
- end
- bwrow = cal_wrow(fraction, bheight)
- log.debug('bwrow:', bwrow)
- delta_lsize = bwrow - awrow
- end
-
- if qf_pos[1] == 'above' or qf_pos[2] == 'top' then
- if lbwrow == bwrow and lfraction == fraction then
- bheight = mgw.bheight or def_hei
- end
- delta_lsize = delta_lsize - bheight + aheight
- end
-
- if delta_lsize == 0 then
- return
- end
-
- log.debug('before topline:', topline, 'delta_lsize:', delta_lsize)
-
- local line_offset = tune_line(winid, topline, delta_lsize)
- topline = math.max(1, topline - line_offset)
- local flag = 0
- if delta_lsize > 0 then
- local reminder = aheight - awrow - 1
- if delta_lsize > reminder then
- flag = 1
- lnum = topline
- end
- else
- if -delta_lsize > awrow then
- flag = 2
- lnum = topline
- end
- end
-
- resetview(topline, lnum)
-
- if flag > 0 then
- mgw.wv = {wv.lnum, wv.col, wv.curswant, uv.hrtime(), flag}
- if flag == 1 then
- resetview(topline, fn.line('w$'))
- end
- log.debug(mgw.wv)
- register_winenter()
- else
- mgw.wv = nil
- end
-
- mgw.bwrow, mgw.fraction, mgw.bheight, mgw.aheight = bwrow, fraction, bheight, aheight
- qfs[qf_winid].magicwin[winid] = mgw
- end)
-
- log.debug('do_enter_revert end', '\n')
-end
-
-local function prefetch_close_revert_topline(qf_winid, winid, qf_pos)
- local topline
- local ok, msg = pcall(fn.getwininfo, winid)
- if ok then
- topline = msg[1].topline
- if qf_pos[1] == 'above' or qf_pos[2] == 'top' then
- topline = topline - tune_line(winid, topline, api.nvim_win_get_height(qf_winid) + 1)
- end
- end
- return topline
-end
-
-local function need_revert(qf_pos)
- local rel_pos, abs_pos = unpack(qf_pos)
- return rel_pos == 'above' or rel_pos == 'below' or abs_pos == 'top' or abs_pos == 'bottom'
-end
-
-local function lastest_mgwin()
- local mgwin = {}
- local holder = qfs.holder()
- for winid, qfsession in pairs(holder) do
- if api.nvim_win_is_valid(winid) then
- --- maybe get multiple mgws, but only return the lastest one
- mgwin = qfsession.magicwin or {}
- else
- qfs[winid] = nil
- end
- end
- return mgwin
-end
-
-function M.clear_winview()
- local mgwin = lastest_mgwin()
- local cur_winid = api.nvim_get_current_win()
- if mgwin[cur_winid] then
- if mgwin[cur_winid].wv then
- local wv = mgwin[cur_winid].wv
- local lnum, col, _, hrtime, flag = unpack(wv)
- if uv.hrtime() - hrtime > 100000000 then
- fn.setpos([['']], {0, lnum, col + 1, 0})
- else
- api.nvim_win_set_cursor(0, {lnum, col})
- if flag == 1 then
- cmd('noa norm! zb')
- else
- cmd('noa norm! zt')
- end
- end
- mgwin[cur_winid].wv = nil
- end
- end
-end
-
-function M.revert_enter_adjacent_wins(qf_winid, file_winid, qf_pos)
- if need_revert(qf_pos) then
- for _, winid in ipairs(qfpos.find_adjacent_wins(qf_winid, file_winid)) do
- if api.nvim_win_is_valid(winid) then
- do_enter_revert(qf_winid, winid, qf_pos)
- end
- end
- end
-end
-
-function M.revert_close_adjacent_wins(qf_winid, file_winid, qf_pos)
- local defer_data = {}
- if need_revert(qf_pos) then
- local mgwins = qfs[qf_winid].magicwin
- for _, winid in ipairs(qfpos.find_adjacent_wins(qf_winid, file_winid)) do
- local topline = prefetch_close_revert_topline(qf_winid, winid, qf_pos)
- if topline then
- local info = {winid = winid, topline = topline}
- local mgw = mgwins[winid]
- if mgw and mgw.wv then
- info.lnum, info.col, info.curswant = unpack(mgw.wv)
- end
- table.insert(defer_data, info)
- end
- end
- end
- unregister_winenter()
-
- return function()
- for _, info in pairs(defer_data) do
- local winid, topline, lnum, col, curswant = info.winid, info.topline, info.lnum,
- info.col, info.curswant
- log.debug('revert_callback:', info, '\n')
- utils.win_execute(winid, function()
- resetview(topline, lnum, col, curswant)
- end)
- end
- end
-end
-
-local function setup()
- cmd([[
- aug BqfMagicWin
- au!
- aug END
- ]])
-end
-
-setup()
-
-return M
diff --git a/lua/bqf/magicwin/core.lua b/lua/bqf/magicwin/core.lua
new file mode 100644
index 0000000..52a4213
--- /dev/null
+++ b/lua/bqf/magicwin/core.lua
@@ -0,0 +1,212 @@
+local M = {}
+local api = vim.api
+local fn = vim.fn
+
+local utils = require('bqf.utils')
+local log = require('bqf.log')
+
+-- Code in this file relates to source code
+-- https://github.com/neovim/neovim/blob/master/src/nvim/window.c
+-- bfraction: before scroll_to_fraction, afraction: after scroll_to_fraction
+-- bfraction = bwrow / bheight and afraction = awrow / aheight
+-- bfraction = afraction ==> bwrow / bheight = awrow / aheight
+
+-- FRACTION_MULT = 16384L
+-- wp->w_fraction = ((long)wp->w_wrow * FRACTION_MULT + FRACTION_MULT / 2) / (long)wp->w_height_inner;
+local function cal_fraction(wrow, height)
+ return math.floor((wrow * 16384 + 8192) / height)
+end
+
+-- wp->w_wrow = ((long)wp->w_fraction * (long)height - 1L) / FRACTION_MULT;
+local function cal_wrow(fraction, height)
+ return math.floor((fraction * height - 1) / 16384)
+end
+
+-- Check out 'void scroll_to_fraction(win_T *wp, int prev_height)' in winodw.c for more details.
+local function evaluate_sline(fraction, height, lnum, lines_size)
+ local sline = cal_wrow(fraction, height)
+ local i = sline
+ for j = lnum - 1, math.max(1, lnum - sline), -1 do
+ i = i - lines_size[j]
+ if i <= 0 then
+ if i < 0 then
+ sline = sline - lines_size[j] - i
+ end
+ break
+ end
+ end
+ return sline
+end
+
+local function do_filter(tbl_info, height, lnum, lines_size)
+ local t = {}
+ for _, info in ipairs(tbl_info) do
+ local sline = evaluate_sline(info, height, lnum, lines_size)
+ if fn.winline() - 1 == sline then
+ table.insert(t, info)
+ end
+ end
+ return t
+end
+
+-- If the lnum hasn't been changed, even if the window is resized, the fraction is still a constant.
+-- And we can use this feature to find out the possible fraction with changing window height.
+-- Check out 'void scroll_to_fraction(win_T *wp, int prev_height)' in winodw.c for more details.
+local function filter_fraction(tbl_info, lnum, lines_size, max_hei)
+ local asc
+ local height = api.nvim_win_get_height(0)
+ local h = height
+ local min_hei = math.max(vim.o.wmh, 1)
+ if max_hei then
+ asc = true
+ else
+ asc = false
+ max_hei = vim.o.lines
+ end
+
+ while #tbl_info > 1 do
+ if h <= min_hei or h > max_hei then
+ break
+ end
+ h = asc and h + 1 or h - 1
+ api.nvim_win_set_height(0, h)
+ if asc and api.nvim_win_get_height(0) ~= h then
+ break
+ end
+ tbl_info = do_filter(tbl_info, h, lnum, lines_size)
+ end
+ api.nvim_win_set_height(0, height)
+ return tbl_info
+end
+
+function M.cal_wrow(fraction, height)
+ return cal_wrow(fraction, height)
+end
+
+function M.evaluate_fraction(winid, lnum, awrow, aheight, bheight, lbwrow, lfraction)
+ -- s_bwrow: the minimum bwrow value
+ -- Below formula we can derive from the known conditions
+ local s_bwrow = math.ceil(awrow * bheight / aheight - 0.5)
+ if s_bwrow < 0 or bheight == aheight then
+ return
+ end
+ -- e_bwrow: the maximum bwrow value
+ -- There are not enough conditions to derive e_bwrow, so we have to figure it out by guessing,
+ -- and confirm the range of bwrow.
+ -- It seems that 10 as a minimum and 1.2 as a scale is good to balance performance and accuracy
+ local e_bwrow = math.max(10, math.ceil(awrow * 1.2 * bheight / aheight - 0.25))
+
+ local per_l_wid = api.nvim_win_get_width(winid) - utils.gutter_size(winid)
+
+ local e_fraction = cal_fraction(e_bwrow, bheight)
+ local e_sline = cal_wrow(e_fraction, aheight)
+
+ if lbwrow then
+ e_sline = math.max(cal_wrow(lfraction, aheight), e_sline)
+ end
+
+ local lines_size = {}
+ local wrap = vim.wo[winid].wrap
+ -- use 9 as additional compensation
+ for i = math.max(1, lnum - e_sline - 9), lnum - 1 do
+ lines_size[i] = wrap and math.ceil(math.max(fn.virtcol({i, '$'}) - 1, 1) / per_l_wid) or 1
+ end
+
+ if lbwrow and awrow == evaluate_sline(lfraction, aheight, lnum, lines_size) then
+ return lfraction
+ end
+
+ local t_frac = {}
+ for bw = s_bwrow, e_bwrow do
+ table.insert(t_frac, cal_fraction(bw, bheight))
+ end
+ log.debug('before t_frac', t_frac)
+
+ t_frac = filter_fraction(t_frac, lnum, lines_size)
+
+ log.debug('first t_frac:', t_frac)
+
+ t_frac = filter_fraction(t_frac, lnum, lines_size, aheight + 9)
+
+ log.debug('second t_frac:', t_frac)
+
+ if #t_frac > 0 then
+ return t_frac[1]
+ end
+end
+
+function M.resetview(topline, lnum, col, curswant)
+ fn.winrestview({topline = topline, lnum = lnum, col = col, curswant = curswant})
+ -- topline may not be changed sometimes without winline()
+ fn.winline()
+end
+
+function M.tune_line(winid, topline, lsizes)
+ if not vim.wo[winid].wrap or lsizes == 0 then
+ return lsizes
+ end
+
+ log.debug('lsizes:', lsizes)
+
+ local i_start, i_end, i_inc, should_continue, len
+ local folded_other_lnum
+ local function neg_one(i)
+ local _ = i
+ return -1
+ end
+
+ if lsizes > 0 then
+ i_start, i_end, i_inc = topline - 1, math.max(1, topline - lsizes), -1
+ should_continue = function(iter)
+ return iter >= i_end
+ end
+ len = lsizes
+ folded_other_lnum = fn.foldclosed
+ else
+ i_start, i_end, i_inc = topline, topline - lsizes - 1, 1
+ should_continue = function(iter)
+ return iter <= i_end
+ end
+ len = -lsizes
+ folded_other_lnum = fn.foldclosedend
+ end
+
+ if vim.wo[winid].foldenable then
+ folded_other_lnum = neg_one
+ end
+ log.debug(i_start, i_end, i_inc, len)
+
+ return utils.win_execute(winid, function()
+ local per_l_wid = api.nvim_win_get_width(winid) - utils.gutter_size(winid)
+ local loff, lsize_sum = 0, 0
+ local i = i_start
+ while should_continue(i) do
+ log.debug('=====================================================')
+ log.debug('i:', i, 'i_end:', i_end)
+ local fo_lnum = folded_other_lnum(i)
+ if fo_lnum == -1 then
+ local per_l_size = math.ceil(math.max(fn.virtcol({i, '$'}) - 1, 1) / per_l_wid)
+ log.debug('lsize_sum:', lsize_sum, 'per_l_size:', per_l_size, 'lnum:', i)
+ lsize_sum = lsize_sum + per_l_size
+ loff = loff + 1
+ else
+ log.debug('fo_lnum:', fo_lnum)
+ lsize_sum = lsize_sum + 1
+ loff = loff + math.abs(fo_lnum - i) + 1
+ i_end = i_end + fo_lnum - i
+ i = fo_lnum
+ end
+ log.debug('loff:', loff)
+ log.debug('=====================================================')
+ i = i + i_inc
+ if lsize_sum >= len then
+ break
+ end
+ end
+ loff = lsizes > 0 and loff or -loff
+ log.debug('line_offset:', loff)
+ return loff
+ end)
+end
+
+return M
diff --git a/lua/bqf/magicwin/handler.lua b/lua/bqf/magicwin/handler.lua
new file mode 100644
index 0000000..277d5f5
--- /dev/null
+++ b/lua/bqf/magicwin/handler.lua
@@ -0,0 +1,343 @@
+local M = {}
+local api = vim.api
+local fn = vim.fn
+local cmd = vim.cmd
+local uv = vim.loop
+
+local mgws = require('bqf.magicwin.session')
+local wpos = require('bqf.wpos')
+local POS
+local utils = require('bqf.utils')
+local log = require('bqf.log')
+local mcore = require('bqf.magicwin.core')
+local config = require('bqf.config')
+
+local enable
+
+local function register_winenter(qwinid)
+ local qbufnr = api.nvim_win_get_buf(qwinid)
+ cmd(('au BqfMagicWin WinEnter * ++once %s'):format(
+ ([[lua require('bqf.magicwin.handler').clear_winview(%d)]]):format(qbufnr)))
+end
+
+local function do_enter_revert(qwinid, winid, qf_pos)
+ log.debug('do_enter_revert start')
+ -- TODO upstream bug
+ -- local f_win_so = vim.wo[winid].scrolloff
+ -- return a big number like '1.4014575443238e+14' if window option is absent
+ -- Use getwinvar to workaround
+ local f_win_so = fn.getwinvar(winid, '&scrolloff')
+ if f_win_so ~= 0 then
+ -- turn off scrolloff and then show us true wrow
+ vim.wo[winid].scrolloff = 0
+ cmd(('au BqfMagicWin WinLeave * ++once %s'):format(
+ ([[lua vim.wo[%d].scrolloff = %d]]):format(winid, f_win_so)))
+ end
+
+ utils.win_execute(winid, function()
+ local qf_hei, win_hei = api.nvim_win_get_height(qwinid), api.nvim_win_get_height(winid)
+ local wv = fn.winsaveview()
+ local topline, lnum = wv.topline, wv.lnum
+ local line_count = api.nvim_buf_line_count(0)
+
+ -- qf winodw height might be changed by user adds new qf items or navigates history
+ -- we need a cache to store previous state
+
+ local qbufnr = api.nvim_win_get_buf(qwinid)
+ local aws = mgws.adjacent_win(qbufnr, winid)
+ local def_hei = qf_hei + win_hei + 1
+ local bheight, aheight = aws.aheight or def_hei, win_hei
+ local lbwrow, lfraction = aws.bwrow, aws.fraction
+ local bwrow, fraction, delta_lsize
+
+ local awrow = fn.winline() - 1
+
+ if topline == 1 and line_count <= win_hei then
+ delta_lsize = 0
+ else
+ if f_win_so >= awrow and awrow > 0 and win_hei > 1 then
+ -- get the true wrow
+ cmd('resize -1 | resize +1')
+ awrow = fn.winline() - 1
+ topline = fn.line('w0')
+ end
+
+ fraction = mcore.evaluate_fraction(winid, lnum, awrow, aheight, bheight, lbwrow,
+ lfraction)
+ log.debug('awrow:', awrow, 'aheight:', aheight, 'bheight:', bheight)
+ log.debug('lbwrow:', lbwrow, 'lfraction:', lfraction)
+ log.debug('fraction:', fraction)
+
+ if not fraction then
+ return
+ end
+ bwrow = mcore.cal_wrow(fraction, bheight)
+ log.debug('bwrow:', bwrow)
+ delta_lsize = bwrow - awrow
+ end
+
+ if qf_pos[1] == POS.ABOVE or qf_pos[2] == POS.TOP then
+ if lbwrow == bwrow and lfraction == fraction then
+ bheight = aws.bheight or def_hei
+ end
+ delta_lsize = delta_lsize - bheight + aheight
+ end
+
+ if delta_lsize == 0 then
+ return
+ end
+
+ log.debug('before topline:', topline, 'delta_lsize:', delta_lsize)
+
+ local line_offset = mcore.tune_line(winid, topline, delta_lsize)
+ topline = math.max(1, topline - line_offset)
+ local flag = 0
+ if delta_lsize > 0 then
+ local reminder = aheight - awrow - 1
+ if delta_lsize > reminder then
+ flag = 1
+ lnum = topline
+ end
+ else
+ if -delta_lsize > awrow then
+ flag = 2
+ lnum = topline
+ end
+ end
+
+ mcore.resetview(topline, lnum)
+
+ local wv_info
+ if flag > 0 then
+ wv_info = {wv.lnum, wv.col, wv.curswant, uv.hrtime(), flag}
+ if flag == 1 then
+ mcore.resetview(topline, fn.line('w$'))
+ end
+ log.debug('wv_info:', wv_info)
+ register_winenter(qwinid)
+ end
+
+ aws:set({
+ bwrow = bwrow,
+ bheight = bheight,
+ aheight = aheight,
+ fraction = fraction,
+ wv = wv_info
+ })
+ end)
+
+ log.debug('do_enter_revert end', '\n')
+end
+
+local function prefetch_close_revert_topline(qwinid, winid, qf_pos)
+ local topline
+ local ok, msg = pcall(fn.getwininfo, winid)
+ if ok then
+ topline = msg[1].topline
+ if qf_pos[1] == POS.ABOVE or qf_pos[2] == POS.TOP then
+ topline = topline - mcore.tune_line(winid, topline, api.nvim_win_get_height(qwinid) + 1)
+ end
+ end
+ return topline
+end
+
+local function need_revert(qf_pos)
+ local rel_pos, abs_pos = unpack(qf_pos)
+ return rel_pos == POS.ABOVE or rel_pos == POS.BELOW or abs_pos == POS.TOP or abs_pos ==
+ POS.BOTTOM
+end
+
+function M.clear_winview(qbufnr)
+ local qwinid = fn.bufwinid(qbufnr)
+ if utils.is_win_valid(qwinid) then
+ for _, winid in ipairs(api.nvim_tabpage_list_wins(0)) do
+ local aws = mgws.adjacent_win(qbufnr, winid)
+ if aws and aws.wv then
+ local lnum, col, _, hrtime, flag = unpack(aws.wv)
+ if uv.hrtime() - hrtime > 100000000 then
+ fn.setpos([['']], {0, lnum, col + 1, 0})
+ else
+ utils.win_execute(winid, function()
+ api.nvim_win_set_cursor(0, {lnum, col})
+ if flag == 1 then
+ cmd('noa norm! zb')
+ else
+ cmd('noa norm! zt')
+ end
+ end)
+ end
+ aws.wv = nil
+ end
+ end
+ end
+end
+
+local function revert_enter_adjacent_wins(qwinid, pwinid, qf_pos)
+ if need_revert(qf_pos) then
+ local wfh_tbl = {}
+ for _, winid in ipairs(api.nvim_tabpage_list_wins(0)) do
+ if vim.wo[winid].winfixheight then
+ vim.wo[winid].winfixheight = false
+ table.insert(wfh_tbl, winid)
+ end
+ end
+ local wiw_bak = vim.o.winwidth
+ vim.o.winwidth = 1
+ pcall(function()
+ for _, winid in ipairs(wpos.find_adjacent_wins(qwinid, pwinid)) do
+ if utils.is_win_valid(winid) then
+ do_enter_revert(qwinid, winid, qf_pos)
+ end
+ end
+ end)
+ vim.o.winwidth = wiw_bak
+ for _, winid in ipairs(wfh_tbl) do
+ vim.wo[winid].winfixheight = true
+ end
+ end
+end
+
+local function revert_close_adjacent_wins(qwinid, pwinid, qf_pos)
+ if need_revert(qf_pos) then
+ local defer_data = {}
+ local qbufnr = api.nvim_win_get_buf(qwinid)
+ for _, winid in ipairs(wpos.find_adjacent_wins(qwinid, pwinid)) do
+ local topline = prefetch_close_revert_topline(qwinid, winid, qf_pos)
+ if topline then
+ local info = {winid = winid, topline = topline}
+ local aws = mgws.adjacent_win(qbufnr, winid)
+ if aws and aws.wv then
+ info.lnum, info.col, info.curswant = unpack(aws.wv)
+ end
+ table.insert(defer_data, info)
+ end
+ end
+
+ return #defer_data > 0 and function()
+ local wiw_bak = vim.o.winwidth
+ vim.o.winwidth = 1
+ pcall(function()
+ for _, info in ipairs(defer_data) do
+ local winid, topline, lnum, col, curswant = info.winid, info.topline, info.lnum,
+ info.col, info.curswant
+ log.debug('revert_callback:', info, '\n')
+ utils.win_execute(winid, function()
+ mcore.resetview(topline, lnum, col, curswant)
+ end)
+ end
+ end)
+ vim.o.winwidth = wiw_bak
+ end or nil
+ end
+end
+
+local function open(winid, last_winid)
+ if not enable then
+ return
+ end
+ local pos = wpos.get_pos(winid, last_winid)
+ revert_enter_adjacent_wins(winid, last_winid, pos)
+end
+
+function M.close(winid, last_winid, bufnr)
+ if not utils.is_win_valid(winid) or not enable then
+ return
+ end
+
+ local pos = wpos.get_pos(winid, last_winid)
+ local winnr = fn.win_id2win(winid)
+ local win_j, win_l
+ utils.win_execute(winid, function()
+ win_j, win_l = fn.winnr('j'), fn.winnr('l')
+ end)
+
+ local whei, wwid, phei, pwid
+ local rel_pos = pos[1]
+ if rel_pos == POS.RIGHT and win_l ~= winnr then
+ wwid, pwid = api.nvim_win_get_width(winid), api.nvim_win_get_width(last_winid)
+ elseif rel_pos == POS.BELOW and win_j ~= winnr then
+ whei, phei = api.nvim_win_get_height(winid), api.nvim_win_get_height(last_winid)
+ end
+
+ local wmagic_defer_cb = revert_close_adjacent_wins(winid, last_winid, pos)
+
+ local cur_winid = api.nvim_get_current_win()
+
+ local wins = vim.tbl_filter(function(id)
+ return fn.win_gettype(id) ~= 'popup'
+ end, api.nvim_tabpage_list_wins(0))
+
+ if vim.o.equalalways and #wins > 2 then
+ -- closing window in other tab or floating window can prevent nvim make windows equal
+ -- after closing target window, but in other tab can't run
+ -- 'win_enter_ext(wp, false, true, false, true, true)' which will triggers 'WinEnter', 'BufEnter'
+ -- and 'CursorMoved' events. Search 'do_autocmd_winclosed' in src/nvim/window.c for details.
+ local scratch = api.nvim_create_buf(false, true)
+
+ local ei_bak = vim.o.ei
+ vim.o.ei = 'all'
+ local ok = pcall(api.nvim_open_win, scratch, true, {
+ relative = 'win',
+ width = 1,
+ height = 1,
+ row = 0,
+ col = 0,
+ style = 'minimal',
+ noautocmd = true
+ })
+ vim.o.ei = ei_bak
+ if ok then
+ api.nvim_win_close(winid, false)
+ api.nvim_buf_delete(scratch, {})
+ end
+ else
+ api.nvim_win_close(winid, false)
+ end
+
+ if utils.is_win_valid(last_winid) and cur_winid == winid then
+ api.nvim_set_current_win(last_winid)
+ end
+
+ if rel_pos == POS.RIGHT and win_l ~= winnr then
+ api.nvim_win_set_width(last_winid, wwid + pwid + 1)
+ elseif rel_pos == POS.BELOW and win_j ~= winnr then
+ api.nvim_win_set_height(last_winid, whei + phei + 1)
+ end
+
+ if wmagic_defer_cb then
+ wmagic_defer_cb()
+ end
+ M.detach(bufnr)
+end
+
+function M.attach(winid, last_winid, bufnr)
+ winid = winid or api.nvim_get_current_win()
+ last_winid = last_winid or fn.win_getid(fn.winnr('#'))
+ bufnr = bufnr or api.nvim_win_get_buf(winid)
+ open(winid, last_winid)
+ cmd(([[
+ aug BqfMagicWin
+ au! *
+ au WinClosed ++nested lua require('bqf.magicwin.handler').close(%d, %d, %d)
+ aug END
+ ]]):format(winid, last_winid, bufnr))
+end
+
+function M.detach(bufnr)
+ cmd(([[au! BqfMagicWin * ]]):format(bufnr))
+ mgws.clean(bufnr)
+end
+
+local function init()
+ cmd([[
+ aug BqfMagicWin
+ au!
+ aug END
+ ]])
+ POS = wpos.POS
+ enable = config.magic_window
+end
+
+init()
+
+return M
diff --git a/lua/bqf/magicwin/session.lua b/lua/bqf/magicwin/session.lua
new file mode 100644
index 0000000..9ea7af2
--- /dev/null
+++ b/lua/bqf/magicwin/session.lua
@@ -0,0 +1,55 @@
+local utils = require('bqf.utils')
+
+local Win = {}
+
+function Win.new(winid)
+ local obj = {}
+ setmetatable(obj, Win)
+ Win.__index = Win
+ obj.winid = winid
+ obj.bwrow = nil
+ obj.bheight = nil
+ obj.aheight = nil
+ obj.fraction = nil
+ obj.wv = nil
+ return obj
+end
+
+function Win:set(o)
+ self.bwrow = o.bwrow
+ self.bheight = o.bheight
+ self.aheight = o.aheight
+ self.fraction = o.fraction
+ self.wv = o.wv
+end
+
+local MagicWinSession = {pool = {}}
+
+function MagicWinSession.get(qbufnr)
+ if not MagicWinSession.pool[qbufnr] then
+ MagicWinSession.pool[qbufnr] = setmetatable({}, {
+ __index = function(tbl, winid)
+ rawset(tbl, winid, Win.new(winid))
+ return tbl[winid]
+ end
+ })
+ end
+ return MagicWinSession.pool[qbufnr]
+end
+
+function MagicWinSession.adjacent_win(qbufnr, winid)
+ return MagicWinSession.get(qbufnr)[winid]
+end
+
+function MagicWinSession.clean(qbufnr)
+ for bufnr in pairs(MagicWinSession.pool) do
+ if not utils.is_buf_loaded(bufnr) then
+ MagicWinSession.pool[bufnr] = nil
+ end
+ end
+ if qbufnr then
+ MagicWinSession.pool[qbufnr] = nil
+ end
+end
+
+return MagicWinSession
diff --git a/lua/bqf/main.lua b/lua/bqf/main.lua
index 216d1fc..156ac3f 100644
--- a/lua/bqf/main.lua
+++ b/lua/bqf/main.lua
@@ -3,20 +3,11 @@ local api = vim.api
local fn = vim.fn
local cmd = vim.cmd
-local qfs = require('bqf.qfsession')
-local qftool = require('bqf.qftool')
-local preview = require('bqf.preview')
+local qfs = require('bqf.qfwin.session')
+local previewer = require('bqf.previewer.handler')
local layout = require('bqf.layout')
+local magicwin = require('bqf.magicwin.handler')
local keymap = require('bqf.keymap')
-local sign = require('bqf.sign')
-
-local function setup()
- cmd([[
- aug Bqf
- au!
- aug END
- ]])
-end
function M.toggle()
if vim.b.bqf_enabled then
@@ -32,87 +23,84 @@ function M.enable()
return
end
- assert(vim.bo.buftype == 'quickfix', 'It is not a quickfix window')
+ local qwinid = api.nvim_get_current_win()
- local qf_winid = api.nvim_get_current_win()
- qfs.attach(qf_winid)
+ local qs = qfs:new(qwinid)
+ assert(qs, 'It is not a quickfix window')
- local qf_type = qftool.type(qf_winid)
+ vim.wo.nu, vim.wo.rnu = true, false
+ vim.wo.wrap = false
+ vim.wo.foldenable, vim.wo.foldcolumn = false, '0'
+ vim.wo.signcolumn = 'number'
- local file_winid = qftool.filewinid(qf_winid)
+ layout.initialize(qwinid)
- if vim.bo.bufhidden == 'wipe' then
- qfs[qf_winid].bufhidden = 'wipe'
- end
-
- vim.wo.number, vim.wo.relativenumber = true, false
- vim.wo.wrap, vim.foldenable = false, false
- vim.wo.foldcolumn, vim.wo.signcolumn = '0', 'number'
-
- layout.init(qf_winid, file_winid, qf_type)
-
- local qf_bufnr = api.nvim_win_get_buf(qf_winid)
- sign.reset(qf_bufnr)
- -- some plugins will change the quickfix window, preview window should init later
- vim.defer_fn(function()
- preview.init_window(qf_winid)
- end, 50)
-
- -- after vim-patch:8.1.0877, quickfix will reuse buffer, below buffer setup is no necessary
- if vim.b.bqf_enabled then
- return
- end
-
- vim.b.bqf_enabled = true
- preview.buf_event()
- keymap.buf_map()
+ previewer.initialize(qwinid)
+ keymap.initialize()
+ magicwin.attach(qwinid, qs:pwinid())
cmd([[
aug Bqf
au! *
- au WinEnter lua require('bqf.main').kill_alone_qf()
+ au WinEnter ++nested lua require('bqf.main').kill_alone_qf()
au WinClosed ++nested lua require('bqf.main').close_qf()
aug END
]])
+ vim.b.bqf_enabled = true
end
function M.disable()
if vim.bo.buftype ~= 'quickfix' then
return
end
- local qf_winid = api.nvim_get_current_win()
- preview.close(qf_winid)
- keymap.buf_unmap()
+ local qwinid = api.nvim_get_current_win()
+ previewer.close(qwinid)
+ keymap.dispose()
vim.b.bqf_enabled = false
cmd('au! Bqf')
cmd('sil! au! BqfPreview * ')
cmd('sil! au! BqfFilterFzf * ')
cmd('sil! au! BqfMagicWin')
- if qfs[qf_winid].bufhidden then
- vim.bo.bufhidden = qfs[qf_winid].bufhidden
+ qfs:dispose()
+end
+
+local function close(winid)
+ local ok, msg = pcall(api.nvim_win_close, winid, false)
+ if not ok then
+ -- Vim:E444: Cannot close last window
+ if msg:match('^Vim:E444') then
+ cmd('new')
+ api.nvim_win_close(winid, true)
+ end
end
- qfs.release(qf_winid)
end
function M.kill_alone_qf()
- pcall(function()
- qftool.filewinid()
- end)
+ local winid = api.nvim_get_current_win()
+ local qs = qfs.get(winid)
+ if qs then
+ if qs:pwinid() < 0 then
+ close(winid)
+ end
+ end
end
function M.close_qf()
local winid = tonumber(fn.expand(''))
- if qfs[winid].bufhidden then
- local qf_bufnr = api.nvim_win_get_buf(winid)
- vim.bo[qf_bufnr].bufhidden = qfs[winid].bufhidden
- end
if winid and api.nvim_win_is_valid(winid) then
- preview.close(winid)
- layout.close_win(winid)
- qfs.release(winid)
+ qfs:dispose()
+ previewer.close(winid)
end
end
-setup()
+local function init()
+ cmd([[
+ aug Bqf
+ au!
+ aug END
+ ]])
+end
+
+init()
return M
diff --git a/lua/bqf/preview.lua b/lua/bqf/preview.lua
deleted file mode 100644
index afaae33..0000000
--- a/lua/bqf/preview.lua
+++ /dev/null
@@ -1,424 +0,0 @@
-local M = {}
-local api = vim.api
-local fn = vim.fn
-local cmd = vim.cmd
-
-local auto_preview, delay_syntax, wrap
-local should_preview_cb
-local keep_preview, orig_pos
-local last_idx
-
-local config = require('bqf.config')
-local qfs = require('bqf.qfsession')
-local qftool = require('bqf.qftool')
-local floatwin = require('bqf.floatwin')
-local utils = require('bqf.utils')
-
-local function setup()
- local pconf = config.preview
- vim.validate({preview = {pconf, 'table'}})
- floatwin.setup({
- win_height = pconf.win_height,
- win_vheight = pconf.win_vheight,
- border_chars = pconf.border_chars
- })
- auto_preview = pconf.auto_preview
- delay_syntax = tonumber(pconf.delay_syntax)
- wrap = pconf.wrap
- should_preview_cb = pconf.should_preview_cb
- vim.validate({
- auto_preview = {auto_preview, 'boolean'},
- delay_syntax = {delay_syntax, 'number'},
- wrap = {wrap, 'boolean'},
- should_preview_cb = {should_preview_cb, 'function', true}
- })
-
- cmd([[
- aug BqfPreview
- au!
- aug END
- ]])
-
- cmd('hi default link BqfPreviewCursor Cursor')
- cmd('hi default link BqfPreviewRange IncSearch')
-end
-
-local function update_border(border_width, qf_items, idx)
- local pos_str = ('[%d/%d]'):format(idx, #qf_items)
- local pbufnr = qf_items[idx].bufnr
- local buf_str = ('buf %d:'):format(pbufnr)
- local name = fn.bufname(pbufnr):gsub('^' .. vim.env.HOME, '~')
- local pad_fit = border_width - 8 - fn.strwidth(buf_str) - fn.strwidth(pos_str)
- if pad_fit - fn.strwidth(name) < 0 then
- name = fn.pathshorten(name)
- if pad_fit - fn.strwidth(name) < 0 then
- name = ''
- end
- end
- local title = (' %s %s %s '):format(pos_str, buf_str, name)
- floatwin.update_title(title)
- floatwin.update_scrollbar()
-end
-
-local function update_mode(qf_winid)
- qf_winid = qf_winid or api.nvim_get_current_win()
- local ps = qfs[qf_winid].preview
- if ps.full then
- floatwin.set_win_height(999, 999)
- else
- local conf = config.preview
- floatwin.set_win_height(conf.win_height, conf.win_vheight)
- end
-end
-
-local function exec_preview(qf_all, idx, file_winid)
- local entry = qf_all.items[idx]
- if not entry then
- return
- end
-
- local lnum, col, pattern = entry.lnum, entry.col, entry.pattern
- vim.wo.wrap, vim.wo.foldenable = wrap, false
- vim.wo.number, vim.wo.relativenumber = true, false
- vim.wo.cursorline, vim.wo.signcolumn = true, 'no'
- vim.wo.foldmethod = 'manual'
-
- if lnum < 1 then
- api.nvim_win_set_cursor(0, {1, 0})
- if pattern ~= '' then
- fn.search(pattern, 'c')
- end
- else
- if not pcall(api.nvim_win_set_cursor, 0, {lnum, math.max(0, col - 1)}) then
- return
- end
- end
-
- utils.zz()
- orig_pos = api.nvim_win_get_cursor(0)
-
- fn.clearmatches()
-
- local lsp_ranges_hl = qf_all.lsp_ranges_hl and not vim.tbl_isempty(qf_all.lsp_ranges_hl) and
- qf_all.lsp_ranges_hl[idx] or {}
- local pattern_hl = qf_all.pattern_hl
-
- local range_ids
- if not vim.tbl_isempty(lsp_ranges_hl) then
- local pos_list = utils.lsp_range2pos_list(lsp_ranges_hl)
- if not vim.tbl_isempty(pos_list) then
- range_ids = utils.matchaddpos('BqfPreviewRange', pos_list)
- end
- elseif pattern_hl and pattern_hl ~= '' then
- local pos_list = utils.pattern2pos_list(pattern_hl)
- if not vim.tbl_isempty(pos_list) then
- range_ids = utils.matchaddpos('BqfPreviewRange', pos_list)
- end
- end
- if lnum > 0 then
- fn.matchaddpos('BqfPreviewCursor', {{lnum, math.max(1, col)}}, 11)
- end
- if not range_ids then
- if lnum < 1 then
- fn.matchadd('BqfPreviewRange', pattern)
- elseif col < 1 then
- fn.matchaddpos('BqfPreviewRange', {{lnum}})
- end
- end
- cmd(('noa call nvim_set_current_win(%d)'):format(file_winid))
-end
-
-local function do_syntax(qf_winid, idx)
- local ps = qfs[qf_winid].preview
- if not ps or idx ~= last_idx then
- return
- end
-
- if not ps.buf_loaded and vim.bo[ps.bufnr].filetype == '' then
- local preview_winid = floatwin.winid()
- if fn.bufwinid(ps.bufnr) == preview_winid then
- -- https://github.com/nvim-treesitter/nvim-treesitter/issues/898
- -- fuxx min.js!
- local lcount = api.nvim_buf_line_count(ps.bufnr)
- local bytes = api.nvim_buf_get_offset(ps.bufnr, lcount)
- -- bytes / lcount < 1000 LGTM :)
- if bytes / lcount < 1000 then
- -- nvim_buf_call is less side-effects than changing window
- -- make sure that buffer in preview window must not in normal window
- api.nvim_buf_call(ps.bufnr, function()
- cmd('filetype detect')
- end)
- end
- end
- end
-end
-
-local function clean_preview_buf(bufnr, loaded_before)
- if bufnr and not loaded_before then
- if api.nvim_buf_is_loaded(bufnr) and fn.buflisted(bufnr) == 0 then
- api.nvim_buf_call(bufnr, function()
- cmd([[delm \"]])
- end)
- cmd('bd! ' .. bufnr)
- end
- end
-end
-
--- https://github.com/neovim/neovim/issues/11525
--- buffers inherit last closed window option if the buffer is loaded
-local function fire_restore_buf_opts(bufnr, loaded_before, fwin_opts)
- if not bufnr or vim.tbl_isempty(fwin_opts) then
- return
- end
- if loaded_before and fn.bufwinid(bufnr) == -1 then
- if not pcall(api.nvim_buf_get_var, bufnr, 'bqf_fwin_opts') then
- api.nvim_buf_set_var(bufnr, 'bqf_fwin_opts', fwin_opts)
- cmd(('au Bqf BufWinEnter ++once %s'):format(bufnr,
- [[lua require('bqf.layout').restore_fwin_opts()]]))
- end
- end
-end
-
-local function reopen(qf_winid)
- qf_winid = qf_winid or api.nvim_get_current_win()
- M.close(qf_winid)
- M.open(qf_winid, nil, true)
-end
-
-function M.auto_enabled()
- return auto_preview
-end
-
-function M.keep_preview()
- keep_preview = true
-end
-
-function M.toggle_mode()
- local qf_winid = api.nvim_get_current_win()
- local ps = qfs[qf_winid].preview
- ps.full = ps.full ~= true
- last_idx = -1
- M.open(qf_winid, nil, true)
-end
-
-function M.close(qf_winid)
- if keep_preview then
- keep_preview = nil
- return
- end
-
- last_idx = -1
- floatwin.close()
-
- qf_winid = qf_winid or api.nvim_get_current_win()
- local ps = qfs[qf_winid].preview
- if ps then
- clean_preview_buf(ps.bufnr, ps.buf_loaded)
- fire_restore_buf_opts(ps.bufnr, ps.buf_loaded, qfs[qf_winid].fwin_opts)
- end
-end
-
-function M.open(qf_winid, qf_idx, force)
- qf_winid = qf_winid or api.nvim_get_current_win()
- local file_winid = qftool.filewinid(qf_winid)
-
- local ps = qfs[qf_winid].preview
- if not ps or fn.winnr('$') == 1 or api.nvim_win_get_config(file_winid).relative ~= '' then
- return
- end
-
- qf_idx = qf_idx or api.nvim_win_get_cursor(qf_winid)[1]
- if qf_idx == last_idx then
- return
- end
-
- last_idx = qf_idx
-
- local qf_all = qftool.getall(qf_winid)
-
- local qf_items = qf_all.items
- if #qf_items == 0 then
- M.close(qf_winid)
- return
- end
-
- local entry = qf_items[qf_idx]
- if not entry then
- return
- end
-
- local pbufnr = entry.bufnr
-
- if pbufnr == 0 or not api.nvim_buf_is_valid(pbufnr) then
- M.close(qf_winid)
- return
- end
-
- local pbuf_loaded = api.nvim_buf_is_loaded(pbufnr)
-
- if not pbuf_loaded and not force and should_preview_cb and not should_preview_cb(pbufnr) then
- M.close(qf_winid)
- return
- end
-
- update_mode(qf_winid)
-
- local ml = vim.bo[pbufnr].ml
- vim.bo[pbufnr].ml = false
-
- local preview_winid, border_winid = floatwin.open(pbufnr, qf_winid, file_winid)
-
- if preview_winid < 0 or border_winid < 0 then
- vim.bo[pbufnr].ml = ml
- return
- end
-
- if ps.bufnr ~= pbufnr then
- clean_preview_buf(ps.bufnr, ps.buf_loaded)
- fire_restore_buf_opts(ps.bufnr, ps.buf_loaded, qfs[qf_winid].fwin_opts)
- ps.bufnr, ps.buf_loaded = pbufnr, pbuf_loaded
- end
-
- utils.win_execute(preview_winid, function()
- exec_preview(qf_all, qf_idx, file_winid)
- end)
-
- update_border(api.nvim_win_get_width(preview_winid), qf_items, qf_idx)
-
- vim.defer_fn(function()
- do_syntax(qf_winid, qf_idx)
- vim.bo[pbufnr].ml = ml
- end, delay_syntax)
-end
-
-function M.init_window(qf_winid)
- -- delayed called, qf_winid maybe invalid
- if not api.nvim_win_is_valid(qf_winid) then
- return
- end
-
- last_idx = -1
- qfs[qf_winid].preview = qfs[qf_winid].preview or {full = false}
- if auto_preview and api.nvim_get_current_win() == qf_winid then
- -- bufhidden=hide after vim-patch:8.1.0877
- if vim.bo.bufhidden == 'wipe' then
- -- TODO I don't know why must use vim.schedule, defer_fn is already wrapped
- local bufnr = api.nvim_win_get_buf(qf_winid)
- vim.schedule(function()
- vim.bo[bufnr].bufhidden = 'hide'
- end)
- end
- M.open(qf_winid)
- end
-end
-
-function M.scroll(direction)
- local preview_winid = floatwin.winid()
- if preview_winid < 0 or not direction then
- return
- end
- local file_winid = qftool.filewinid()
- utils.win_execute(preview_winid, function()
- if direction == 0 then
- api.nvim_win_set_cursor(preview_winid, orig_pos)
- else
- -- ^D = 0x04, ^U = 0x15
- fn.execute(('norm! %c'):format(direction > 0 and 0x04 or 0x15))
- end
- utils.zz()
- cmd(('noa call nvim_set_current_win(%d)'):format(file_winid))
- end)
- floatwin.update_scrollbar()
-end
-
-function M.toggle()
- auto_preview = auto_preview ~= true
- if auto_preview then
- api.nvim_echo({{'Enable preview automatically', 'WarningMsg'}}, true, {})
- M.open()
- else
- api.nvim_echo({{'Disable preview automatically', 'WarningMsg'}}, true, {})
- M.close()
- end
-end
-
-function M.toggle_item()
- if floatwin.validate_window() then
- M.close()
- else
- M.open(nil, nil, true)
- end
-end
-
-function M.move_cursor()
- local qf_winid = api.nvim_get_current_win()
- local ps = qfs[qf_winid].preview
- if not ps then
- return
- end
- if auto_preview then
- M.open()
- else
- if api.nvim_win_get_cursor(qf_winid)[1] ~= last_idx then
- M.close()
- end
- end
-end
-
-function M.tabenter_event()
- if qftool.validate_qf() and auto_preview then
- M.open(nil, nil, true)
- end
-end
-
-function M.redraw_win(qf_winid)
- if floatwin.validate_window() then
- reopen(qf_winid)
- end
-end
-
--- Enabling preview and executing cpfile or cnfile or cfdo hits previewed buffer may cause
--- quickfix window enter the entry buffer, it only produces in quickfix not for location.
-function M.fix_qf_jump(qf_bufnr)
- local qf_winid = api.nvim_get_current_win()
- local ok, msg = pcall(qftool.filewinid, qf_winid)
- if ok then
- local file_winid = msg
- local buf_entered = api.nvim_get_current_buf()
- api.nvim_win_set_buf(qf_winid, qf_bufnr)
- api.nvim_set_current_win(file_winid)
- api.nvim_win_set_buf(file_winid, buf_entered)
- else
- -- no need after vim-patch:8.1.0877
- api.nvim_buf_delete(qf_bufnr, {})
- end
-end
-
-function M.buf_event()
- -- TODO I hate these autocmd string!!!!!!!!!!!!!!!!!!!!!
- local bufnr = api.nvim_get_current_buf()
- api.nvim_exec([[
- aug BqfPreview
- au! *
- au VimResized lua require('bqf.preview').redraw_win()
- au TabEnter lua require('bqf.preview').tabenter_event()
- au CursorMoved lua require('bqf.preview').move_cursor()
- ]], false)
- cmd(('au WinLeave,BufWipeout %s'):format(
- ([[lua require('bqf.preview').close(vim.fn.bufwinid(%d))]]):format(bufnr)))
- cmd(('au BufHidden exe "%s %s"'):format('au BqfPreview BufEnter * ++once ++nested',
- ([[lua require('bqf.preview').fix_qf_jump(%d)]]):format(bufnr)))
-
- -- bufhidden=hide after vim-patch:8.1.0877
- if vim.bo.bufhidden == 'wipe' then
- cmd('au QuitPre ++nested bw')
- cmd([[au BufEnter lua vim.bo.bufhidden = 'hide']])
- cmd(('au BufLeave exe "%s %s"'):format('au BqfPreview BufEnter * ++once',
- ([[sil! lua vim.bo[%d].bufhidden = 'wipe']]):format(bufnr)))
- end
- cmd('aug END')
-end
-
-setup()
-
-return M
diff --git a/lua/bqf/previewer/border.lua b/lua/bqf/previewer/border.lua
new file mode 100644
index 0000000..b1d0b4d
--- /dev/null
+++ b/lua/bqf/previewer/border.lua
@@ -0,0 +1,136 @@
+-- singleton
+local api = vim.api
+local fn = vim.fn
+
+local utils = require('bqf.utils')
+
+local FloatWin = require('bqf.previewer.floatwin')
+local Border = setmetatable({}, {__index = FloatWin})
+
+function Border:build(o)
+ o = o or {}
+ self.__index = self
+ self.floatwin = FloatWin
+ self.chars = o.chars
+ self.winid = 0
+ self.bufnr = 0
+ return self
+end
+
+function Border:update(pbufnr, idx, size)
+ local pos_str = ('[%d/%d]'):format(idx, size)
+ local buf_str = ('buf %d:'):format(pbufnr)
+ local modified = vim.bo[pbufnr].modified and '[+] ' or ''
+ local name = fn.bufname(pbufnr):gsub('^' .. vim.env.HOME, '~')
+ local width = api.nvim_win_get_width(self.winid)
+ local pad_fit = width - 10 - fn.strwidth(buf_str) - fn.strwidth(pos_str)
+ if pad_fit - fn.strwidth(name) < 0 then
+ name = fn.pathshorten(name)
+ if pad_fit - fn.strwidth(name) < 0 then
+ name = ''
+ end
+ end
+ local title = (' %s %s %s %s'):format(pos_str, buf_str, name, modified)
+ self:update_title(title)
+ self:update_scrollbar()
+end
+
+function Border:update_buf(opts)
+ local width, height = opts.width, opts.height
+ local top = self.chars[5] .. self.chars[3]:rep(width - 2) .. self.chars[6]
+ local mid = self.chars[1] .. (' '):rep(width - 2) .. self.chars[2]
+ local bot = self.chars[7] .. self.chars[4]:rep(width - 2) .. self.chars[8]
+ local lines = {top}
+ for _ = 1, height - 2 do
+ table.insert(lines, mid)
+ end
+ table.insert(lines, bot)
+ if not utils.is_buf_loaded(self.bufnr) then
+ local bufnr = fn.bufnr('^BqfPreviewBorder$')
+ if bufnr > 0 then
+ self.bufnr = bufnr
+ else
+ self.bufnr = api.nvim_create_buf(false, true)
+ api.nvim_buf_set_name(self.bufnr, 'BqfPreviewBorder')
+ end
+ -- run nvim with `-M` will reset modifiable's default value to false
+ vim.bo[self.bufnr].modifiable = true
+ vim.bo[self.bufnr].bufhidden = 'hide'
+ end
+ api.nvim_buf_set_lines(self.bufnr, 0, -1, false, lines)
+end
+
+function Border:update_scrollbar()
+ local buf = api.nvim_win_get_buf(self.floatwin.winid)
+ local line_count = api.nvim_buf_line_count(buf)
+
+ local winfo = fn.getwininfo(self.floatwin.winid)[1]
+ local topline, height = winfo.topline, winfo.height
+
+ local bar_size = math.min(height, math.ceil(height * height / line_count))
+
+ local bar_pos = math.ceil(height * topline / line_count)
+ if bar_pos + bar_size > height then
+ bar_pos = height - bar_size + 1
+ end
+
+ local lines = api.nvim_buf_get_lines(self.bufnr, 1, -2, true)
+ for i = 1, #lines do
+ local bar_char
+ if i >= bar_pos and i < bar_pos + bar_size then
+ bar_char = self.chars[#self.chars]
+ else
+ bar_char = self.chars[2]
+ end
+ local line = lines[i]
+ lines[i] = fn.strcharpart(line, 0, fn.strwidth(line) - 1) .. bar_char
+ end
+ api.nvim_buf_set_lines(self.bufnr, 1, -2, false, lines)
+end
+
+function Border:update_title(title)
+ local top = api.nvim_buf_get_lines(self.bufnr, 0, 1, 0)[1]
+ local prefix = fn.strcharpart(top, 0, 3)
+ local suffix = fn.strcharpart(top, fn.strwidth(title) + 3, fn.strwidth(top))
+ title = ('%s%s%s'):format(prefix, title, suffix)
+ api.nvim_buf_set_lines(self.bufnr, 0, 1, true, {title})
+end
+
+function Border:cal_wopts()
+ local wopts = self._wopts or self.floatwin:wopts()
+ if vim.tbl_isempty(wopts) then
+ return {}
+ else
+ local anchor, zindex, width, height, col, row = wopts.anchor, wopts.zindex, wopts.width,
+ wopts.height, wopts.col, wopts.row
+ return vim.tbl_extend('force', wopts, {
+ anchor = anchor,
+ style = 'minimal',
+ width = width + 2,
+ height = height + 2,
+ col = anchor:match('W') and col - 1 or col + 1,
+ row = anchor:match('N') and row - 1 or row + 1,
+ zindex = zindex - 1
+ })
+ end
+end
+
+function Border:display()
+ local wopts = self:cal_wopts()
+
+ if vim.tbl_isempty(wopts) then
+ return
+ end
+
+ if self:validate() then
+ self:update_buf(wopts)
+ wopts.noautocmd = nil
+ api.nvim_win_set_config(self.winid, wopts)
+ else
+ self:update_buf(wopts)
+ Border:open(self.bufnr, wopts)
+ vim.wo[self.winid].winhl = 'Normal:BqfPreviewBorder'
+ end
+end
+
+return Border
diff --git a/lua/bqf/previewer/floatwin.lua b/lua/bqf/previewer/floatwin.lua
new file mode 100644
index 0000000..f60845b
--- /dev/null
+++ b/lua/bqf/previewer/floatwin.lua
@@ -0,0 +1,130 @@
+-- singleton
+local api = vim.api
+local fn = vim.fn
+
+local utils = require('bqf.utils')
+
+local FloatWin = {}
+
+function FloatWin:build(o)
+ o = o or {}
+ self.__index = self
+ self.qwinid = o.qwinid
+ self.pwinid = o.pwinid
+ self.win_height = o.win_height
+ self.win_vheight = o.win_vheight
+ self.wrap = o.wrap
+ self.wpos = require('bqf.wpos')
+ self.winid = nil
+ self.bufnr = nil
+ return self
+end
+
+function FloatWin:cal_wopts()
+ local POS = self.wpos.POS
+ local rel_pos, abs_pos = unpack(self.wpos.get_pos(self.qwinid, self.pwinid))
+
+ local qinfo = fn.getwininfo(self.qwinid)[1]
+ local width, height, col, row, anchor
+ if rel_pos == POS.ABOVE or rel_pos == POS.BELOW or abs_pos == POS.TOP or abs_pos == POS.BOTTOM then
+ local row_pos = qinfo.winrow
+ width = qinfo.width - 2
+ col = 1
+ if rel_pos == POS.ABOVE or abs_pos == POS.TOP then
+ anchor = 'NW'
+ height = math.min(self.win_height, vim.o.lines - 4 - row_pos - qinfo.height)
+ row = qinfo.height + 2
+ else
+ anchor = 'SW'
+ height = math.min(self.win_height, row_pos - 4)
+ row = -2
+ end
+ elseif rel_pos == POS.LEFT or rel_pos == POS.RIGHT or abs_pos == POS.LEFT_FAR or abs_pos ==
+ POS.RIGHT_FAR then
+ if abs_pos == POS.LEFT_FAR then
+ width = vim.o.columns - fn.win_screenpos(2)[2] - 1
+ elseif abs_pos == POS.RIGHT_FAR then
+ width = qinfo.wincol - 4
+ else
+ width = api.nvim_win_get_width(self.pwinid) - 2
+ end
+ height = math.min(self.win_vheight, qinfo.height - 2)
+ local winline = fn.winline()
+ row = height >= winline and 1 or winline - height - 1
+ if rel_pos == POS.LEFT or abs_pos == POS.LEFT_FAR then
+ anchor = 'NW'
+ col = qinfo.width + 2
+ else
+ anchor = 'NE'
+ col = -2
+ end
+ else
+ return {}
+ end
+
+ if width < 1 or height < 1 then
+ return {}
+ end
+
+ return {
+ relative = 'win',
+ win = self.qwinid,
+ focusable = false,
+ anchor = anchor,
+ width = width,
+ height = height,
+ col = col,
+ row = row,
+ noautocmd = true,
+ zindex = 52
+ }
+end
+
+function FloatWin:validate()
+ return utils.is_win_valid(self.winid)
+end
+
+function FloatWin:open(bufnr, wopts)
+ self.winid = api.nvim_open_win(bufnr, false, wopts)
+ return self.winid
+end
+
+function FloatWin:close()
+ if self:validate() then
+ api.nvim_win_close(self.winid, true)
+ end
+end
+
+function FloatWin:display()
+ local wopts = self:cal_wopts()
+ self._wopts = wopts
+
+ if vim.tbl_isempty(wopts) then
+ return
+ end
+
+ if self:validate() then
+ wopts.noautocmd = nil
+ api.nvim_win_set_config(self.winid, wopts)
+ else
+ local bufnr = fn.bufnr('^BqfPreviewFloatWin$')
+ if bufnr > 0 then
+ self.bufnr = bufnr
+ else
+ self.bufnr = api.nvim_create_buf(false, true)
+ api.nvim_buf_set_name(self.bufnr, 'BqfPreviewFloatWin')
+ end
+ vim.bo[self.bufnr].bufhidden = 'hide'
+ self:open(self.bufnr, wopts)
+ local lwo = vim.wo[self.winid]
+ lwo.wrap = self.wrap
+ lwo.spell, lwo.list = false, false
+ lwo.nu, lwo.rnu = true, false
+ lwo.fen, lwo.fdm, lwo.fdc = false, 'manual', '0'
+ lwo.cursorline = true
+ lwo.signcolumn, lwo.colorcolumn = 'no', ''
+ lwo.winhl = 'Normal:BqfPreviewFloat'
+ end
+end
+
+return FloatWin
diff --git a/lua/bqf/previewer/handler.lua b/lua/bqf/previewer/handler.lua
new file mode 100644
index 0000000..a6bc8e6
--- /dev/null
+++ b/lua/bqf/previewer/handler.lua
@@ -0,0 +1,349 @@
+local M = {}
+local api = vim.api
+local fn = vim.fn
+local cmd = vim.cmd
+
+local auto_preview, delay_syntax
+local should_preview_cb
+local keep_preview, orig_pos
+local win_height, win_vheight
+local wrap, border_chars
+local last_idx
+local PLACEHOLDER_TBL
+
+local config = require('bqf.config')
+local qfs = require('bqf.qfwin.session')
+local pvs = require('bqf.previewer.session')
+local ts = require('bqf.previewer.treesitter')
+local utils = require('bqf.utils')
+
+local function exec_preview(entry, lsp_range_hl, pattern_hl)
+ local lnum, col, pattern = entry.lnum, entry.col, entry.pattern
+
+ if lnum < 1 then
+ api.nvim_win_set_cursor(0, {1, 0})
+ if pattern ~= '' then
+ fn.search(pattern, 'c')
+ end
+ elseif not pcall(api.nvim_win_set_cursor, 0, {lnum, math.max(0, col - 1)}) then
+ return
+ end
+
+ utils.zz()
+ orig_pos = api.nvim_win_get_cursor(0)
+
+ fn.clearmatches()
+
+ local pos_list = {}
+ if lsp_range_hl and not vim.tbl_isempty(lsp_range_hl) then
+ pos_list = utils.lsp_range2pos_list(lsp_range_hl)
+ elseif pattern_hl and pattern_hl ~= '' then
+ pos_list = utils.pattern2pos_list(pattern_hl)
+ elseif utils.is_dev() then
+ local end_lnum, end_col = entry.end_lnum, entry.end_col
+ pos_list = utils.qf_range2pos_list(lnum, col, end_lnum, end_col)
+ end
+
+ if not vim.tbl_isempty(pos_list) then
+ utils.matchaddpos('BqfPreviewRange', pos_list)
+ else
+ if lnum < 1 then
+ fn.matchadd('BqfPreviewRange', pattern)
+ elseif col < 1 then
+ fn.matchaddpos('BqfPreviewRange', {{lnum}})
+ end
+ end
+
+ if lnum > 0 then
+ fn.matchaddpos('BqfPreviewCursor', {{lnum, math.max(1, col)}}, 11)
+ end
+end
+
+local function preview_session(qwinid)
+ qwinid = qwinid or api.nvim_get_current_win()
+ return pvs.get(qwinid) or PLACEHOLDER_TBL
+end
+
+local function do_syntax(qwinid, idx, pbufnr)
+ local ps = preview_session(qwinid)
+ if ps == PLACEHOLDER_TBL or idx ~= last_idx or (pbufnr == ps.bufnr and ps.syntax) then
+ return
+ end
+
+ local fbufnr = ps.float_bufnr()
+
+ -- https://github.com/nvim-treesitter/nvim-treesitter/issues/898
+ -- fuxx min.js!
+ local lcount = api.nvim_buf_line_count(fbufnr)
+ local bytes = api.nvim_buf_get_offset(fbufnr, lcount)
+ -- bytes / lcount < 500 LGTM :)
+ if bytes / lcount < 500 then
+ local ei_bak = vim.o.ei
+ local ok, ft = pcall(api.nvim_buf_call, fbufnr, function()
+ vim.o.ei = 'FileType'
+ vim.bo.ft = 'bqfpreview'
+ cmd(('do filetypedetect BufRead %s'):format(
+ fn.fnameescape(api.nvim_buf_get_name(ps.bufnr))))
+ return vim.bo.ft
+ end)
+ vim.o.ei = ei_bak
+
+ if ok and ft ~= 'bqfpreview' then
+ ps.syntax = ts.attach(pbufnr, fbufnr, ft)
+ if not ps.syntax then
+ vim.bo[fbufnr].syntax = ft
+ ps.syntax = true
+ end
+ end
+ end
+end
+
+function M.auto_enabled()
+ return auto_preview
+end
+
+function M.keep_preview()
+ keep_preview = true
+end
+
+function M.toggle_mode()
+ local qwinid = api.nvim_get_current_win()
+ local ps = preview_session(qwinid)
+ if ps == PLACEHOLDER_TBL then
+ return
+ end
+
+ ps.full = ps.full ~= true
+ last_idx = -1
+ M.open(qwinid, nil, true)
+end
+
+function M.close(qwinid)
+ if keep_preview then
+ keep_preview = nil
+ return
+ end
+
+ last_idx = -1
+ pvs.close()
+
+ ts.shrink_cache()
+
+ qwinid = qwinid or api.nvim_get_current_win()
+ local ps = preview_session(qwinid)
+ if ps then
+ ps.bufnr = nil
+ end
+end
+
+function M.open(qwinid, qidx, force)
+ qwinid = qwinid or api.nvim_get_current_win()
+ local qs = qfs.get(qwinid)
+ local qlist = qs:list()
+ local pwinid = qs:pwinid()
+ local ps = preview_session(qwinid)
+
+ if ps == PLACEHOLDER_TBL or api.nvim_tabpage_list_wins(0) == 1 or fn.win_gettype(pwinid) ~= '' then
+ return
+ end
+
+ qidx = qidx or api.nvim_win_get_cursor(qwinid)[1]
+ if qidx == last_idx then
+ return
+ end
+
+ last_idx = qidx
+
+ local entry = qlist:get_entry(qidx)
+ if not entry then
+ M.close(qwinid)
+ return
+ end
+
+ local pbufnr = entry.bufnr
+
+ if pbufnr == 0 or not api.nvim_buf_is_valid(pbufnr) then
+ M.close(qwinid)
+ return
+ end
+
+ if ps.bufnr ~= pbufnr and not force and should_preview_cb and not should_preview_cb(pbufnr) then
+ M.close(qwinid)
+ return
+ end
+
+ ps:valid_or_build(pwinid)
+
+ pvs.display()
+
+ local fbufnr = pvs.float_bufnr()
+ if not fbufnr then
+ return
+ end
+
+ if ps.bufnr ~= pbufnr then
+ pvs.floatbuf_reset()
+ ts.disable_active(fbufnr)
+
+ utils.transfer_buf(pbufnr, fbufnr)
+ ps.bufnr = pbufnr
+ ps.syntax = ts.try_attach(pbufnr, fbufnr)
+ end
+
+ if not ps.syntax then
+ vim.defer_fn(function()
+ do_syntax(qwinid, qidx, pbufnr)
+ end, delay_syntax)
+ end
+
+ local ctx = qlist:get_context().bqf or {}
+ local lsp_ranges_hl, pattern_hl = ctx.lsp_ranges_hl, ctx.pattern_hl
+ local lsp_range_hl
+ if type(lsp_ranges_hl) == 'table' then
+ lsp_range_hl = lsp_ranges_hl[qidx]
+ end
+
+ pvs.floatwin_exec(function()
+ exec_preview(entry, lsp_range_hl, pattern_hl)
+ cmd(('noa call nvim_set_current_win(%d)'):format(pwinid))
+ end)
+
+ local size = qlist:get_qflist({size = 0}).size
+ pvs.update_border(pbufnr, qidx, size)
+end
+
+function M.scroll(direction)
+ if pvs.validate() and direction then
+ local qs = qfs.get(api.nvim_get_current_win())
+ local pwinid = qs:pwinid()
+ pvs.floatwin_exec(function()
+ if direction == 0 then
+ api.nvim_win_set_cursor(0, orig_pos)
+ else
+ -- ^D = 0x04, ^U = 0x15
+ cmd(('norm! %c'):format(direction > 0 and 0x04 or 0x15))
+ end
+ utils.zz()
+ cmd(('noa call nvim_set_current_win(%d)'):format(pwinid))
+ end)
+ pvs.update_scrollbar()
+ end
+end
+
+function M.toggle()
+ local ps = preview_session()
+ if ps == PLACEHOLDER_TBL then
+ return
+ end
+ auto_preview = auto_preview ~= true
+ if auto_preview then
+ api.nvim_echo({{'Enable preview automatically', 'WarningMsg'}}, true, {})
+ M.open()
+ else
+ api.nvim_echo({{'Disable preview automatically', 'WarningMsg'}}, true, {})
+ M.close()
+ end
+end
+
+function M.toggle_item()
+ if pvs.validate() then
+ M.close()
+ else
+ M.open(nil, nil, true)
+ end
+end
+
+function M.move_cursor()
+ local qwinid = api.nvim_get_current_win()
+ local ps = preview_session(qwinid)
+ if ps == PLACEHOLDER_TBL then
+ return
+ end
+
+ if auto_preview then
+ M.open()
+ else
+ if api.nvim_win_get_cursor(qwinid)[1] ~= last_idx then
+ M.close()
+ end
+ end
+end
+
+function M.redraw_win()
+ if pvs.validate() then
+ M.close()
+ M.open()
+ end
+end
+
+function M.initialize(qwinid)
+ cmd([[
+ aug BqfPreview
+ au! *
+ au VimResized lua require('bqf.previewer.handler').redraw_win()
+ au CursorMoved lua require('bqf.previewer.handler').move_cursor()
+ au WinLeave,BufWipeout lua require('bqf.previewer.handler').close()
+ aug END
+ ]])
+
+ pvs:new(qwinid, {
+ win_height = win_height,
+ win_vheight = win_vheight,
+ wrap = wrap,
+ border_chars = border_chars
+ })
+
+ -- some plugins will change the quickfix window, preview window should init later
+ vim.defer_fn(function()
+ last_idx = -1
+ -- delayed called, qwinid maybe invalid
+ if not utils.is_win_valid(qwinid) then
+ return
+ end
+
+ if auto_preview and api.nvim_get_current_win() == qwinid then
+ M.open(qwinid)
+ end
+ end, 50)
+end
+
+local function init()
+ local pconf = config.preview
+ vim.validate({preview = {pconf, 'table'}})
+ auto_preview = pconf.auto_preview
+ delay_syntax = tonumber(pconf.delay_syntax)
+ wrap = pconf.wrap
+ should_preview_cb = pconf.should_preview_cb
+ border_chars = pconf.border_chars
+ win_height = tonumber(pconf.win_height)
+ win_vheight = tonumber(pconf.win_vheight or win_height)
+ vim.validate({
+ auto_preview = {auto_preview, 'boolean'},
+ delay_syntax = {delay_syntax, 'number'},
+ wrap = {wrap, 'boolean'},
+ should_preview_cb = {should_preview_cb, 'function', true},
+ border_chars = {
+ border_chars, function(chars)
+ return type(chars) == 'table' and #chars == 9
+ end, 'a table with 9 chars'
+ },
+ win_height = {win_height, 'number'},
+ win_vheight = {win_vheight, 'number'}
+ })
+ cmd('hi default link BqfPreviewFloat Normal')
+ cmd('hi default link BqfPreviewBorder Normal')
+ cmd('hi default link BqfPreviewCursor Cursor')
+ cmd('hi default link BqfPreviewRange IncSearch')
+
+ cmd([[
+ aug BqfPreview
+ au!
+ aug END
+ ]])
+
+ PLACEHOLDER_TBL = {}
+end
+
+init()
+
+return M
diff --git a/lua/bqf/previewer/session.lua b/lua/bqf/previewer/session.lua
new file mode 100644
index 0000000..ef19a72
--- /dev/null
+++ b/lua/bqf/previewer/session.lua
@@ -0,0 +1,112 @@
+local api = vim.api
+local cmd = vim.cmd
+
+local floatwin = require('bqf.previewer.floatwin')
+local border = require('bqf.previewer.border')
+local utils = require('bqf.utils')
+
+local PreviewerSession = {pool = {}}
+
+function PreviewerSession:new(winid, o)
+ o = o or {}
+ local obj = {}
+ setmetatable(obj, self)
+ self.__index = self
+ obj.winid = winid
+ obj.win_height = o.win_height
+ obj.win_vheight = o.win_vheight
+ obj.wrap = o.wrap
+ obj.border_chars = o.border_chars
+ obj.bufnr = nil
+ obj.syntax = nil
+ obj.full = false
+ self.pool[winid] = obj
+ return obj
+end
+
+function PreviewerSession.get(winid)
+ winid = winid or api.nvim_get_current_win()
+ return PreviewerSession.pool[winid]
+end
+
+function PreviewerSession.clean()
+ for w_id in pairs(PreviewerSession.pool) do
+ if not utils.is_win_valid(w_id) then
+ PreviewerSession.pool[w_id] = nil
+ end
+ end
+end
+
+function PreviewerSession.floatbuf_reset()
+ local fwinid = floatwin.winid
+ local fbufnr = floatwin.bufnr
+ local bbufnr = border.bufnr
+
+ -- 1. make ml_flags empty
+ -- 2. treesitter can't clean parser cache until to unload buffer
+ -- https://github.com/neovim/neovim/pull/14995
+ cmd(('noa call nvim_win_set_buf(%d, %d)'):format(fwinid, bbufnr))
+ cmd(('noa bun %d'):format(fbufnr))
+ cmd(('noa call nvim_win_set_buf(%d, %d)'):format(fwinid, fbufnr))
+
+end
+
+function PreviewerSession.floatwin_exec(func)
+ if PreviewerSession.validate() then
+ utils.win_execute(floatwin.winid, func)
+ end
+end
+
+function PreviewerSession.float_bufnr()
+ return floatwin.bufnr
+end
+
+function PreviewerSession.border_bufnr()
+ return border.bufnr
+end
+
+function PreviewerSession.float_winid()
+ return floatwin.winid
+end
+
+function PreviewerSession.border_winid()
+ return border.winid
+end
+
+function PreviewerSession.close()
+ floatwin:close()
+ border:close()
+end
+
+function PreviewerSession.validate()
+ return floatwin:validate() and border:validate()
+end
+
+function PreviewerSession.update_border(pbufnr, qidx, size)
+ border:update(pbufnr, qidx, size)
+end
+
+function PreviewerSession.update_scrollbar()
+ border:update_scrollbar()
+end
+
+function PreviewerSession.display()
+ floatwin:display()
+ border:display()
+end
+
+function PreviewerSession:valid_or_build(owinid)
+ if not floatwin:validate() then
+ floatwin:build({qwinid = self.winid, pwinid = owinid, wrap = self.wrap})
+ end
+ if not border:validate() then
+ border:build({chars = self.border_chars})
+ end
+ if self.full then
+ floatwin.win_height, floatwin.win_vheight = 999, 999
+ else
+ floatwin.win_height, floatwin.win_vheight = self.win_height, self.win_vheight
+ end
+end
+
+return PreviewerSession
diff --git a/lua/bqf/previewer/treesitter.lua b/lua/bqf/previewer/treesitter.lua
new file mode 100644
index 0000000..1e9d634
--- /dev/null
+++ b/lua/bqf/previewer/treesitter.lua
@@ -0,0 +1,151 @@
+local M = {}
+
+local api = vim.api
+
+local parsers, configs
+local parsers_cache, queries_cache
+local parsers_litmit, queries_limit
+local lru
+local initialized
+
+local function prepare_context(parser, pbufnr, fbufnr, loaded)
+ local cb
+ if loaded then
+ parser._source = fbufnr
+ cb = parser._callbacks
+ parser._callbacks = vim.deepcopy(cb)
+ end
+
+ local hl_config = configs.get_module('highlight')
+ for k, v in pairs(hl_config.custom_captures) do
+ vim.treesitter.highlighter.hl_map[k] = v
+ end
+ local lang = parser:lang()
+
+ local ts_hl = vim.treesitter.highlighter.new(parser)
+ local query = queries_cache:get(lang)
+ if query then
+ ts_hl._queries[lang] = query
+ else
+ queries_cache:set(lang, ts_hl:get_query(lang))
+ end
+
+ if loaded then
+ parser._source = pbufnr
+ parser._callbacks = cb
+ end
+ local is_table = type(hl_config.additional_vim_regex_highlighting) == 'table'
+ if hl_config.additional_vim_regex_highlighting and
+ (not is_table or vim.tbl_contains(hl_config.additional_vim_regex_highlighting, lang)) then
+ vim.bo[pbufnr].syntax = 'on'
+ end
+end
+
+function M.disable_active(bufnr)
+ if not initialized then
+ return
+ end
+ if vim.treesitter.highlighter.active[bufnr] then
+ vim.treesitter.highlighter.active[bufnr] = nil
+ end
+end
+
+function M.try_attach(pbufnr, fbufnr)
+ local ret = false
+ if not initialized then
+ return ret
+ end
+ local loaded = api.nvim_buf_is_loaded(pbufnr)
+ local parser
+ if loaded then
+ parser = parsers.get_parser(pbufnr)
+ else
+ parser = parsers_cache:get(pbufnr)
+ if parser then
+ local sbufnr = parser:source()
+ if not api.nvim_buf_is_valid(sbufnr) then
+ parser = nil
+ parsers_cache:set(pbufnr, nil)
+ end
+ end
+ end
+ if parser and configs.is_enabled('highlight', parser:lang()) then
+ prepare_context(parser, pbufnr, fbufnr, loaded)
+ ret = true
+ end
+ return ret
+end
+
+function M.attach(pbufnr, fbufnr, ft)
+ local ret = false
+ if not initialized then
+ return ret
+ end
+ local lang = parsers.ft_to_lang(ft)
+ if not configs.is_enabled('highlight', lang) then
+ return ret
+ end
+
+ local parser
+ local loaded = api.nvim_buf_is_loaded(pbufnr)
+
+ parser = parsers_cache:get(pbufnr)
+ if loaded then
+ if parser then
+ -- delete old cache if buffer has loaded
+ parsers_cache:set(pbufnr, nil)
+ end
+ parser = parsers.get_parser(pbufnr, lang)
+ else
+ parser = parsers.get_parser(fbufnr, lang)
+ parsers_cache:set(pbufnr, parser)
+ end
+ if parser then
+ prepare_context(parser, pbufnr, fbufnr, loaded)
+ ret = true
+ end
+ return ret
+end
+
+function M.shrink_cache()
+ if not initialized then
+ return
+ end
+
+ -- shrink cache, keep usage fo memory proper
+ local cnt = parsers_litmit / 4
+ for bufnr in parsers_cache:pairs() do
+ if api.nvim_buf_is_loaded(bufnr) or not api.nvim_buf_is_valid(bufnr) or cnt < 1 then
+ parsers_cache:set(bufnr, nil)
+ else
+ cnt = cnt - 1
+ end
+ end
+
+ cnt = queries_limit / 2
+ for bufnr in queries_cache:pairs() do
+ if cnt < 1 then
+ queries_cache:set(bufnr, nil)
+ else
+ cnt = cnt - 1
+ end
+ end
+end
+
+local function init()
+ initialized, parsers = pcall(require, 'nvim-treesitter.parsers')
+ if not initialized then
+ return
+ end
+ initialized = true
+ configs = require('nvim-treesitter.configs')
+ lru = require('bqf.struct.lru')
+
+ parsers_litmit, queries_limit = 40, 10
+ parsers_cache = lru:new(parsers_litmit)
+ queries_cache = lru:new(queries_limit)
+end
+
+init()
+
+return M
diff --git a/lua/bqf/qfpos.lua b/lua/bqf/qfpos.lua
deleted file mode 100644
index c1d5831..0000000
--- a/lua/bqf/qfpos.lua
+++ /dev/null
@@ -1,101 +0,0 @@
-local M = {}
-local fn = vim.fn
-
-local function node_info(winlayout, winid, p_indicator, level, index)
- level = level or 0
- index = index or 1
- local indicator = winlayout[1]
- if indicator == 'leaf' then
- if winlayout[2] == winid then
- return p_indicator, level, index
- end
- else
- for i = 1, #winlayout[2] do
- local p, d, idx = node_info(winlayout[2][i], winid, indicator, level + 1, i)
- if p then
- return p, d, idx
- end
- end
- end
- return
-end
-
-local function adjacent_wins(winlayout, is_bottom)
- local wins = {}
- local ind, tbl = winlayout[1], winlayout[2]
- if ind == 'leaf' then
- wins = {tbl}
- elseif ind == 'col' then
- wins = adjacent_wins(tbl[is_bottom and #tbl or 1], is_bottom)
- else
- for i = 1, #tbl do
- local wins2 = adjacent_wins(tbl[i], is_bottom)
- for j = 1, #wins2 do
- wins[#wins + 1] = wins2[j]
- end
- end
- end
- return wins
-end
-
-function M.find_adjacent_wins(qf_winid, file_winid)
- local wins = {}
- local rel_pos, abs_pos = unpack(M.get_pos(qf_winid, file_winid))
- if rel_pos == 'above' or rel_pos == 'below' then
- wins = {file_winid}
- elseif abs_pos == 'top' or abs_pos == 'bottom' then
- local nest = fn.winlayout()[2]
- if abs_pos == 'top' then
- wins = adjacent_wins(nest[2], false)
- else
- wins = adjacent_wins(nest[#nest - 1], true)
- end
- end
- return wins
-end
-
--- get_pos is fast enough, no need to add a cache
-function M.get_pos(qf_winid, file_winid)
- local layout = fn.winlayout()
- local nested = layout[2]
- local rel_pos, abs_pos = 'unknown', 'unknown'
- if type(nested) ~= 'table' or #nested < 2 then
- return {rel_pos, abs_pos}
- end
- local qf_p_ind, qf_level, qf_index = node_info(layout, qf_winid)
- if qf_level == 1 then
- if qf_index == 1 then
- if qf_p_ind == 'col' then
- abs_pos = 'top'
- else
- abs_pos = 'left_far'
- end
- elseif qf_index == #nested then
- if qf_p_ind == 'col' then
- abs_pos = 'bottom'
- else
- abs_pos = 'right_far'
- end
- end
- end
- local f_p_ind, f_level, f_index = node_info(layout, file_winid)
- if f_level == qf_level then
- local offset_index = f_index - qf_index
- if f_p_ind == 'col' then
- if offset_index == 1 then
- rel_pos = 'above'
- elseif offset_index == -1 then
- rel_pos = 'below'
- end
- else
- if offset_index == 1 then
- rel_pos = 'left'
- elseif offset_index == -1 then
- rel_pos = 'right'
- end
- end
- end
- return {rel_pos, abs_pos}
-end
-
-return M
diff --git a/lua/bqf/qfsession.lua b/lua/bqf/qfsession.lua
deleted file mode 100644
index 741e5eb..0000000
--- a/lua/bqf/qfsession.lua
+++ /dev/null
@@ -1,39 +0,0 @@
-local M = {}
-local holder = {}
-
-local api = vim.api
-
-setmetatable(M, {
- __index = function(_, k)
- if type(k) ~= 'number' then
- return nil
- end
- -- TODO some events inside non-quickfix window will require a quickfix session,
- -- return empty table that holder never maintain.
- return holder[k] or {}
- end
-})
-
-function M.attach(winid)
- if not holder[winid] then
- holder[winid] = {}
- end
- return holder[winid]
-end
-
-function M.release(winid)
- if winid then
- holder[winid] = nil
- end
- for w_id in pairs(holder) do
- if not api.nvim_win_is_valid(w_id) then
- holder[w_id] = nil
- end
- end
-end
-
-function M.holder()
- return holder
-end
-
-return M
diff --git a/lua/bqf/qftool.lua b/lua/bqf/qftool.lua
deleted file mode 100644
index 38a78bd..0000000
--- a/lua/bqf/qftool.lua
+++ /dev/null
@@ -1,190 +0,0 @@
-local M = {}
-local api = vim.api
-local fn = vim.fn
-local cmd = vim.cmd
-
-local qfs = require('bqf.qfsession')
-
-local cache = {count = 0}
-
-local function close(winid)
- local ok, msg = pcall(api.nvim_win_close, winid, false)
- if not ok then
- -- Vim:E444: Cannot close last window
- if msg:match('^Vim:E444') then
- cmd('new')
- api.nvim_win_close(winid, true)
- end
- end
-end
-
-function M.validate_qf(winid)
- winid = winid or api.nvim_get_current_win()
-
- -- Invalid window id
- -- Key not found: quickfix_title
- local ret = pcall(function()
- api.nvim_win_get_var(winid, 'quickfix_title')
- end)
-
- -- quickfix_title is undefined, copen when quickfix list is empty
- if not ret then
- ret = pcall(function()
- ret = fn.getwininfo(winid)[1].quickfix == 1
- end) and ret
- end
- return ret
-end
-
-function M.filewinid(winid)
- winid = winid or api.nvim_get_current_win()
- local file_winid = qfs[winid].file_winid
- if not file_winid or not api.nvim_win_is_valid(file_winid) then
- if M.type(winid) == 'loc' then
- file_winid = fn.getloclist(winid, {filewinid = 0}).filewinid
- else
- file_winid = fn.win_getid(fn.winnr('#'))
- if file_winid <= 0 and not api.nvim_win_is_valid(file_winid) or
- M.validate_qf(file_winid) then
- local tabpage = api.nvim_win_get_tabpage(winid)
- for _, w_id in ipairs(api.nvim_tabpage_list_wins(tabpage)) do
- if api.nvim_win_is_valid(w_id) and not M.validate_qf(w_id) and
- api.nvim_win_get_config(w_id).relative == '' then
- file_winid = w_id
- break
- end
- end
- end
- end
- if file_winid <= 0 or not api.nvim_win_is_valid(file_winid) or M.validate_qf(file_winid) then
- close(winid)
- qfs[winid].file_winid = -1
- -- assert(false, 'A valid file window is not found in current tabpage')
- end
- qfs[winid].file_winid = file_winid
- end
- return qfs[winid].file_winid
-end
-
-function M.type(winid)
- winid = winid or api.nvim_get_current_win()
-
- vim.validate({winid = {winid, M.validate_qf, 'a valid quickfix window'}})
-
- if not qfs[winid].qf_type then
- qfs[winid].qf_type = fn.getwininfo(winid)[1].loclist == 1 and 'loc' or 'qf'
- end
- return qfs[winid].qf_type
-end
-
-function M.getall(winid)
- winid = winid or api.nvim_get_current_win()
-
- local qf_info = M.get({id = 0, changedtick = 0}, winid)
- local qf_all = cache[qf_info.id]
- if qf_all and qf_all.changedtick == qf_info.changedtick then
- return qf_all
- end
-
- qf_all = M.get({all = 0}, winid)
- if type(qf_all.context) == 'table' then
- local bqf_ctx = qf_all.context.bqf
- if bqf_ctx then
- qf_all.pattern_hl = bqf_ctx.pattern_hl
- qf_all.lsp_ranges_hl = bqf_ctx.lsp_ranges_hl
- end
- end
-
- cache[qf_all.id] = qf_all
-
- -- help GC manually
- cache.count = cache.count + 1
- if cache.count == 10 then
- local new_cache = {count = 0}
- for i = 1, M.get({nr = '$'}, winid).nr do
- local id = M.get({nr = i, id = 0}, winid).id
- new_cache[id] = cache[id]
- end
- cache = new_cache
- end
-
- return qf_all
-end
-
-function M.get(what, winid)
- local qf_type = M.type(winid)
- winid = winid or api.nvim_get_current_win()
- if not what or type(what) == 'table' and vim.tbl_isempty(what) then
- return qf_type == 'loc' and fn.getloclist(winid) or fn.getqflist()
- else
- return qf_type == 'loc' and fn.getloclist(winid, what) or fn.getqflist(what)
- end
-end
-
-function M.set(what, winid)
- local qf_type = M.type(winid)
- if qf_type == 'loc' then
- fn.setloclist(winid or api.nvim_get_current_win(), {}, ' ', what)
- else
- fn.setqflist({}, ' ', what)
- end
-end
-
-function M.update(what, winid)
- local qf_all = M.getall(winid)
-
- local qf_type = M.type(winid)
- if qf_type == 'loc' then
- fn.setloclist(winid or api.nvim_get_current_win(), {}, 'r', what)
- else
- fn.setqflist({}, 'r', what)
- end
-
- local qf_info = M.get({changedtick = 0}, winid)
- qf_all.changedtick = qf_info.changedtick
-end
-
-function M.file(next)
- local lnum, col = unpack(api.nvim_win_get_cursor(0))
- local qf_all = M.getall()
- local items, size = qf_all.items, qf_all.size
- local cur_bufnr = items[lnum].bufnr
- local start, stop, step = unpack(next and {lnum + 1, size, 1} or {lnum - 1, 1, -1})
-
- for i = start, stop, step do
- if items[i].valid == 1 and items[i].bufnr ~= cur_bufnr then
- M.update({idx = i})
- api.nvim_win_set_cursor(0, {i, col})
- return
- end
- end
- api.nvim_echo({{'No more items', 'WarningMsg'}}, true, {})
-end
-
-function M.history(direction)
- local prefix = M.type() == 'loc' and 'l' or 'c'
- local cur_nr, last_nr = M.get({nr = 0}).nr, M.get({nr = '$'}).nr
- if last_nr <= 1 then
- return
- end
-
- local ok, msg = pcall(cmd, ([[sil exe '%d%s%s']]):format(vim.v.count1, prefix,
- direction and 'newer' or 'older'))
- if not ok then
- if msg:match(':E380: At bottom') then
- cmd(([[sil exe '%d%snewer']]):format(last_nr - cur_nr, prefix))
- elseif msg:match(':E381: At top') then
- cmd(([[sil exe '%d%solder']]):format(last_nr - 1, prefix))
- end
- end
-
- local qf_list = M.get({nr = 0, size = 0, title = 0})
- local nr, size, title = qf_list.nr, qf_list.size, qf_list.title
-
- api.nvim_echo({
- {'('}, {tostring(nr), 'Identifier'}, {' of '}, {tostring(last_nr), 'Identifier'}, {') ['},
- {tostring(size), 'Type'}, {'] '}, {' >> ' .. title, 'Title'}
- }, false, {})
-end
-
-return M
diff --git a/lua/bqf/qfwin/handler.lua b/lua/bqf/qfwin/handler.lua
new file mode 100644
index 0000000..dce355f
--- /dev/null
+++ b/lua/bqf/qfwin/handler.lua
@@ -0,0 +1,221 @@
+local M = {}
+local api = vim.api
+local cmd = vim.cmd
+
+local qfs = require('bqf.qfwin.session')
+local utils = require('bqf.utils')
+
+function M.sign_reset()
+ local qwinid = api.nvim_get_current_win()
+ local qs = qfs.get(qwinid)
+ local qlist = qs:list()
+ local sign = qlist:get_sign()
+ sign:reset()
+end
+
+function M.sign_toggle(rel, lnum, bufnr)
+ lnum = lnum or api.nvim_win_get_cursor(0)[1]
+ bufnr = bufnr or api.nvim_get_current_buf()
+ local qwinid = api.nvim_get_current_win()
+ local qs = qfs.get(qwinid)
+ local qlist = qs:list()
+ local sign = qlist:get_sign()
+ sign:toggle(lnum, bufnr)
+ if rel ~= 0 then
+ cmd(('norm! %s'):format(rel > 0 and 'j' or 'k'))
+ end
+end
+
+function M.sign_toggle_buf(lnum, bufnr)
+ bufnr = bufnr or api.nvim_get_current_buf()
+ lnum = lnum or api.nvim_win_get_cursor(0)[1]
+ local qwinid = api.nvim_get_current_win()
+ local qs = qfs.get(qwinid)
+ local qlist = qs:list()
+ local items = qlist:get_items()
+ local entry_bufnr = items[lnum].bufnr
+ local lnum_list = {}
+ for l, entry in ipairs(items) do
+ if entry.bufnr == entry_bufnr then
+ table.insert(lnum_list, l)
+ end
+ end
+ local sign = qlist:get_sign()
+ sign:toggle(lnum_list, bufnr)
+end
+
+-- only work under map with
+function M.sign_vm_toggle(bufnr)
+ local mode = api.nvim_get_mode().mode
+ vim.validate({
+ mode = {
+ mode, function(m)
+ -- ^V = 0x16
+ return m:lower() == 'v' or m == ('%c'):format(0x16)
+ end, 'visual mode'
+ }
+ })
+ -- ^[ = 0x1b
+ cmd(('norm! %c'):format(0x1b))
+ bufnr = bufnr or api.nvim_get_current_buf()
+ local s_lnum = api.nvim_buf_get_mark(bufnr, '<')[1]
+ local e_lnum = api.nvim_buf_get_mark(bufnr, '>')[1]
+ local lnum_list = {}
+ for i = s_lnum, e_lnum do
+ table.insert(lnum_list, i)
+ end
+ local qwinid = api.nvim_get_current_win()
+ local qs = qfs.get(qwinid)
+ local qlist = qs:list()
+ local sign = qlist:get_sign()
+ sign:toggle(lnum_list, bufnr)
+end
+
+function M.sign_clear(bufnr)
+ local qwinid = api.nvim_get_current_win()
+ local qs = qfs.get(qwinid)
+ local qlist = qs:list()
+ local sign = qlist:get_sign()
+ sign:clear(bufnr)
+end
+
+function M.nav_history(direction)
+ local qwinid = api.nvim_get_current_win()
+ local qs = qfs.get(qwinid)
+ local qlist = qs:list()
+ local prefix = qlist.type == 'loc' and 'l' or 'c'
+ local cur_nr, last_nr = qlist:get_qflist({nr = 0}).nr, qlist:get_qflist({nr = '$'}).nr
+ if last_nr <= 1 then
+ return
+ end
+
+ local ok, msg = pcall(cmd, ([[sil exe '%d%s%s']]):format(vim.v.count1, prefix,
+ direction and 'newer' or 'older'))
+ if not ok then
+ if msg:match(':E380: At bottom') then
+ cmd(([[sil exe '%d%snewer']]):format(last_nr - cur_nr, prefix))
+ elseif msg:match(':E381: At top') then
+ cmd(([[sil exe '%d%solder']]):format(last_nr - 1, prefix))
+ end
+ end
+
+ local qinfo = qlist:get_qflist({nr = 0, size = 0, title = 0})
+ local nr, size, title = qinfo.nr, qinfo.size, qinfo.title
+
+ api.nvim_echo({
+ {'('}, {tostring(nr), 'Identifier'}, {' of '}, {tostring(last_nr), 'Identifier'}, {') ['},
+ {tostring(size), 'Type'}, {'] '}, {' >> ' .. title, 'Title'}
+ }, false, {})
+end
+
+function M.nav_file(next)
+ local lnum, col = unpack(api.nvim_win_get_cursor(0))
+ local qwinid = api.nvim_get_current_win()
+ local qs = qfs.get(qwinid)
+ local qlist = qs:list()
+ local items, size = qlist:get_items(), qlist:get_qflist({size = 0}).size
+ local cur_bufnr = items[lnum].bufnr
+ local start, stop, step = unpack(next and {lnum + 1, size, 1} or {lnum - 1, 1, -1})
+
+ for i = start, stop, step do
+ if items[i].valid == 1 and items[i].bufnr ~= cur_bufnr then
+ qlist:change_idx(i)
+ api.nvim_win_set_cursor(0, {i, col})
+ return
+ end
+ end
+ api.nvim_echo({{'No more items', 'WarningMsg'}}, true, {})
+end
+
+local function validate_size(qlist)
+ local valid = qlist:get_qflist({size = 0}).size > 0
+ if not valid then
+ api.nvim_err_writeln('E42: No Errors')
+ end
+ return valid
+end
+
+local function do_edit(qwinid, idx, close, action)
+ qwinid = qwinid or api.nvim_get_current_win()
+ local qs = qfs.get(qwinid)
+ local pwinid = qs:pwinid()
+ assert(utils.is_win_valid(pwinid), 'file window is invalid')
+
+ local qlist = qs:list()
+ if not validate_size(qlist) then
+ return false
+ end
+
+ idx = idx or api.nvim_win_get_cursor(qwinid)[1]
+ qlist:change_idx(idx)
+ local entry = qlist:get_entry(idx)
+ local bufnr, lnum, col = entry.bufnr, entry.lnum, entry.col
+
+ if close then
+ api.nvim_win_close(qwinid, true)
+ end
+
+ api.nvim_set_current_win(pwinid)
+
+ local last_bufnr = api.nvim_get_current_buf()
+ local last_bufname = api.nvim_buf_get_name(last_bufnr)
+ local last_bufoff = api.nvim_buf_get_offset(0, 1)
+ if action and not utils.is_unname_buf(last_bufnr, last_bufname, last_bufoff) then
+ action()
+ end
+
+ api.nvim_set_current_buf(bufnr)
+ vim.bo.buflisted = true
+ pcall(api.nvim_win_set_cursor, 0, {lnum, math.max(0, col - 1)})
+
+ if utils.is_unname_buf(last_bufnr, last_bufname, last_bufoff) then
+ api.nvim_buf_delete(last_bufnr, {})
+ end
+ return true
+end
+
+function M.open(close, qwinid, idx)
+ if do_edit(qwinid, idx, close) then
+ if vim.wo.foldenable and vim.o.fdo:match('quickfix') then
+ cmd('norm! zv')
+ end
+
+ utils.zz()
+ end
+end
+
+function M.split(vertical, qwinid, idx)
+ if do_edit(qwinid, idx, true, function()
+ cmd(('%ssp'):format(vertical and 'v' or ''))
+ end) then
+ utils.zz()
+ end
+end
+
+function M.tabedit(stay, qwinid, idx)
+ local last_tp = api.nvim_get_current_tabpage()
+ qwinid = qwinid or api.nvim_get_current_win()
+ local dummy_bufnr
+ if do_edit(qwinid, idx, false, function()
+ cmd(('%s tabedit'):format(stay and 'noa' or ''))
+ dummy_bufnr = api.nvim_get_current_buf()
+ end) then
+ utils.zz()
+
+ if dummy_bufnr then
+ -- user may use other autocmds to wipe out unnamed buffer
+ if api.nvim_buf_is_valid(dummy_bufnr) and api.nvim_buf_get_name(dummy_bufnr) == '' then
+ cmd(('noa bw %d'):format(dummy_bufnr))
+ end
+ end
+ local cur_tp = api.nvim_get_current_tabpage()
+
+ api.nvim_set_current_win(qwinid)
+
+ if last_tp ~= cur_tp and not stay then
+ api.nvim_set_current_tabpage(cur_tp)
+ end
+ end
+end
+
+return M
diff --git a/lua/bqf/qfwin/list.lua b/lua/bqf/qfwin/list.lua
new file mode 100644
index 0000000..52098c2
--- /dev/null
+++ b/lua/bqf/qfwin/list.lua
@@ -0,0 +1,161 @@
+local M = {}
+
+local api = vim.api
+local fn = vim.fn
+
+local QfList = {item_cache = {id = 0, entryies = {}}}
+QfList.pool = setmetatable({}, {
+ __index = function(tbl, id0)
+ rawset(tbl, id0, QfList:new(id0))
+ return tbl[id0]
+ end
+})
+
+local function split_id(id0)
+ local id, filewinid = unpack(vim.split(id0, ':'))
+ return tonumber(id), tonumber(filewinid)
+end
+
+local function build_id(qid, filewinid)
+ return ('%d:%d'):format(qid, filewinid or 0)
+end
+
+local function get_qflist(filewinid)
+ return filewinid > 0 and function(what)
+ return fn.getloclist(filewinid, what)
+ end or fn.getqflist
+end
+
+local function set_qflist(filewinid)
+ return filewinid > 0 and function(...)
+ return fn.setloclist(filewinid, ...)
+ end or fn.setqflist
+end
+
+function QfList:new(id0)
+ local obj = {}
+ setmetatable(obj, self)
+ self.__index = self
+ local id, filewinid = split_id(id0)
+ obj.id = id
+ obj.filewinid = filewinid
+ obj.type = filewinid == 0 and 'qf' or 'loc'
+ obj.getqflist = get_qflist(filewinid)
+ obj.setqflist = set_qflist(filewinid)
+ obj.changedtick = 0
+ return obj
+end
+
+function QfList:new_qflist(what)
+ return self.setqflist({}, ' ', what)
+end
+
+function QfList:set_qflist(what)
+ return self.setqflist({}, 'r', what)
+end
+
+function QfList:get_qflist(what)
+ return self.getqflist(what)
+end
+
+function QfList:get_changedtick()
+ local cd = self.getqflist({id = self.id, changedtick = 0}).changedtick
+ if cd ~= self.changedtick then
+ self.context = nil
+ self.sign = nil
+ QfList.item_cache = {id = 0, entryies = {}}
+ end
+ return cd
+end
+
+function QfList:get_context()
+ local ctx
+ local cd = self:get_changedtick()
+ if not self.context then
+ local qinfo = self.getqflist({id = self.id, context = 0})
+ self.changedtick = cd
+ local c = qinfo.context
+ self.context = type(c) == 'table' and c or {}
+ end
+ ctx = self.context
+ return ctx
+end
+
+function QfList:get_sign()
+ local sg
+ local cd = self:get_changedtick()
+ if not self.sign then
+ self.changedtick = cd
+ self.sign = require('bqf.qfwin.sign'):new()
+ end
+ sg = self.sign
+ return sg
+end
+
+function QfList:get_items()
+ local entryies
+ local c = QfList.item_cache
+ local c_id, c_entryies = c.id, c.entryies
+ local cd = self:get_changedtick()
+ if cd == self.changedtick and c_id == self.id then
+ entryies = c_entryies
+ end
+ if not entryies then
+ local qinfo = self.getqflist({id = self.id, items = 0})
+ entryies = qinfo.items
+ QfList.item_cache = {id = self.id, entryies = entryies}
+ end
+ return entryies
+end
+
+function QfList:get_entry(idx)
+ local cd = self:get_changedtick()
+
+ local e
+ local c = QfList.item_cache
+ if cd == self.changedtick and c.id == self.id then
+ e = c.entryies[idx]
+ else
+ local items = self.getqflist({id = self.id, idx = idx, items = 0}).items
+ if #items == 1 then
+ e = items[1]
+ end
+ end
+ return e
+end
+
+function QfList:change_idx(idx)
+ local old_idx = self:get_qflist({idx = idx})
+ if idx ~= old_idx then
+ self:set_qflist({idx = idx})
+ self.changedtick = self.getqflist({id = self.id, changedtick = 0}).changedtick
+ end
+end
+
+function M.get(qwinid, id)
+ local qid, filewinid
+ if not id then
+ qwinid = qwinid or api.nvim_get_current_win()
+ local what = {id = 0, filewinid = 0}
+ local winfo = fn.getwininfo(qwinid)[1]
+ if winfo.quickfix == 1 then
+ local qinfo = winfo.loclist == 1 and fn.getloclist(0, what) or fn.getqflist(what)
+ qid, filewinid = qinfo.id, qinfo.filewinid
+ else
+ return nil
+ end
+ else
+ qid, filewinid = unpack(id)
+ end
+ return QfList.pool[build_id(qid, filewinid or 0)]
+end
+
+function M.verify()
+ for id0, o in pairs(QfList.pool) do
+ if o.getqflist({id = o.id}).id ~= o.id then
+ QfList.pool[id0] = nil
+ end
+ end
+end
+
+return M
diff --git a/lua/bqf/qfwin/session.lua b/lua/bqf/qfwin/session.lua
new file mode 100644
index 0000000..b66517b
--- /dev/null
+++ b/lua/bqf/qfwin/session.lua
@@ -0,0 +1,104 @@
+local api = vim.api
+local fn = vim.fn
+
+local list = require('bqf.qfwin.list')
+local utils = require('bqf.utils')
+
+local validate = (function()
+ if fn.has('nvim-0.6') == 1 then
+ return function(winid)
+ local win_type = fn.win_gettype(winid)
+ return win_type == 'quickfix' or win_type == 'loclist'
+ end
+ else
+ return function(winid)
+ winid = winid or api.nvim_get_current_win()
+ local ok, ret
+ ok = pcall(function()
+ ret = fn.getwininfo(winid)[1].quickfix == 1
+ end)
+ return ok and ret
+ end
+ end
+end)()
+
+local is_normal_win_type = (function()
+ if fn.has('nvim-0.6') == 1 then
+ return function(winid)
+ return fn.win_gettype(winid) == ''
+ end
+ else
+ return function(winid)
+ return not validate(winid) and fn.win_gettype(winid) == ''
+ end
+ end
+end)()
+
+local function get_pwinid(winid, qlist)
+ local pwinid
+ if qlist.type == 'loc' then
+ pwinid = qlist.filewinid
+ else
+ pwinid = fn.win_getid(fn.winnr('#'))
+ if pwinid <= 0 or not validate(pwinid) then
+ local tabpage = api.nvim_win_get_tabpage(winid)
+ for _, owinid in ipairs(api.nvim_tabpage_list_wins(tabpage)) do
+ if is_normal_win_type(owinid) then
+ pwinid = owinid
+ break
+ end
+ end
+ end
+ end
+ if pwinid <= 0 or validate(pwinid) then
+ pwinid = -1
+ end
+ return pwinid
+end
+
+local QfSession = {pool = {}}
+
+function QfSession:new(winid)
+ local obj = {}
+ setmetatable(obj, self)
+ self.__index = self
+ obj.winid = winid
+ obj._list = list.get(winid)
+ if not obj._list then
+ return nil
+ end
+ obj._list:get_sign():reset()
+ obj._pwinid = get_pwinid(winid, obj._list)
+ self.pool[winid] = obj
+ return obj
+end
+
+function QfSession.get(winid)
+ winid = winid or api.nvim_get_current_win()
+ return QfSession.pool[winid]
+end
+
+function QfSession:list()
+ return self._list
+end
+
+function QfSession:validate()
+ return validate(self.winid)
+end
+
+function QfSession:pwinid()
+ if not utils.is_win_valid(self._pwinid) then
+ self._pwinid = get_pwinid(self.winid, self._list)
+ end
+ return self._pwinid
+end
+
+function QfSession:dispose()
+ for w_id in pairs(self.pool) do
+ if not utils.is_win_valid(w_id) then
+ self.pool[w_id] = nil
+ end
+ end
+end
+
+return QfSession
diff --git a/lua/bqf/qfwin/sign.lua b/lua/bqf/qfwin/sign.lua
new file mode 100644
index 0000000..36748c0
--- /dev/null
+++ b/lua/bqf/qfwin/sign.lua
@@ -0,0 +1,140 @@
+local api = vim.api
+local fn = vim.fn
+local cmd = vim.cmd
+
+local sprior
+local sgroup
+local sname
+local Sign = {}
+
+function Sign:new()
+ local obj = {}
+ setmetatable(obj, self)
+ self.__index = self
+ obj.items = {}
+ return obj
+end
+
+function Sign:place(lnum, bufnr)
+ bufnr = bufnr or api.nvim_get_current_buf()
+ if type(lnum) == 'table' then
+ local lnum_off_i = 1
+ local function place(p_list)
+ local ids = fn.sign_placelist(p_list)
+ for i = 1, #ids do
+ self.items[lnum[lnum_off_i]] = ids[i]
+ lnum_off_i = lnum_off_i + 1
+ end
+ end
+ local count, cycle = 0, 100
+ local place_list = {}
+ for _, l in ipairs(lnum) do
+ table.insert(place_list, {
+ id = 0,
+ group = sgroup,
+ name = sname,
+ buffer = bufnr,
+ lnum = l,
+ priority = sprior
+ })
+ count = count + 1
+ if count % cycle == 0 then
+ place(place_list)
+ count = 0
+ place_list = {}
+ end
+ end
+ if count > 0 then
+ place(place_list)
+ end
+ else
+ local id = fn.sign_place(0, sgroup, sname, bufnr, {lnum = lnum, priority = sprior})
+ self.items[lnum] = id
+ end
+end
+
+function Sign:unplace(lnum, bufnr)
+ bufnr = bufnr or api.nvim_get_current_buf()
+ if type(lnum) == 'table' then
+ local count, cycle = 0, 100
+ local unplace_list = {}
+ for _, l in ipairs(lnum) do
+ local id = self.items[l]
+ if id then
+ table.insert(unplace_list, {id = id, group = sgroup, buffer = bufnr})
+ self.items[l] = nil
+ count = count + 1
+ if count % cycle == 0 then
+ fn.sign_unplacelist(unplace_list)
+ count = 0
+ unplace_list = {}
+ end
+ end
+ end
+ if count > 0 then
+ fn.sign_unplacelist(unplace_list)
+ end
+ else
+ local id = self.items[lnum]
+ if id then
+ fn.sign_unplace(sgroup, {buffer = bufnr, id = id})
+ self.items[lnum] = nil
+ end
+ end
+
+end
+
+function Sign:list()
+ return self.items
+end
+
+function Sign:toggle(lnum, bufnr)
+ bufnr = bufnr or api.nvim_get_current_buf()
+ if type(lnum) == 'table' then
+ local p_lnum_list, up_lnum_list = {}, {}
+ for _, l in pairs(lnum) do
+ if self.items[l] then
+ table.insert(up_lnum_list, l)
+ else
+ table.insert(p_lnum_list, l)
+ end
+ end
+ self:place(p_lnum_list, bufnr)
+ self:unplace(up_lnum_list, bufnr)
+ else
+ if self.items[lnum] then
+ self:unplace(lnum, bufnr)
+ else
+ self:place(lnum, bufnr)
+ end
+ end
+end
+
+function Sign:reset(bufnr)
+ bufnr = bufnr or api.nvim_get_current_buf()
+ local p_lnum_list = {}
+ local signs = self.items
+ self:clear(bufnr)
+ for lnum in pairs(signs) do
+ table.insert(p_lnum_list, lnum)
+ end
+ self:place(p_lnum_list, bufnr)
+end
+
+function Sign:clear(bufnr)
+ self.items = {}
+ bufnr = bufnr or api.nvim_get_current_buf()
+ fn.sign_unplace(sgroup, {buffer = bufnr})
+end
+
+local function init()
+ sprior = 20
+ sgroup = 'BqfSignGroup'
+ sname = 'BqfSign'
+ cmd('hi default BqfSign ctermfg=14 guifg=Cyan')
+ fn.sign_define('BqfSign', {text = ' ^', texthl = 'BqfSign'})
+end
+
+init()
+
+return Sign
diff --git a/lua/bqf/sign.lua b/lua/bqf/sign.lua
deleted file mode 100644
index 9cc747d..0000000
--- a/lua/bqf/sign.lua
+++ /dev/null
@@ -1,107 +0,0 @@
-local M = {}
-local api = vim.api
-local fn = vim.fn
-local cmd = vim.cmd
-
-local qftool = require('bqf.qftool')
-
-local function setup()
- cmd('hi default BqfSign ctermfg=14 guifg=Cyan')
- fn.sign_define('BqfSign', {text = ' ^', texthl = 'BqfSign'})
-end
-
-local function place(bufnr, lnum)
- bufnr = bufnr or api.nvim_get_current_buf()
- lnum = lnum or api.nvim_win_get_cursor(0)[1]
- local id = fn.sign_place(lnum, 'BqfSignGroup', 'BqfSign', bufnr, {lnum = lnum, priority = 20})
- local qf_all = qftool.getall()
- if not qf_all.signs or type(qf_all.signs) ~= 'table' then
- qf_all.signs = {[lnum] = id}
- else
- qf_all.signs[lnum] = id
- end
-end
-
-local function unplace(bufnr, id)
- fn.sign_unplace('BqfSignGroup', {buffer = bufnr, id = id})
- local qf_all = qftool.getall()
- if qf_all.signs then
- if not id then
- qf_all.signs = nil
- else
- for lnum, id0 in pairs(qf_all.signs) do
- if id == id0 then
- qf_all.signs[lnum] = nil
- break
- end
- end
- end
- end
-end
-
-function M.toggle(rel, lnum, bufnr)
- lnum = lnum or api.nvim_win_get_cursor(0)[1]
- bufnr = bufnr or api.nvim_get_current_buf()
- local signs = fn.sign_getplaced(bufnr, {group = 'BqfSignGroup', lnum = lnum})[1].signs
- if signs and #signs > 0 then
- unplace(bufnr, signs[1].id)
- else
- place(bufnr, lnum)
- end
- if rel ~= 0 then
- cmd(('norm! %s'):format(rel > 0 and 'j' or 'k'))
- end
-end
-
-function M.toggle_buf(lnum, bufnr)
- bufnr = bufnr or api.nvim_get_current_buf()
- lnum = lnum or api.nvim_win_get_cursor(0)[1]
- local qf_all = qftool.getall()
- local items = qf_all.items
- local entry_bufnr = items[lnum].bufnr
- for idx, entry in ipairs(items) do
- if entry.bufnr == entry_bufnr then
- M.toggle(0, idx, bufnr)
- end
- end
-end
-
--- only work under map with
-function M.vm_toggle(bufnr)
- local mode = api.nvim_get_mode().mode
- vim.validate({
- mode = {
- mode, function(m)
- -- ^V = 0x16
- return m:lower() == 'v' or m == ('%c'):format(0x16)
- end, 'visual mode'
- }
- })
- -- ^[ = 0x1b
- fn.execute(('norm! %c'):format(0x1b))
- bufnr = bufnr or api.nvim_get_current_buf()
- local s_linenr = api.nvim_buf_get_mark(bufnr, '<')[1]
- local e_linenr = api.nvim_buf_get_mark(bufnr, '>')[1]
- for lnum = s_linenr, e_linenr do
- M.toggle(0, lnum, bufnr)
- end
-end
-
-function M.reset(bufnr)
- fn.sign_unplace('BqfSignGroup', {buffer = bufnr})
- local qf_all = qftool.getall()
- for lnum in pairs(qf_all.signs or {}) do
- place(bufnr, lnum)
- end
-end
-
-function M.clear(bufnr)
- bufnr = bufnr or api.nvim_get_current_buf()
- fn.sign_unplace('BqfSignGroup', {buffer = bufnr})
- local qf_all = qftool.getall()
- qf_all.signs = nil
-end
-
-setup()
-
-return M
diff --git a/lua/bqf/struct/lru.lua b/lua/bqf/struct/lru.lua
new file mode 100644
index 0000000..3c8b45c
--- /dev/null
+++ b/lua/bqf/struct/lru.lua
@@ -0,0 +1,157 @@
+local LRU = {Node = {}}
+
+function LRU.Node:_new(o)
+ local obj = {}
+ setmetatable(obj, self)
+ self.__index = self
+ obj.prev = o.prev
+ obj.next = o.next
+ obj.id = o.id
+ obj.obj = o.obj
+ return obj
+end
+
+function LRU:_after_access(node)
+ local id = node.id
+ if id ~= self.head.id then
+ local np = node.prev
+ local nn = node.next
+ if id == self.tail.id then
+ np.next = nil
+ self.tail = np
+ else
+ np.next = nn
+ nn.prev = np
+ end
+ local old_head = self.head
+ old_head.prev = node
+ node.prev = nil
+ node.next = old_head
+ self.head = node
+ end
+end
+
+function LRU:new(limit)
+ local obj = {}
+ setmetatable(obj, self)
+ self.__index = self
+ obj.size = 0
+ obj.limit = limit or 15
+ obj.head = nil
+ obj.tail = nil
+ obj.entries = {}
+ return obj
+end
+
+function LRU:first()
+ local first_id, first_obj
+ if self.head then
+ first_id, first_obj = self.head.id, self.head.obj
+ end
+ return first_id, first_obj
+end
+
+function LRU:last()
+ local last_id, last_obj
+ if self.tail then
+ last_id, last_obj = self.tail.id, self.tail.obj
+ self:_after_access(self.tail)
+ end
+ return last_id, last_obj
+end
+
+function LRU:get(id)
+ local obj
+ local node = self.entries[id]
+ if node then
+ obj = node.obj
+ self:_after_access(node)
+ end
+ return obj
+end
+
+function LRU:_del(id)
+ local old
+ local node = self.entries[id]
+ if node then
+ self.entries[id] = nil
+ old = node.obj
+ local np = node.prev
+ local nn = node.next
+ if id == self.head.id then
+ self.head = nn
+ if self.head then
+ self.head.prev = nil
+ else
+ self.tail = nil
+ end
+ elseif id == self.tail.id then
+ self.tail = np
+ if self.tail then
+ self.tail.next = nil
+ else
+ self.head = nil
+ end
+ else
+ np.next = nn
+ nn.prev = np
+ end
+ self.size = self.size - 1
+ end
+ return old
+end
+
+function LRU:set(id, obj)
+ local old
+ local node = self.entries[id]
+ if node then
+ if obj then
+ old = node.obj
+ node.obj = obj
+ self:_after_access(node)
+ else
+ old = self:_del(id)
+ end
+ elseif obj then
+ local new_node = LRU.Node:_new({prev = nil, next = nil, id = id, obj = obj})
+ if self.head then
+ local old_head = self.head
+ new_node.next = old_head
+ old_head.prev = new_node
+ self.head = new_node
+ else
+ self.head = new_node
+ self.tail = self.head
+ end
+
+ if self.size + 1 > self.limit then
+ local old_tail = self.tail
+ self.entries[old_tail.id] = nil
+ local new_tail = old_tail.prev
+ new_tail.next = nil
+ self.tail = new_tail
+ else
+ self.size = self.size + 1
+ end
+ self.entries[id] = new_node
+ end
+ return old
+end
+
+function LRU:pairs(reverse)
+ local node = reverse and self.tail or self.head
+ return function()
+ local id, obj
+ if node then
+ id, obj = node.id, node.obj
+ if reverse then
+ node = node.prev
+ else
+ node = node.next
+ end
+ end
+ return id, obj
+ end
+end
+
+return LRU
diff --git a/lua/bqf/supply.lua b/lua/bqf/supply.lua
deleted file mode 100644
index e2613c6..0000000
--- a/lua/bqf/supply.lua
+++ /dev/null
@@ -1,18 +0,0 @@
-local M = {}
-
-function M.tbl_kv_map(func, tbl)
- local new_tbl = {}
- for k, v in pairs(tbl) do
- new_tbl[k] = func(k, v)
- end
- return new_tbl
-end
-
-function M.tbl_concat(t1, t2)
- for i = 1, #t2 do
- t1[#t1 + 1] = t2[i]
- end
- return t1
-end
-
-return M
diff --git a/lua/bqf/utils.lua b/lua/bqf/utils.lua
index 3b10475..dcc5789 100644
--- a/lua/bqf/utils.lua
+++ b/lua/bqf/utils.lua
@@ -3,16 +3,15 @@ local api = vim.api
local fn = vim.fn
local cmd = vim.cmd
-local ansi = {
- black = 30,
- red = 31,
- green = 32,
- yellow = 33,
- blue = 34,
- magenta = 35,
- cyan = 36,
- white = 37
-}
+M.is_dev = (function()
+ local is_dev
+ return function()
+ if is_dev == nil then
+ is_dev = fn.has('nvim-0.6') == 1
+ end
+ return is_dev
+ end
+end)()
local function color2csi24b(color_num, fg)
local r = math.floor(color_num / 2 ^ 16)
@@ -39,6 +38,17 @@ function M.syntax_list(bufnr)
return list
end
+local ansi = {
+ black = 30,
+ red = 31,
+ green = 32,
+ yellow = 33,
+ blue = 34,
+ magenta = 35,
+ cyan = 36,
+ white = 37
+}
+
function M.render_str(str, group_name, def_fg, def_bg)
vim.validate({
str = {str, 'string'},
@@ -82,7 +92,7 @@ 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
- fn.execute(zb:format(lnum1))
+ cmd(zb:format(lnum1))
return
end
cmd('norm! zvzz')
@@ -90,58 +100,62 @@ function M.zz()
cmd('norm! L')
local lnum2 = api.nvim_win_get_cursor(0)[1]
if lnum2 + fn.getwinvar(0, '&scrolloff') >= lcount then
- fn.execute(zb:format(lnum2))
+ cmd(zb:format(lnum2))
end
if lnum1 ~= lnum2 then
cmd('keepj norm! ``')
end
end
-function M.lsp_range2pos_list(lsp_ranges)
- local s_line, s_char, e_line, e_char, s_lnum, e_lnum
- if not pcall(function()
- s_line, s_char = lsp_ranges.start.line, lsp_ranges.start.character
- e_line, e_char = lsp_ranges['end'].line, lsp_ranges['end'].character
- end) then
- return {}
- end
- s_lnum, e_lnum = s_line + 1, e_line + 1
- if s_line > e_line or (s_line == e_line and s_char > e_char) then
+function M.is_unname_buf(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
+
+local function range2pos_list(lnum, col, end_lnum, end_col)
+ if lnum > end_lnum or (lnum == end_col and col >= end_col) then
return {}
end
- if s_line == e_line then
- return {{s_lnum, s_char + 1, e_char - s_char}}
+ if lnum == end_lnum then
+ return {{lnum, col, end_col - col}}
end
- local pos_list = {{s_lnum, s_char + 1, 999}}
- for i = 1, e_line - s_line - 1 do
- table.insert(pos_list, {s_lnum + i})
+ local pos_list = {{lnum, col, 999}}
+ for i = 1, end_lnum - lnum - 1 do
+ table.insert(pos_list, {lnum + i})
end
- local pos = {e_lnum, 1, e_char}
+ local pos = {end_lnum, 1, end_col - 1}
table.insert(pos_list, pos)
return pos_list
end
-function M.pattern2pos_list(pattern_hl)
- local s_lnum, s_col, e_lnum, e_col
+function M.qf_range2pos_list(lnum, col, end_lnum, end_col)
+ return range2pos_list(lnum, col, end_lnum, end_col)
+end
+
+function M.lsp_range2pos_list(range)
+ local s_line, s_char, e_line, e_char
if not pcall(function()
- s_lnum, s_col = unpack(fn.searchpos(pattern_hl, 'cn'))
- e_lnum, e_col = unpack(fn.searchpos(pattern_hl, 'cen'))
+ s_line, s_char = range.start.line, range.start.character
+ e_line, e_char = range['end'].line, range['end'].character
end) then
return {}
end
- if s_lnum == 0 or s_col == 0 or e_lnum == 0 or e_col == 0 then
+ local lnum, end_lnum = s_line + 1, e_line + 1
+ local col, end_col = s_char + 1, e_char + 1
+ return range2pos_list(lnum, col, end_lnum, end_col)
+end
+
+function M.pattern2pos_list(pattern)
+ local lnum, col, end_lnum, end_col
+ if not pcall(function()
+ lnum, col = unpack(fn.searchpos(pattern, 'cn'))
+ end_lnum, end_col = unpack(fn.searchpos(pattern, 'cen'))
+ end_col = end_col + 1
+ end) then
return {}
end
- if s_lnum == e_lnum then
- return {{s_lnum, s_col, e_col - s_col + 1}}
- end
- local pos_list = {{s_lnum, s_col, 999}}
- for i = 1, e_lnum - s_lnum - 1 do
- table.insert(pos_list, {s_lnum + i})
- end
- local pos = {e_lnum, 1, e_col}
- table.insert(pos_list, pos)
- return pos_list
+ return range2pos_list(lnum, col, end_lnum, end_col)
end
function M.matchaddpos(hl, plist, prior)
@@ -175,11 +189,19 @@ function M.gutter_size(winid)
return size
end
-function M.win_execute(winid, func)
+function M.is_win_valid(winid)
+ return winid and type(winid) == 'number' and winid > 0 and api.nvim_win_is_valid(winid)
+end
+
+function M.is_buf_loaded(bufnr)
+ return bufnr and type(bufnr) == 'number' and bufnr > 0 and api.nvim_buf_is_loaded(bufnr)
+end
+
+function M.win_execute(winid, func, ...)
vim.validate({
winid = {
winid, function(w)
- return w and api.nvim_win_is_valid(w)
+ return M.is_win_valid(w)
end, 'a valid window'
},
func = {func, 'function'}
@@ -190,11 +212,11 @@ function M.win_execute(winid, func)
if cur_winid ~= winid then
cmd(noa_set_win:format(winid))
end
- local ret = func()
+ local _, msg = pcall(func, ...)
if cur_winid ~= winid then
cmd(noa_set_win:format(cur_winid))
end
- return ret
+ return msg
end
local function syn_keyword(bufnr)
@@ -297,8 +319,84 @@ function M.gen_is_keyword(bufnr)
end
end
-function M.is_special(b)
- return b <= 32
+function M.tbl_kv_map(func, tbl)
+ local new_tbl = {}
+ for k, v in pairs(tbl) do
+ new_tbl[k] = func(k, v)
+ end
+ return new_tbl
+end
+
+-- 1. use uv read file will cause much cpu usage and memory usage
+-- 2. type of result returned by read is string, it must convert to table first
+-- 3. nvim_buf_set_lines is expensive for flushing all buffers
+function M.transfer_buf(from, to)
+ local function transfer_file(rb, wb)
+ local e_path = fn.fnameescape(api.nvim_buf_get_name(rb))
+ local ok, msg = pcall(api.nvim_buf_call, wb, function()
+ cmd(([[
+ noa call deletebufline(%d, 1, '$')
+ noa sil 0read %s
+ noa call deletebufline(%d, '$')
+ ]]):format(wb, e_path, wb))
+ end)
+ return ok, msg
+ end
+ local from_loaded = api.nvim_buf_is_loaded(from)
+ if from_loaded then
+ if vim.bo[from].modified then
+ local lines = api.nvim_buf_get_lines(from, 0, -1, false)
+ api.nvim_buf_set_lines(to, 0, -1, false, lines)
+ else
+ if not transfer_file(from, to) then
+ local lines = api.nvim_buf_get_lines(from, 0, -1, false)
+ api.nvim_buf_set_lines(to, 0, -1, false, lines)
+ end
+ end
+ else
+ local ok, msg = transfer_file(from, to)
+ if not ok then
+ if msg:match([[:E484: Can't open file]]) then
+ cmd(('noa call bufload(%d)'):format(from))
+ local lines = api.nvim_buf_get_lines(from, 0, -1, false)
+ cmd(('noa bun %d'):format(from))
+ api.nvim_buf_set_lines(to, 0, -1, false, lines)
+ end
+
+ end
+ end
+ vim.bo[to].modified = false
+end
+
+function M.expandtab(str, ts, start)
+ start = start or 1
+ local new = str:sub(1, start - 1)
+ -- without check type to improve performance
+ -- if str and type(str) == 'string' then
+ 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
+ -- end
+ return new
end
return M
diff --git a/lua/bqf/wpos.lua b/lua/bqf/wpos.lua
new file mode 100644
index 0000000..7b49dbb
--- /dev/null
+++ b/lua/bqf/wpos.lua
@@ -0,0 +1,125 @@
+local M = {}
+local fn = vim.fn
+
+local POS = {
+ UNKNOWN = 0,
+ ABOVE = 1,
+ BELOW = 2,
+ TOP = 3,
+ BOTTOM = 4,
+ LEFT = 5,
+ RIGHT = 6,
+ LEFT_FAR = 7,
+ RIGHT_FAR = 8
+}
+
+M.POS = POS
+
+local function node_info(winlayout, parent, winid, level, index)
+ level = level or 0
+ index = index or 1
+ local indicator = winlayout[1]
+ if indicator == 'leaf' then
+ if winlayout[2] == winid then
+ return parent, level, index
+ end
+ else
+ for i = 1, #winlayout[2] do
+ local p, d, idx = node_info(winlayout[2][i], winlayout, winid, level + 1, i)
+ if p then
+ return p, d, idx
+ end
+ end
+ end
+ return
+end
+
+local function adjacent_wins(winlayout, is_bottom)
+ local wins = {}
+ local ind, tbl = winlayout[1], winlayout[2]
+ if ind == 'leaf' then
+ wins = {tbl}
+ elseif ind == 'col' then
+ wins = adjacent_wins(tbl[is_bottom and #tbl or 1], is_bottom)
+ else
+ for i = 1, #tbl do
+ local wins2 = adjacent_wins(tbl[i], is_bottom)
+ for j = 1, #wins2 do
+ wins[#wins + 1] = wins2[j]
+ end
+ end
+ end
+ return wins
+end
+
+function M.find_bottom_wins()
+ return adjacent_wins(fn.winlayout(), true)
+end
+
+function M.find_adjacent_wins(winid, owinid)
+ local wins = {}
+ local rel_pos, abs_pos = unpack(M.get_pos(winid, owinid))
+ if rel_pos == POS.ABOVE or rel_pos == POS.BELOW then
+ wins = {owinid}
+ elseif abs_pos == POS.TOP or abs_pos == POS.BOTTOM then
+ local nest = fn.winlayout()[2]
+ if abs_pos == POS.TOP then
+ wins = adjacent_wins(nest[2], false)
+ else
+ wins = adjacent_wins(nest[#nest - 1], true)
+ end
+ end
+ return wins
+end
+
+function M.get_pos(winid, owinid)
+ local layout = fn.winlayout()
+ local nested = layout[2]
+ local rel_pos, abs_pos = POS.UNKNOWN, POS.UNKNOWN
+ if type(nested) ~= 'table' or #nested < 2 then
+ return {rel_pos, abs_pos}
+ end
+ local parent_layout, child_level, child_index = node_info(layout, nil, winid)
+
+ -- winid doesn't exist in current tabpage
+ if not parent_layout or type(parent_layout) ~= 'table' then
+ return {rel_pos, abs_pos}
+ end
+ local parent_indicator, child_layout = unpack(parent_layout)
+ if child_level == 1 then
+ if child_index == 1 then
+ if parent_indicator == 'col' then
+ abs_pos = POS.TOP
+ else
+ abs_pos = POS.LEFT_FAR
+ end
+ elseif child_index == #nested then
+ if parent_indicator == 'col' then
+ abs_pos = POS.BOTTOM
+ else
+ abs_pos = POS.RIGHT_FAR
+ end
+ end
+ end
+ for i, wly in ipairs(child_layout) do
+ if wly[1] == 'leaf' and wly[2] == owinid then
+ local offset_index = i - child_index
+ if parent_indicator == 'col' then
+ if offset_index == 1 then
+ rel_pos = POS.ABOVE
+ elseif offset_index == -1 then
+ rel_pos = POS.BELOW
+ end
+ elseif parent_indicator == 'row' then
+ if offset_index == 1 then
+ rel_pos = POS.LEFT
+ elseif offset_index == -1 then
+ rel_pos = POS.RIGHT
+ end
+ end
+ end
+ end
+ return {rel_pos, abs_pos}
+end
+
+return M