local has_devicons, devicons = pcall(require, "nvim-web-devicons") local p = require "plenary.path" local util = require "frecency.util" local os_home = vim.loop.os_homedir() local os_path_sep = p.path.sep local actions = require "telescope.actions" local conf = require("telescope.config").values local entry_display = require "telescope.pickers.entry_display" local finders = require "telescope.finders" local pickers = require "telescope.pickers" local sorters = require "telescope.sorters" local ts_util = require "telescope.utils" local db = require "frecency.db" ---TODO: Describe FrecencyPicker fields ---@class FrecencyPicker ---@field db FrecencyDB: where the files will be stored ---@field results table ---@field active_filter string ---@field active_filter_tag string ---@field previous_buffer string ---@field cwd string ---@field lsp_workspaces table ---@field picker table ---@field updated boolean: true if a new entry is added into DB local m = { results = {}, active_filter = nil, active_filter_tag = nil, last_filter = nil, previous_buffer = nil, cwd = nil, lsp_workspaces = {}, picker = {}, updated = false, } m.__index = m ---@class FrecencyConfig ---@field show_unindexed boolean: default true ---@field show_filter_column boolean|string[]: default true ---@field workspaces table: default {} ---@field disable_devicons boolean: default false ---@field default_workspace string: default nil m.config = { show_scores = true, show_unindexed = true, show_filter_column = true, workspaces = {}, disable_devicons = false, default_workspace = nil, } ---Setup frecency picker m.set_prompt_options = function(buffer) vim.bo[buffer].filetype = "frecency" vim.bo[buffer].completefunc = "v:lua.require'telescope'.extensions.frecency.complete" vim.keymap.set("i", "", "pumvisible() ? '' : ''", { buffer = buffer, expr = true }) vim.keymap.set("i", "", "pumvisible() ? '' : ''", { buffer = buffer, expr = true }) end ---returns `true` if workspaces exit ---@param bufnr integer ---@param force boolean? ---@return boolean workspaces_exist m.fetch_lsp_workspaces = function(bufnr, force) if not vim.tbl_isempty(m.lsp_workspaces) and not force then return true end local lsp_workspaces = vim.api.nvim_buf_call(bufnr, vim.lsp.buf.list_workspace_folders) if not vim.tbl_isempty(lsp_workspaces) then m.lsp_workspaces = lsp_workspaces return true end m.lsp_workspaces = {} return false end ---Update Frecency Picker result ---@param filter string ---@return boolean m.update = function(filter) local filter_updated = false local ws_dir = filter and m.config.workspaces[filter] or nil if filter == "LSP" and not vim.tbl_isempty(m.lsp_workspaces) then ws_dir = m.lsp_workspaces[1] elseif filter == "CWD" then ws_dir = m.cwd end if ws_dir ~= m.active_filter then filter_updated = true m.active_filter, m.active_filter_tag = ws_dir, filter end m.results = (vim.tbl_isempty(m.results) or m.updated or filter_updated) and db.get_files { ws_path = ws_dir, show_unindexed = m.config.show_unindexed } or m.results return filter_updated end ---@param opts table telescope picker table ---@return fun(filename: string): string m.filepath_formatter = function(opts) local path_opts = {} for k, v in pairs(opts) do path_opts[k] = v end return function(filename) path_opts.cwd = m.active_filter or m.cwd return ts_util.transform_path(path_opts, filename) end end m.should_show_tail = function() local filters = type(m.config.show_filter_column) == "table" and m.config.show_filter_column or { "LSP", "CWD" } return vim.tbl_contains(filters, m.active_filter_tag) end ---Create entry maker function. ---@param entry table ---@return function m.maker = function(entry) local filter_column_width = (function() if m.active_filter then if m.should_show_tail() then -- TODO: Only add +1 if m.show_filter_thing is true, +1 is for the trailing slash return #(ts_util.path_tail(m.active_filter)) + 1 end return #(p:new(m.active_filter):make_relative(os_home)) + 1 end return 0 end)() local displayer = entry_display.create { separator = "", hl_chars = { [os_path_sep] = "TelescopePathSeparator" }, items = (function() local i = m.config.show_scores and { { width = 8 } } or {} if has_devicons and not m.config.disable_devicons then table.insert(i, { width = 2 }) end if m.config.show_filter_column then table.insert(i, { width = filter_column_width }) end table.insert(i, { remaining = true }) return i end)(), } local filter_path = (function() if m.config.show_filter_column and m.active_filter then return m.should_show_tail() and ts_util.path_tail(m.active_filter) .. os_path_sep or p:new(m.active_filter):make_relative(os_home) .. os_path_sep end return "" end)() local formatter = m.filepath_formatter(m.opts) return { filename = entry.path, ordinal = entry.path, name = entry.path, score = entry.score, display = function(e) return displayer((function() local i = m.config.show_scores and { { entry.score, "TelescopeFrecencyScores" } } or {} if has_devicons and not m.config.disable_devicons then table.insert(i, { devicons.get_icon(e.name, string.match(e.name, "%a+$"), { default = true }) }) end if m.config.show_filter_column then table.insert(i, { filter_path, "Directory" }) end table.insert(i, { formatter(e.name), util.buf_is_loaded(e.name) and "TelescopeBufferLoaded" or "", }) return i end)()) end, } end ---Find files ---@param opts table: telescope picker opts m.fd = function(opts) opts = opts or {} if not opts.path_display then opts.path_display = function(path_opts, filename) local original_filename = filename filename = p:new(filename):make_relative(path_opts.cwd) if not m.active_filter then if vim.startswith(filename, os_home) then filename = "~/" .. p:new(filename):make_relative(os_home) elseif filename ~= original_filename then filename = "./" .. filename end end return filename end end m.previous_buffer, m.cwd, m.opts = vim.fn.bufnr "%", vim.fn.expand(opts.cwd or vim.loop.cwd()), opts -- TODO: should we update this every time it calls frecency on other buffers? m.fetch_lsp_workspaces(m.previous_buffer) m.update() local picker_opts = { prompt_title = "Frecency", finder = finders.new_table { results = m.results, entry_maker = m.maker }, previewer = conf.file_previewer(opts), sorter = sorters.get_substr_matcher(opts), } picker_opts.on_input_filter_cb = function(query_text) local o = {} local delim = m.config.filter_delimiter or ":" -- check for :filter: in query text local matched, new_filter = query_text:match("^%s*(" .. delim .. "(%S+)" .. delim .. ")") new_filter = new_filter or opts.workspace or m.config.default_workspace o.prompt = matched and query_text:sub(matched:len() + 1) or query_text if m.update(new_filter) then m.last_filter = new_filter o.updated_finder = finders.new_table { results = m.results, entry_maker = m.maker } end return o end picker_opts.attach_mappings = function(prompt_bufnr) actions.select_default:replace_if(function() return vim.fn.complete_info().pum_visible == 1 end, function() local keys = vim.fn.complete_info().selected == -1 and "" or ":" local accept_completion = vim.api.nvim_replace_termcodes(keys, true, false, true) vim.api.nvim_feedkeys(accept_completion, "n", true) end) return true end m.picker = pickers.new(opts, picker_opts) m.picker:find() m.set_prompt_options(m.picker.prompt_bufnr) end ---TODO: this seems to be forgotten and just exported in old implementation. ---@return table m.workspace_tags = function() -- Add user config workspaces. -- TODO: validate that workspaces are existing directories local tags = {} for k, _ in pairs(m.config.workspaces) do table.insert(tags, k) end -- Add CWD filter -- NOTE: hmmm :cwd::lsp: is easier to write. table.insert(tags, "CWD") -- Add LSP workpace(s) if m.fetch_lsp_workspaces(m.previous_buffer, true) then table.insert(tags, "LSP") end -- TODO: sort tags - by collective frecency? (?????? is this still relevant) return tags end m.complete = function(findstart, base) if findstart == 1 then local line = vim.api.nvim_get_current_line() local start = line:find ":" -- don't complete if there's already a completed `:tag:` in line if not start or line:find(":", start + 1) then return -3 end return start else if vim.fn.pumvisible() == 1 and #vim.v.completed_item > 0 then return "" end local matches = vim.tbl_filter(function(v) return vim.startswith(v, base) end, m.workspace_tags()) return #matches > 0 and matches or "" end end ---Setup Frecency Picker ---@param db FrecencyDB ---@param config FrecencyConfig m.setup = function(config) m.config = vim.tbl_extend("keep", config, m.config) db.set_config(config) --- Seed files table with oldfiles when it's empty. if db.sqlite.files:count() == 0 then -- TODO: this needs to be scheduled for after shada load?? for _, path in ipairs(vim.v.oldfiles) do db.sqlite.files:insert { path = path, count = 0 } -- TODO: remove when sql.nvim#97 is closed end vim.notify(("Telescope-Frecency: Imported %d entries from oldfiles."):format(#vim.v.oldfiles)) end -- TODO: perhaps ignore buffer without file path here? local group = vim.api.nvim_create_augroup("TelescopeFrecency", {}) vim.api.nvim_create_autocmd({ "BufWinEnter", "BufWritePost" }, { group = group, callback = function(args) local path = vim.api.nvim_buf_get_name(args.buf) local has_added_entry = db.update(path) m.updated = m.updated or has_added_entry end, }) vim.api.nvim_create_user_command("FrecencyValidate", function(cmd_info) db.validate { force = cmd_info.bang } end, { bang = true, desc = "Clean up DB for telescope-frecency" }) if db.config.auto_validate then db.validate { auto = true } end end return m