From 544d5eabc8550265f08ef2c2289b4ea0dacd44ab Mon Sep 17 00:00:00 2001 From: bhagwan Date: Mon, 11 Oct 2021 01:23:00 -0700 Subject: [PATCH] new preview scrollbar option and highlights, win|preview options rework --- README.md | 68 +++-- doc/fzf-lua.txt | 68 +++-- lua/fzf-lua/config.lua | 154 ++++++++--- lua/fzf-lua/core.lua | 24 +- lua/fzf-lua/init.lua | 4 +- lua/fzf-lua/previewer/builtin.lua | 25 +- lua/fzf-lua/providers/lsp.lua | 4 +- lua/fzf-lua/utils.lua | 23 +- lua/fzf-lua/win.lua | 420 +++++++++++++++++++++--------- 9 files changed, 538 insertions(+), 252 deletions(-) diff --git a/README.md b/README.md index 84b73a3..23f3488 100644 --- a/README.md +++ b/README.md @@ -217,16 +217,48 @@ require'fzf-lua'.setup { -- "aboveleft vnew : split left -- Only valid when using a float window -- (i.e. when 'split' is not defined) - win_height = 0.85, -- window height - win_width = 0.80, -- window width - win_row = 0.30, -- window row position (0=top, 1=bottom) - win_col = 0.50, -- window col position (0=left, 1=right) - -- win_border = false, -- window border? or borderchars? - win_border = { '╭', '─', '╮', '│', '╯', '─', '╰', '│' }, - hl_normal = 'Normal', -- window normal color - hl_border = 'Normal', -- change to 'FloatBorder' if exists + height = 0.85, -- window height + width = 0.80, -- window width + row = 0.35, -- window row position (0=top, 1=bottom) + col = 0.50, -- window col position (0=left, 1=right) + -- border argument passthrough to nvim_open_win(), also used + -- to manually draw the border characters around the preview + -- window, can be set to 'false' to remove all borders or to + -- 'none', 'single', 'double' or 'rounded' (default) + border = { '╭', '─', '╮', '│', '╯', '─', '╰', '│' }, fullscreen = false, -- start fullscreen? - window_on_create = function() + hl = { + normal = 'Normal', -- window normal color (fg+bg) + border = 'Normal', -- border color (try 'FloatBorder') + -- Only valid with the builtin previewer: + cursor = 'Cursor', -- cursor highlight (grep/LSP matches) + cursorline = 'CursorLine', -- cursor line + -- title = 'Normal', -- preview border title (file/buffer) + -- scrollbar_f = 'PmenuThumb', -- scrollbar "full" section highlight + -- scrollbar_e = 'PmenuSbar', -- scrollbar "empty" section highlight + }, + preview = { + -- default = 'bat', -- override the default previewer? + -- default uses the 'builtin' previewer + border = 'border', -- border|noborder, applies only to + -- native fzf previewers (bat/cat/git/etc) + wrap = 'nowrap', -- wrap|nowrap + hidden = 'nohidden', -- hidden|nohidden + vertical = 'down:45%', -- up|down:size + horizontal = 'right:60%', -- right|left:size + layout = 'flex', -- horizontal|vertical|flex + flip_columns = 120, -- #cols to switch to horizontal on flex + -- Only valid with the builtin previewer: + title = true, -- preview border title (file/buf)? + scrollbar = 'float', -- `false` or string:'float|border' + -- float: in-window floating border + -- border: in-border chars (see below) + scrolloff = '-2', -- float scrollbar offset from right + -- applies only when scrollbar = 'float' + scrollchars = {'█', '' }, -- scrollbar chars ({ , } + -- applies only when scrollbar = 'border' + }, + on_create = function() -- called once upon creation of the fzf main window -- can be used to add custom fzf-lua mappings, e.g: -- vim.api.nvim_buf_set_keymap(0, "t", "", "", @@ -296,15 +328,6 @@ require'fzf-lua'.setup { ["header"] = { "fg", "Comment" }, ["gutter"] = { "bg", "Normal" }, }, ]] - preview_border = 'border', -- border|noborder - preview_wrap = 'nowrap', -- wrap|nowrap - preview_opts = 'nohidden', -- hidden|nohidden - preview_vertical = 'down:45%', -- up|down:size - preview_horizontal = 'right:60%', -- right|left:size - preview_layout = 'flex', -- horizontal|vertical|flex - flip_columns = 120, -- #cols to switch to horizontal on flex - -- default_previewer = "bat", -- override the default previewer? - -- by default uses the builtin previewer previewers = { cat = { cmd = "cat", @@ -327,14 +350,9 @@ require'fzf-lua'.setup { builtin = { delay = 100, -- delay(ms) displaying the preview -- prevents lag on fast scrolling - title = true, -- preview title? - scrollbar = true, -- scrollbar? - scrollchar = '█', -- scrollbar character syntax = true, -- preview syntax highlight? syntax_limit_l = 0, -- syntax limit (lines), 0=nolimit syntax_limit_b = 1024*1024, -- syntax limit (bytes), 0=nolimit - hl_cursor = 'Cursor', -- cursor highlight - hl_cursorline = 'CursorLine', -- cursor line highlight }, }, -- provider setup @@ -469,8 +487,8 @@ require'fzf-lua'.setup { ["ctrl-y"] = function(selected) print(selected[1]) end, }, winopts = { - win_height = 0.55, - win_width = 0.30, + height = 0.55, + width = 0.30, }, post_reset_cb = function() -- reset statusline highlights after diff --git a/doc/fzf-lua.txt b/doc/fzf-lua.txt index ad19006..d5143b7 100644 --- a/doc/fzf-lua.txt +++ b/doc/fzf-lua.txt @@ -251,16 +251,48 @@ Consult the list below for available settings: -- "aboveleft vnew : split left -- Only valid when using a float window -- (i.e. when 'split' is not defined) - win_height = 0.85, -- window height - win_width = 0.80, -- window width - win_row = 0.30, -- window row position (0=top, 1=bottom) - win_col = 0.50, -- window col position (0=left, 1=right) - -- win_border = false, -- window border? or borderchars? - win_border = { '╭', '─', '╮', '│', '╯', '─', '╰', '│' }, - hl_normal = 'Normal', -- window normal color - hl_border = 'Normal', -- change to 'FloatBorder' if exists + height = 0.85, -- window height + width = 0.80, -- window width + row = 0.35, -- window row position (0=top, 1=bottom) + col = 0.50, -- window col position (0=left, 1=right) + -- border argument passthrough to nvim_open_win(), also used + -- to manually draw the border characters around the preview + -- window, can be set to 'false' to remove all borders or to + -- 'none', 'single', 'double' or 'rounded' (default) + border = { '╭', '─', '╮', '│', '╯', '─', '╰', '│' }, fullscreen = false, -- start fullscreen? - window_on_create = function() + hl = { + normal = 'Normal', -- window normal color (fg+bg) + border = 'Normal', -- border color (try 'FloatBorder') + -- Only valid with the builtin previewer: + cursor = 'Cursor', -- cursor highlight (grep/LSP matches) + cursorline = 'CursorLine', -- cursor line + -- title = 'Normal', -- preview border title (file/buffer) + -- scrollbar_f = 'PmenuThumb', -- scrollbar "full" section highlight + -- scrollbar_e = 'PmenuSbar', -- scrollbar "empty" section highlight + }, + preview = { + -- default = 'bat', -- override the default previewer? + -- default uses the 'builtin' previewer + border = 'border', -- border|noborder, applies only to + -- native fzf previewers (bat/cat/git/etc) + wrap = 'nowrap', -- wrap|nowrap + hidden = 'nohidden', -- hidden|nohidden + vertical = 'down:45%', -- up|down:size + horizontal = 'right:60%', -- right|left:size + layout = 'flex', -- horizontal|vertical|flex + flip_columns = 120, -- #cols to switch to horizontal on flex + -- Only valid with the builtin previewer: + title = true, -- preview border title (file/buf)? + scrollbar = 'float', -- `false` or string:'float|border' + -- float: in-window floating border + -- border: in-border chars (see below) + scrolloff = '-2', -- float scrollbar offset from right + -- applies only when scrollbar = 'float' + scrollchars = {'█', '' }, -- scrollbar chars ({ , } + -- applies only when scrollbar = 'border' + }, + on_create = function() -- called once upon creation of the fzf main window -- can be used to add custom fzf-lua mappings, e.g: -- vim.api.nvim_buf_set_keymap(0, "t", "", "", @@ -330,15 +362,6 @@ Consult the list below for available settings: ["header"] = { "fg", "Comment" }, ["gutter"] = { "bg", "Normal" }, }, ]] - preview_border = 'border', -- border|noborder - preview_wrap = 'nowrap', -- wrap|nowrap - preview_opts = 'nohidden', -- hidden|nohidden - preview_vertical = 'down:45%', -- up|down:size - preview_horizontal = 'right:60%', -- right|left:size - preview_layout = 'flex', -- horizontal|vertical|flex - flip_columns = 120, -- #cols to switch to horizontal on flex - -- default_previewer = "bat", -- override the default previewer? - -- by default uses the builtin previewer previewers = { cat = { cmd = "cat", @@ -361,14 +384,9 @@ Consult the list below for available settings: builtin = { delay = 100, -- delay(ms) displaying the preview -- prevents lag on fast scrolling - title = true, -- preview title? - scrollbar = true, -- scrollbar? - scrollchar = '█', -- scrollbar character syntax = true, -- preview syntax highlight? syntax_limit_l = 0, -- syntax limit (lines), 0=nolimit syntax_limit_b = 1024*1024, -- syntax limit (bytes), 0=nolimit - hl_cursor = 'Cursor', -- cursor highlight - hl_cursorline = 'CursorLine', -- cursor line highlight }, }, -- provider setup @@ -503,8 +521,8 @@ Consult the list below for available settings: ["ctrl-y"] = function(selected) print(selected[1]) end, }, winopts = { - win_height = 0.55, - win_width = 0.30, + height = 0.55, + width = 0.30, }, post_reset_cb = function() -- reset statusline highlights after diff --git a/lua/fzf-lua/config.lua b/lua/fzf-lua/config.lua index 14186dc..7a6c0ad 100644 --- a/lua/fzf-lua/config.lua +++ b/lua/fzf-lua/config.lua @@ -19,18 +19,50 @@ if M._has_devicons and not M._devicons.has_loaded() then M._devicons.setup() end +function M._default_previewer_fn() + return M.globals.default_previewer or M.globals.winopts.preview.default +end + M.globals = { winopts = { - win_height = 0.85, - win_width = 0.80, - win_row = 0.30, - win_col = 0.50, - win_border = true, - borderchars = { '╭', '─', '╮', '│', '╯', '─', '╰', '│' }, - hl_normal = 'Normal', - hl_border = 'Normal', + height = 0.85, + width = 0.80, + row = 0.35, + col = 0.55, + border = 'rounded', fullscreen = false, - window_on_create = function() + hl = { + normal = 'Normal', + border = 'Normal', + -- builtin preview only + cursor = 'Cursor', + cursorline = 'CursorLine', + -- title = 'Normal', + -- scrollbar_f = 'PmenuThumb', + -- scrollbar_e = 'PmenuSbar', + }, + preview = { + default = "builtin", + border = 'float', + wrap = 'nowrap', + hidden = 'nohidden', + vertical = 'down:45%', + horizontal = 'right:60%', + layout = 'flex', + flip_columns = 120, + title = true, + scrollbar = 'border', + scrolloff = '-2', + scrollchar = '', + scrollchars = {'█', '' }, + }, + _borderchars = { + ["none"] = {' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ' }, + ["single"] = {'┌', '─', '┐', '│', '┘', '─', '└', '│' }, + ["double"] = {'╔', '═', '╗', '║', '╝', '═', '╚', '║' }, + ["rounded"] = {'╭', '─', '╮', '│', '╯', '─', '╰', '│' }, + }, + on_create = function() -- vim.cmd("set winhl=Normal:Normal,FloatBorder:Normal") end, }, @@ -69,14 +101,6 @@ M.globals = { ['--height'] = '100%', ['--layout'] = 'reverse', }, - preview_border = 'border', - preview_wrap = 'nowrap', - preview_opts = 'nohidden', - preview_vertical = 'down:45%', - preview_horizontal = 'right:60%', - preview_layout = 'flex', - flip_columns = 120, - default_previewer = "builtin", previewers = { cat = { cmd = "cat", @@ -110,20 +134,16 @@ M.globals = { -- https://github.com/junegunn/fzf/issues/2417#issuecomment-809886535 delay = 100, title = true, - scrollbar = true, - scrollchar = '█', syntax = true, syntax_delay = 0, syntax_limit_l = 0, syntax_limit_b = 1024*1024, - hl_cursor = 'Cursor', - hl_cursorline = 'CursorLine', _ctor = previewers.builtin.buffer_or_file, }, }, } M.globals.files = { - previewer = function() return M.globals.default_previewer end, + previewer = M._default_previewer_fn, prompt = '> ', cmd = nil, -- default: auto detect find|fd file_icons = true and M._has_devicons, @@ -149,7 +169,7 @@ M.globals.files = { -- so we can reference 'M.globals.files' M.globals.git = { files = { - previewer = function() return M.globals.default_previewer end, + previewer = M._default_previewer_fn, prompt = 'GitFiles> ', cmd = "git ls-files --exclude-standard", file_icons = true and M._has_devicons, @@ -203,7 +223,7 @@ M.globals.git = { }, } M.globals.grep = { - previewer = function() return M.globals.default_previewer end, + previewer = M._default_previewer_fn, prompt = 'Rg> ', input_prompt = 'Grep For> ', cmd = nil, -- default: auto detect rg|grep @@ -218,7 +238,7 @@ M.globals.grep = { glob_separator = "%s%-%-" -- query separator pattern (lua): ' --' } M.globals.args = { - previewer = function() return M.globals.default_previewer end, + previewer = M._default_previewer_fn, prompt = 'Args> ', files_only = true, file_icons = true and M._has_devicons, @@ -228,7 +248,7 @@ M.globals.args = { } M.globals.args.actions["ctrl-x"] = actions.arg_del M.globals.oldfiles = { - previewer = function() return M.globals.default_previewer end, + previewer = M._default_previewer_fn, prompt = 'History> ', file_icons = true and M._has_devicons, color_icons = true, @@ -236,7 +256,7 @@ M.globals.oldfiles = { actions = M.globals.files.actions, } M.globals.quickfix = { - previewer = function() return M.globals.default_previewer end, + previewer = M._default_previewer_fn, prompt = 'Quickfix> ', separator = '▏', file_icons = true and M._has_devicons, @@ -245,7 +265,7 @@ M.globals.quickfix = { actions = M.globals.files.actions, } M.globals.loclist = { - previewer = function() return M.globals.default_previewer end, + previewer = M._default_previewer_fn, prompt = 'Locations> ', separator = '▏', file_icons = true and M._has_devicons, @@ -310,7 +330,7 @@ M.globals.blines = { }, } M.globals.tags = { - previewer = function() return M.globals.default_previewer end, + previewer = M._default_previewer_fn, prompt = 'Tags> ', ctags_file = "tags", file_icons = true and M._has_devicons, @@ -319,7 +339,7 @@ M.globals.tags = { actions = M.globals.files.actions, } M.globals.btags = { - previewer = function() return M.globals.default_previewer end, + previewer = M._default_previewer_fn, prompt = 'BTags> ', ctags_file = "tags", file_icons = true and M._has_devicons, @@ -334,8 +354,8 @@ M.globals.colorschemes = { ["default"] = actions.colorscheme, }, winopts = { - win_height = 0.55, - win_width = 0.50, + height = 0.55, + width = 0.50, }, } M.globals.helptags = { @@ -364,7 +384,7 @@ M.globals.manpages = { }, } M.globals.lsp = { - previewer = function() return M.globals.default_previewer end, + previewer = M._default_previewer_fn, prompt = '> ', file_icons = true and M._has_devicons, color_icons = true, @@ -384,8 +404,8 @@ M.globals.lsp = { M.globals.builtin = { prompt = 'Builtin> ', winopts = { - win_height = 0.65, - win_width = 0.50, + height = 0.65, + width = 0.50, }, actions = { ["default"] = actions.run_builtin, @@ -516,18 +536,67 @@ else } end + function M.normalize_opts(opts, defaults) if not opts then opts = {} end - if not opts.fzf_opts then opts.fzf_opts = {} end + + -- First, merge with provider defaults opts = vim.tbl_deep_extend("keep", opts, defaults) - opts.keymap = vim.tbl_deep_extend("keep", opts.keymap or {}, M.globals.keymap) - if defaults.winopts then - if not opts.winopts then opts.winopts = {} end - opts.winopts = vim.tbl_deep_extend("keep", opts.winopts, defaults.winopts) + + -- Merge required tables from globals + for _, k in ipairs({ 'winopts', 'keymap', 'fzf_opts', 'previewers' }) do + opts[k] = vim.tbl_deep_extend("keep", opts[k] or {}, M.globals[k] or {}) end + + -- backward compatibility, rhs overrides lhs + -- (rhs being the "old" option) + local backward_compat = { + ['winopts.row'] = 'winopts.win_row', + ['winopts.col'] = 'winopts.win_col', + ['winopts.width'] = 'winopts.win_width', + ['winopts.height'] = 'winopts.win_height', + ['winopts.border'] = 'winopts.win_border', + ['winopts.on_create'] = 'winopts.window_on_create', + ['winopts.preview.wrap'] = 'preview_wrap', + ['winopts.preview.border'] = 'preview_border', + ['winopts.preview.hidden'] = 'preview_opts', + ['winopts.preview.vertical'] = 'preview_vertical', + ['winopts.preview.horizontal'] = 'preview_horizontal', + ['winopts.preview.layout'] = 'preview_layout', + ['winopts.preview.flip_columns'] = 'flip_columns', + ['winopts.preview.default'] = 'default_previewer', + ['winopts.hl.normal'] = 'winopts.hl_normal', + ['winopts.hl.border'] = 'winopts.hl_border', + ['winopts.hl.cursor'] = 'previewers.builtin.hl_cursor', + ['winopts.hl.cursorline'] = 'previewers.builtin.hl_cursorline', + ['winopts.preview.title'] = 'previewers.builtin.title', + ['winopts.preview.scrollbar'] = 'previewers.builtin.scrollbar', + ['winopts.preview.scrollchar'] = 'previewers.builtin.scrollchar', + } + + -- recursive key loopkup, can also set new value + local map_recurse = function(m, s, v, w) + local keys = utils.strsplit(s, '.') + local val, map = m, nil + for i=1,#keys do + map = val + val = val[keys[i]] + if not val then break end + if v~=nil and i==#keys then map[keys[i]] = v end + end + if v and w then utils.warn(w) end + return val + end + + -- interate backward compat map, retrieve values from opts or globals + for k, v in pairs(backward_compat) do + map_recurse(opts, k, map_recurse(opts, v) or map_recurse(M.globals, v)) + -- ,("'%s' is now defined under '%s'"):format(v, k)) + end + if type(opts.previewer) == 'function' then -- we use a function so the user can override - -- globals.default_previewer + -- globals.winopts.preview.default opts.previewer = opts.previewer() end if type(opts.previewer) == 'table' then @@ -566,6 +635,9 @@ function M.normalize_opts(opts, defaults) -- are we using skim? opts._is_skim = opts.fzf_bin:find('sk') ~= nil + -- mark as normalized + opts._normalized = true + return opts end diff --git a/lua/fzf-lua/core.lua b/lua/fzf-lua/core.lua index def924e..0a80f59 100644 --- a/lua/fzf-lua/core.lua +++ b/lua/fzf-lua/core.lua @@ -8,6 +8,10 @@ local win = require "fzf-lua.win" local M = {} M.fzf = function(opts, contents) + -- normalize with globals if not already normalized + if not opts._normalized then + opts = config.normalize_opts(opts, {}) + end -- setup the fzf window and preview layout local fzf_win = win(opts) if not fzf_win then return end @@ -66,19 +70,17 @@ M.get_devicon = function(file, ext) return icon..config.globals.file_icon_padding:gsub(" ", utils.nbsp), hl end -M.preview_window = function(opts) - local o = vim.tbl_deep_extend("keep", opts, config.globals) - local preview_vertical = string.format('%s:%s:%s:%s', - o.preview_opts, o.preview_border, o.preview_wrap, o.preview_vertical) - local preview_horizontal = string.format('%s:%s:%s:%s', - o.preview_opts, o.preview_border, o.preview_wrap, o.preview_horizontal) - if o.preview_layout == "vertical" then - return preview_vertical - elseif o.preview_layout == "flex" then - return utils._if(vim.o.columns>o.flip_columns, preview_horizontal, preview_vertical) +M.preview_window = function(o) + local preview_args = ("%s:%s:%s:"):format( + o.winopts.preview.hidden, o.winopts.preview.border, o.winopts.preview.wrap) + if o.winopts.preview.layout == "horizontal" or + o.winopts.preview.layout == "flex" and + vim.o.columns>o.winopts.preview.flip_columns then + preview_args = preview_args .. o.winopts.preview.horizontal else - return preview_horizontal + preview_args = preview_args .. o.winopts.preview.vertical end + return preview_args end M.get_color = function(hl_group, what) diff --git a/lua/fzf-lua/init.lua b/lua/fzf-lua/init.lua index ab62940..252099e 100644 --- a/lua/fzf-lua/init.lua +++ b/lua/fzf-lua/init.lua @@ -31,10 +31,10 @@ function M.setup(opts) utils.warn("'previewers.builtin.keymap' moved under 'keymap.builtin', see ':help fzf-lua-customization'") end if globals.previewers.builtin.wrap ~= nil then - utils.warn("'previewers.builtin.wrap' is not longer in use, set 'preview_wrap' to 'wrap' or 'nowrap' instead") + utils.warn("'previewers.builtin.wrap' is not longer in use, set 'winopts.preview.wrap' to 'wrap' or 'nowrap' instead") end if globals.previewers.builtin.hidden ~= nil then - utils.warn("'previewers.builtin.hidden' is not longer in use, set 'preview_opts' to 'hidden' or 'nohidden' instead") + utils.warn("'previewers.builtin.hidden' is not longer in use, set 'winopts.preview.hidden' to 'hidden' or 'nohidden' instead") end -- override BAT_CONFIG_PATH to prevent a -- conflct with '$XDG_DATA_HOME/bat/config' diff --git a/lua/fzf-lua/previewer/builtin.lua b/lua/fzf-lua/previewer/builtin.lua index f01fc15..0e1ec3c 100644 --- a/lua/fzf-lua/previewer/builtin.lua +++ b/lua/fzf-lua/previewer/builtin.lua @@ -31,18 +31,11 @@ function Previewer.base:new(o, opts, fzf_win) self.type = "builtin" self.win = fzf_win self.delay = o.delay - self.title = o.title - self.scrollbar = o.scrollbar - if o.scrollchar and type(o.scrollchar) == 'string' then - self.win.winopts.scrollchar = o.scrollchar - end + self.title = self.win.winopts.preview.title self.syntax = o.syntax self.syntax_delay = o.syntax_delay self.syntax_limit_b = o.syntax_limit_b self.syntax_limit_l = o.syntax_limit_l - self.hl_cursor = o.hl_cursor - self.hl_cursorline = o.hl_cursorline - self.hl_range = o.hl_range self.backups = {} return self end @@ -234,9 +227,7 @@ function Previewer.base:scroll(direction) end) end end - if self.scrollbar then - self.win:update_scrollbar() - end + self.win:update_scrollbar() end @@ -373,8 +364,8 @@ local function set_cursor_hl(self, entry) fn.clearmatches() - if lnum and lnum > 0 and col and col > 1 then - fn.matchaddpos(self.hl_cursor, {{lnum, math.max(1, col)}}, 11) + if self.win.winopts.hl.cursor and lnum and lnum > 0 and col and col > 1 then + fn.matchaddpos(self.win.winopts.hl.cursor, {{lnum, math.max(1, col)}}, 11) end end @@ -391,9 +382,7 @@ function Previewer.buffer_or_file:update_border(entry) end self.win:update_title(title) end - if self.scrollbar then - self.win:update_scrollbar() - end + self.win:update_scrollbar() end function Previewer.buffer_or_file:preview_buf_post(entry) @@ -477,9 +466,7 @@ function Previewer.help_tags:populate_preview_buf(entry_str) end) api.nvim_win_set_buf(self.win.preview_winid, self.preview_bufnr) api.nvim_win_set_cursor(self.win.preview_winid, self.orig_pos) - if self.scrollbar then - self.win:update_scrollbar() - end + self.win:update_scrollbar() if self.prev_help_bufnr ~= self.preview_bufnr and -- only delete the help buffer when the help -- tag triggers opening a different help file diff --git a/lua/fzf-lua/providers/lsp.lua b/lua/fzf-lua/providers/lsp.lua index b5d9b38..3d41054 100644 --- a/lua/fzf-lua/providers/lsp.lua +++ b/lua/fzf-lua/providers/lsp.lua @@ -395,7 +395,7 @@ M.diagnostics = function(opts) if not opts then return end local lsp_clients = vim.lsp.buf_get_clients(0) - if utils.tbl_length(lsp_clients) == 0 then + if utils.tbl_isempty(lsp_clients) then utils.info("LSP: no client attached") return end @@ -568,7 +568,7 @@ local function check_capabilities(feature) if supported_client then return true else - if utils.tbl_length(clients) == 0 then + if utils.tbl_isempty(clients) then utils.info("LSP: no client attached") else utils.info("LSP: server does not support " .. feature) diff --git a/lua/fzf-lua/utils.lua b/lua/fzf-lua/utils.lua index d1a3ef2..b4e8ace 100644 --- a/lua/fzf-lua/utils.lua +++ b/lua/fzf-lua/utils.lua @@ -164,13 +164,9 @@ function M.tbl_length(T) return count end -function M.tbl_has(table, key) - return table[key] ~= nil -end - -function M.tbl_or(key, tbl1, tbl2) - if tbl1[key] ~= nil then return tbl1[key] - else return tbl2[key] end +function M.tbl_isempty(T) + if not T or not next(T) then return true end + return false end function M.tbl_concat(...) @@ -414,4 +410,17 @@ function M.io_system(cmd, use_lua_io) end end +function M.fzf_bind_to_neovim(key) + local conv_map = { + ['alt'] = 'A', + ['ctrl'] = 'C', + ['shift'] = 'S', + } + key = key:lower() + for k, v in pairs(conv_map) do + key = key:gsub(k, v) + end + return ("<%s>"):format(key) +end + return M diff --git a/lua/fzf-lua/win.lua b/lua/fzf-lua/win.lua index 49bd4b0..53696b8 100644 --- a/lua/fzf-lua/win.lua +++ b/lua/fzf-lua/win.lua @@ -18,6 +18,14 @@ setmetatable(FzfWin, { function FzfWin:setup_keybinds() if not self:validate() then return end if not self.keymap or not self.keymap.builtin then return end + -- find the toggle_preview + if self.keymap.fzf then + for k, v in pairs(self.keymap.fzf) do + if v == 'toggle-preview' then + self._fzf_toggle_prev_bind = utils.fzf_bind_to_neovim(k) + end + end + end local keymap_tbl = { ['toggle-fullscreen'] = { module = 'win', fnc = 'toggle_fullscreen()' }, } @@ -65,9 +73,12 @@ local generate_layout = function(winopts) if winopts.split then anchor = 'NW' prev_row = 1 + prev_width = prev_width - 2 prev_height = prev_height - 1 if vert_split then prev_col = 1 + else + prev_col = prev_col - 1 end else anchor = 'SW' @@ -79,6 +90,7 @@ local generate_layout = function(winopts) prev_col = 1 prev_row = height + padding prev_height = prev_height - 1 + prev_width = prev_width - 2 else prev_row = row + height + 3 end @@ -86,34 +98,29 @@ local generate_layout = function(winopts) elseif preview_pos == 'left' or preview_pos == 'right' then prev_height = height prev_width = utils.round(width * preview_size/100) - width = width - prev_width + width = width - prev_width - 2 if preview_pos == 'left' then anchor = 'NE' - col = col + prev_width - prev_col = col - padding + col = col + prev_width + 2 + prev_col = col - 1 if winopts.split then prev_row = 1 + prev_width = prev_width - 1 prev_height = prev_height - padding if vert_split then anchor = 'NW' prev_col = 1 - prev_width = prev_width + 1 else - prev_width = prev_width - 1 + prev_col = col - 3 end end else anchor = 'NW' if winopts.split then prev_row = 1 + prev_col = width + 4 + prev_width = prev_width - 3 prev_height = prev_height - padding - if vert_split then - prev_col = width + 2 - prev_width = prev_width - 1 - else - prev_col = width + 3 - prev_width = prev_width - padding - end else prev_col = col + width + 3 end @@ -132,72 +139,113 @@ local generate_layout = function(winopts) } end -local normalize_winopts = function(opts) - if not opts then opts = {} end - if not opts.winopts then opts.winopts = {} end - opts = vim.tbl_deep_extend("keep", opts, config.globals) - opts.winopts = vim.tbl_deep_extend("keep", opts.winopts, config.globals.winopts) +local strip_borderchars_hl = function(border) + local default = nil + if type(border) == 'string' then + default = config.globals.winopts._borderchars[border] + end + if not default then + default = config.globals.winopts._borderchars['rounded'] + end + if not border or type(border) ~= 'table' or #border<8 then + return default + end + local borderchars = {} + for i=1, 8 do + if type(border[i]) == 'string' then + table.insert(borderchars, border[i]) + elseif type(border[i]) == 'table' and type(border[i][1]) == 'string' then + -- can happen when border chars contains a highlight, i.e: + -- border = { {'╭', 'NormalFloat'}, {'─', 'NormalFloat'}, ... } + table.insert(borderchars, border[i][1]) + else + table.insert(borderchars, default[i]) + end + end + -- assert(#borderchars == 8) + return borderchars +end + +local normalize_winopts = function(o) + -- make a local copy of opts so we + -- don't pollute the user's options + local opts = o or {} + opts.winopts = vim.tbl_deep_extend("keep", opts.winopts or {}, config.globals.winopts) + opts.winopts_fn = opts.winopts_fn or config.globals.winopts_fn opts.winopts_raw = opts.winopts_raw or config.globals.winopts_raw - local raw = {} - if opts.winopts_raw and type(opts.winopts_raw) == "function" then - raw = opts.winopts_raw() + local winopts = utils.tbl_deep_clone(opts.winopts) + + if type(opts.winopts_fn) == "function" then + winopts = vim.tbl_deep_extend("force", winopts, opts.winopts_fn()) + end + if type(opts.winopts_raw) == "function" then + winopts = vim.tbl_deep_extend("force", winopts, opts.winopts_raw()) end - local winopts = opts.winopts - local height = raw.height or math.floor(vim.o.lines * winopts.win_height) - local width = raw.width or math.floor(vim.o.columns * winopts.win_width) - local row = raw.row or math.floor((vim.o.lines - height) * winopts.win_row) - local col = raw.col or math.floor((vim.o.columns - width) * winopts.win_col) - local border = raw.border or winopts.win_border - local scrollchar = raw.scrollchar or winopts.scrollchar - local hl_normal = raw.hl_normal or winopts.hl_normal - local hl_border = raw.hl_border or winopts.hl_border - local fullscreen = raw.fullscreen or winopts.fullscreen + local max_width = vim.o.columns-2 + local max_height = vim.o.lines-4 + winopts.width = math.min(max_width, winopts.width) + winopts.height = math.min(max_height, winopts.height) + if not winopts.height or winopts.height <= 1 then + winopts.height = math.floor(max_height * winopts.height) + end + if not winopts.width or winopts.width <= 1 then + winopts.width = math.floor(max_width * winopts.width) + end + if not winopts.row or winopts.row < 1 then + winopts.row = math.floor((vim.o.lines - winopts.height) * winopts.row) + end + if not winopts.col or winopts.col < 1 then + winopts.col = math.floor((vim.o.columns - winopts.width) * winopts.col) + end + winopts.col = math.min(winopts.col, max_width-winopts.width) + winopts.row = math.min(winopts.row, max_height-winopts.height) -- normalize border option for nvim_open_win() - if border == false then - border = {} - for i=1, 8 do border[i] = ' ' end - elseif border == true or border == nil then - border = config.globals.winopts.borderchars + if not winopts.border or winopts.border == true then + winopts.border = 'rounded' + elseif winopts.border == false then + winopts.border = 'none' end + -- We only allow 'none|single|double|rounded' + if type(winopts.border) == 'string' then + winopts.border = config.globals.winopts._borderchars[winopts.border] or + config.globals.winopts._borderchars['rounded'] + end + + -- Store a version of borderchars with no highlights + -- to be used in the border drawing functions + winopts.nohl_borderchars = strip_borderchars_hl(winopts.border) -- parse preview options - local preview = opts.preview_horizontal - if opts.preview_layout == "vertical" then - preview = opts.preview_vertical - elseif opts.preview_layout == "flex" then - preview = utils._if(vim.o.columns>opts.flip_columns, opts.preview_horizontal, opts.preview_vertical) + local preview + if winopts.preview.layout == "horizontal" or + winopts.preview.layout == "flex" and + vim.o.columns > winopts.preview.flip_columns then + preview = winopts.preview.horizontal + else + preview = winopts.preview.vertical end -- builtin previewer params - local prev_pos = preview:match("[^:]+") or 'right' - local prev_size = tonumber(preview:match(":(%d+)%%")) or 50 + winopts.preview_pos = preview:match("[^:]+") or 'right' + winopts.preview_size = tonumber(preview:match(":(%d+)%%")) or 50 - return { - height = height, width = width, row = row, col = col, border = border, - window_on_create = raw.window_on_create or winopts.window_on_create, - split = raw.split or winopts.split, - hl_normal = hl_normal, hl_border = hl_border, - -- builtin previewer params - scrollchar = scrollchar, - preview_pos = prev_pos, preview_size = prev_size, - fullscreen = fullscreen - } + return winopts end function FzfWin:reset_win_highlights(win, is_border) local hl = ("Normal:%s,FloatBorder:%s"):format( - self.winopts.hl_normal, self.winopts.hl_border) - if self._previewer and self._previewer.hl_cursorline then - hl = hl .. (",CursorLine:%s"):format(self._previewer.hl_cursorline) + self.winopts.hl.normal, self.winopts.hl.border) + if self._previewer and self.winopts.hl.cursorline then + hl = hl .. (",CursorLine:%s"):format(self.winopts.hl.cursorline) end if is_border then -- our border is manuually drawn so we need -- to replace Normal with the border color - hl = ("Normal:%s"):format(self.winopts.hl_border) + hl = ("Normal:%s"):format(self.winopts.hl.border) end vim.api.nvim_win_set_option(win, 'winhighlight', hl) end @@ -234,7 +282,7 @@ function FzfWin.autoclose() end local function opt_matches(opts, key, str) - local opt = opts[key] or config.globals[key] + local opt = opts.winopts.preview[key] or config.globals.winopts.preview[key] return opt and opt:match(str) end @@ -248,8 +296,9 @@ function FzfWin:new(o) self = setmetatable({}, { __index = self }) self.winopts = normalize_winopts(o) self.fullscreen = self.winopts.fullscreen - self.preview_wrap = not opt_matches(o, 'preview_wrap', 'nowrap') - self.preview_hidden = not opt_matches(o, 'preview_opts', 'nohidden') + self.preview_wrap = not opt_matches(o, 'wrap', 'nowrap') + self.preview_hidden = not opt_matches(o, 'hidden', 'nohidden') + self.preview_border = not opt_matches(o, 'border', 'noborder') self.keymap = o.keymap self.previewer = o.previewer self.previewer_type = o.previewer_type @@ -285,23 +334,25 @@ function FzfWin:fs_preview_layout(fs) local height_diff = 0 local width_diff = 0 if preview_pos == 'down' or preview_pos == 'up' then - width_diff = vim.o.columns - border_winopts.width - 1 + width_diff = vim.o.columns - border_winopts.width if preview_pos == 'down' then height_diff = vim.o.lines - border_winopts.row - border_winopts.height - 2 elseif preview_pos == 'up' then height_diff = border_winopts.row - border_winopts.height end - prev_winopts.col = prev_winopts.col - width_diff/2 - border_winopts.col = border_winopts.col - width_diff/2 + border_winopts.col = 0 + prev_winopts.col = border_winopts.col + 1 elseif preview_pos == 'left' or preview_pos == 'right' then height_diff = vim.o.lines - border_winopts.height - 2 if preview_pos == 'left' then + border_winopts.col = border_winopts.col - 1 + prev_winopts.col = prev_winopts.col - 1 width_diff = border_winopts.col - border_winopts.width elseif preview_pos == 'right' then - width_diff = vim.o.columns - border_winopts.col - border_winopts.width - 1 + width_diff = vim.o.columns - border_winopts.col - border_winopts.width end - prev_winopts.row = prev_winopts.row - height_diff/2 - border_winopts.row = border_winopts.row - height_diff/2 + border_winopts.row = 0 + prev_winopts.row = border_winopts.row + 1 end prev_winopts.height = prev_winopts.height + height_diff @@ -340,7 +391,6 @@ function FzfWin:preview_layout() local winopts = {relative = 'editor', focusable = false, style = 'minimal'} if self.winopts.split then winopts = {relative = 'win', win = self.fzf_winid, focusable = false, style = 'minimal'} - width = width - 2 end local preview_opts = vim.tbl_extend('force', winopts, { anchor = anchor, @@ -371,35 +421,14 @@ function FzfWin:preview_winids() return self.preview_winid, self.border_winid end -local strip_border_highlights = function(border) - local default = config.globals.winopts.borderchars - if not border or type(border) ~= 'table' or #border<8 then - return default - end - local borderchars = {} - for i=1, 8 do - if type(border[i]) == 'string' then - table.insert(borderchars, border[i]) - elseif type(border[i]) == 'table' and type(border[i][1]) == 'string' then - -- can happen when border chars contains a highlight, i.e: - -- border = { {'╭', 'NormalFloat'}, {'─', 'NormalFloat'}, ... } - table.insert(borderchars, border[i][1]) - else - table.insert(borderchars, default[i]) - end - end - -- assert(#borderchars == 8) - return borderchars -end - function FzfWin:update_border_buf() local border_buf = self.border_buf local border_winopts = self.border_winopts - local border_chars = strip_border_highlights(self.winopts.border) + local borderchars = self.winopts.nohl_borderchars local width, height = border_winopts.width, border_winopts.height - local top = border_chars[1] .. border_chars[2]:rep(width - 2) .. border_chars[3] - local mid = border_chars[8] .. (' '):rep(width - 2) .. border_chars[4] - local bot = border_chars[7] .. border_chars[6]:rep(width - 2) .. border_chars[5] + local top = borderchars[1] .. borderchars[2]:rep(width - 2) .. borderchars[3] + local mid = borderchars[8] .. (' '):rep(width - 2) .. borderchars[4] + local bot = borderchars[7] .. borderchars[6]:rep(width - 2) .. borderchars[5] local lines = {top} for _ = 1, height - 2 do table.insert(lines, mid) @@ -581,9 +610,9 @@ function FzfWin:create() self:reset_win_highlights(self.fzf_winid) - if self.winopts.window_on_create and - type(self.winopts.window_on_create) == 'function' then - self.winopts.window_on_create() + if self.winopts.on_create and + type(self.winopts.on_create) == 'function' then + self.winopts.on_create() end -- create or redraw the preview win @@ -614,6 +643,19 @@ function FzfWin:close_preview() if self.preview_winid and vim.api.nvim_win_is_valid(self.preview_winid) then api.nvim_win_close(self.preview_winid, true) end + if self._sbuf1 and vim.api.nvim_buf_is_valid(self._sbuf1) then + vim.api.nvim_buf_delete(self._sbuf1, {force=true}) + end + if self._swin1 and vim.api.nvim_win_is_valid(self._swin1) then + api.nvim_win_close(self._swin1, true) + end + if self._sbuf2 and vim.api.nvim_buf_is_valid(self._sbuf2) then + vim.api.nvim_buf_delete(self._sbuf2, {force=true}) + end + if self._swin2 and vim.api.nvim_win_is_valid(self._swin2) then + api.nvim_win_close(self._swin2, true) + end + self._sbuf1, self._sbuf2, self._swin1, self._swin2 = nil, nil, nil, nil self.border_buf = nil self.border_winid = nil self.preview_winid = nil @@ -652,43 +694,174 @@ function FzfWin.win_leave() _self:close() end -function FzfWin:update_scrollbar() - if not self:validate_preview() then return end - local border_winid = self.border_winid - local preview_winid = self.preview_winid - local border_chars = strip_border_highlights(self.winopts.border) - local scrollchar = self.winopts.scrollchar - 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) +function FzfWin:clear_border_highlights() + if self.border_winid and vim.api.nvim_win_is_valid(self.border_winid) then + vim.fn.clearmatches(self.border_winid) + end +end - local win_info = fn.getwininfo(preview_winid)[1] - local topline, height = win_info.topline, win_info.height +function FzfWin:set_title_hl() + if self.winopts.hl.title and self._title_len and self._title_len>0 then + vim.api.nvim_win_call(self.border_winid, function() + fn.matchaddpos(self.winopts.hl.title, {{1, 9, self._title_len}}, 11) + end) + end +end + +function FzfWin:update_scrollbar_border(o) + + -- do not display on files that are fully contained + if o.bar_height >= o.line_count then return end + + local borderchars = self.winopts.nohl_borderchars + local scrollchars = self.winopts.preview.scrollchars - local bar_size = math.min(height, math.ceil(height * height / line_count)) + -- bar_offset starts at 0, first line is 1 + o.bar_offset = o.bar_offset+1 - local bar_pos = math.ceil(height * topline / line_count) - if bar_pos + bar_size > height then - bar_pos = height - bar_size + 1 + -- backward compatibility before 'scrollchar' was a table + if type(self.winopts.preview.scrollchar) == 'string' and + #self.winopts.preview.scrollchar > 0 then + scrollchars[1] = self.winopts.preview.scrollchar + end + for i=1,2 do + if not scrollchars[i] or #scrollchars[i]==0 then + scrollchars[i] = borderchars[4] + end end - -- only accept a string - if not scrollchar or type(scrollchar) ~= 'string' then - scrollchar = '█' + -- matchaddpos() can't handle more than 8 items at once + local add_to_tbl = function(tbl, item) + local len = utils.tbl_length(tbl) + if len==0 or utils.tbl_length(tbl[len])==8 then + table.insert(tbl, {}) + len = len+1 + end + table.insert(tbl[len], item) end - local lines = api.nvim_buf_get_lines(border_buf, 1, -2, true) + local full, empty = {}, {} + local lines = api.nvim_buf_get_lines(self.border_buf, 1, -2, true) for i = 1, #lines do + local line, linew = lines[i], fn.strwidth(lines[i]) local bar_char - if i >= bar_pos and i < bar_pos + bar_size then - bar_char = scrollchar + if i >= o.bar_offset and i < o.bar_offset + o.bar_height then + bar_char = scrollchars[1] + add_to_tbl(full, {i+1, linew+2, 1}) + else + bar_char = scrollchars[2] + add_to_tbl(empty, {i+1, linew+2, 1}) + end + lines[i] = fn.strcharpart(line, 0, linew - 1) .. bar_char + end + api.nvim_buf_set_lines(self.border_buf, 1, -2, 0, lines) + -- border highlights + if self.winopts.hl.scrollbar_f or self.winopts.hl.scrollbar_e then + vim.api.nvim_win_call(self.border_winid, function() + if self.winopts.hl.scrollbar_f then + for i=1,#full do + fn.matchaddpos(self.winopts.hl.scrollbar_f, full[i], 11) + end + end + if self.winopts.hl.scrollbar_e then + for i=1,#empty do + fn.matchaddpos(self.winopts.hl.scrollbar_e, empty[i], 11) + end + end + end) + end +end + +local function ensure_tmp_buf(bufnr) + if bufnr and vim.api.nvim_buf_is_valid(bufnr) then + return bufnr + end + bufnr = api.nvim_create_buf(false, true) + -- running nvim with `-M` will reset modifiable's default value to false + vim.bo[bufnr].modifiable = true + vim.bo[bufnr].bufhidden = 'wipe' + return bufnr +end + +function FzfWin:hide_scrollbar() + if self._swin1 and vim.api.nvim_win_is_valid(self._swin1) then + vim.api.nvim_win_hide(self._swin1) + self._swin1 = nil + end + if self._swin2 and vim.api.nvim_win_is_valid(self._swin2) then + vim.api.nvim_win_hide(self._swin2) + self._swin2 = nil + end +end + +function FzfWin:update_scrollbar_float(o) + -- do not display on files that are fully contained + if o.bar_height >= o.line_count then + self:hide_scrollbar() + else + local info = o.wininfo + local style1 = {} + style1.relative = 'editor' + style1.style = 'minimal' + style1.width = 1 + style1.height = info.height + style1.row = info.winrow - 1 + style1.col = info.wincol + info.width + + (tonumber(self.winopts.preview.scrolloff) or -2) + style1.zindex = info.zindex or 998 + if self._swin1 and vim.api.nvim_win_is_valid(self._swin1) then + vim.api.nvim_win_set_config(self._swin1, style1) else - bar_char = border_chars[4] + style1.noautocmd = true + self._sbuf1 = ensure_tmp_buf(self._sbuf1) + self._swin1 = vim.api.nvim_open_win(self._sbuf1, false, style1) + local hl = self.winopts.hl.scrollbar_e or 'PmenuSbar' + vim.api.nvim_win_set_option(self._swin1, 'winhighlight', + ('Normal:%s,NormalNC:%s,NormalFloat:%s'):format(hl, hl, hl)) + end + local style2 = utils.tbl_deep_clone(style1) + style2.height = o.bar_height + style2.row = style1.row + o.bar_offset + style2.zindex = style1.zindex + 1 + if self._swin2 and vim.api.nvim_win_is_valid(self._swin2) then + vim.api.nvim_win_set_config(self._swin2, style2) + else + style2.noautocmd = true + self._sbuf2 = ensure_tmp_buf(self._sbuf2) + self._swin2 = vim.api.nvim_open_win(self._sbuf2, false, style2) + local hl = self.winopts.hl.scrollbar_f or 'PmenuThumb' + vim.api.nvim_win_set_option(self._swin2, 'winhighlight', + ('Normal:%s,NormalNC:%s,NormalFloat:%s'):format(hl, hl, hl)) 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 FzfWin:update_scrollbar() + if not self.winopts.preview.scrollbar + or self.winopts.preview.scrollbar == 'none' + or not self:validate_preview() then + return + end + + local buf = api.nvim_win_get_buf(self.preview_winid) + + local o = {} + o.wininfo = fn.getwininfo(self.preview_winid)[1] + o.line_count = api.nvim_buf_line_count(buf) + + local topline, height = o.wininfo.topline, o.wininfo.height + o.bar_height = math.min(height, math.ceil(height * height / o.line_count)) + o.bar_offset = math.min(height - o.bar_height, math.floor(height * topline / o.line_count)) + + -- reset highlights before we move the scrollbar + self:clear_border_highlights() + self:set_title_hl() + + if self.winopts.preview.scrollbar == 'float' then + self:update_scrollbar_float(o) + else + self:update_scrollbar_border(o) + end end function FzfWin:update_title(title) @@ -700,17 +873,21 @@ function FzfWin:update_title(title) if #title > width-right_pad then title = title:sub(1, width-right_pad) .. " " end + -- save for set_title_hl + self._title_len = #title 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}) + self:set_title_hl() end -- keybind methods below function FzfWin.toggle_fullscreen() - if not _self then return end + if not _self or _self.winopts.split then return end local self = _self self.fullscreen = not self.fullscreen + self:hide_scrollbar() if self and self:validate() then self:redraw() end @@ -723,6 +900,9 @@ function FzfWin.toggle_preview() if not _self then return end local self = _self self.preview_hidden = not self.preview_hidden + if self.winopts.split and self._fzf_toggle_prev_bind then + utils.feed_keys_termcodes(self._fzf_toggle_prev_bind) + end if self.preview_hidden and self:validate_preview() then self:close_preview() self:redraw()