aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md13
-rw-r--r--lua/denote-fzf-lua.lua182
-rw-r--r--lua/denote-fzf-lua/config.lua1
-rwxr-xr-xlua/denote-fzf-lua/scripts/fallback_search_files.sh8
-rwxr-xr-xlua/denote-fzf-lua/scripts/search_files.sh8
5 files changed, 71 insertions, 141 deletions
diff --git a/README.md b/README.md
index 5464462..7aebe3d 100644
--- a/README.md
+++ b/README.md
@@ -55,15 +55,22 @@ Example config via [lazy.nvim](https://github.com/folke/lazy.nvim)
* [`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`](https://github.com/jqnatividad/qsv) - Fast `column` replacement
* (OPTIONAL) `bat` - Nicer preview than `cat`
* (OPTIONAL) `ripgrep` - Required to search note contents
```
-Arch Linux: sudo pacman -S fzf ripgrep bat
-Debian: sudo apt install fzf ripgrep bat
+Arch Linux: sudo pacman -S fzf ripgrep fd sd bat
+ yay -S qsv-bin
+
+Debian: sudo apt install fzf ripgrep fd-find sd bat
+ export PATH=/usr/lib/cargo/bin/:$PATH (to add `fd` to path)
+ Install qsv from Github
```
-All file parsing and table formatting is done in pure Lua, so the only runtime dependency beyond `fzf-lua` is `fzf` itself.
+If the optional dependencies are missing `denote-fzf-lua` falls back to standard Unix tools (`find`, `sed`, `column`).
# :DenoteSearch command
diff --git a/lua/denote-fzf-lua.lua b/lua/denote-fzf-lua.lua
index c2999bb..167ecb3 100644
--- a/lua/denote-fzf-lua.lua
+++ b/lua/denote-fzf-lua.lua
@@ -5,120 +5,57 @@ local fzflua = require("fzf-lua")
local builtin = require("fzf-lua.previewer.builtin")
local FIELDS = { "path", "date", "time", "sig", "title", "keywords", "ext" }
+local lua_dir = debug.getinfo(1, "S").source:match("@?(.*/)")
+local script_dir = lua_dir .. "denote-fzf-lua/scripts/"
-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, handle = pcall(vim.loop.fs_scandir, current)
- if ok and handle then
- while true do
- local name, typ = vim.loop.fs_scandir_next(handle)
- if not name then break end
- local full = current .. "/" .. name
- if typ == "directory" then
- table.insert(stack, full)
- elseif (typ == "file" or typ == "link") and name:match("^%d%d%d%d%d%d%d%dT%d%d%d%d%d%d") then
- table.insert(results, full)
- end
- end
- end
- end
-
- return results
+local function format_field(field, delim)
+ if field == "." then return "" end
+ return delim .. delim .. field:gsub("%s", delim)
end
-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
+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
- else
- return nil
- end
-
- if pos <= #body and body:sub(pos) == "__" then
- keywords = body:sub(pos + 2)
end
-
- return {
- 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,
- }
+ return table.concat(indices, ",")
end
-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
+function M.has_prereqs(force_slow)
+ if vim.fn.executable("fzf") ~= 1 then return "no" end
+ if force_slow then return "partial" end
+ for _, cmd in ipairs({ "fd", "sd", "qsv" }) do
+ if vim.fn.executable(cmd) ~= 1 then return "partial" end
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
+function M.get_script(force_slow)
+ local prereqs = M.has_prereqs(force_slow)
+ if prereqs == "no" then
+ error("Missing dependency: fzf")
+ return
+ elseif prereqs == "yes" then
+ return script_dir .. "search_files.sh"
+ else
+ return script_dir .. "fallback_search_files.sh"
end
+end
- local lines = {}
+function M.reconstruct_filename(row)
+ if not row or row == "" then return "" end
+ row = row:gsub("%s+$", "")
local parts = {}
- for i, field in ipairs(FIELDS) do
- parts[i] = string.format("%-" .. widths[i] .. "s", field)
- end
- table.insert(lines, table.concat(parts, " "))
-
- 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 lines
+ row = row:gsub("%s%s+", "|")
+ parts.path, parts.year, parts.month, parts.day, parts.hour, parts.min, parts.sec, parts.sig, parts.title, parts.keywords, parts.ext =
+ row:match("^(.*/)|(%d%d%d%d)%-(%d%d)%-(%d%d)|(%d%d):(%d%d):(%d%d)|([^|]+)|([^|]+)|([^|]+)|(%..+)")
+ if not parts.path then return "" end
+ local sig = format_field(parts.sig, "=")
+ local title = format_field(parts.title, "-")
+ local keywords = format_field(parts.keywords, "_")
+ return parts.path .. parts.year .. parts.month .. parts.day .. "T" .. parts.hour .. parts.min .. parts.sec .. sig .. title .. keywords .. parts.ext
end
M.reconstruct_previewer = builtin.buffer_or_file:extend()
@@ -137,41 +74,9 @@ function M.reconstruct_previewer:parse_entry(entry_str)
}
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 = {}
- 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 =
- 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
-
-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 files = list_denote_files(options.dir)
- if #files == 0 then
- vim.notify("denote-fzf-lua: no Denote files found in " .. options.dir, vim.log.levels.WARN)
- return
- end
- local lines = format_denote_table(files)
+ local script = M.get_script(options.force_slow_mode)
+ if not script then return end
local opts = {}
opts.previewer = M.reconstruct_previewer
@@ -185,7 +90,8 @@ function M.search_files(options)
vim.api.nvim_command("edit " .. vim.fn.fnameescape(filename))
end,
}
- fzflua.fzf_exec(lines, opts)
+ local dir = vim.fn.expand(options.dir)
+ fzflua.fzf_exec(script .. " " .. dir, opts)
end
function M.search_contents(options)
@@ -194,7 +100,7 @@ function M.search_contents(options)
return
end
local opts = {}
- opts.cwd = options.dir
+ opts.cwd = vim.fn.expand(options.dir)
opts.previewer = "builtin"
opts.fzf_opts = options.fzf_lua_opts.fzf_opts
opts.actions = fzflua.defaults.actions.files
diff --git a/lua/denote-fzf-lua/config.lua b/lua/denote-fzf-lua/config.lua
index cd451e2..04a135f 100644
--- a/lua/denote-fzf-lua/config.lua
+++ b/lua/denote-fzf-lua/config.lua
@@ -2,6 +2,7 @@ local M = {}
M.defaults = {
dir = "~/notes",
+ force_slow_mode = false,
search_fields = {
path = false,
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..f6ee4fc
--- /dev/null
+++ b/lua/denote-fzf-lua/scripts/fallback_search_files.sh
@@ -0,0 +1,8 @@
+#!/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
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..4367049
--- /dev/null
+++ b/lua/denote-fzf-lua/scripts/search_files.sh
@@ -0,0 +1,8 @@
+#!/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