local M = {} local config = require("denote-fzf-lua.config") local fzflua = require("fzf-lua") local builtin = require("fzf-lua.previewer.builtin") local FIELDS = { "path", "date", "time", "sig", "title", "keywords", "ext" } 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 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 = "", 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 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 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 local lines = {} 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 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 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 = {} 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) local lines = format_denote_table(files) local 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.actions = { 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 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.previewer = "builtin" opts.fzf_opts = options.fzf_lua_opts.fzf_opts opts.actions = fzflua.defaults.actions.files fzflua.fzf_live(function(query) local q = type(query) == "table" and table.concat(query, " ") or query or "" return "rg --column --color=always -i -- " .. vim.fn.shellescape(q) end, opts) end function M.load_cmd(options) vim.api.nvim_create_user_command("DenoteSearch", function(opts) if opts.fargs[1] == "files" then M.search_files(options) elseif opts.fargs[1] == "contents" then M.search_contents(options) else error("Unsupported operation " .. opts.fargs[1]) end end, { nargs = 1, complete = function() return { "files", "contents" } end, }) end function M.setup(options) options = vim.tbl_deep_extend("force", config.defaults, options or {}) fzflua.setup(options.fzf_lua_opts) M.load_cmd(options) end return M