diff options
| -rw-r--r-- | README.md | 87 | ||||
| -rw-r--r-- | lua/denote-fzf-lua.lua | 157 | ||||
| -rw-r--r-- | lua/denote-fzf-lua/config.lua | 67 | ||||
| -rwxr-xr-x | lua/denote-fzf-lua/scripts/fallback_search_files.sh | 15 |
4 files changed, 166 insertions, 160 deletions
@@ -1,34 +1,95 @@ # denote-fzf-lua -Neovim plugin that uses `fzf-lua` to search a directory of notes formatted with the Emacs Denote file-naming scheme: +Neovim plugin that uses [`fzf-lua`](https://github.com/ibhagwan/fzf-lua) to search a directory of notes formatted with the Emacs Denote file-naming scheme: `DATE==SIGNATURE--TITLE__KEYWORDS.EXTENSION` -The nice custom table for Denote files is the only unique 'feature' of this plugin. If you don't care about that you should just use `fzf-lua` on its own. +The nice custom table for Denote files is the only unique feature of this plugin. If you don't care about that you should just configure `fzf-lua` alone however you like it. + +# Installation / Config + +Example config via [lazy.nvim](https://github.com/folke/lazy.nvim) + +```lua +{ + "DefaultGen/denote-fzf-lua", + opts = { + dir = "~/notes" -- Denote notes directory + + -- Toggle which Denote fields are displayed in search + search_fields = { + path = false, + date = true, + time = false, + sig = false, + title = true, + tags = true, + ext = false, + }, + + -- OPTIONAL: + -- fzf-lua plugin options. Check fzf-lua docs for full details. + -- Use this to set custom window and fzf options (and more) + fzf_lua_opts = { + winopts = { + height = 0.85, + width = 0.80, + row = 0.35, + col = 0.50, + preview = { + layout = 'vertical', -- vertical, horizontal, or flex + vertical = 'down:45%', -- Alt: horizontal = 'right:50%' + }, + }, + -- Options sent to fzf. If you don't include these, it will be + -- set to the defaults below (which look like the screenshot) + fzf_opts = { + ['--reverse'] = true, + ['--no-info'] = true, + ['--no-separator'] = true, + ['--no-hscroll'] = true, + ['-i'] = true, + }, + }, + }, +}, +``` ## Dependencies -* `fzf` - Fuzzy finder -* `ripgrep` - Fast `grep` replacement -* `fd` (OPTIONAL) - Fast `find` replacement -* `sd` (OPTIONAL) - Fast `sed` replacement -* `qsv` (OPTIONAL) - Fast `column` replacement +* [`ibhagwan/fzf-lua`](https://github.com/ibhagwan/fzf-lua) - Neovim plugin +* `fzf` - Fuzzy finder +* (OPTIONAL) `fd` - Fast `find` replacement +* (OPTIONAL) `sd` - Fast `sed` replacement +* (OPTIONAL) `qsv`- Fast `column` replacement +* (OPTIONAL) `bat` - Nicer preview than `cat` +* (OPTIONAL) `ripgrep` - Required to search note contents ``` -Arch Linux: sudo pacman -S fzf ripgrep fd sd +Arch Linux: sudo pacman -S fzf ripgrep fd sd bat yay -S qsv-bin -Debian: sudo apt install fzf ripgrep fd sd +Debian: sudo apt install fzf ripgrep fd-find sd bat + export PATH=/usr/lib/cargo/bin/:$PATH (to add `fd` to path) -qsv is available here: https://github.com/jqnatividad/qsv +qsv: https://github.com/jqnatividad/qsv ``` -If the optional dependencies are missing `denote-fzf-lua` falls back to standard Unix tools. - -As a rough benchmark, the Rust tools can perform a search through 10k notes in 0.1s vs. 0.2s for standard tools on my PC. For 100k notes it's 0.6s vs. 2.1s. +If the optional dependencies are missing `denote-fzf-lua` falls back to standard Unix tools. The Rust tools are 2-3x faster. On my PC this is a difference of 0.1s vs 0.2s for 10k notes, or 0.6s vs 1.9s for 100k notes. `qsv` is used over the smaller, more ubiquitous `xsv` because it can table format very large inputs (e.g. 100k notes) without errors. +# :DenoteSearch command + +```vim +" Searches filenames in `dir` with fzf and displays results as a table +:DenoteSearch files + +" Standard rg search through file contents in `dir`. +" Nothing special about this, it's just a convenient command that uses the same fzf-lua options +:DenoteSearch contents +``` + # License GNU AGPL (`fzf-lua` license) diff --git a/lua/denote-fzf-lua.lua b/lua/denote-fzf-lua.lua index 3e68c40..846361d 100644 --- a/lua/denote-fzf-lua.lua +++ b/lua/denote-fzf-lua.lua @@ -2,25 +2,52 @@ local M = {} local config = require("denote-fzf-lua.config") local fzflua = require('fzf-lua') +local builtin = require("fzf-lua.previewer.builtin") ----@param options table of user options ---- Check for dependencies and optional dependencies -function M.has_prereqs(options) +--- fzf-lua custom previewer (recombines search result fields into a filename) +M.recombine_previewer = builtin.buffer_or_file:extend() + +function M.recombine_previewer:new(o, opts, fzf_win) + M.recombine_previewer.super.new(self, o, opts, fzf_win) + setmetatable(self, M.recombine_previewer) + return self +end + +function M.recombine_previewer:parse_entry(entry_str) + local path = entry_str:match("([^:]+:%d%d:[^:]+):?") + return { + path = M.recombine_filename(path), + line = 1, + col = 1, + } +end + +function M.copy_table(table) + local new_table = {} + for k, v in pairs(table) do + new_table[k] = v + end + return new_table +end + +---@param force_slow bool - If true, act as if we don't have dependencies +---Check for dependencies and optional dependencies +function M.has_prereqs(force_slow) if vim.fn.executable('fzf') ~= 1 then return "no" end - if options.force_slow_mode then return "partial" end + if force_slow then return "partial" end if vim.fn.executable('fd') ~= 1 then return "partial" end if vim.fn.executable('sd') ~= 1 then return "partial" end if vim.fn.executable('qsv') ~= 1 then return "partial" end return "yes" end ----@param options table of user options ---- Sets the full path of the appropriate search script (regular or fallback) -function M.set_script_path(options) +---@param force_slow bool - If true, act as if we don't have dependencies +---Sets the full path of the appropriate search script (regular or fallback) +function M.set_script_path(force_slow) local lua_file_path = debug.getinfo(1, "S").source:sub(2) local lua_file_dir = vim.fn.fnamemodify(lua_file_path, ":h") local script_path = lua_file_dir .. "/denote-fzf-lua/scripts/" - local dependencies = M.has_prereqs(options) + local dependencies = M.has_prereqs(force_slow) if dependencies == "yes" then script_path = script_path .. "search_files.sh" elseif dependencies == "partial" then @@ -38,103 +65,65 @@ function M.format_fzf_with_nth(options) local fzf_with_nth = "" local fields = {"path", "date", "time", "sig", "title", "tags", "ext" } for i, v in ipairs(fields) do - if options.fzf.search_fields[v] then + if options.search_fields[v] then fzf_with_nth = fzf_with_nth .. i .. ',' end end return fzf_with_nth:sub(1, -2) end ----@param filename chopped up string with fields separated by | +---@param f string - Field from search result +---@param delim delimiter character +---Puts a field from the search results back into Denote format (e.g. two words to --two-words) +function M.repopulate_field(f, delim) + if f == "." then + return "" + else + return delim .. delim .. f:gsub("%s",delim) + end +end + +---@param filename chopped up string with fields separated by multiple spaces function M.recombine_filename(filename) + vim.print(filename) local t = {} filename = filename:gsub("%s%s+", "|") t.path, t.year, t.month, t.day, t.hour, t.min, t.sec, t.sig, t.title, t.tags, t.ext = filename:match("^(.*/)|(%d%d%d%d)%-(%d%d)%-(%d%d)|(%d%d):(%d%d):(%d%d)|([^|]+)|([^|]+)|([^|]+)|(%..+)") - if t.sig == "." then - t.sig = "" - else - t.sig = "==" .. t.sig:gsub("%s", "=") - end - if t.title == "." then - t.title = "" - else - t.title = "--" .. t.title:gsub("%s", "-") - end - if t.tags == "." then - t.tags = "" - else - t.tags = "__" .. t.tags:gsub("%s", "_") - end + t.sig = M.repopulate_field(t.sig,"=") + t.title = M.repopulate_field(t.title,"-") + t.tags = M.repopulate_field(t.tags,"_") return t.path .. t.year .. t.month .. t.day .. "T" .. t.hour .. t.min .. t.sec .. t.sig .. t.title .. t.tags .. t.ext end ---- Sets fzf preview to bat or cat depending on program availability -function M.set_preview_program() - if vim.fn.executable('bat') == 1 then - return "xargs bat -p --color=always" - else - return "xargs cat" - end -end - --- Sets fzf preview for contents search. $file is the filename, $line is the rg match line. -function M.set_rg_preview() - if vim.fn.executable('bat') == 1 then - return "bat $file -p --color=always --highlight-line=$line" - else - return "cat $file" - end -end - ---@param options table of user options ---Opens a window to search filenames function M.search_files(options) - script_path = M.set_script_path(options) + script_path = M.set_script_path(options.force_slow_mode) if not script_path then return end - options.fzf.opts["--with-nth"] = M.format_fzf_with_nth(options) - options.fzf.opts['--header-lines'] = 1 - fzflua.fzf_exec(script_path .. " " .. options.dir, - { - preview = options.fzf.preview .. M.set_preview_program(), - -- selected[1] is the chopped up filename with 2+ spaces between fields - actions = { ['default'] = function(selected, opts) - local filename = M.recombine_filename(selected[1]) - vim.api.nvim_command('edit ' .. filename) - end}, - fzf_opts = options.fzf.opts, - }) + local opts = {} + opts.previewer = M.recombine_previewer + opts.fzf_opts = M.copy_table(options.fzf_lua_opts.fzf_opts) + opts.fzf_opts["--with-nth"] = M.format_fzf_with_nth(options) + opts.fzf_opts['--header-lines'] = 1 + opts.fzf_opts['--delimiter'] = "\\s{2,}" + opts.actions = { + ['default'] = function(selected, opts) + local filename = M.recombine_filename(selected[1]) + vim.api.nvim_command('edit ' .. filename) + end } + fzflua.fzf_exec(script_path .. " " .. options.dir, opts) end ----@param f string returned from rg. "/path/filename:line..." -function M.open_file_at_line(f) - local filename, line = f:match("^(.-):(%d+)") - vim.api.nvim_command('edit +' .. line .. " " .. filename) -end - ----@param options table +--- Performs a standard rg search function M.search_contents(options) - local r = fzflua.fzf_live("rg " .. options.rg.opts .. " -- <query> " .. options.dir .. " 2>/dev/null", - { - preview = options.rg.fzf.preview .. M.set_rg_preview(), - actions = { ['default'] = function(selected, opts) - local filename_line = selected[1] - filename_line = filename_line .. ":1" --Default :1 in case no line returned - M.open_file_at_line(filename_line) - end}, - fzf_opts = options.rg.fzf.opts, - }) -end - -function M.search2(options) - opts = {} - opts.prompt = "> " - opts.actions = fzflua.defaults.actions.files + local opts = {} + opts.cwd = options.dir opts.previewer = "builtin" - opts.cwd = options.dir - opts.fzf_opts = options.rg.fzf.opts + opts.fzf_opts = options.fzf_lua_opts.fzf_opts + opts.actions = fzflua.defaults.actions.files return fzflua.fzf_live(function(q) - return "rg --column --color=always -- " .. vim.fn.shellescape(q or '') + return "rg --column --color=always -i -- " .. vim.fn.shellescape(q or '') end, opts) end @@ -145,7 +134,7 @@ function M.load_cmd(options) if opts.fargs[1] == "files" then M.search_files(options) elseif opts.fargs[1] == "contents" then - M.search2(options) + M.search_contents(options) else error("Unsupported operation " .. opts.fargs[1]) end @@ -160,7 +149,7 @@ end ---@param options? table user configuration function M.setup(options) options = vim.tbl_deep_extend("force", config.defaults, options or {}) - fzflua.setup({ winopts = options.winopts }) + fzflua.setup(options.fzf_lua_opts) M.load_cmd(options) end diff --git a/lua/denote-fzf-lua/config.lua b/lua/denote-fzf-lua/config.lua index 8d466ba..fa878ce 100644 --- a/lua/denote-fzf-lua/config.lua +++ b/lua/denote-fzf-lua/config.lua @@ -1,30 +1,11 @@ local M = {} M.defaults = { - dir = "~/notes", - force_slow_mode = false, -- Disables optional dependencies + dir = "~/notes", -- Notes directory + force_slow_mode = false, -- Disables optional dependencies (fd, sd, qsv) - -- Set any fzf-lua winopts here (See fzf-lua documentation) - winopts = { - height = 0.85, - width = 0.80, - row = 0.35, - col = 0.50, - }, - - -- Options sent to fzf (only relevant for filename search) - fzf = { - opts = { - ['--reverse'] = true, - ['--no-info'] = true, - ['--no-separator'] = true, - ['--no-hscroll'] = true, - ['--preview-window'] = "bottom:50%", - ['-i'] = true, - ['--delimiter'] = "\\s{2,}", -- Don't change this, change search_fields. - }, - -- Which Denote fields are displayed in the search results - search_fields = { + -- Choose which fields appear in search results + search_fields = { path = false, date = true, time = false, @@ -32,32 +13,22 @@ M.defaults = { title = true, tags = true, ext = false, - }, - -- This recombines the chopped up filename from ./scripts/search_files.sh. The output needs to be piped to xargs bat or xargs cat. - preview = [[date={2};time={3}; datetime=$(echo ${date}T${time} | tr -d '\-:'); \ - sig={4}; sig=$(echo $sig | tr -d '.' | tr " " "="); if [ ! -z ${sig} ] ; then sig="==${sig}"; fi; \ - title={5}; title=$(echo $title | tr -d '.' | tr ' ' '-'); if [ ! -z ${title} ] ; then title="--${title}"; fi; \ - tags={6}; tags=$(echo $tags | tr -d '.' | tr ' ' '_'); if [ ! -z ${tags} ] ; then tags="__${tags}"; fi; \ - ext={7}; \ - echo {1}${datetime}${sig}${title}${tags}${ext} | ]], }, - -- Options for ripgrep (Only relevant for contents search) - rg = { - opts = "--column --color=always", - fzf = { - opts = { - ['--reverse'] = true, - ['--no-info'] = true, - ['--no-separator'] = true, - ['--no-hscroll'] = true, - ['--preview-window'] = 'bottom:50%', - ['-i'] = true, - }, - -- Checks if $line=$file in case rg --files is used and doesn't return a line number - -- This currently does nothing until I figure out if there's a good way to chop up filenames in the rg search - preview = "file=$(echo {} | cut -d ':' -f1); \ - line=$(echo {} | cut -d ':' -f2); \ - if [ \"$line\" = \"$file\" ] ; then line=0; fi;", + + -- Settings for fzf-lua. See fzf-lua doc for full list of options + fzf_lua_opts = { + winopts = { + height = 0.85, + width = 0.80, + row = 0.35, + col = 0.50, + }, + fzf_opts = { + ['--reverse'] = true, + ['--no-info'] = true, + ['--no-separator'] = true, + ['--no-hscroll'] = true, + ['-i'] = true, }, }, } diff --git a/lua/denote-fzf-lua/scripts/fallback_search_files.sh b/lua/denote-fzf-lua/scripts/fallback_search_files.sh index 9bc8135..430d058 100755 --- a/lua/denote-fzf-lua/scripts/fallback_search_files.sh +++ b/lua/denote-fzf-lua/scripts/fallback_search_files.sh @@ -6,21 +6,6 @@ tr '\=\-_' ' ' |\ sed -E 's/([0-9][0-9][0-9][0-9])([0-9][0-9])([0-9][0-9])T([0-9][0-9])([0-9][0-9])([0-9][0-9])/\1-\2-\3,\4:\5:\6/' |\ sed 's/,,/,.,/g' |\ column -t -s "," -N Path,Date,Time,Sig,Title,Tags,Ext -# fzf --delimiter='\s{2,}' --header-lines=1 -i $2 --preview='\ -# sig={4}; sig=$(echo $sig | tr -d . | tr " " =); if [ ! -z ${sig} ] ; then sig="==${sig}"; fi; \ -# title={5}; title=$(echo $title | tr -d . | tr " " -); if [ ! -z ${title} ] ; then title="--${title}"; fi; \ -# tags={6}; tags=$(echo $tags | tr -d . | tr " " _); if [ ! -z ${tags} ] ; then tags="__${tags}"; fi; \ -# ext={7}; \ -# echo {1}\|{2}\|{3}\|{4}\|{5}\|{6}\|{7} |\ -# sed -E "s/^(.*\/)?\|([0-9][0-9][0-9][0-9])-([0-9][0-9])-([0-9][0-9])\|([0-9][0-9]):([0-9][0-9]):([0-9][0-9])\|.*/\1\2\3\4T\5\6\7$sig$title$tags$ext/" |\ -# xargs cat' \ -# --bind='enter:become(\ -# sig={4}; sig=$(echo $sig | tr -d . | tr " " =); if [ ! -z ${sig} ] ; then sig="==${sig}"; fi; \ -# title={5}; title=$(echo $title | tr -d . | tr " " -); if [ ! -z ${title} ] ; then title="--${title}"; fi; \ -# tags={6}; tags=$(echo $tags | tr -d . | tr " " _); if [ ! -z ${tags} ] ; then tags="__${tags}"; fi; \ -# ext={7}; \ -# echo {1}\|{2}\|{3}\|{4}\|{5}\|{6}\|{7} | \ -# sed -E "s/^(.*\/)?\|([0-9][0-9][0-9][0-9])-([0-9][0-9])-([0-9][0-9])\|([0-9][0-9]):([0-9][0-9]):([0-9][0-9])\|.*/\1\2\3\4T\5\6\7$sig$title$tags$ext/")+abort' # 1. find everything in directory # 2. sed deletes every line that isn't a Denote note (can't do this with find alone because -regex can't match optional capture groups) |
