From 36d850b29b387768e76e59799029d1e69aee2522 Mon Sep 17 00:00:00 2001 From: ibhagwan <59988195+ibhagwan@users.noreply.github.com> Date: Sat, 10 Jul 2021 20:16:57 -0700 Subject: [PATCH] Initial commit --- README.md | 2 + lua/fzf-lua/actions.lua | 144 ++++++++++++ lua/fzf-lua/config.lua | 294 +++++++++++++++++++++++++ lua/fzf-lua/core.lua | 199 +++++++++++++++++ lua/fzf-lua/init.lua | 192 ++++++++++++++++ lua/fzf-lua/path.lua | 99 +++++++++ lua/fzf-lua/providers/buffers.lua | 148 +++++++++++++ lua/fzf-lua/providers/colorschemes.lua | 64 ++++++ lua/fzf-lua/providers/files.lua | 66 ++++++ lua/fzf-lua/providers/grep.lua | 193 ++++++++++++++++ lua/fzf-lua/providers/helptags.lua | 125 +++++++++++ lua/fzf-lua/providers/manpages.lua | 63 ++++++ lua/fzf-lua/providers/oldfiles.lua | 70 ++++++ lua/fzf-lua/providers/quickfix.lua | 89 ++++++++ lua/fzf-lua/utils.lua | 174 +++++++++++++++ plugin/fzf-lua.vim | 8 + 16 files changed, 1930 insertions(+) create mode 100644 README.md create mode 100644 lua/fzf-lua/actions.lua create mode 100644 lua/fzf-lua/config.lua create mode 100644 lua/fzf-lua/core.lua create mode 100644 lua/fzf-lua/init.lua create mode 100644 lua/fzf-lua/path.lua create mode 100644 lua/fzf-lua/providers/buffers.lua create mode 100644 lua/fzf-lua/providers/colorschemes.lua create mode 100644 lua/fzf-lua/providers/files.lua create mode 100644 lua/fzf-lua/providers/grep.lua create mode 100644 lua/fzf-lua/providers/helptags.lua create mode 100644 lua/fzf-lua/providers/manpages.lua create mode 100644 lua/fzf-lua/providers/oldfiles.lua create mode 100644 lua/fzf-lua/providers/quickfix.lua create mode 100644 lua/fzf-lua/utils.lua create mode 100644 plugin/fzf-lua.vim diff --git a/README.md b/README.md new file mode 100644 index 0000000..9076c87 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# fzf-lua +Improved fzf.vim written in lua diff --git a/lua/fzf-lua/actions.lua b/lua/fzf-lua/actions.lua new file mode 100644 index 0000000..672c3a6 --- /dev/null +++ b/lua/fzf-lua/actions.lua @@ -0,0 +1,144 @@ +local M = {} + +-- return fzf '--expect=' string from actions keyval tbl +M.expect = function(actions) + if not actions then return '' end + local keys = {} + for k, v in pairs(actions) do + if k ~= "default" and v ~= false then + table.insert(keys, k) + end + end + if #keys > 0 then + return table.concat(keys, ',') + end + return nil +end + +M.act = function(actions, action, selected) + if not actions or not action then return end + if #action == 0 then action = "default" end + if actions[action] then + actions[action](selected) + end +end + +M.vimcmd = function(vimcmd, selected) + if not selected or #selected < 2 then return end + for i = 2, #selected do + vim.cmd(vimcmd .. " " .. vim.fn.fnameescape(selected[i])) + end +end + +M.vimcmd_file = function(vimcmd, selected) + if not selected or #selected < 2 then return end + for i = 2, #selected do + -- check if the file contains line + local file, line = selected[i]:match("^([^ :]+):(%d+)") + if file and line then + vim.cmd(string.format("%s +%s %s", vimcmd, line, vim.fn.fnameescape(file))) + else + vim.cmd(vimcmd .. " " .. vim.fn.fnameescape(selected[i])) + end + end +end + +-- file actions +M.file_edit = function(selected) + local vimcmd = "e" + M.vimcmd_file(vimcmd, selected) +end + +M.file_split = function(selected) + local vimcmd = "new" + M.vimcmd_file(vimcmd, selected) +end + +M.file_vsplit = function(selected) + local vimcmd = "vnew" + M.vimcmd_file(vimcmd, selected) +end + +M.file_tabedit = function(selected) + local vimcmd = "tanbew" + M.vimcmd_file(vimcmd, selected) +end + +M.file_sel_to_qf = function(selected) + if not selected or #selected < 2 then return end + local qf_list = {} + for i = 2, #selected do + -- check if the file contains line + local file, line, col, text = selected[i]:match("^([^ :]+):(%d+):(%d+):(.*)") + if file and line and col then + table.insert(qf_list, {filename = file, lnum = line, col = col, text = text}) + else + table.insert(qf_list, {filename = selected[i], lnum = 1, col = 1}) + end + end + vim.fn.setqflist(qf_list) + vim.cmd 'copen' +end + +-- buffer actions +M.buf_edit = function(selected) + local vimcmd = "b" + M.vimcmd(vimcmd, selected) +end + +M.buf_split = function(selected) + local vimcmd = "split | b" + M.vimcmd(vimcmd, selected) +end + +M.buf_vsplit = function(selected) + local vimcmd = "vertical split | b" + M.vimcmd(vimcmd, selected) +end + +M.buf_tabedit = function(selected) + local vimcmd = "tab split | b" + M.vimcmd(vimcmd, selected) +end + +M.buf_del = function(selected) + local vimcmd = "bd" + M.vimcmd(vimcmd, selected) +end + +M.colorscheme = function(selected) + if not selected or #selected < 2 then return end + vim.cmd("colorscheme " .. selected[2]) +end + +M.help = function(selected) + local vimcmd = "help" + M.vimcmd(vimcmd, selected) +end + +M.help_vert = function(selected) + local vimcmd = "vert help" + M.vimcmd(vimcmd, selected) +end + +M.help_tab = function(selected) + local vimcmd = "tab help" + M.vimcmd(vimcmd, selected) +end + +M.man = function(selected) + local vimcmd = "Man" + M.vimcmd(vimcmd, selected) +end + +M.man_vert = function(selected) + local vimcmd = "vert Man" + M.vimcmd(vimcmd, selected) +end + +M.man_tab = function(selected) + local vimcmd = "tab Man" + M.vimcmd(vimcmd, selected) +end + +return M diff --git a/lua/fzf-lua/config.lua b/lua/fzf-lua/config.lua new file mode 100644 index 0000000..396e235 --- /dev/null +++ b/lua/fzf-lua/config.lua @@ -0,0 +1,294 @@ +local utils = require "fzf-lua.utils" +local actions = require "fzf-lua.actions" + +-- Clear the default command or it would interfere with our options +vim.env.FZF_DEFAULT_OPTS = '' + +local M = {} + +M.win_height = 0.85 +M.win_width = 0.80 +M.win_row = 0.30 +M.win_col = 0.50 +M.win_border = true +M.default_prompt = '> ' +M.fzf_layout = 'reverse' +M.preview_cmd = nil -- auto detect head|bat +M.preview_border = 'border' +M.preview_wrap = 'nowrap' +M.preview_vertical = 'down:45%' +M.preview_horizontal = 'right:60%' +M.preview_layout = 'flex' +M.flip_columns = 120 +M.bat_theme = nil +M.bat_opts = "--italic-text=always --style=numbers,changes --color always" + +M.files = { + prompt = '> ', + cmd = nil, -- default: auto detect find|fd + file_icons = true and pcall(require, "nvim-web-devicons"), + color_icons = true, + git_icons = true, + git_diff_cmd = "git diff --name-status --relative HEAD", + git_untracked_cmd = "git ls-files --exclude-standard --others", + find_opts = "-type f -printf '%P\n'", + fd_opts = + [[--color never --type f --hidden --follow ]] .. + [[--exclude .git --exclude node_modules --exclude '*.pyc']], + actions = { + ["default"] = actions.file_edit, + ["ctrl-s"] = actions.file_split, + ["ctrl-v"] = actions.file_vsplit, + ["ctrl-t"] = actions.file_tabedit, + ["ctrl-q"] = actions.file_sel_to_qf, + } +} + +M.grep = { + prompt = 'Rg> ', + input_prompt = 'Grep For> ', + cmd = nil, -- default: auto detect rg|grep + file_icons = true and pcall(require, "nvim-web-devicons"), + color_icons = true, + git_icons = true, + git_diff_cmd = M.files.git_diff_cmd, + git_untracked_cmd = M.files.git_untracked_cmd, + grep_opts = "--line-number --recursive --color=auto", + rg_opts = "--column --line-number --no-heading --color=always --smart-case", + actions = { + ["default"] = actions.file_edit, + ["ctrl-s"] = actions.file_split, + ["ctrl-v"] = actions.file_vsplit, + ["ctrl-t"] = actions.file_tabedit, + ["ctrl-q"] = actions.file_sel_to_qf, + } +} + +M.oldfiles = { + prompt = 'History> ', + file_icons = true and pcall(require, "nvim-web-devicons"), + color_icons = true, + git_icons = false, + git_diff_cmd = M.files.git_diff_cmd, + git_untracked_cmd = M.files.git_untracked_cmd, + actions = { + ["default"] = actions.file_edit, + ["ctrl-s"] = actions.file_split, + ["ctrl-v"] = actions.file_vsplit, + ["ctrl-t"] = actions.file_tabedit, + ["ctrl-q"] = actions.file_sel_to_qf, + } +} + +M.quickfix = { + prompt = 'Quickfix> ', + separator = '▏', + file_icons = true and pcall(require, "nvim-web-devicons"), + color_icons = true, + git_icons = false, + git_diff_cmd = M.files.git_diff_cmd, + git_untracked_cmd = M.files.git_untracked_cmd, + actions = { + ["default"] = actions.file_edit, + ["ctrl-s"] = actions.file_split, + ["ctrl-v"] = actions.file_vsplit, + ["ctrl-t"] = actions.file_tabedit, + ["ctrl-q"] = actions.file_sel_to_qf, + } +} + +M.loclist = { + prompt = 'Locations> ', + separator = '▏', + file_icons = true and pcall(require, "nvim-web-devicons"), + color_icons = true, + git_icons = false, + git_diff_cmd = M.files.git_diff_cmd, + git_untracked_cmd = M.files.git_untracked_cmd, + actions = { + ["default"] = actions.file_edit, + ["ctrl-s"] = actions.file_split, + ["ctrl-v"] = actions.file_vsplit, + ["ctrl-t"] = actions.file_tabedit, + ["ctrl-q"] = actions.file_sel_to_qf, + } +} + +M.git = { + prompt = 'GitFiles> ', + cmd = "git ls-files --exclude-standard", + file_icons = true and pcall(require, "nvim-web-devicons"), + color_icons = true, + git_icons = true, + actions = M.files.actions, +} + +M.buffers = { + prompt = 'Buffers> ', + file_icons = true and pcall(require, "nvim-web-devicons"), + color_icons = true, + sort_lastused = true, + show_all_buffers = true, + ignore_current_buffer = false, + cwd_only = false, + actions = { + ["default"] = actions.buf_edit, + ["ctrl-s"] = actions.buf_split, + ["ctrl-v"] = actions.buf_vsplit, + ["ctrl-t"] = actions.buf_tabedit, + ["ctrl-x"] = actions.buf_del, + } +} + +M.colorschemes = { + prompt = 'Colorschemes> ', + live_preview = true, + actions = { + ["default"] = actions.colorscheme, + }, + winopts = { + win_height = 0.55, + win_width = 0.50, + }, +} + +M.helptags = { + prompt = 'Help> ', + actions = { + ["default"] = actions.help, + ["ctrl-s"] = actions.help, + ["ctrl-v"] = actions.help_vert, + ["ctrl-t"] = actions.help_tab, + }, +} + +M.manpages = { + prompt = 'Man> ', + cmd = "man -k .", + actions = { + ["default"] = actions.man, + ["ctrl-s"] = actions.man, + ["ctrl-v"] = actions.man_vert, + ["ctrl-t"] = actions.man_tab, + }, +} + +-- toggle preview +-- toggle preview text wrap +-- | page down|up +-- | half page down|up +-- | preview page down|up +-- toggle select-all +-- clear query +-- send selected to quicfix +-- send all to quicfix +M.fzf_binds = { + 'f2:toggle-preview', + 'f3:toggle-preview-wrap', + 'shift-down:preview-page-down', + 'shift-up:preview-page-up', + 'ctrl-d:half-page-down', + 'ctrl-u:half-page-up', + 'ctrl-f:page-down', + 'ctrl-b:page-up', + 'ctrl-a:toggle-all', + 'ctrl-u:clear-query', +} + +M.file_icon_colors = { + ["lua"] = "blue", + ["vim"] = "green", + ["sh"] = "cyan", + ["zsh"] = "cyan", + ["bash"] = "cyan", + ["py"] = "green", + ["md"] = "yellow", + ["c"] = "blue", + ["cpp"] = "blue", + ["h"] = "magenta", + ["hpp"] = "magenta", + ["js"] = "blue", + ["ts"] = "cyan", + ["tsx"] = "cyan", + ["css"] = "magenta", + ["yml"] = "yellow", + ["yaml"] = "yellow", + ["json"] = "yellow", + ["toml"] = "yellow", + ["conf"] = "yellow", + ["build"] = "red", + ["txt"] = "white", + ["gif"] = "green", + ["jpg"] = "green", + ["png"] = "green", + ["svg"] = "green", + ["sol"] = "red", + ["desktop"] = "magenta", +} + +M.git_icons = { + ["M"] = "M", + ["D"] = "D", + ["A"] = "A", + ["?"] = "?" +} + +M.git_icon_colors = { + ["M"] = "yellow", + ["D"] = "red", + ["A"] = "green", + ["?"] = "magenta" +} + +M.window_on_create = function() + -- Set popup background same as normal windows + vim.cmd("set winhl=Normal:Normal") +end + +M.winopts = function(opts) + + opts = M.getopts(opts, M, { + "win_height", "win_width", + "win_row", "win_col", "border", + "window_on_create", + }) + + local height = math.floor(vim.o.lines * opts.win_height) + local width = math.floor(vim.o.columns * opts.win_width) + local row = math.floor((vim.o.lines - height) * opts.win_row) + local col = math.floor((vim.o.columns - width) * opts.win_col) + + return { + -- style = 'minimal', + height = height, width = width, row = row, col = col, + border = opts.win_border, + window_on_create = opts.window_on_create + } +end + +M.preview_window = function() + local preview_veritcal = string.format('%s:%s:%s', M.preview_border, M.preview_wrap, M.preview_vertical) + local preview_horizontal = string.format('%s:%s:%s', M.preview_border, M.preview_wrap, M.preview_horizontal) + if M.preview_layout == "vertical" then + return preview_veritcal + elseif M.preview_layout == "flex" then + return utils._if(vim.o.columns>M.flip_columns, preview_horizontal, preview_veritcal) + else + return preview_horizontal + end +end + + +-- called to merge caller opts and default config +-- before calling a provider method +function M.getopts(opts, cfg, keys) + if not opts then opts = {} end + if keys then + for _, k in ipairs(keys) do + if opts[k] == nil then opts[k] = cfg[k] end + end + end + return opts +end + +return M diff --git a/lua/fzf-lua/core.lua b/lua/fzf-lua/core.lua new file mode 100644 index 0000000..2673f55 --- /dev/null +++ b/lua/fzf-lua/core.lua @@ -0,0 +1,199 @@ +local fzf = require "fzf" +local path = require "fzf-lua.path" +local utils = require "fzf-lua.utils" +local config = require "fzf-lua.config" +local actions = require "fzf-lua.actions" + +local M = {} + +M.get_devicon = function(file, ext) + local icon = nil + if #file > 0 and pcall(require, "nvim-web-devicons") then + icon = require'nvim-web-devicons'.get_icon(file, ext) + end + return utils._if(icon == nil, '', icon) +end + +M.preview_cmd = function(opts, cfg) + opts = opts or {} + opts.filespec = opts.filespec or '{}' + opts.preview_cmd = opts.preview_cmd or cfg.preview_cmd + opts.preview_args = opts.preview_args or '' + opts.bat_opts = opts.bat_opts or cfg.bat_opts + local preview = nil + if not opts.cwd then opts.cwd = '' + elseif #opts.cwd > 0 then + opts.cwd = path.add_trailing(opts.cwd) + end + if opts.preview_cmd and #opts.preview_cmd > 0 then + preview = string.format("%s %s -- %s%s", opts.preview_cmd, opts.preview_args, opts.cwd, opts.filespec) + elseif vim.fn.executable("bat") == 1 then + preview = string.format("bat %s %s -- %s%s", opts.bat_opts, opts.preview_args, opts.cwd, opts.filespec) + else + preview = string.format("head -n $FZF_PREVIEW_LINES %s -- %s%s", opts.preview_args, opts.cwd, opts.filespec) + end + if preview ~= nil then + -- We use bash to do math on the environment variable, so + -- let's make sure this command runs in bash + -- preview = "bash -c " .. vim.fn.shellescape(preview) + preview = vim.fn.shellescape(preview) + end + return preview +end + +M.build_fzf_cli = function(opts) + local cfg = require'fzf-lua.config' + opts.prompt = opts.prompt or cfg.default_prompt + opts.preview_offset = opts.preview_offset or '' + local cli = string.format( + [[ --layout=%s --bind=%s --prompt=%s]] .. + [[ --preview-window='%s%s' --preview=%s]] .. + [[ --expect=%s --ansi --info=inline]] .. + [[ %s %s]], + cfg.fzf_layout, + utils._if(opts.fzf_binds, opts.fzf_binds, + vim.fn.shellescape(table.concat(cfg.fzf_binds, ','))), + vim.fn.shellescape(opts.prompt), + utils._if(opts.preview_window, opts.preview_window, cfg.preview_window()), + utils._if(#opts.preview_offset>0, ":"..opts.preview_offset, ''), + utils._if(opts.preview, opts.preview, M.preview_cmd(opts, cfg)), + utils._if(opts.actions, actions.expect(opts.actions), 'ctrl-s,ctrl-v,ctrl-t'), + utils._if(opts.nomulti, '--no-multi', '--multi'), + utils._if(opts.cli_args, opts.cli_args, '') + ) + -- print(cli) + return cli +end + +-- invisible unicode char as icon|git separator +-- this way we can split our string by space +local nbsp = "\u{00a0}" + +local get_diff_files = function() + local diff_files = {} + local status = vim.fn.systemlist(config.files.git_diff_cmd) + if not utils.shell_error() then + for i = 1, #status do + local split = vim.split(status[i], " ") + diff_files[split[2]] = split[1] + end + end + + return diff_files +end + +local get_untracked_files = function() + local untracked_files = {} + local status = vim.fn.systemlist(config.files.git_untracked_cmd) + if vim.v.shell_error == 0 then + for i = 1, #status do + untracked_files[status[i]] = "?" + end + end + + return untracked_files +end + +local color_icon = function(icon, ext) + if ext then + return utils.ansi_codes[config.file_icon_colors[ext] or "dark_grey"](icon) + else + return utils.ansi_codes[config.git_icon_colors[icon] or "green"](icon) + end +end + +local get_git_icon = function(file, diff_files, untracked_files) + if diff_files and diff_files[file] then + return config.git_icons[diff_files[file]] + end + if untracked_files and untracked_files[file] then + return config.git_icons[untracked_files[file]] + end + return nbsp +end + +M.make_entry_file = function(opts, x) + local icon + local prefix = '' + if opts.cwd and #opts.cwd > 0 then + x = path.relative(x, opts.cwd) + end + if opts.file_icons then + local extension = path.extension(x) + icon = M.get_devicon(x, extension) + if opts.color_icons then icon = color_icon(icon, extension) end + prefix = prefix .. icon + end + if opts.git_icons then + icon = get_git_icon(x, opts.diff_files, opts.untracked_files) + if opts.color_icons then icon = color_icon(icon) end + prefix = prefix .. utils._if(#prefix>0, nbsp, '') .. icon + end + if #prefix > 0 then + x = prefix .. " " .. x + end + return x +end + +local function trim_entry(string) + string = string:gsub("^[^ ]* ", "") + return string +end + +M.fzf_files = function(opts) + + if not opts or not opts.fzf_fn then + utils.warn("Core.fzf_files(opts) cannot run without callback fn") + return + end + + -- reset git tracking + opts.diff_files, opts.untracked_files = nil, nil + if not utils.is_git_repo() then opts.git_icons = false end + + if opts.cwd and #opts.cwd > 0 then + opts.cwd = vim.fn.expand(opts.cwd) + end + + coroutine.wrap(function () + + if opts.git_icons then + opts.diff_files = get_diff_files() + opts.untracked_files = get_untracked_files() + end + + local has_prefix = opts.file_icons or opts.git_icons + if not opts.filespec then + opts.filespec = utils._if(has_prefix, "{2}", "{}") + end + + local selected = fzf.fzf(opts.fzf_fn, + M.build_fzf_cli(opts), config.winopts(opts.winopts)) + + if not selected then return end + + if #selected > 1 then + for i = 2, #selected do + if has_prefix then + selected[i] = trim_entry(selected[i]) + end + if opts.cwd and #opts.cwd > 0 then + selected[i] = path.join({opts.cwd, selected[i]}) + end + if opts.cb_selected then + local cb_ret = opts.cb_selected(opts, selected[i]) + if cb_ret then selected[i] = cb_ret end + end + end + end + + -- dump fails after fzf for some odd reason + -- functions are still valid as can seen by pairs + -- _G.dump(opts.actions) + actions.act(opts.actions, selected[1], selected) + + end)() + +end + +return M diff --git a/lua/fzf-lua/init.lua b/lua/fzf-lua/init.lua new file mode 100644 index 0000000..fe543b3 --- /dev/null +++ b/lua/fzf-lua/init.lua @@ -0,0 +1,192 @@ +if not pcall(require, "fzf") then + return +end + +local fzf = require "fzf" +local utils = require "fzf-lua.utils" +local config = require "fzf-lua.config" + +local M = {} + +local getopt = function(opts, key, expected_type, default) + if opts and opts[key] ~= nil then + if type(opts[key]) == expected_type then + return opts[key] + else + utils.info( + string.format("Expected '%s' for config option '%s', got '%s'", + expected_type, key, type(opts[key])) + ) + end + elseif default ~= nil then + return default + else + return nil + end +end + +local setopt = function(cfg, opts, key, type) + cfg[tostring(key)] = getopt(opts, key, type, cfg[tostring(key)]) +end + +local setopts = function(cfg, opts, tbl) + for k, v in pairs(tbl) do + setopt(cfg, opts, k, v) + end +end + +local setopt_tbl = function(cfg, opts, key) + if opts and opts[key] then + for k, v in pairs(opts[key]) do + if not cfg[key] then cfg[key] = {} end + cfg[key][k] = v + end + end +end + +function M.setup(opts) + setopts(config, opts, { + win_height = "number", + win_width = "number", + win_row = "number", + win_col = "number", + win_border = "boolean", + default_prompt = "string", + fzf_layout = "string", + fzf_binds = "table", + preview_cmd = "string", + preview_border = "string", + preview_wrap = "string", + preview_vertical = "string", + preview_horizontal = "string", + preview_layout = "string", + flip_columns = "number", + window_on_create = "function", + bat_theme = "string", + bat_opts = "string", + }) + setopts(config.files, opts.files, { + prompt = "string", + cmd = "string", + git_icons = "boolean", + file_icons = "boolean", + color_icons = "boolean", + fd_opts = "string", + find_opts = "string", + git_diff_cmd = "string", + git_untracked_cmd = "string", + }) + setopts(config.grep, opts.grep, { + prompt = "string", + input_prompt = "string", + cmd = "string", + git_icons = "boolean", + file_icons = "boolean", + color_icons = "boolean", + rg_opts = "string", + grep_opts = "string", + git_diff_cmd = "string", + git_untracked_cmd = "string", + }) + setopts(config.oldfiles, opts.oldfiles, { + prompt = "string", + git_icons = "boolean", + file_icons = "boolean", + color_icons = "boolean", + git_diff_cmd = "string", + git_untracked_cmd = "string", + cwd_only = "boolean", + include_current_session = "boolean", + }) + setopts(config.quickfix, opts.quickfix, { + prompt = "string", + cwd = "string", + separator = "string", + git_icons = "boolean", + file_icons = "boolean", + color_icons = "boolean", + git_diff_cmd = "string", + git_untracked_cmd = "string", + }) + setopts(config.loclist, opts.loclist, { + prompt = "string", + cwd = "string", + separator = "string", + git_icons = "boolean", + file_icons = "boolean", + color_icons = "boolean", + git_diff_cmd = "string", + git_untracked_cmd = "string", + }) + setopts(config.git, opts.git, { + prompt = "string", + cmd = "string", + git_icons = "boolean", + file_icons = "boolean", + color_icons = "boolean", + }) + setopts(config.buffers, opts.buffers, { + prompt = "string", + git_prompt = "string", + file_icons = "boolean", + color_icons = "boolean", + sort_lastused = "boolean", + show_all_buffers = "boolean", + ignore_current_buffer = "boolean", + cwd_only = "boolean", + }) + setopts(config.colorschemes, opts.colorschemes, { + prompt = "string", + live_preview = "boolean", + post_reset_cb = "function", + }) + setopts(config.manpages, opts.manpages, { + prompt = "string", + cmd = "string", + }) + setopts(config.helptags, opts.helptags, { + prompt = "string", + }) + -- table overrides without losing defaults + for _, k in ipairs({ + "git", "files", "oldfiles", "buffers", + "grep", "quickfix", "loclist", + "colorschemes", "helptags", "manpages", + }) do + setopt_tbl(config[k], opts[k], "actions") + setopt_tbl(config[k], opts[k], "winopts") + end + setopt_tbl(config, opts, "git_icons") + setopt_tbl(config, opts, "git_icon_colors") + setopt_tbl(config, opts, "file_icon_colors") + -- override the bat preview theme if set by caller + if config.bat_theme and #config.bat_theme > 0 then + vim.env.BAT_THEME = config.bat_theme + end + -- reset default window opts if set by user + fzf.default_window_options = config.winopts() +end + +-- we usually send winopts with every fzf.fzf call +-- but set default window options just in case +fzf.default_window_options = config.winopts() + +M.fzf_files = require'fzf-lua.core'.fzf_files +M.files = require'fzf-lua.providers.files'.files +M.grep = require'fzf-lua.providers.grep'.grep +M.live_grep = require'fzf-lua.providers.grep'.live_grep +M.grep_last = require'fzf-lua.providers.grep'.grep_last +M.grep_cword = require'fzf-lua.providers.grep'.grep_cword +M.grep_cWORD = require'fzf-lua.providers.grep'.grep_cWORD +M.grep_visual = require'fzf-lua.providers.grep'.grep_visual +M.grep_curbuf = require'fzf-lua.providers.grep'.grep_curbuf +M.git_files = require'fzf-lua.providers.files'.git_files +M.oldfiles = require'fzf-lua.providers.oldfiles'.oldfiles +M.quickfix = require'fzf-lua.providers.quickfix'.quickfix +M.loclist = require'fzf-lua.providers.quickfix'.loclist +M.buffers = require'fzf-lua.providers.buffers'.buffers +M.help_tags = require'fzf-lua.providers.helptags'.helptags +M.man_pages = require'fzf-lua.providers.manpages'.manpages +M.colorschemes = require'fzf-lua.providers.colorschemes'.colorschemes + +return M diff --git a/lua/fzf-lua/path.lua b/lua/fzf-lua/path.lua new file mode 100644 index 0000000..62e33ee --- /dev/null +++ b/lua/fzf-lua/path.lua @@ -0,0 +1,99 @@ +local M = {} + +M.separator = function() + return '/' +end + +M.tail = (function() + local os_sep = M.separator() + local match_string = '[^' .. os_sep .. ']*$' + + return function(path) + return string.match(path, match_string) + end +end)() + +function M.to_matching_str(path) + return path:gsub('(%-)', '(%%-)'):gsub('(%.)', '(%%.)'):gsub('(%_)', '(%%_)') +end + +function M.join(paths) + return table.concat(paths, M.separator()) +end + +function M.split(path) + return path:gmatch('[^'..M.separator()..']+'..M.separator()..'?') +end + +---Get the basename of the given path. +---@param path string +---@return string +function M.basename(path) + path = M.remove_trailing(path) + local i = path:match("^.*()" .. M.separator()) + if not i then return path end + return path:sub(i + 1, #path) +end + +function M.extension(path) + -- path = M.basename(path) + -- return path:match(".+%.(.*)") + -- search for the first dotten string part up to space + -- then match anything after the dot up to ':/\.' + path = path:match("(%.[^ :\t\x1b]+)") + if not path then return path end + return path:match("^.*%.([^ :\\/]+)") +end + +---Get the path to the parent directory of the given path. Returns `nil` if the +---path has no parent. +---@param path string +---@param remove_trailing boolean +---@return string|nil +function M.parent(path, remove_trailing) + path = " " .. M.remove_trailing(path) + local i = path:match("^.+()" .. M.separator()) + if not i then return nil end + path = path:sub(2, i) + if remove_trailing then + path = M.remove_trailing(path) + end + return path +end + +---Get a path relative to another path. +---@param path string +---@param relative_to string +---@return string +function M.relative(path, relative_to) + local p, _ = path:gsub("^" .. M.to_matching_str(M.add_trailing(relative_to)), "") + return p +end + +function M.add_trailing(path) + if path:sub(-1) == M.separator() then + return path + end + + return path..M.separator() +end + +function M.remove_trailing(path) + local p, _ = path:gsub(M.separator()..'$', '') + return p +end + +function M.shorten(path, max_length) + if string.len(path) > max_length - 1 then + path = path:sub(string.len(path) - max_length + 1, string.len(path)) + local i = path:match("()" .. M.separator()) + if not i then + return "…" .. path + end + return "…" .. path:sub(i, -1) + else + return path + end +end + +return M diff --git a/lua/fzf-lua/providers/buffers.lua b/lua/fzf-lua/providers/buffers.lua new file mode 100644 index 0000000..75a4fab --- /dev/null +++ b/lua/fzf-lua/providers/buffers.lua @@ -0,0 +1,148 @@ +if not pcall(require, "fzf") then + return +end + +local action = require("fzf.actions").action +local core = require "fzf-lua.core" +local path = require "fzf-lua.path" +local utils = require "fzf-lua.utils" +local config = require "fzf-lua.config" +local actions = require "fzf-lua.actions" +local fn, api = vim.fn, vim.api + +local M = {} + +local function getbufnumber(line) + return tonumber(string.match(line, "%[(%d+)")) +end + +local function getfilename(line) + return string.match(line, "%[.*%] (.+)") +end + +M.buffers = function(opts) + + opts = config.getopts(opts, config.buffers, { + "prompt", "actions", "winopts", + "file_icons", "color_icons", "sort_lastused", + "show_all_buffers", "ignore_current_buffer", + "cwd_only", + }) + + local act = action(function (items, fzf_lines, _) + -- only preview first item + local item = items[1] + local buf = getbufnumber(item) + if api.nvim_buf_is_loaded(buf) then + return api.nvim_buf_get_lines(buf, 0, fzf_lines, false) + else + local name = getfilename(item) + if fn.filereadable(name) ~= 0 then + return fn.readfile(name, "", fzf_lines) + end + return "UNLOADED: " .. name + end + end) + + coroutine.wrap(function () + local items = {} + + local bufnrs = vim.tbl_filter(function(b) + if 1 ~= vim.fn.buflisted(b) then + return false + end + -- only hide unloaded buffers if opts.show_all_buffers is false, keep them listed if true or nil + if opts.show_all_buffers == false and not vim.api.nvim_buf_is_loaded(b) then + return false + end + if opts.ignore_current_buffer and b == vim.api.nvim_get_current_buf() then + return false + end + if opts.cwd_only and not string.find(vim.api.nvim_buf_get_name(b), vim.loop.cwd(), 1, true) then + return false + end + return true + end, vim.api.nvim_list_bufs()) + if not next(bufnrs) then return end + + local header_line = false + local buffers = {} + for _, bufnr in ipairs(bufnrs) do + local flag = bufnr == vim.fn.bufnr('') and '%' or (bufnr == vim.fn.bufnr('#') and '#' or ' ') + + local element = { + bufnr = bufnr, + flag = flag, + info = vim.fn.getbufinfo(bufnr)[1], + } + + if opts.sort_lastused and (flag == "#" or flag == "%") then + if flag == "%" then header_line = true end + local idx = ((buffers[1] ~= nil and buffers[1].flag == "%") and 2 or 1) + table.insert(buffers, idx, element) + else + table.insert(buffers, element) + end + end + + for _, buf in pairs(buffers) do + -- local hidden = buf.info.hidden == 1 and 'h' or 'a' + local hidden = '' + local readonly = vim.api.nvim_buf_get_option(buf.bufnr, 'readonly') and '=' or ' ' + local changed = buf.info.changed == 1 and '+' or ' ' + local flags = hidden .. readonly .. changed + local leftbr = utils.ansi_codes.clear('[') + local rightbr = utils.ansi_codes.clear(']') + local bufname = string.format("%s:%s", + utils._if(#buf.info.name>0, path.relative(buf.info.name, vim.loop.cwd()), "[No Name]"), + utils._if(buf.info.lnum>0, buf.info.lnum, "")) + if buf.flag == '%' then + flags = utils.ansi_codes.red(buf.flag) .. flags + bufname = utils.ansi_codes.green(bufname) + leftbr = utils.ansi_codes.green('[') + rightbr = utils.ansi_codes.green(']') + elseif buf.flag == '#' then + flags = utils.ansi_codes.cyan(buf.flag) .. flags + else + flags = " " .. flags + end + local bufnrstr = string.format("%s%s%s", leftbr, + utils.ansi_codes.yellow(string.format(buf.bufnr)), rightbr) + local buficon = '' + if opts.file_icons then + local extension = path.extension(buf.info.name) + buficon = core.get_devicon(buf.info.name, extension) + if opts.color_icons then + buficon = utils.ansi_codes[config.file_icon_colors[extension] or "dark_grey"](buficon) .. " " + end + end + local item_str = string.format("%s%s %s %s%s", + utils._if(buf.bufnr>9, '' , ' '), + bufnrstr, flags, buficon, bufname) + table.insert(items, item_str) + end + + local selected = require("fzf").fzf(items, + core.build_fzf_cli({ + prompt = opts.prompt, + preview = act, + actions = opts.actions, + cli_args = utils._if(header_line and not opts.ignore_current_buffer, + '--header-lines=1', '') + }), + config.winopts(opts)) + + if not selected then return end + + if #selected > 1 then + for i = 2, #selected do + selected[i] = tostring(getbufnumber(selected[i])) + end + end + + actions.act(opts.actions, selected[1], selected) + + end)() +end + +return M diff --git a/lua/fzf-lua/providers/colorschemes.lua b/lua/fzf-lua/providers/colorschemes.lua new file mode 100644 index 0000000..c7729cd --- /dev/null +++ b/lua/fzf-lua/providers/colorschemes.lua @@ -0,0 +1,64 @@ +if not pcall(require, "fzf") then + return +end + +local fzf = require "fzf" +local action = require("fzf.actions").action +local core = require "fzf-lua.core" +local utils = require "fzf-lua.utils" +local config = require "fzf-lua.config" +local actions = require "fzf-lua.actions" + +local function get_current_colorscheme() + if vim.g.colors_name then + return vim.g.colors_name + else + return 'default' + end +end + +local M = {} + +M.colorschemes = function(opts) + + opts = config.getopts(opts, config.colorschemes, { + "prompt", "actions", "winopts", "live_preview", "post_reset_cb", + }) + + coroutine.wrap(function () + local prev_act = action(function (args) + if opts.live_preview and args then + local colorscheme = args[1] + vim.cmd("colorscheme " .. colorscheme) + end + end) + + local current_colorscheme = get_current_colorscheme() + local current_background = vim.o.background + local colors = vim.list_extend(opts.colors or {}, vim.fn.getcompletion('', 'color')) + local selected = fzf.fzf(colors, + core.build_fzf_cli({ + prompt = opts.prompt, + preview = prev_act, preview_window = 'right:0', + actions = opts.actions, + nomulti = true, + }), + config.winopts(opts.winopts)) + + if not selected then + vim.o.background = current_background + vim.cmd("colorscheme " .. current_colorscheme) + vim.o.background = current_background + else + actions.act(opts.actions, selected[1], selected) + end + + if opts.post_reset_cb then + opts.post_reset_cb() + end + + end)() + +end + +return M diff --git a/lua/fzf-lua/providers/files.lua b/lua/fzf-lua/providers/files.lua new file mode 100644 index 0000000..62e75cf --- /dev/null +++ b/lua/fzf-lua/providers/files.lua @@ -0,0 +1,66 @@ +if not pcall(require, "fzf") then + return +end + +-- local fzf = require "fzf" +local fzf_helpers = require("fzf.helpers") +-- local path = require "fzf-lua.path" +local core = require "fzf-lua.core" +local utils = require "fzf-lua.utils" +local config = require "fzf-lua.config" + +local M = {} + +local get_files_cmd = function(opts) + if opts.cmd and #opts.cmd>0 then + return opts.cmd + end + local command = nil + if vim.fn.executable("fd") == 1 then + if not opts.cwd or #opts.cwd == 0 then + command = string.format('fd %s', opts.fd_opts) + else + command = string.format('fd %s . %s', opts.fd_opts, + vim.fn.shellescape(opts.cwd)) + end + else + command = string.format('find %s %s', + utils._if(opts.cwd and #opts.cwd>0, vim.fn.shellescape(opts.cwd), '.'), + opts.find_opts) + end + return command +end + +M.files = function(opts) + opts = config.getopts(opts, config.files, { + "cmd", "prompt", "actions", "winopts", + "file_icons", "color_icons", "git_icons", + "fd_opts", "find_opts", + }) + + + local command = get_files_cmd(opts) + + opts.fzf_fn = fzf_helpers.cmd_line_transformer(command, + function(x) + return core.make_entry_file(opts, x) + end) + + return core.fzf_files(opts) +end + +M.git_files = function(opts) + local output = vim.fn.systemlist("git status") + if utils.shell_error() then + utils.info(unpack(output)) + return + end + opts = config.getopts(opts, config.git, { + "cmd", "prompt", "actions", "winopts", + "file_icons", "color_icons", "git_icons", + }) + -- opts.cmd sets this to "git ls-files" + return M.files(opts) +end + +return M diff --git a/lua/fzf-lua/providers/grep.lua b/lua/fzf-lua/providers/grep.lua new file mode 100644 index 0000000..f1cc122 --- /dev/null +++ b/lua/fzf-lua/providers/grep.lua @@ -0,0 +1,193 @@ +if not pcall(require, "fzf") then + return +end + +-- local fzf = require "fzf" +local fzf_helpers = require("fzf.helpers") +local path = require "fzf-lua.path" +local core = require "fzf-lua.core" +local utils = require "fzf-lua.utils" +local config = require "fzf-lua.config" + +local M = {} + +local get_grep_cmd = function(opts) + + local command = nil + if opts.cmd and #opts.cmd > 0 then + command = opts.cmd + elseif vim.fn.executable("rg") == 1 then + command = string.format("rg %s", opts.rg_opts) + else + command = string.format("grep %s", opts.grep_opts) + end + + -- filename takes precedence over directory + local search_path = '' + if opts.filename and #opts.filename>0 then + search_path = vim.fn.shellescape(opts.filename) + elseif opts.cwd and #opts.cwd>0 then + search_path = vim.fn.shellescape(opts.cwd) + end + + return string.format("%s -- %s %s", command, + utils._if(opts.last_search and #opts.last_search>0, + vim.fn.shellescape(opts.last_search), "{q}"), + search_path + ) +end + +M.grep = function(opts) + + opts = config.getopts(opts, config.grep, { + "cmd", "prompt", "actions", "winopts", + "file_icons", "color_icons", "git_icons", + "search", "input_prompt", + "rg_opts", "grep_opts", + }) + + if opts.repeat_last_search == true then + opts.search = config.grep.last_search + end + -- save the next search as last_search so we + -- let the caller have an option to run the + -- same search again + -- print("1", opts.last_search, opts.search) + if not opts.search or #opts.search == 0 then + config.grep.last_search = vim.fn.input(opts.input_prompt) + else + config.grep.last_search = opts.search + end + opts.last_search = config.grep.last_search + if not opts.last_search or #opts.last_search == 0 then + utils.info("Please provider valid search string") + return + end + + local command = get_grep_cmd(opts) + + opts.fzf_fn = fzf_helpers.cmd_line_transformer( + command, + function(x) + return core.make_entry_file(opts, x) + end) + + --[[ opts.cb_selected = function(_, x) + return x + end ]] + + opts.cli_args = "--delimiter='[: ]'" + opts.preview_args = "--highlight-line={3}" -- bat higlight + --[[ + # Preview with bat, matching line in the middle of the window below + # the fixed header of the top 3 lines + # + # ~3 Top 3 lines as the fixed header + # +{2} Base scroll offset extracted from the second field + # +3 Extra offset to compensate for the 3-line header + # /2 Put in the middle of the preview area + # + '--preview-window '~3:+{2}+3/2'' + ]] + opts.preview_offset = "+{3}-/2" + core.fzf_files(opts) +end + + +M.live_grep = function(opts) + + opts = config.getopts(opts, config.grep, { + "cmd", "prompt", "actions", "winopts", + "file_icons", "color_icons", "git_icons", + "search", "input_prompt", + "rg_opts", "grep_opts", + }) + + -- resetting last_search will return + -- {q} placeholder in our command + opts.last_search = opts.search + local initial_command = get_grep_cmd(opts) + opts.last_search = nil + local reload_command = get_grep_cmd(opts) .. " || true" + + --[[ local fzf_binds = utils.tbl_deep_clone(config.fzf_binds) + table.insert(fzf_binds, string.format("change:reload:%s", reload_command)) + opts.fzf_binds = vim.fn.shellescape(table.concat(fzf_binds, ',')) ]] + + opts.cli_args = "--delimiter='[: ]' " .. + string.format("--phony --query=%s --bind=%s", + utils._if(opts.search and #opts.search>0, opts.search, [['']]), + vim.fn.shellescape(string.format("change:reload:%s", reload_command))) + + opts.preview_args = "--highlight-line={3}" -- bat higlight + --[[ + # Preview with bat, matching line in the middle of the window below + # the fixed header of the top 3 lines + # + # ~3 Top 3 lines as the fixed header + # +{2} Base scroll offset extracted from the second field + # +3 Extra offset to compensate for the 3-line header + # /2 Put in the middle of the preview area + # + '--preview-window '~3:+{2}+3/2'' + ]] + opts.preview_offset = "+{3}-/2" + + + -- TODO: + -- this is not getting called past the initial command + -- until we fix that we cannot use icons as they interfere + -- with the extension parsing + opts.git_icons = false + opts.file_icons = false + opts.filespec = '{1}' + opts.preview_offset = "+{2}-/2" + opts.preview_args = "--highlight-line={2}" -- bat higlight + + opts.fzf_fn = fzf_helpers.cmd_line_transformer( + initial_command, + function(x) + return core.make_entry_file(opts, x) + end) + + core.fzf_files(opts) +end + +M.grep_last = function(opts) + if not opts then opts = {} end + opts.repeat_last_search = true + return M.grep(opts) +end + +M.grep_cword = function(opts) + if not opts then opts = {} end + opts.search = vim.fn.expand("") + return M.grep(opts) +end + +M.grep_cWORD = function(opts) + if not opts then opts = {} end + opts.search = vim.fn.expand("") + return M.grep(opts) +end + +M.grep_visual = function(opts) + if not opts then opts = {} end + opts.search = utils.get_visual_selection() + return M.grep(opts) +end + +M.grep_curbuf = function(opts) + if not opts then opts = {} end + opts.rg_opts = config.grep.rg_opts .. " --with-filename" + opts.filename = vim.api.nvim_buf_get_name(0) + if #opts.filename > 0 then + opts.filename = path.relative(opts.filename, vim.loop.cwd()) + return M.live_grep(opts) + else + utils.info("Rg current buffer requires actual file on disk") + return + end +end + +return M diff --git a/lua/fzf-lua/providers/helptags.lua b/lua/fzf-lua/providers/helptags.lua new file mode 100644 index 0000000..fea6224 --- /dev/null +++ b/lua/fzf-lua/providers/helptags.lua @@ -0,0 +1,125 @@ +if not pcall(require, "fzf") then + return +end + +local fzf = require "fzf" +local path = require "fzf-lua.path" +local core = require "fzf-lua.core" +local utils = require "fzf-lua.utils" +local config = require "fzf-lua.config" +local actions = require "fzf-lua.actions" + + +local M = {} + +local fzf_function = function (cb) + local opts = {} + opts.lang = config.helptags.lang or vim.o.helplang + opts.fallback = utils._if(config.helptags.fallback ~= nil, config.helptags.fallback, true) + + local langs = vim.split(opts.lang, ',', true) + if opts.fallback and not vim.tbl_contains(langs, 'en') then + table.insert(langs, 'en') + end + local langs_map = {} + for _, lang in ipairs(langs) do + langs_map[lang] = true + end + + local tag_files = {} + local function add_tag_file(lang, file) + if langs_map[lang] then + if tag_files[lang] then + table.insert(tag_files[lang], file) + else + tag_files[lang] = {file} + end + end + end + + local help_files = {} + local all_files = vim.fn.globpath(vim.o.runtimepath, 'doc/*', 1, 1) + for _, fullpath in ipairs(all_files) do + local file = path.tail(fullpath) + if file == 'tags' then + add_tag_file('en', fullpath) + elseif file:match('^tags%-..$') then + local lang = file:sub(-2) + add_tag_file(lang, fullpath) + else + help_files[file] = fullpath + end + end + + local add_tag = function(t, fzf_cb, co) + --[[ local tag = string.format("%-58s\t%s", + utils.ansi_codes.blue(t.name), + utils._if(t.name and #t.name>0, path.basename(t.name), '')) ]] + local tag = utils.ansi_codes.magenta(t.name) + fzf_cb(tag, function() + coroutine.resume(co) + end) + end + + coroutine.wrap(function () + local co = coroutine.running() + local tags_map = {} + local delimiter = string.char(9) + for _, lang in ipairs(langs) do + for _, file in ipairs(tag_files[lang] or {}) do + local lines = vim.split(utils.read_file(file), '\n', true) + for _, line in ipairs(lines) do + -- TODO: also ignore tagComment starting with ';' + if not line:match'^!_TAG_' then + local fields = vim.split(line, delimiter, true) + if #fields == 3 and not tags_map[fields[1]] then + add_tag({ + name = fields[1], + filename = help_files[fields[2]], + cmd = fields[3], + lang = lang, + }, cb, co) + tags_map[fields[1]] = true + -- pause here until we call coroutine.resume() + coroutine.yield() + end + end + end + end + end + -- done + -- cb(nil) + end)() +end + + +M.helptags = function(opts) + + opts = config.getopts(opts, config.helptags, { + "prompt", "actions", "winopts", + }) + + coroutine.wrap(function () + + -- local prev_act = action(function (args) end) + + local selected = fzf.fzf(fzf_function, + core.build_fzf_cli({ + prompt = opts.prompt, + -- preview = prev_act, + preview_window = 'right:0', + actions = opts.actions, + cli_args = "--nth 1", + nomulti = true, + }), + config.winopts(opts.winopts)) + + if not selected then return end + + actions.act(opts.actions, selected[1], selected) + + end)() + +end + +return M diff --git a/lua/fzf-lua/providers/manpages.lua b/lua/fzf-lua/providers/manpages.lua new file mode 100644 index 0000000..3f546e4 --- /dev/null +++ b/lua/fzf-lua/providers/manpages.lua @@ -0,0 +1,63 @@ +if not pcall(require, "fzf") then + return +end + +local fzf = require "fzf" +local fzf_helpers = require("fzf.helpers") +local core = require "fzf-lua.core" +local utils = require "fzf-lua.utils" +local config = require "fzf-lua.config" +local actions = require "fzf-lua.actions" + + +local M = {} + +local function getmanpage(line) + -- match until comma or space + return string.match(line, "[^, ]+") +end + +M.manpages = function(opts) + + opts = config.getopts(opts, config.manpages, { + "prompt", "actions", "winopts", "cmd", + }) + + coroutine.wrap(function () + + -- local prev_act = action(function (args) end) + + local fzf_fn = fzf_helpers.cmd_line_transformer(opts.cmd, function(x) + -- split by first occurence of ' - ' (spaced hyphen) + local man, desc = x:match("^(.-) %- (.*)$") + return string.format("%-45s %s", + utils.ansi_codes.red(man), desc) + end) + + local selected = fzf.fzf(fzf_fn, + core.build_fzf_cli({ + prompt = opts.prompt, + -- preview = prev_act, + preview_window = 'right:0', + actions = opts.actions, + cli_args = "--tiebreak begin --nth 1,2", + nomulti = true, + }), + config.winopts(opts.winopts)) + + if not selected then return end + + if #selected > 1 then + for i = 2, #selected do + selected[i] = getmanpage(selected[i]) + print(selected[i]) + end + end + + actions.act(opts.actions, selected[1], selected) + + end)() + +end + +return M diff --git a/lua/fzf-lua/providers/oldfiles.lua b/lua/fzf-lua/providers/oldfiles.lua new file mode 100644 index 0000000..b40a207 --- /dev/null +++ b/lua/fzf-lua/providers/oldfiles.lua @@ -0,0 +1,70 @@ +if not pcall(require, "fzf") then + return +end + +-- local fzf = require "fzf" +local fzf_helpers = require("fzf.helpers") +-- local path = require "fzf-lua.path" +local core = require "fzf-lua.core" +local utils = require "fzf-lua.utils" +local config = require "fzf-lua.config" + +local M = {} + +M.oldfiles = function(opts) + opts = config.getopts(opts, config.oldfiles, { + "prompt", "actions", "winopts", + "file_icons", "color_icons", "git_icons", + "include_current_session", "cwd_only", + }) + + local current_buffer = vim.api.nvim_get_current_buf() + local current_file = vim.api.nvim_buf_get_name(current_buffer) + local results = {} + + if opts.include_current_session then + for _, buffer in ipairs(vim.split(vim.fn.execute(':buffers! t'), "\n")) do + local match = tonumber(string.match(buffer, '%s*(%d+)')) + if match then + local file = vim.api.nvim_buf_get_name(match) + if vim.loop.fs_stat(file) and match ~= current_buffer then + table.insert(results, file) + end + end + end + end + + for _, file in ipairs(vim.v.oldfiles) do + if vim.loop.fs_stat(file) and not vim.tbl_contains(results, file) and file ~= current_file then + table.insert(results, file) + end + end + + if opts.cwd_only then + opts.cwd = vim.loop.cwd() + local cwd = opts.cwd + cwd = cwd:gsub([[\]],[[\\]]) + results = vim.tbl_filter(function(file) + return vim.fn.matchstrpos(file, cwd)[2] ~= -1 + end, results) + end + + opts.fzf_fn = function (cb) + for _, x in ipairs(results) do + x = core.make_entry_file(opts, x) + cb(x, function(err) + if err then return end + -- cb(nil) -- to close the pipe to fzf, this removes the loading + -- indicator in fzf + end) + end + end + + --[[ opts.cb_selected = function(_, x) + print("o:", x) + end ]] + + return core.fzf_files(opts) +end + +return M diff --git a/lua/fzf-lua/providers/quickfix.lua b/lua/fzf-lua/providers/quickfix.lua new file mode 100644 index 0000000..19b3fa7 --- /dev/null +++ b/lua/fzf-lua/providers/quickfix.lua @@ -0,0 +1,89 @@ +if not pcall(require, "fzf") then + return +end + +-- local fzf = require "fzf" +local fzf_helpers = require("fzf.helpers") +-- local path = require "fzf-lua.path" +local core = require "fzf-lua.core" +local utils = require "fzf-lua.utils" +local config = require "fzf-lua.config" + +local M = {} + +local quickfix_run = function(opts, cfg, locations) + if not locations then return {} end + local results = {} + for _, entry in ipairs(locations) do + local filename = entry.filename or vim.api.nvim_buf_get_name(entry.bufnr) + table.insert(results, string.format("%s:%s:%s:\t%s", + filename, --utils.ansi_codes.magenta(filename), + utils.ansi_codes.green(tostring(entry.lnum)), + utils.ansi_codes.blue(tostring(entry.col)), + entry.text)) + end + + opts = config.getopts(opts, cfg, { + "cwd", "prompt", "actions", "winopts", + "file_icons", "color_icons", "git_icons", + "separator" + }) + + opts.fzf_fn = function (cb) + for _, x in ipairs(results) do + x = core.make_entry_file(opts, x) + cb(x, function(err) + if err then return end + -- cb(nil) -- to close the pipe to fzf, this removes the loading + -- indicator in fzf + end) + end + end + + --[[ opts.cb_selected = function(_, x) + return x + end ]] + + opts.cli_args = "--delimiter='[: \\t]'" + opts.preview_args = "--highlight-line={3}" -- bat higlight + --[[ + # Preview with bat, matching line in the middle of the window below + # the fixed header of the top 3 lines + # + # ~3 Top 3 lines as the fixed header + # +{2} Base scroll offset extracted from the second field + # +3 Extra offset to compensate for the 3-line header + # /2 Put in the middle of the preview area + # + '--preview-window '~3:+{2}+3/2'' + ]] + opts.preview_offset = "+{3}-/2" + return core.fzf_files(opts) +end + +M.quickfix = function(opts) + local locations = vim.fn.getqflist() + if vim.tbl_isempty(locations) then + utils.info("Quickfix list is empty.") + return + end + + return quickfix_run(opts, config.quickfix, locations) +end + +M.loclist = function(opts) + local locations = vim.fn.getloclist(0) + + for _, value in pairs(locations) do + value.filename = vim.api.nvim_buf_get_name(value.bufnr) + end + + if vim.tbl_isempty(locations) then + utils.info("Location list is empty.") + return + end + + return quickfix_run(opts, config.loclist, locations) +end + +return M diff --git a/lua/fzf-lua/utils.lua b/lua/fzf-lua/utils.lua new file mode 100644 index 0000000..c10ae41 --- /dev/null +++ b/lua/fzf-lua/utils.lua @@ -0,0 +1,174 @@ +-- help to inspect results, e.g.: +-- ':lua _G.dump(vim.fn.getwininfo())' +function _G.dump(...) + local objects = vim.tbl_map(vim.inspect, { ... }) + print(unpack(objects)) +end + +local M = {} + +M._if = function(bool, a, b) + if bool then + return a + else + return b + end +end + +function M._echo_multiline(msg) + for _, s in ipairs(vim.fn.split(msg, "\n")) do + vim.cmd("echom '" .. s:gsub("'", "''").."'") + end +end + +function M.info(msg) + vim.cmd('echohl Directory') + M._echo_multiline("[Fzf-lua] " .. msg) + vim.cmd('echohl None') +end + +function M.warn(msg) + vim.cmd('echohl WarningMsg') + M._echo_multiline("[Fzf-lua] " .. msg) + vim.cmd('echohl None') +end + +function M.err(msg) + vim.cmd('echohl ErrorMsg') + M._echo_multiline("[Fzf-lua] " .. msg) + vim.cmd('echohl None') +end + +function M.shell_error() + return vim.v.shell_error ~= 0 +end + +function M.is_git_repo() + vim.fn.system("git status") + return M._if(M.shell_error(), false, true) +end + +M.read_file = function(filepath) + local fd = vim.loop.fs_open(filepath, "r", 438) + if fd == nil then return '' end + local stat = assert(vim.loop.fs_fstat(fd)) + if stat.type ~= 'file' then return '' end + local data = assert(vim.loop.fs_read(fd, stat.size, 0)) + assert(vim.loop.fs_close(fd)) + return data +end + +M.read_file_async = function(filepath, callback) + vim.loop.fs_open(filepath, "r", 438, function(err_open, fd) + if err_open then + print("We tried to open this file but couldn't. We failed with following error message: " .. err_open) + return + end + vim.loop.fs_fstat(fd, function(err_fstat, stat) + assert(not err_fstat, err_fstat) + if stat.type ~= 'file' then return callback('') end + vim.loop.fs_read(fd, stat.size, 0, function(err_read, data) + assert(not err_read, err_read) + vim.loop.fs_close(fd, function(err_close) + assert(not err_close, err_close) + return callback(data) + end) + end) + end) + end) +end + + +function M.tbl_deep_clone(t) + if not t then return end + local clone = {} + + for k, v in pairs(t) do + if type(v) == "table" then + clone[k] = M.tbl_deep_clone(v) + else + clone[k] = v + end + end + + return clone +end + +function M.tbl_length(T) + local count = 0 + for _ in pairs(T) do count = count + 1 end + 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 +end + +function M.tbl_concat(...) + local result = {} + local n = 0 + + for _, t in ipairs({...}) do + for i, v in ipairs(t) do + result[n + i] = v + end + n = n + #t + end + + return result +end + +function M.tbl_pack(...) + return {n=select('#',...); ...} +end + +function M.tbl_unpack(t, i, j) + return unpack(t, i or 1, j or t.n or #t) +end + +M.ansi_codes = {} +M.ansi_colors = { + clear = "\x1b[0m", + bold = "\x1b[1m", + black = "\x1b[0;30m", + red = "\x1b[0;31m", + green = "\x1b[0;32m", + yellow = "\x1b[0;33m", + blue = "\x1b[0;34m", + magenta = "\x1b[0;35m", + cyan = "\x1b[0;36m", + grey = "\x1b[0;90m", + dark_grey = "\x1b[0;97m", + white = "\x1b[0;98m", +} + +for color, escseq in pairs(M.ansi_colors) do + M.ansi_codes[color] = function(string) + if string == nil or #string == 0 then return '' end + return escseq .. string .. M.ansi_colors.clear + end +end + +function M.get_visual_selection() + -- must exit visual mode or program croaks + -- :visual leaves ex-mode back to normal mode + -- use 'gv' to reselect the text + vim.cmd[[visual]] + local _, csrow, cscol, _ = unpack(vim.fn.getpos("'<")) + local _, cerow, cecol, _ = unpack(vim.fn.getpos("'>")) + local lines = vim.fn.getline(csrow, cerow) + -- local n = cerow-csrow+1 + local n = M.tbl_length(lines) + if n <= 0 then return '' end + lines[n] = string.sub(lines[n], 1, cecol) + lines[1] = string.sub(lines[1], cscol) + print(n, csrow, cscol, cerow, cecol, table.concat(lines, "\n")) + return table.concat(lines, "\n") +end + +return M diff --git a/plugin/fzf-lua.vim b/plugin/fzf-lua.vim new file mode 100644 index 0000000..c78ed4a --- /dev/null +++ b/plugin/fzf-lua.vim @@ -0,0 +1,8 @@ +if exists('g:loaded_fzf_lua') | finish | endif + +if !has('nvim-0.5') + echohl Error + echomsg "Fzf-lua is only available for Neovim versions 0.5 and above" + echohl clear + finish +endif