From 23a89932e57b50a8e9064b9c7ba329e62c88bc44 Mon Sep 17 00:00:00 2001 From: DefaultGen Date: Sun, 16 Jun 2024 05:23:09 -0400 Subject: First working fzf-lua implementation --- lua/denote-fzf-lua.lua | 112 +++++++++++++++++++++ lua/denote-fzf-lua/config.lua | 52 ++++++++++ .../scripts/fallback_search_files.sh | 36 +++++++ lua/denote-fzf-lua/scripts/rg.sh | 4 + lua/denote-fzf-lua/scripts/search_files.sh | 36 +++++++ 5 files changed, 240 insertions(+) create mode 100644 lua/denote-fzf-lua.lua create mode 100644 lua/denote-fzf-lua/config.lua create mode 100755 lua/denote-fzf-lua/scripts/fallback_search_files.sh create mode 100755 lua/denote-fzf-lua/scripts/rg.sh create mode 100755 lua/denote-fzf-lua/scripts/search_files.sh (limited to 'lua') diff --git a/lua/denote-fzf-lua.lua b/lua/denote-fzf-lua.lua new file mode 100644 index 0000000..1013afb --- /dev/null +++ b/lua/denote-fzf-lua.lua @@ -0,0 +1,112 @@ +local M = {} + +local config = require("denote-fzf-lua.config") +local fzflua = require('fzf-lua') + + +--- Check for dependencies and optional dependencies +function M.has_prereqs() + if vim.fn.executable('fzf') ~= 1 then return "no" end + if vim.fn.executable('rg') ~= 1 then return "no" 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('xsv') ~= 1 then return "partial" end + return "yes" +end + +---Returns the full path of the appropriate script +function M.get_script_path() + 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() + if dependencies == "yes" then + script_path = script_path .. "search_files.sh" + elseif dependencies == "partial" then + script_path = script_path .. "fallback_search_files.sh" + else + error("Missing dependencies: fzf and rg") + return false + end + return script_path +end + +---@param options table of user options +---Format the --with-nth argument for fzf (which fields are shown) +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 + fzf_with_nth = fzf_with_nth .. i .. ',' + end + end + return fzf_with_nth:sub(1, -2) +end + +-- TODO: This is absolutely wrecked +function M.open_file(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 + local file = t.path .. t.year .. t.month .. t.day .. "T" .. t.hour .. t.min .. t.sec .. t.sig .. t.title .. t.tags .. t.ext + vim.api.nvim_command('edit ' .. file) +end + +---@param options table of user options +---Opens a window to search filenames +function M.files(options) + local script_path = M.get_script_path() + if not script_path then return end + options.fzf.opts["--with-nth"] = M.format_fzf_with_nth(options) + fzflua.fzf_exec(script_path .. " " .. options.dir, + { + preview = options.fzf.preview, + actions = options.fzf.actions, + fzf_opts = options.fzf.opts, + }) + -- local fzf_args = M.format_fzf_with_nth(options) .. " " .. options.fzf.args + -- vim.api.nvim_command('edit ' .. stdout) +end + +---@param options table of user options +function M.load_cmd(options) + vim.api.nvim_create_user_command("DenoteSearch", function(opts) + if opts.fargs[1] == "files" then + M.files(options) + elseif opts.fargs[1] == "contents" then + M.conents(options) + else + error("Unsupported operation " .. opts.fargs[1]) + end + end, { + nargs = 1, + complete = function() + return {"files", "contents"} + end, + }) +end + +---@param options? table user configuration +function M.setup(options) + config.options = vim.tbl_deep_extend("force", config.defaults, options or {}) + M.load_cmd(config.options) +end + +return M diff --git a/lua/denote-fzf-lua/config.lua b/lua/denote-fzf-lua/config.lua new file mode 100644 index 0000000..6520ef8 --- /dev/null +++ b/lua/denote-fzf-lua/config.lua @@ -0,0 +1,52 @@ +local M = {} + +M.defaults = { + dir = "~/notes", + window = { + width_percent = 80, + height_percent = 80, + x_offset = 0, + y_offset = 0, + }, + fzf = { + opts = { + ['--reverse'] = true, + ['--no-info'] = true, + ['--no-separator'] = true, + ['--no-hscroll'] = true, + ['--preview-window'] = "bottom:50%", + ['-i'] = true, + ['--delimiter'] = "\\s{2,}", + ['--header-lines'] = 1 + }, + search_fields = { + path = false, + date = true, + time = false, + sig = false, + title = true, + tags = true, + ext = false, + }, + -- This recombines the chopped up filename from ./scripts/search_files.sh and outputs it to cat + -- TODO: Rewrite this with a lua function + preview = [[\ +sig={4}; sig=$(echo $sig | sd "\W" "" | sd " " "="); 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} | \ +sd "(?P\/.*\/)?\|(?P\d\d\d\d)-(?P\d\d)-(?P\d\d)\|(?P\d\d):(?P\d\d):(?P\d\d)\|.*" "\$path\$d1\$d2\${d3}T\$t1\$t2\$t3$sig$title$tags$ext" |\ +xargs cat]], + -- selected[1] is the chopped up filename with 2+ spaces between fields + actions = { + ['default'] = function(selected, opts) + require'denote-fzf-lua'.open_file(selected[1]) + end + } + } +} + +M.options = M.defaults + +return M diff --git a/lua/denote-fzf-lua/scripts/fallback_search_files.sh b/lua/denote-fzf-lua/scripts/fallback_search_files.sh new file mode 100755 index 0000000..a617b64 --- /dev/null +++ b/lua/denote-fzf-lua/scripts/fallback_search_files.sh @@ -0,0 +1,36 @@ +#!/usr/bin/sh +find $1 |\ +sed -E "/^(.*\/)?([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])(==[^=\-_\.]+)?(--[^\-_\.]+)?(__[^\.]+)?(\.[a-z0-9]+)$/!d" |\ +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])(==([^=\-_\.]+))?(--([^\-_\.]+))?(__([^\.]+))?(\.[a-z0-9]+)$/\1|\2|\4|\6|\8|\9/" |\ +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' + +# I am sorry for this. If I learn an equally fast, more readable way to implement this in Lua I will. +# 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) +# 3. sed splits the Denote filename into | delimited fields +# 4. tr replaces sig, title, tags special characters with spaces +# 5. sed splits the date and time nicely +# 6. sed replaces blank fields with a period (column doesn't work with null fields) +# 7. column pretty prints the filenames in a table +# 8. fzf --delimiter breaks the table up into fields on 2+ space +# fzf --nth and --with-nth (args passed in $2) determine which fields are shown +# fzf --preview takes all the fzf fields {1}..{7} and recombines them into a filename, passed to cat +# fzf --bind recombines the filename and just outputs it diff --git a/lua/denote-fzf-lua/scripts/rg.sh b/lua/denote-fzf-lua/scripts/rg.sh new file mode 100755 index 0000000..2cfc400 --- /dev/null +++ b/lua/denote-fzf-lua/scripts/rg.sh @@ -0,0 +1,4 @@ +#!/usr/bin/sh +rg --color=always --line-number --no-heading --smart-case "${@:1:$#-1}" ${*: -1} + + diff --git a/lua/denote-fzf-lua/scripts/search_files.sh b/lua/denote-fzf-lua/scripts/search_files.sh new file mode 100755 index 0000000..67367af --- /dev/null +++ b/lua/denote-fzf-lua/scripts/search_files.sh @@ -0,0 +1,36 @@ +#/usr/bin/sh +fd '^\d{8}T\d{6}(==[a-z0-9=]+)?(\-\-[a-z0-9-]+)?(__[a-z0-9_]+)?\.\w+$' $1 |\ +sd '(?P\/.*\/)?(?P\d{8}T\d{6})(==(?P[a-z0-9=]+))?(--(?P[a-z0-9-]+))(__(?P<tags>[a-z0-9_]+))?(?P<ext>\.\w+)' '$path,$datetime,$sig,$title,$tags,$ext' |\ +tr '\=\-_' ' ' |\ +sd '(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})' '$1-$2-$3,$4:$5:$6' |\ +sd ',,' ',.,' |\ +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 | sd "\W" "" | sd " " "="); if [ ! -z ${sig} ] ; then sig="==${sig}"; fi; \ +# title={5}; title=$(echo $title | sd " " "-"); if [ ! -z ${title} ] ; then title="--${title}"; fi; \ +# tags={6}; tags=$(echo $tags | sd "\." "" | sd " " "_"); if [ ! -z ${tags} ] ; then tags="__${tags}"; fi; \ +# ext={7}; \ +# echo {1}\|{2}\|{3}\|{4}\|{5}\|{6}\|{7} | \ +# sd "(?P<path>\/.*\/)?\|(?P<d1>\d\d\d\d)-(?P<d2>\d\d)-(?P<d3>\d\d)\|(?P<t1>\d\d):(?P<t2>\d\d):(?P<t3>\d\d)\|.*" "\$path\$d1\$d2\${d3}T\$t1\$t2\$t3$sig$title$tags$ext" |\ +# xargs cat' \ +# --bind='enter:become(\ +# sig={4}; sig=$(echo $sig | sd "\W" "" | sd " " "="); if [ ! -z ${sig} ] ; then sig="==${sig}"; fi; \ +# title={5}; title=$(echo $title | sd " " "-"); if [ ! -z ${title} ] ; then title="--${title}"; fi; \ +# tags={6}; tags=$(echo $tags | sd "\." "" | sd " " "_"); if [ ! -z ${tags} ] ; then tags="__${tags}"; fi; \ +# ext={7}; \ +# echo {1}\|{2}\|{3}\|{4}\|{5}\|{6}\|{7} | \ +# sd "(?P<prefix>\/.*\/)?\|(?P<d1>\d\d\d\d)-(?P<d2>\d\d)-(?P<d3>\d\d)\|(?P<t1>\d\d):(?P<t2>\d\d):(?P<t3>\d\d)\|.*" "\$prefix\$d1\$d2\${d3}T\$t1\$t2\$t3$sig$title$tags$ext")+abort' +# +# I am sorry for this. If I learn an equally fast, more readable way to implement this in Lua I will. +# 1. echo table headers (for xsv) +# 2. fd all Denote files in $1 directory +# 3. sd chops the Denote filename fields into a CSV +# 4. tr replaces special characters in sig, title, and tags with spaces +# 5. sd formats the date and time as YYYY-MM-DD HH:MM:SS +# 6. sd replaces blank fields with . (ensures fzf can count fields correctly) +# 7. xsv to print the fields in tabular format +# 8. fzf the resulting table including $2 args list +# 9 fzf --delimiter breaks the table up into fields on 2+ space +# fzf --nth and --with-nth (args passed in $2) determine which fields are shown +# fzf --preview takes all the fzf fields {1}..{7} and recombines them into a filename, passed to cat +# fzf --bind recombines the filename and just outputs it -- cgit v1.2.3