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