diff options
| author | historia <[not public]> | 2026-06-15 21:15:19 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-06-15 21:18:55 -0400 |
| commit | 3cdd2f186c302f2f6c9e63cf035e4f51a434a97b (patch) | |
| tree | 5070ce4f33dfbca5978cc16928b545e0b50bb186 /lua | |
| parent | 69c854a8e4a8b10f940d9a0192d2d40d3b306943 (diff) | |
| download | denote-fzf-lua-3cdd2f186c302f2f6c9e63cf035e4f51a434a97b.tar.gz | |
refactor: rewrote shell in lua. removed dependencies.
Diffstat (limited to 'lua')
| -rw-r--r-- | lua/denote-fzf-lua.lua | 259 | ||||
| -rw-r--r-- | lua/denote-fzf-lua/config.lua | 23 | ||||
| -rwxr-xr-x | lua/denote-fzf-lua/scripts/fallback_search_files.sh | 16 | ||||
| -rwxr-xr-x | lua/denote-fzf-lua/scripts/search_files.sh | 16 |
4 files changed, 172 insertions, 142 deletions
diff --git a/lua/denote-fzf-lua.lua b/lua/denote-fzf-lua.lua index f545bc0..3fc4384 100644 --- a/lua/denote-fzf-lua.lua +++ b/lua/denote-fzf-lua.lua @@ -1,135 +1,205 @@ local M = {} local config = require("denote-fzf-lua.config") -local fzflua = require('fzf-lua') +local fzflua = require("fzf-lua") local builtin = require("fzf-lua.previewer.builtin") ---- fzf-lua custom previewer (recombines search result fields into a filename) -M.recombine_previewer = builtin.buffer_or_file:extend() +local FIELDS = { "path", "date", "time", "sig", "title", "keywords", "ext" } -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 +local function list_denote_files(dir) + local results = {} + local stack = { vim.fn.expand(dir) } + + while #stack > 0 do + local current = table.remove(stack) + local ok, entries = pcall(vim.fn.readdir, current) + if ok then + for _, entry in ipairs(entries) do + local full = current .. "/" .. entry + local stat = vim.loop.fs_stat(full) + if stat then + if stat.type == "directory" then + table.insert(stack, full) + elseif stat.type == "file" and entry:match("^%d%d%d%d%d%d%d%dT%d%d%d%d%d%d") then + table.insert(results, full) + end + end + end + end + end + + return results end -function M.recombine_previewer:parse_entry(entry_str) - local path = entry_str:match("([^:]+:%d%d:[^:]+):?") +local function parse_denote(basename) + local dt = basename:sub(1, 15) + if not dt:match("^%d%d%d%d%d%d%d%dT%d%d%d%d%d%d$") then return nil end + + local date = dt:sub(1, 4) .. "-" .. dt:sub(5, 6) .. "-" .. dt:sub(7, 8) + local time = dt:sub(10, 11) .. ":" .. dt:sub(12, 13) .. ":" .. dt:sub(14, 15) + + local dot_idx = basename:find("%.", 16, true) + if not dot_idx then return nil end + local ext = basename:sub(dot_idx) + local body = basename:sub(16, dot_idx - 1) + + local sig, title, keywords = "", "", "" + local pos = 1 + + if body:sub(pos, pos + 1) == "==" then + local sig_end = body:find("%-%-", pos + 2) + if sig_end then + sig = body:sub(pos + 2, sig_end - 1) + pos = sig_end + end + end + + if body:sub(pos, pos + 1) == "--" then + local title_end = body:find("__", pos + 2) + if title_end then + title = body:sub(pos + 2, title_end - 1) + pos = title_end + else + title = body:sub(pos + 2) + pos = #body + 1 + end + else + return nil + end + + if pos <= #body and body:sub(pos) == "__" then + keywords = body:sub(pos + 2) + end + return { - path = M.recombine_filename(path), - line = 1, - col = 1, + path = "", + date = date, + time = time, + sig = sig ~= "" and sig:gsub("=", " ") or ".", + title = title ~= "" and title:gsub("%-", " ") or ".", + keywords = keywords ~= "" and keywords:gsub("_", " ") or ".", + ext = ext, } end -function M.copy_table(table) - local new_table = {} - for k, v in pairs(table) do - new_table[k] = v +local function format_denote_table(files) + local parsed = {} + for _, filepath in ipairs(files) do + local basename = vim.fn.fnamemodify(filepath, ":t") + local dirpath = vim.fn.fnamemodify(filepath, ":h") .. "/" + local info = parse_denote(basename) + if info then + info.path = dirpath + table.insert(parsed, info) + end end - return new_table -end ----@param force_slow boolean - 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 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 + local widths = {} + for i, field in ipairs(FIELDS) do + widths[i] = #field + end + for _, info in ipairs(parsed) do + for i, field in ipairs(FIELDS) do + widths[i] = math.max(widths[i], #info[field]) + end + end ----@param force_slow boolean - 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(force_slow) - 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 dependency: fzf") - return false + local lines = {} + local parts = {} + for i, field in ipairs(FIELDS) do + parts[i] = string.format("%-" .. widths[i] .. "s", field) end - return script_path -end + table.insert(lines, table.concat(parts, " ")) ----@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", "keywords", "ext" } - for i, v in ipairs(fields) do - if options.search_fields[v] then - fzf_with_nth = fzf_with_nth .. i .. ',' - end + for _, info in ipairs(parsed) do + parts = {} + for i, field in ipairs(FIELDS) do + parts[i] = string.format("%-" .. widths[i] .. "s", info[field]) + end + table.insert(lines, table.concat(parts, " ")) end - return fzf_with_nth:sub(1, -2) + + return lines end ----@param f string - Field from search result ----@param delim string 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 +M.reconstruct_previewer = builtin.buffer_or_file:extend() + +function M.reconstruct_previewer:new(o, opts, fzf_win) + M.reconstruct_previewer.super.new(self, o, opts, fzf_win) + setmetatable(self, M.reconstruct_previewer) + return self end ----@param filename string chopped up string with fields separated by multiple spaces -function M.recombine_filename(filename) - if filename == nil then return "" end +function M.reconstruct_previewer:parse_entry(entry_str) + return { + path = M.reconstruct_filename(entry_str), + line = 1, + col = 1, + } +end + +local function format_field(field, delim) + if field == "." then return "" end + return delim .. delim .. field:gsub("%s", delim) +end + +function M.reconstruct_filename(row) + if not row or row == "" then return "" end local t = {} - filename = filename:gsub("%s%s+", "|") + row = row:gsub("%s%s+", "|") t.path, t.year, t.month, t.day, t.hour, t.min, t.sec, t.sig, t.title, t.keywords, t.ext = - filename:match("^(.*/)|(%d%d%d%d)%-(%d%d)%-(%d%d)|(%d%d):(%d%d):(%d%d)|([^|]+)|([^|]+)|([^|]+)|(%..+)") - t.sig = M.repopulate_field(t.sig,"=") - t.title = M.repopulate_field(t.title,"-") - t.keywords = M.repopulate_field(t.keywords,"_") + row:match("^(.*/)|(%d%d%d%d)%-(%d%d)%-(%d%d)|(%d%d):(%d%d):(%d%d)|([^|]+)|([^|]+)|([^|]+)|(%..+)") + if not t.path then return "" end + t.sig = format_field(t.sig, "=") + t.title = format_field(t.title, "-") + t.keywords = format_field(t.keywords, "_") return t.path .. t.year .. t.month .. t.day .. "T" .. t.hour .. t.min .. t.sec .. t.sig .. t.title .. t.keywords .. t.ext end ----@param options table of user options ----Opens a window to search filenames +function M.format_fzf_with_nth(options) + local indices = {} + for i, field in ipairs(FIELDS) do + if options.search_fields[field] then + table.insert(indices, i) + end + end + return table.concat(indices, ",") +end + function M.search_files(options) - local script_path = M.set_script_path(options.force_slow_mode) - if not script_path then return end + local files = list_denote_files(options.dir) + local lines = format_denote_table(files) + local opts = {} - opts.previewer = M.recombine_previewer - opts.fzf_opts = M.copy_table(options.fzf_lua_opts.fzf_opts) + opts.previewer = M.reconstruct_previewer + opts.fzf_opts = vim.deepcopy(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.fzf_opts["--header-lines"] = 1 + opts.fzf_opts["--delimiter"] = "\\s{2,}" opts.actions = { - ['default'] = function(selected) - local filename = M.recombine_filename(selected[1]) - vim.api.nvim_command('edit ' .. filename) - end } - fzflua.fzf_exec(script_path .. " " .. options.dir, opts) + default = function(selected) + local filename = M.reconstruct_filename(selected[1]) + vim.api.nvim_command("edit " .. vim.fn.fnameescape(filename)) + end, + } + fzflua.fzf_exec(lines, opts) end ---- Performs a standard rg search function M.search_contents(options) + if vim.fn.executable("rg") ~= 1 then + vim.notify("denote-fzf-lua: ripgrep is required for content search", vim.log.levels.WARN) + return + end local opts = {} - opts.cwd = options.dir + opts.cwd = options.dir opts.previewer = "builtin" - opts.fzf_opts = options.fzf_lua_opts.fzf_opts - opts.actions = fzflua.defaults.actions.files - return fzflua.fzf_live(function(q) - local query = type(q) == "table" and table.concat(q, " ") or q or '' - return "rg --column --color=always -i -- " .. vim.fn.shellescape(query) + opts.fzf_opts = options.fzf_lua_opts.fzf_opts + opts.actions = fzflua.defaults.actions.files + fzflua.fzf_live(function(query) + return "rg --column --color=always -i -- " .. vim.fn.shellescape(query or "") end, opts) end ----@param options table of user options ----Creates Neovim command :DenoteSearch function M.load_cmd(options) vim.api.nvim_create_user_command("DenoteSearch", function(opts) if opts.fargs[1] == "files" then @@ -142,12 +212,11 @@ function M.load_cmd(options) end, { nargs = 1, complete = function() - return {"files", "contents"} + return { "files", "contents" } end, }) end ----@param options? table user configuration function M.setup(options) options = vim.tbl_deep_extend("force", config.defaults, options or {}) fzflua.setup(options.fzf_lua_opts) diff --git a/lua/denote-fzf-lua/config.lua b/lua/denote-fzf-lua/config.lua index 04d3bdc..cd451e2 100644 --- a/lua/denote-fzf-lua/config.lua +++ b/lua/denote-fzf-lua/config.lua @@ -1,21 +1,18 @@ local M = {} M.defaults = { - dir = "~/notes", -- Notes directory - force_slow_mode = false, -- Disables optional dependencies (fd, sd, qsv) + dir = "~/notes", - -- Choose which fields appear in search results search_fields = { - path = false, - date = true, - time = false, - sig = false, - title = true, - keywords = true, - ext = false, + path = false, + date = true, + time = false, + sig = false, + title = true, + keywords = true, + ext = false, }, - -- Settings for fzf-lua. See fzf-lua doc for full list of options fzf_lua_opts = { winopts = { height = 0.85, @@ -24,9 +21,7 @@ M.defaults = { col = 0.50, }, fzf_opts = { - ['--layout'] = false, ['--reverse'] = true, - ['--info'] = false, ['--no-info'] = true, ['--no-separator'] = true, ['--no-hscroll'] = true, @@ -35,6 +30,4 @@ M.defaults = { }, } -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 deleted file mode 100755 index fd15ca2..0000000 --- a/lua/denote-fzf-lua/scripts/fallback_search_files.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/sh -find $1 -not -path '*/.*' |\ -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,Keywords,Ext - -# 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 (pa -# 4. tr replaces sig, title, keywords special characters with spaces -# 5. sed splits the date and time nicely -# 6. sed replaces blank fields with a period -# 7. column pretty prints the filenames in a table diff --git a/lua/denote-fzf-lua/scripts/search_files.sh b/lua/denote-fzf-lua/scripts/search_files.sh deleted file mode 100755 index 2bfd271..0000000 --- a/lua/denote-fzf-lua/scripts/search_files.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/sh -(echo "Path,Date,Time,Sig,Title,Keywords,Ext"; -fd '^\d{8}T\d{6}(==[a-z0-9=]+)?(\-\-[a-z0-9-]+)?(__[a-z0-9_]+)?\.\w+$' $1 |\ -sd '(?P<path>\/.*\/)?(?P<datetime>\d{8}T\d{6})(==(?P<sig>[a-z0-9=]+))?(--(?P<title>[a-z0-9-]+))(__(?P<keywords>[a-z0-9_]+))?(?P<ext>\.\w+)' '$path,$datetime,$sig,$title,$keywords,$ext' |\ -tr '\=\-_' ' ' |\ -sd '(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})' '$1-$2-$3,$4:$5:$6') |\ -sd ',,' ',.,' |\ -qsv table - -# 1. echo the table headers -# 2. fd only Denote files in $1 directory (and subdirectories) -# 3. sd chops the Denote filename fields into a CSV -# 4. tr replaces special characters in sig, title, and keywords with spaces -# 5. sd formats the date and time as YYYY-MM-DD HH:MM:SS -# 6. sd replaces blank fields with . (fzf can't handle blank fields) -# 7. qsv to print the fields in tabular format |
