aboutsummaryrefslogtreecommitdiff
path: root/lua/denote-fzf-lua.lua
diff options
context:
space:
mode:
Diffstat (limited to 'lua/denote-fzf-lua.lua')
-rw-r--r--lua/denote-fzf-lua.lua259
1 files changed, 164 insertions, 95 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)