From 2868828a59a776c9f19ae6aa0bb68cff6cb40047 Mon Sep 17 00:00:00 2001 From: bhagwan Date: Fri, 5 Apr 2024 09:02:46 -0700 Subject: [PATCH] WIP --- .luarc.jsonc | 5 +- OPTIONS.md | 104 ++++++++++++++++++ doc/fzf-lua-opts.txt | 120 +++++++++++++++++++++ lua/fzf-lua/cmd.lua | 232 ++++++++++++++++++++++++++-------------- lua/fzf-lua/cmp_src.lua | 84 +++++++++++++++ lua/fzf-lua/config.lua | 14 ++- lua/fzf-lua/init.lua | 6 +- plugin/fzf-lua.lua | 85 ++------------- plugin/fzf-lua.vim | 21 +--- 9 files changed, 490 insertions(+), 181 deletions(-) create mode 100644 OPTIONS.md create mode 100644 doc/fzf-lua-opts.txt create mode 100644 lua/fzf-lua/cmp_src.lua diff --git a/.luarc.jsonc b/.luarc.jsonc index 6bc8764e..8f4265dc 100644 --- a/.luarc.jsonc +++ b/.luarc.jsonc @@ -15,7 +15,10 @@ "$VIMRUNTIME/lua", "${3rd}/luv/library", "$XDG_DATA_HOME/nvim/lazy/plenary.nvim/lua", - "$LOCALAPPDATA/nvim-data/lazy/plenary.nvim/lua" + "$LOCALAPPDATA/nvim-data/lazy/plenary.nvim/lua", + // For "cmp_src.lua" type resolving + "$XDG_DATA_HOME/nvim/lazy/nvim-cmp/lua", + "$LOCALAPPDATA/nvim-data/lazy/nvim-cmp/lua" ], "checkThirdParty": false, "maxPreload": 2000, diff --git a/OPTIONS.md b/OPTIONS.md new file mode 100644 index 00000000..8d72807b --- /dev/null +++ b/OPTIONS.md @@ -0,0 +1,104 @@ +# Fzf-Lua Options + +## Setup + +--- + +## Globals + +Globals are options that aren't picker-specific and can be used with all fzf-lua commands, for +example, positioning the floating window at the bottom line using `globals.winopts.row`: + +> The `globals` prefix denotates the scope of the option and is therefore omitted + +Using `FzfLua` user command: +```lua +:FzfLua files winopts.row=1 +``` + +Using Lua: +```lua +:lua require("fzf-lua").files({ winopts = { row = 1 } }) +-- Using the recursive option format +:lua require("fzf-lua").files({ ["winopts.row"] = 1 }) +``` + +#### globals.query + +Type: `string`, Default: `nil` + +Initial query (prompt text), passed to fzf as `--query` flag. + +#### globals.header + +Type: `string|false`, Default: `nil` + +Header line, set to any string to display a header line, set to `false` to disable fzf-lua +interactive headers (e.g. "ctrl-g to disable .gitignore", etc). + +#### globals.winopts.row + +Type: `number`, Default: `0.35` + +Screen row where to place the fzf-lua float window, between 0-1 will represent precentage of `vim.o.lines` (0: top, 1: bottom), if >= 1 will attempt to place the float in the exact screen line. + +#### globals.winopts.col + +Type: `number`, Default: `0.55` + +Screen column where to place the fzf-lua float window, between 0-1 will represent precentage of `vim.o.columns` (0: leftmost, 1: rightmost), if >= 1 will attempt to place the float in the exact screen column. + +#### globals.winopts.preview.border + +Type: `string`, Default: `border` + +Applies only to fzf native previewers (i.e. `bat`, `git_status`), set to `noborder` to hide the preview border, consult `man fzf` for all vailable options. + +--- + +### Cmd: files + +Files picker, will enumrate the filesystem of the current working directory using `fd`, `rg` and `grep` or `dir.exe`. + +#### files.cwd + +Type: `string`, Default: `nil` + +Sets the current working directory. + +#### files.cwd_prompt + +Type: `boolean`, Default: `true` + +Display the current working directory in the prompt (`fzf.vim` style). + +#### files.cwd_prompt_shorten_len + +Type: `number`, Default: `32` + +Prompt over this length will be shortened, e.g. `~/.config/nvim/lua/` will be shortened to `~/.c/n/lua/` (for more info see `:help pathshorten`). + +*Requires `cwd_prompt=true` + +#### files.cwd_prompt_shorten_val + +Type: `number`, Default: `1` + +Length of shortened prompt path parts, e.g. set to `2`, `~/.config/nvim/lua/` will be shortened to `~/.co/nv/lua/` (for more info see `:help pathshorten`). + +*Requires `cwd_prompt=true` + +### Cmd: LSP commands + +#### lsp_references + +LSP references + +#### async_or_timeout + +Type: `number|boolean`, Default: `5000` + +Whether LSP calls are made block, set to `true` for asynchronous, otherwise defines the timeout +(ms) for the LPS request via `vim.lsp.buf_request_sync`. + + diff --git a/doc/fzf-lua-opts.txt b/doc/fzf-lua-opts.txt new file mode 100644 index 00000000..723a4a4b --- /dev/null +++ b/doc/fzf-lua-opts.txt @@ -0,0 +1,120 @@ +*fzf-lua-opts.txt* For Neovim >= 0.8.0 Last change: 2024 April 06 + +============================================================================== +Table of Contents *fzf-lua-opts-table-of-contents* + +Setup ................................................... |fzf-lua-opts-setup| +Globals ............................................... |fzf-lua-opts-globals| +Cmd: files ......................................... |fzf-lua-opts-cmd:-files| +Cmd: LSP commands ........................... |fzf-lua-opts-cmd:-lsp-commands| + +============================================================================== +FZF-LUA OPTIONS *fzf-lua-opts-fzf-lua-options* + + + +------------------------------------------------------------------------------ +SETUP *fzf-lua-opts-setup* + + + +------------------------------------------------------------------------------ +GLOBALS *fzf-lua-opts-globals* + + + + +winopts.row *fzf-lua-opts-winopts.row* + +Type: `number`, Default: `0.35` + +Screen row where to place the fzf-lua float window, between 0-1 will represent +precentage of `vim.o.lines` (0: top, 1: bottom), if >= 1 will attempt to place +the float in the exact screen line. + + + +winopts.col *fzf-lua-opts-winopts.col* + +Type: `number`, Default: `0.55` + +Screen column where to place the fzf-lua float window, between 0-1 will +represent precentage of `vim.o.columns` (0: leftmost, 1: rightmost), if >= 1 +will attempt to place the float in the exact screen column. + + + +winopts.preview.border *fzf-lua-opts-winopts.preview.border* + +Type: `string`, Default: `border` + +Applies only to fzf native previewers (i.e. `bat`, `git_status`), set to +`noborder` to hide the preview border, consult `man fzf` for all vailable +options. + + + +CMD: FILES *fzf-lua-opts-cmd:-files* + +Files picker, will enumrate the filesystem of the current working directory +using `fd`, `rg` and `grep` or `dir.exe`. + + + +files.cwd *fzf-lua-opts-files.cwd* + +Type: `string`, Default: `nil` + +Sets the current working directory. + + + +files.cwd_prompt *fzf-lua-opts-files.cwd_prompt* + +Type: `boolean`, Default: `true` + +Display the current working directory in the prompt (`fzf.vim` style). + + + +files.cwd_prompt_shorten_len *fzf-lua-opts-files.cwd_prompt_shorten_len* + +Type: `number`, Default: `32` + +Prompt over this length will be shortened, e.g. `~/.config/nvim/lua/` will be +shortened to `~/.c/n/lua/` (for more info see `:help pathshorten`). + +*Requires `cwd_prompt=true` + + + +files.cwd_prompt_shorten_val *fzf-lua-opts-files.cwd_prompt_shorten_val* + +Type: `number`, Default: `1` + +Length of shortened prompt path parts, e.g. set to `2`, `~/.config/nvim/lua/` +will be shortened to `~/.co/nv/lua/` (for more info see `:help pathshorten`). + +*Requires `cwd_prompt=true` + + + +CMD: LSP COMMANDS *fzf-lua-opts-cmd:-lsp-commands* + + + +lsp_references *fzf-lua-opts-lsp_references* + +LSP references + + + +async_or_timeout *fzf-lua-opts-async_or_timeout* + +Type: `number|boolean`, Default: `5000` + +Whether LSP calls are made block, set to `true` for asynchronous, otherwise +defines the timeout (ms) for the LPS request via `vim.lsp.buf_request_sync`. + + +vim:tw=78:ts=8:ft=help:norl: \ No newline at end of file diff --git a/lua/fzf-lua/cmd.lua b/lua/fzf-lua/cmd.lua index 5d18bc21..849dd9e3 100644 --- a/lua/fzf-lua/cmd.lua +++ b/lua/fzf-lua/cmd.lua @@ -1,103 +1,171 @@ --- Modified from Telescope 'command.lua' local builtin = require "fzf-lua" +local path = require "fzf-lua.path" local utils = require "fzf-lua.utils" -local command = {} - -local arg_value = { - ["nil"] = nil, - ['""'] = "", - ['"'] = "", -} - -local bool_type = { - ["false"] = false, - ["true"] = true, -} - --- convert command line string arguments to --- lua number boolean type and nil values -local function convert_user_opts(user_opts) - local _switch = { - ["boolean"] = function(key, val) - if val == "false" then - user_opts[key] = false - return - end - user_opts[key] = true - end, - ["number"] = function(key, val) - user_opts[key] = tonumber(val) - end, - ["string"] = function(key, val) - if arg_value[val] ~= nil then - user_opts[key] = arg_value[val] - return - end - - if bool_type[val] ~= nil then - user_opts[key] = bool_type[val] - end - end, - } - - local _switch_metatable = { - __index = function(_, k) - utils.info(string.format("Type of %s does not match", k)) - end, - } +local defaults = require "fzf-lua.defaults".defaults +local serpent = require "fzf-lua.lib.serpent" - setmetatable(_switch, _switch_metatable) +local M = {} - for key, val in pairs(user_opts) do - _switch["string"](key, val) - end -end +function M.run_command(cmd, ...) + local args = { ... } + cmd = cmd or "builtin" --- receive the viml command args --- it should output a table value like --- { --- cmd = 'files', --- opts = { --- cwd = '***', --- } -local function run_command(args) - local user_opts = args or {} - if next(user_opts) == nil or not user_opts.cmd then - utils.info("missing command args") + if not builtin[cmd] then + utils.info(string.format("invalid command '%s'", cmd)) return end - local cmd = user_opts.cmd - local opts = user_opts.opts or {} + local opts = {} - if next(opts) ~= nil then - convert_user_opts(opts) + for _, arg in ipairs(args) do + local key = arg:match("^[^=]+") + local val = arg:match("=") and arg:match("=(.*)$") + local ok, loaded = serpent.load(val or "true") + if ok and (type(loaded) ~= "table" or not vim.tbl_isempty(loaded)) then + opts[key] = loaded + else + opts[key] = val or true + end end - if builtin[cmd] then - builtin[cmd](opts) - else - utils.info(string.format("invalid command '%s'", cmd)) + builtin[cmd](opts) +end + +---@return table +function M.options_md() + -- Only attempt to load from file once, if failed we ditch the docs + if M._options_md ~= nil then return M._options_md end + M._options_md = {} + local filepath = path.join({ vim.g.fzf_lua_root, "OPTIONS.md" }) + local lines = vim.split(utils.read_file(filepath), "\n") + local section + for _, l in ipairs(lines or {}) do + if l:match("^#") or l:match(" "cwd" + o = o:match("%..*$"):sub(2) + if not vim.tbl_contains(opts, o) then + table.insert(opts, o) + end + end, opts_from_docs) + + table.sort(opts) + + opts = vim.tbl_filter(function(val) + return vim.startswith(val, l[#l]) + end, opts) + + return cmp_items and to_cmp_items(opts, { cmd = cmd_cfg_key }) or opts end -return command +return M diff --git a/lua/fzf-lua/cmp_src.lua b/lua/fzf-lua/cmp_src.lua new file mode 100644 index 00000000..23e25bc3 --- /dev/null +++ b/lua/fzf-lua/cmp_src.lua @@ -0,0 +1,84 @@ +local Src = {} + +Src.new = function(_) + local self = setmetatable({}, { + __index = Src, + }) + return self +end + +---Return whether this source is available in the current context or not (optional). +---@return boolean +function Src:is_available() + local mode = vim.api.nvim_get_mode().mode:sub(1, 1) + return mode == "c" and vim.fn.getcmdtype() == ":" +end + +---Return the debug name of this source (optional). +---@return string +function Src:get_debug_name() + return "fzf-lua" +end + +---Invoke completion (required). +---@param params cmp.SourceCompletionApiParams +---@param callback fun(response: lsp.CompletionResponse|nil) +function Src:complete(params, callback) + if not params.context.cursor_before_line:match("FzfLua") then + return callback() + end + return callback(require("fzf-lua.cmd")._candidates(params.context.cursor_before_line, true)) +end + +---@param completion_item lsp.CompletionItem +---@return lsp.MarkupContent? +function Src:_get_documentation(completion_item) + local options_md = require("fzf-lua.cmd").options_md() + if not options_md or vim.tbl_isempty(options_md) then return end + local markdown = options_md[completion_item.label] + if not markdown and completion_item.data then + -- didn't find matching the label directly, search globals + -- this will match "winopts.row" as "globals.winopts.row" + markdown = options_md["globals." .. completion_item.label] + end + if not markdown and completion_item.data and completion_item.data.cmd then + -- didn't find matching the label or globals, search provider specific + -- e.g. for "cwd_prompt" option we search the dict for "files.cwd_prompt" + markdown = options_md[completion_item.data.cmd .. "." .. completion_item.label] + end + return markdown and { kind = "markdown", value = markdown } or nil +end + +---Resolve completion item (optional). +-- This is called right before the completion is about to be displayed. +---Useful for setting the text shown in the documentation window (`completion_item.documentation`). +---@param completion_item lsp.CompletionItem +---@param callback fun(completion_item: lsp.CompletionItem|nil) +function Src:resolve(completion_item, callback) + completion_item.documentation = self:_get_documentation(completion_item) + callback(completion_item) +end + +function Src._register_cmdline() + local ok, cmp = pcall(require, "cmp") + if not ok then return end + cmp.register_source("FzfLua", Src) + local cmdline_cfg = require("cmp.config").cmdline + local has_fzf_lua = false + for _, s in ipairs(cmdline_cfg[":"].sources or {}) do + if s.name == "FzfLua" then + has_fzf_lua = true + end + end + if not has_fzf_lua then + if cmdline_cfg[":"] then + table.insert(cmdline_cfg[":"].sources or {}, { + group_index = 1, + name = "FzfLua", + option = {} + }) + end + end +end + +return Src diff --git a/lua/fzf-lua/config.lua b/lua/fzf-lua/config.lua index 293ef951..ba86e10d 100644 --- a/lua/fzf-lua/config.lua +++ b/lua/fzf-lua/config.lua @@ -141,9 +141,17 @@ function M.normalize_opts(opts, globals, __resume_key) -- expand opts that were specified with a dot -- e.g. `:FzfLua files winopts.border=single` - for k, v in pairs(opts) do - if k:match("%.") then - utils.map_set(opts, k, v) + do + -- convert keys only after full iteration or we will + -- miss keys due to messing with map ordering + local to_convert = {} + for k, _ in pairs(opts) do + if k:match("%.") then + table.insert(to_convert, k) + end + end + for _, k in ipairs(to_convert) do + utils.map_set(opts, k, opts[k]) opts[k] = nil end end diff --git a/lua/fzf-lua/init.lua b/lua/fzf-lua/init.lua index 9a4d58aa..aef33a6c 100644 --- a/lua/fzf-lua/init.lua +++ b/lua/fzf-lua/init.lua @@ -19,14 +19,14 @@ do local currFile = debug.getinfo(1, "S").source:gsub("^@", "") vim.g.fzf_lua_directory = path.normalize(path.parent(currFile)) - local fzf_lua_root = path.parent(path.parent(vim.g.fzf_lua_directory)) + vim.g.fzf_lua_root = path.parent(path.parent(vim.g.fzf_lua_directory)) -- Manually source the vimL script containing ':FzfLua' cmd -- does nothing if already loaded due to `vim.g.loaded_fzf_lua` - source_vimL({ fzf_lua_root, "plugin", "fzf-lua.vim" }) + source_vimL({ vim.g.fzf_lua_root, "plugin", "fzf-lua.vim" }) -- Autoload scipts dynamically loaded on `vim.fn[fzf_lua#...]` call -- `vim.fn.exists("*fzf_lua#...")` will return 0 unless we manuall source - source_vimL({ fzf_lua_root, "autoload", "fzf_lua.vim" }) + source_vimL({ vim.g.fzf_lua_root, "autoload", "fzf_lua.vim" }) -- Set var post source as the top of the file `require` will return 0 -- due to it potentially being loaded before "autoload/fzf_lua.vim" utils.__HAS_AUTOLOAD_FNS = vim.fn.exists("*fzf_lua#getbufinfo") == 1 diff --git a/plugin/fzf-lua.lua b/plugin/fzf-lua.lua index 86f48a79..53fabd7b 100644 --- a/plugin/fzf-lua.lua +++ b/plugin/fzf-lua.lua @@ -1,6 +1,4 @@ -if vim.g.loaded_fzf_lua == 1 then - return -end +if vim.g.loaded_fzf_lua == 1 then return end vim.g.loaded_fzf_lua = 1 -- Should never be called, below nvim 0.7 "plugin/fzf-lua.vim" @@ -11,81 +9,16 @@ if vim.fn.has("nvim-0.7") ~= 1 then end vim.api.nvim_create_user_command("FzfLua", function(opts) - require("fzf-lua.cmd").load_command(unpack(opts.fargs)) + require("fzf-lua.cmd").run_command(unpack(opts.fargs)) end, { nargs = "*", complete = function(_, line) - local metatable = require("fzf-lua") - local builtin_list = vim.tbl_filter(function(k) - return metatable._excluded_metamap[k] == nil - end, vim.tbl_keys(metatable)) - - local l = vim.split(line, "%s+") - local n = #l - 2 - - if n == 0 then - local commands = vim.tbl_flatten({ builtin_list }) - table.sort(commands) - - return vim.tbl_filter(function(val) - return vim.startswith(val, l[2]) - end, commands) - end - - -- Not all commands have their opts under the same key - local function cmd2key(cmd) - local cmd2cfg = { - { - patterns = { "^git_", "^dap", "^tmux_" }, - transform = function(c) return c:gsub("_", ".") end - }, - { - patterns = { "^lsp_code_actions$" }, - transform = function(_) return "lsp.code_actions" end - }, - { patterns = { "^lsp_.*_symbols$" }, transform = function(_) return "lsp.symbols" end }, - { patterns = { "^lsp_" }, transform = function(_) return "lsp" end }, - { patterns = { "^diagnostics_" }, transform = function(_) return "dianostics" end }, - { patterns = { "^tags" }, transform = function(_) return "tags" end }, - { patterns = { "grep" }, transform = function(_) return "grep" end }, - { patterns = { "^complete_bline$" }, transform = function(_) return "complete_line" end }, - } - for _, v in pairs(cmd2cfg) do - for _, p in ipairs(v.patterns) do - if cmd:match(p) then return v.transform(cmd) end - end - end - return cmd - end - - local utils = require("fzf-lua.utils") - local defaults = require("fzf-lua.defaults").defaults - local cmd_opts = utils.map_get(defaults, cmd2key(l[2])) or {} - local opts = vim.tbl_filter(function(k) - return not k:match("^_") - end, vim.tbl_keys(utils.map_flatten(cmd_opts))) - - -- Add globals recursively, e.g. `winopts.fullscreen` - -- will be later retrieved using `utils.map_get(...)` - for k, v in pairs({ - winopts = false, - keymap = false, - fzf_opts = false, - fzf_tmux_opts = false, - __HLS = "hls", -- rename prefix - }) do - opts = vim.tbl_flatten({ opts, vim.tbl_keys(utils.map_flatten(defaults[k] or {}, v or k)) }) - end - - -- Add generic options that apply to all pickers - for _, o in ipairs({ "query" }) do - table.insert(opts, o) - end - - table.sort(opts) - - return vim.tbl_filter(function(val) - return vim.startswith(val, l[#l]) - end, opts) + return require("fzf-lua.cmd")._candidates(line) end, }) + +-- If available register as nvim-cmp source +local ok, _ = pcall(require, "cmp") +if ok then + require("fzf-lua.cmp_src")._register_cmdline() +end diff --git a/plugin/fzf-lua.vim b/plugin/fzf-lua.vim index 2b6282f0..3a266c3f 100644 --- a/plugin/fzf-lua.vim +++ b/plugin/fzf-lua.vim @@ -15,22 +15,11 @@ let g:loaded_fzf_lua = 1 " FzfLua builtin lists function! s:fzflua_complete(arg, line, pos) abort - let l:builtin_list = luaeval('vim.tbl_filter( - \ function(k) - \ if require("fzf-lua")._excluded_metamap[k] then - \ return false - \ end - \ return true - \ end, - \ vim.tbl_keys(require("fzf-lua")))') - call sort(l:builtin_list) - - let list = [l:builtin_list] - let l = split(a:line[:a:pos-1], '\%(\%(\%(^\|[^\\]\)\\\)\@) +command! -nargs=* -complete=custom,s:fzflua_complete FzfLua + \ lua require("fzf-lua.cmd").run_command()