diff options
Diffstat (limited to 'lua/denote-fzf-lua.lua')
| -rw-r--r-- | lua/denote-fzf-lua.lua | 259 |
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) |
