mirror of
https://github.com/kristoferssolo/telescope-frecency.nvim.git
synced 2026-02-04 23:02:06 +00:00
refactor: reach the master (#93)
* refactor(db): use new sql.nvim api * refactor(*): create const, algo and add more util * refactor(picker): try to cleanup and make it easy to read FIXME: it seems that I didn't refactor the entry maker right. * refactor(*): move util to lua/frecency * misc(db): flag possible bug * refactor(db): reflect changes introduced in https://github.com/tami5/sql.nvim/pull/96 * refactor(db): move set_config to frecency.lua * new(db): ignore term and octo paths This may possibly fix the issue with entry.count being nil. * misc(picker): move health out of export * refactor(util) * refactor(db): general * misc(*): nothing major * refactor(db): abbreviate table namespace access * refactor(picker): working-ish * fix(*): general * refactor(picker): move fd function to the end of the file * refactor(*): remove the need for db.init * new(db): use foreign keys * sync with sqlite.lua@#105 * feat: add settings for StyLua * fix: detect the valid module in healthcheck See #65 * style: fix by StyLua * fix: detect CWD tag to cut paths See #66 * fix: show icon before directory See #67 * fix: deal with show_filter_column option validly * feat: support opts.workspace (#68) * doc(readme): update config example (#33) remove comma causing error. * doc(readme): fix packer install instructions (#34) Co-authored-by: tami5 <65782666+tami5@users.noreply.github.com> * doc: follow sqlite new release (#40) * refactor(sql_wrapper): follow sqlite new release * update readme * refactor: follow telescope's interface changes See #46 * feat: add default_workspace tag See #43 * fix: fetch workspaces in addition to completing See #72 * Update url for sqlite dependency (#64) The old repository on github redirects to this one. * fix: use vim.notify not to block outputs See #75 * feat: opts.path_display to customize See #76 * Enable to specify tags to show the tails (#77) * Enable to specify tags to show the tails * Add doc for show_filter_column * feat: use more reasonable matcher to sort See #73 * Fix broken Frecency Algorithm link in README.md (#82) MDN appear to have removed the Frecency Algorithm page linked in the README document. Updating link to use archived version. * fix: set the telescope default filetype in prompt See #81 * feat: use the newer API to define autocmds See #79 * refactor: use new API and add check for devicons * feat: detect entries when it has added new ones See #87 * fix: use valid widths to show entries * fix: use substr matcher to use sorting by scores See #94 * Revert "fix: set the telescope default filetype in prompt" This reverts commit 4937f7045438412e31a77374f91230bdbcbeb34d. * fix: enable to filter by valid paths * refactor: do nothing until calling setup() See #80 * fix: fix typo for `workspaces` * fix: show `0` score for unindexed instead of `nil` * style: fix by StyLua * fix: return a valid entry with get() It have used where() to get the entry and where() uses get() to fetch from DB. But this table has been customized by overwriting get(), so it has a bug to return the same entry every time it calls. This fixes it. * refactor: get the buffer name explicitly * fix: clean up logic to validate DB * feat: enable to work db_root option * fix: show no msg when no need to validate in auto --------- Co-authored-by: Tami <65782666+tami5@users.noreply.github.com> Co-authored-by: Munif Tanjim <hello@muniftanjim.dev> Co-authored-by: premell <65544203+premell@users.noreply.github.com> Co-authored-by: Anshuman Medhi <amedhi@connect.ust.hk> Co-authored-by: Lucas Hoffmann <lucc@users.noreply.github.com> Co-authored-by: Rohan Orton <rohan.orton@gmail.com>
This commit is contained in:
committed by
GitHub
parent
0a4a521471
commit
3f709af755
@@ -1,353 +1,29 @@
|
||||
local has_telescope, telescope = pcall(require, "telescope")
|
||||
|
||||
-- TODO: make sure scandir unindexed have opts.ignore_patterns applied
|
||||
-- TODO: make filters handle mulitple directories
|
||||
|
||||
if not has_telescope then
|
||||
error "This plugin requires telescope.nvim (https://github.com/nvim-telescope/telescope.nvim)"
|
||||
end
|
||||
|
||||
local config = {}
|
||||
|
||||
local state = {
|
||||
results = {},
|
||||
active_filter = nil,
|
||||
active_filter_tag = nil,
|
||||
last_filter = nil,
|
||||
previous_buffer = nil,
|
||||
cwd = nil,
|
||||
show_scores = false,
|
||||
default_workspace = nil,
|
||||
user_workspaces = {},
|
||||
lsp_workspaces = {},
|
||||
picker = {},
|
||||
}
|
||||
|
||||
-- returns `true` if workspaces exist
|
||||
---@param bufnr number
|
||||
---@param force? boolean
|
||||
---@return boolean workspaces_exist
|
||||
local function fetch_lsp_workspaces(bufnr, force)
|
||||
if not vim.tbl_isempty(state.lsp_workspaces) and not force then
|
||||
return true
|
||||
local telescope = (function()
|
||||
local ok, m = pcall(require, "telescope")
|
||||
if not ok then
|
||||
error "telescope-frecency: couldn't find telescope.nvim, please install"
|
||||
end
|
||||
return m
|
||||
end)()
|
||||
|
||||
local lsp_workspaces = vim.api.nvim_buf_call(bufnr, vim.lsp.buf.list_workspace_folders)
|
||||
if not vim.tbl_isempty(lsp_workspaces) then
|
||||
state.lsp_workspaces = lsp_workspaces
|
||||
return true
|
||||
end
|
||||
|
||||
state.lsp_workspaces = {}
|
||||
return false
|
||||
end
|
||||
|
||||
local function get_workspace_tags()
|
||||
-- Add user config workspaces. TODO: validate that workspaces are existing directories
|
||||
local tags = {}
|
||||
for k, _ in pairs(state.user_workspaces) do
|
||||
table.insert(tags, k)
|
||||
end
|
||||
|
||||
-- Add CWD filter
|
||||
table.insert(tags, "CWD")
|
||||
|
||||
-- Add LSP workpace(s)
|
||||
if fetch_lsp_workspaces(state.previous_buffer, true) then
|
||||
table.insert(tags, "LSP")
|
||||
end
|
||||
|
||||
-- print(vim.inspect(tags))
|
||||
-- TODO: sort tags - by collective frecency?
|
||||
return tags
|
||||
end
|
||||
|
||||
local function complete(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 tags = get_workspace_tags()
|
||||
local matches = vim.tbl_filter(function(v)
|
||||
return vim.startswith(v, base)
|
||||
end, tags)
|
||||
|
||||
return #matches > 0 and matches or ""
|
||||
end
|
||||
end
|
||||
|
||||
local frecency = function(opts)
|
||||
local has_devicons, devicons = pcall(require, "nvim-web-devicons")
|
||||
local entry_display = require "telescope.pickers.entry_display"
|
||||
local finders = require "telescope.finders"
|
||||
local Path = require "plenary.path"
|
||||
local pickers = require "telescope.pickers"
|
||||
local utils = require "telescope.utils"
|
||||
|
||||
local db_client = require "telescope._extensions.frecency.db_client"
|
||||
|
||||
-- start the database client
|
||||
db_client.init(
|
||||
config.db_root,
|
||||
config.ignore_patterns,
|
||||
vim.F.if_nil(config.db_safe_mode, true),
|
||||
vim.F.if_nil(config.auto_validate, true)
|
||||
)
|
||||
|
||||
opts = opts or {}
|
||||
|
||||
state.previous_buffer = vim.fn.bufnr "%"
|
||||
state.cwd = vim.fn.expand(opts.cwd or vim.fn.getcwd())
|
||||
|
||||
fetch_lsp_workspaces(state.previous_buffer)
|
||||
|
||||
local os_home = vim.loop.os_homedir()
|
||||
|
||||
local function should_show_tail()
|
||||
local filters = type(state.show_filter_column) == "table" and state.show_filter_column or { "LSP", "CWD" }
|
||||
return vim.tbl_contains(filters, state.active_filter_tag)
|
||||
end
|
||||
|
||||
local function get_display_cols()
|
||||
local directory_col_width = 0
|
||||
if state.active_filter then
|
||||
if should_show_tail() then
|
||||
-- TODO: Only add +1 if opts.show_filter_thing is true, +1 is for the trailing slash
|
||||
directory_col_width = #(utils.path_tail(state.active_filter)) + 1
|
||||
else
|
||||
directory_col_width = #(Path:new(state.active_filter):make_relative(os_home)) + 1
|
||||
end
|
||||
end
|
||||
local res = {}
|
||||
res[1] = state.show_scores and { width = 8 } or nil
|
||||
if has_devicons and not state.disable_devicons then
|
||||
table.insert(res, { width = 2 }) -- icon column
|
||||
end
|
||||
if state.show_filter_column then
|
||||
table.insert(res, { width = directory_col_width })
|
||||
end
|
||||
table.insert(res, { remaining = true })
|
||||
return res
|
||||
end
|
||||
local os_path_sep = utils.get_separator()
|
||||
|
||||
local displayer = entry_display.create {
|
||||
separator = "",
|
||||
hl_chars = { [os_path_sep] = "TelescopePathSeparator" },
|
||||
items = get_display_cols(),
|
||||
}
|
||||
|
||||
if not opts.path_display then
|
||||
opts.path_display = function (path_opts, filename)
|
||||
local original_filename = filename
|
||||
|
||||
filename = Path:new(filename):make_relative(path_opts.cwd)
|
||||
if not state.active_filter then
|
||||
if vim.startswith(filename, os_home) then
|
||||
filename = "~/" .. Path:new(filename):make_relative(os_home)
|
||||
elseif filename ~= original_filename then
|
||||
filename = "./" .. filename
|
||||
end
|
||||
end
|
||||
|
||||
return filename
|
||||
end
|
||||
end
|
||||
|
||||
local function filepath_formatter(path_opts0)
|
||||
local path_opts = {}
|
||||
for k, v in pairs(path_opts0) do
|
||||
path_opts[k] = v
|
||||
end
|
||||
|
||||
return function (filename)
|
||||
path_opts.cwd = state.active_filter or state.cwd
|
||||
return utils.transform_path(path_opts, filename)
|
||||
end
|
||||
end
|
||||
|
||||
local formatter = filepath_formatter(opts)
|
||||
|
||||
local bufnr, buf_is_loaded, display_filename, hl_filename, display_items, icon, icon_highlight
|
||||
local make_display = function(entry)
|
||||
bufnr = vim.fn.bufnr
|
||||
buf_is_loaded = vim.api.nvim_buf_is_loaded
|
||||
display_filename = entry.name
|
||||
hl_filename = buf_is_loaded(bufnr(display_filename)) and "TelescopeBufferLoaded" or ""
|
||||
display_filename = formatter(display_filename)
|
||||
|
||||
display_items = state.show_scores and { { entry.score, "TelescopeFrecencyScores" } } or {}
|
||||
|
||||
if has_devicons and not state.disable_devicons then
|
||||
icon, icon_highlight = devicons.get_icon(entry.name, string.match(entry.name, "%a+$"), { default = true })
|
||||
table.insert(display_items, { icon, icon_highlight })
|
||||
end
|
||||
|
||||
-- TODO: store the column lengths here, rather than recalculating in get_display_cols()
|
||||
if state.show_filter_column then
|
||||
local filter_path = ""
|
||||
if state.active_filter then
|
||||
if should_show_tail() then
|
||||
filter_path = utils.path_tail(state.active_filter) .. os_path_sep
|
||||
else
|
||||
filter_path = Path:new(state.active_filter):make_relative(os_home) .. os_path_sep
|
||||
end
|
||||
end
|
||||
|
||||
table.insert(display_items, { filter_path, "Directory" })
|
||||
end
|
||||
|
||||
table.insert(display_items, { display_filename, hl_filename })
|
||||
|
||||
return displayer(display_items)
|
||||
end
|
||||
|
||||
local update_results = function(filter)
|
||||
local filter_updated = false
|
||||
|
||||
-- validate tag
|
||||
local ws_dir = filter and state.user_workspaces[filter]
|
||||
if filter == "LSP" and not vim.tbl_isempty(state.lsp_workspaces) then
|
||||
ws_dir = state.lsp_workspaces[1]
|
||||
end
|
||||
|
||||
if filter == "CWD" then
|
||||
ws_dir = state.cwd
|
||||
end
|
||||
|
||||
if ws_dir ~= state.active_filter then
|
||||
filter_updated = true
|
||||
state.active_filter = ws_dir
|
||||
state.active_filter_tag = filter
|
||||
end
|
||||
|
||||
if vim.tbl_isempty(state.results) or db_client.has_updated_results() or filter_updated then
|
||||
state.results = db_client.get_file_scores(state.show_unindexed, ws_dir)
|
||||
end
|
||||
return filter_updated
|
||||
end
|
||||
|
||||
-- populate initial results
|
||||
update_results()
|
||||
|
||||
local entry_maker = function(entry)
|
||||
return {
|
||||
filename = entry.filename,
|
||||
display = make_display,
|
||||
ordinal = entry.filename,
|
||||
name = entry.filename,
|
||||
score = entry.score,
|
||||
}
|
||||
end
|
||||
|
||||
local delim = opts.filter_delimiter or ":"
|
||||
local filter_re = "^%s*(" .. delim .. "(%S+)" .. delim .. ")"
|
||||
|
||||
state.picker = pickers.new(opts, {
|
||||
prompt_title = "Frecency",
|
||||
on_input_filter_cb = function(query_text)
|
||||
-- check for :filter: in query text
|
||||
local matched, new_filter = query_text:match(filter_re)
|
||||
if matched then
|
||||
query_text = query_text:sub(matched:len() + 1)
|
||||
end
|
||||
new_filter = new_filter or opts.workspace or state.default_workspace
|
||||
|
||||
local new_finder
|
||||
local results_updated = update_results(new_filter)
|
||||
if results_updated then
|
||||
displayer = entry_display.create {
|
||||
separator = "",
|
||||
hl_chars = { [os_path_sep] = "TelescopePathSeparator" },
|
||||
items = get_display_cols(),
|
||||
}
|
||||
|
||||
state.last_filter = new_filter
|
||||
new_finder = finders.new_table {
|
||||
results = state.results,
|
||||
entry_maker = entry_maker,
|
||||
}
|
||||
-- print(vim.inspect(new_finder))
|
||||
end
|
||||
|
||||
return { prompt = query_text, updated_finder = new_finder }
|
||||
end,
|
||||
attach_mappings = function(prompt_bufnr)
|
||||
require "telescope.actions".select_default:replace_if(function()
|
||||
local compinfo = vim.fn.complete_info()
|
||||
return compinfo.pum_visible == 1
|
||||
end, function()
|
||||
local compinfo = vim.fn.complete_info()
|
||||
local keys = compinfo.selected == -1 and "<C-e><Bs><Right>" or "<C-y><Right>:"
|
||||
local accept_completion = vim.api.nvim_replace_termcodes(keys, true, false, true)
|
||||
vim.api.nvim_feedkeys(accept_completion, "n", true)
|
||||
end)
|
||||
|
||||
return true
|
||||
end,
|
||||
finder = finders.new_table {
|
||||
results = state.results,
|
||||
entry_maker = entry_maker,
|
||||
},
|
||||
previewer = require("telescope.config").values.file_previewer(opts),
|
||||
sorter = require'telescope.sorters'.get_substr_matcher(opts),
|
||||
})
|
||||
state.picker:find()
|
||||
|
||||
local buffer = state.picker.prompt_bufnr
|
||||
vim.api.nvim_buf_set_option(buffer, "filetype", "frecency")
|
||||
vim.api.nvim_buf_set_option(buffer, "completefunc", "v:lua.require'telescope'.extensions.frecency.complete")
|
||||
vim.keymap.set("i", "<Tab>", "pumvisible() ? '<C-n>' : '<C-x><C-u>'", { buffer = buffer, expr = true })
|
||||
vim.keymap.set("i", "<S-Tab>", "pumvisible() ? '<C-p>' : ''", { buffer = buffer, expr = true })
|
||||
end
|
||||
|
||||
local function set_config_state(opt_name, value, default)
|
||||
state[opt_name] = value == nil and default or value
|
||||
end
|
||||
|
||||
local function checkhealth()
|
||||
local has_sql, _ = pcall(require, "sqlite")
|
||||
if has_sql then
|
||||
vim.health.report_ok "sql.nvim installed."
|
||||
-- return "MOOP"
|
||||
else
|
||||
vim.health.report_error "Need sql.nvim to be installed."
|
||||
end
|
||||
if pcall(require, "nvim-web-devicons") then
|
||||
vim.health.report_ok "nvim-web-devicons installed."
|
||||
else
|
||||
vim.health.report_info "nvim-web-devicons is not installed."
|
||||
end
|
||||
end
|
||||
local picker = require "frecency.picker"
|
||||
|
||||
return telescope.register_extension {
|
||||
setup = function(ext_config)
|
||||
set_config_state("db_root", ext_config.db_root, nil)
|
||||
set_config_state("show_scores", ext_config.show_scores, false)
|
||||
set_config_state("show_unindexed", ext_config.show_unindexed, true)
|
||||
set_config_state("show_filter_column", ext_config.show_filter_column, true)
|
||||
set_config_state("user_workspaces", ext_config.workspaces, {})
|
||||
set_config_state("disable_devicons", ext_config.disable_devicons, false)
|
||||
set_config_state("default_workspace", ext_config.default_workspace, nil)
|
||||
config = vim.deepcopy(ext_config)
|
||||
|
||||
vim.api.nvim_create_user_command("FrecencyValidate", function (cmd_info)
|
||||
local safe_mode = not cmd_info.bang
|
||||
require"telescope._extensions.frecency.db_client".validate(safe_mode)
|
||||
end, { bang = true, desc = "Clean up DB for telescope-frecency" })
|
||||
setup = picker.setup,
|
||||
health = function()
|
||||
if ({ pcall(require, "sqlite") })[1] then
|
||||
vim.health.report_ok "sql.nvim installed."
|
||||
else
|
||||
vim.health.report_error "sql.nvim is required for telescope-frecency.nvim to work."
|
||||
end
|
||||
if ({ pcall(require, "nvim-web-devicons") })[1] then
|
||||
vim.health.report_ok "nvim-web-devicons installed."
|
||||
else
|
||||
vim.health.report_info "nvim-web-devicons is not installed."
|
||||
end
|
||||
end,
|
||||
exports = {
|
||||
frecency = frecency,
|
||||
complete = complete,
|
||||
frecency = picker.fd,
|
||||
complete = picker.complete,
|
||||
},
|
||||
health = checkhealth,
|
||||
}
|
||||
|
||||
@@ -1,239 +0,0 @@
|
||||
local sqlwrap = require("telescope._extensions.frecency.sql_wrapper")
|
||||
local scandir = require("plenary.scandir").scan_dir
|
||||
local util = require("telescope._extensions.frecency.util")
|
||||
|
||||
local MAX_TIMESTAMPS = 10
|
||||
local DB_REMOVE_SAFETY_THRESHOLD = 10
|
||||
|
||||
-- modifier used as a weight in the recency_score calculation:
|
||||
local recency_modifier = {
|
||||
[1] = { age = 240 , value = 100 }, -- past 4 hours
|
||||
[2] = { age = 1440 , value = 80 }, -- past day
|
||||
[3] = { age = 4320 , value = 60 }, -- past 3 days
|
||||
[4] = { age = 10080 , value = 40 }, -- past week
|
||||
[5] = { age = 43200 , value = 20 }, -- past month
|
||||
[6] = { age = 129600, value = 10 } -- past 90 days
|
||||
}
|
||||
|
||||
local default_ignore_patterns = {
|
||||
"*.git/*", "*/tmp/*"
|
||||
}
|
||||
|
||||
local sql_wrapper = nil
|
||||
local ignore_patterns = {}
|
||||
|
||||
local function import_oldfiles()
|
||||
local oldfiles = vim.api.nvim_get_vvar("oldfiles")
|
||||
for _, filepath in pairs(oldfiles) do
|
||||
sql_wrapper:update(filepath)
|
||||
end
|
||||
vim.notify(("Telescope-Frecency: Imported %d entries from oldfiles."):format(#oldfiles))
|
||||
end
|
||||
|
||||
local function file_is_ignored(filepath)
|
||||
local is_ignored = false
|
||||
for _, pattern in pairs(ignore_patterns) do
|
||||
if util.filename_match(filepath, pattern) then
|
||||
is_ignored = true
|
||||
goto continue
|
||||
end
|
||||
end
|
||||
|
||||
::continue::
|
||||
return is_ignored
|
||||
end
|
||||
|
||||
local function validate_db(safe_mode)
|
||||
if not sql_wrapper then return {} end
|
||||
|
||||
local queries = sql_wrapper.queries
|
||||
local files = sql_wrapper:do_transaction(queries.file_get_entries, {})
|
||||
local pending_remove = {}
|
||||
for _, entry in pairs(files) do
|
||||
if not util.fs_stat(entry.path).exists -- file no longer exists
|
||||
or file_is_ignored(entry.path) then -- cleanup entries that match the _current_ ignore list
|
||||
table.insert(pending_remove, entry)
|
||||
end
|
||||
end
|
||||
|
||||
local confirmed = false
|
||||
if not safe_mode then
|
||||
confirmed = true
|
||||
elseif #pending_remove > DB_REMOVE_SAFETY_THRESHOLD then
|
||||
-- don't allow removal of >N values from DB without confirmation
|
||||
local user_response = vim.fn.confirm("Telescope-Frecency: remove " .. #pending_remove .. " entries from SQLite3 database?", "&Yes\n&No", 2)
|
||||
if user_response == 1 then
|
||||
confirmed = true
|
||||
else
|
||||
vim.defer_fn(function() vim.notify("TelescopeFrecency: validation aborted.") end, 50)
|
||||
end
|
||||
else
|
||||
confirmed = true
|
||||
end
|
||||
|
||||
if #pending_remove > 0 then
|
||||
if confirmed == true then
|
||||
for _, entry in pairs(pending_remove) do
|
||||
-- remove entries from file and timestamp tables
|
||||
sql_wrapper:do_transaction(queries.file_delete_entry , {where = {id = entry.id }})
|
||||
sql_wrapper:do_transaction(queries.timestamp_delete_entry, {where = {file_id = entry.id}})
|
||||
end
|
||||
vim.notify(('Telescope-Frecency: removed %d missing entries.'):format(#pending_remove))
|
||||
else
|
||||
vim.notify("Telescope-Frecency: validation aborted.")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- TODO: make init params a keyed table
|
||||
local function init(db_root, config_ignore_patterns, safe_mode, auto_validate)
|
||||
if sql_wrapper then return end
|
||||
sql_wrapper = sqlwrap:new()
|
||||
local first_run = sql_wrapper:bootstrap(db_root)
|
||||
ignore_patterns = config_ignore_patterns or default_ignore_patterns
|
||||
|
||||
if auto_validate then
|
||||
validate_db(safe_mode)
|
||||
end
|
||||
|
||||
if first_run then
|
||||
-- TODO: this needs to be scheduled for after shada load
|
||||
vim.defer_fn(import_oldfiles, 100)
|
||||
end
|
||||
|
||||
-- setup autocommands
|
||||
local group = vim.api.nvim_create_augroup("TelescopeFrecency", {})
|
||||
vim.api.nvim_create_autocmd({ "BufWinEnter", "BufWritePost" }, {
|
||||
group = group,
|
||||
callback = function(args)
|
||||
local db_client = require "telescope._extensions.frecency.db_client"
|
||||
db_client.autocmd_handler(args.match)
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
local function calculate_file_score(frequency, timestamps)
|
||||
local recency_score = 0
|
||||
for _, ts in pairs(timestamps) do
|
||||
for _, rank in ipairs(recency_modifier) do
|
||||
if ts.age <= rank.age then
|
||||
recency_score = recency_score + rank.value
|
||||
goto continue
|
||||
end
|
||||
end
|
||||
::continue::
|
||||
end
|
||||
|
||||
return frequency * recency_score / MAX_TIMESTAMPS
|
||||
end
|
||||
|
||||
local function filter_timestamps(timestamps, file_id)
|
||||
local res = {}
|
||||
for _, entry in pairs(timestamps) do
|
||||
if entry.file_id == file_id then
|
||||
table.insert(res, entry)
|
||||
end
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
-- -- TODO: optimize this
|
||||
-- local function find_in_table(tbl, target)
|
||||
-- for _, entry in pairs(tbl) do
|
||||
-- if entry.path == target then return true end
|
||||
-- end
|
||||
-- return false
|
||||
-- end
|
||||
|
||||
-- local function async_callback(result)
|
||||
-- -- print(vim.inspect(result))
|
||||
-- end
|
||||
|
||||
local function filter_workspace(workspace_path, show_unindexed)
|
||||
local queries = sql_wrapper.queries
|
||||
show_unindexed = show_unindexed == nil and true or show_unindexed
|
||||
|
||||
local res = {}
|
||||
|
||||
res = sql_wrapper:do_transaction(queries.file_get_descendant_of, {path = workspace_path.."%"})
|
||||
if type(res) == "boolean" then res = {} end -- TODO: do this in sql_wrapper:transaction
|
||||
|
||||
local scan_opts = {
|
||||
respect_gitignore = true,
|
||||
depth = 100,
|
||||
hidden = true
|
||||
}
|
||||
|
||||
-- TODO: handle duplicate entries
|
||||
if show_unindexed then
|
||||
local unindexed_files = scandir(workspace_path, scan_opts)
|
||||
for _, file in pairs(unindexed_files) do
|
||||
if not file_is_ignored(file) then -- this causes some slowdown on large dirs
|
||||
table.insert(res, {
|
||||
id = 0,
|
||||
path = file,
|
||||
count = 0,
|
||||
directory_id = 0
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return res
|
||||
end
|
||||
|
||||
local updated = false
|
||||
|
||||
local function get_file_scores(show_unindexed, workspace_path)
|
||||
if not sql_wrapper then return {} end
|
||||
|
||||
local queries = sql_wrapper.queries
|
||||
local files = sql_wrapper:do_transaction(queries.file_get_entries, {})
|
||||
local timestamp_ages = sql_wrapper:do_transaction(queries.timestamp_get_all_entry_ages, {})
|
||||
|
||||
local scores = {}
|
||||
if vim.tbl_isempty(files) then return scores end
|
||||
files = workspace_path and filter_workspace(workspace_path, show_unindexed) or files
|
||||
|
||||
local score
|
||||
for _, file_entry in ipairs(files) do
|
||||
score = file_entry.count == 0 and 0 or calculate_file_score(file_entry.count, filter_timestamps(timestamp_ages, file_entry.id))
|
||||
table.insert(scores, {
|
||||
filename = file_entry.path,
|
||||
score = score
|
||||
})
|
||||
end
|
||||
|
||||
-- sort the table
|
||||
table.sort(scores, function(a, b) return a.score > b.score end)
|
||||
|
||||
updated = false
|
||||
|
||||
return scores
|
||||
end
|
||||
|
||||
local function autocmd_handler(filepath)
|
||||
if not sql_wrapper or util.string_isempty(filepath) then return end
|
||||
|
||||
-- check if file is registered as loaded
|
||||
if not vim.b.telescope_frecency_registered then
|
||||
-- allow [noname] files to go unregistered until BufWritePost
|
||||
if not util.fs_stat(filepath).exists then return end
|
||||
if file_is_ignored(filepath) then return end
|
||||
|
||||
vim.b.telescope_frecency_registered = 1
|
||||
updated = sql_wrapper:update(filepath)
|
||||
end
|
||||
end
|
||||
|
||||
local function has_updated_results()
|
||||
return updated
|
||||
end
|
||||
|
||||
return {
|
||||
init = init,
|
||||
get_file_scores = get_file_scores,
|
||||
autocmd_handler = autocmd_handler,
|
||||
validate = validate_db,
|
||||
has_updated_results = has_updated_results,
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
local util = require("telescope._extensions.frecency.util")
|
||||
local vim = vim
|
||||
|
||||
local has_sqlite, sqlite = pcall(require, "sqlite")
|
||||
if not has_sqlite then
|
||||
error("This plugin requires sqlite.lua (https://github.com/tami5/sqlite.lua) " .. tostring(sqlite))
|
||||
end
|
||||
|
||||
-- TODO: pass in max_timestamps from db.lua
|
||||
local MAX_TIMESTAMPS = 10
|
||||
|
||||
local db_table = {}
|
||||
db_table.files = "files"
|
||||
db_table.timestamps = "timestamps"
|
||||
db_table.workspaces = "workspaces"
|
||||
--
|
||||
|
||||
-- TODO: NEXT!
|
||||
-- extend substr sorter to have modes:
|
||||
-- when current string is prefixed by `:foo`, results are tag_names that come from tags/workspaces table. (if `:foo ` token is incomplete it is ignored)
|
||||
-- when a complete workspace tag is matched ':foobar:', results are indexed_files filtered by if their parent_dir is a descendant of the workspace_dir
|
||||
-- a recursive scan_dir() result is added to the :foobar: filter results; any non-indexed_files are given a score of zero, and are alphabetically sorted below the indexed_results
|
||||
|
||||
-- make tab completion for tab_names in insert mode`:foo|` state: cycles through available options
|
||||
|
||||
local M = {}
|
||||
|
||||
function M:new()
|
||||
local o = {}
|
||||
setmetatable(o, self)
|
||||
self.__index = self
|
||||
self.db = nil
|
||||
|
||||
return o
|
||||
end
|
||||
|
||||
function M:bootstrap(db_root)
|
||||
if self.db then return end
|
||||
|
||||
-- opts = opts or {}
|
||||
-- self.max_entries = opts.max_entries or 2000
|
||||
|
||||
-- create the db if it doesn't exist
|
||||
db_root = db_root or vim.fn.stdpath('data')
|
||||
local db_filename = db_root .. "/file_frecency.sqlite3"
|
||||
self.db = sqlite:open(db_filename)
|
||||
if not self.db then
|
||||
vim.notify("Telescope-Frecency: error in opening DB", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
local first_run = false
|
||||
if not self.db:exists(db_table.files) then
|
||||
first_run = true
|
||||
-- create tables if they don't exist
|
||||
self.db:create(db_table.files, {
|
||||
id = {"INTEGER", "PRIMARY", "KEY"},
|
||||
count = "INTEGER",
|
||||
path = "TEXT"
|
||||
})
|
||||
self.db:create(db_table.timestamps, {
|
||||
id = {"INTEGER", "PRIMARY", "KEY"},
|
||||
file_id = "INTEGER",
|
||||
timestamp = "REAL"
|
||||
-- FOREIGN KEY(file_id) REFERENCES files(id)
|
||||
})
|
||||
end
|
||||
|
||||
self.db:close()
|
||||
return first_run
|
||||
end
|
||||
|
||||
--
|
||||
|
||||
function M:do_transaction(t, params)
|
||||
-- print(vim.inspect(t))
|
||||
-- print(vim.inspect(params))
|
||||
return self.db:with_open(function(db)
|
||||
local case = {
|
||||
[1] = function() return db:select(t.cmd_data, params) end,
|
||||
[2] = function() return db:insert(t.cmd_data, params) end,
|
||||
[3] = function() return db:delete(t.cmd_data, params) end,
|
||||
[4] = function() return db:eval(t.cmd_data, params) end,
|
||||
}
|
||||
return case[t.cmd]()
|
||||
end)
|
||||
end
|
||||
|
||||
local cmd = {
|
||||
select = 1,
|
||||
insert = 2,
|
||||
delete = 3,
|
||||
eval = 4,
|
||||
}
|
||||
|
||||
local queries = {
|
||||
file_get_descendant_of = {
|
||||
cmd = cmd.eval,
|
||||
cmd_data = "SELECT * FROM files WHERE path LIKE :path"
|
||||
},
|
||||
file_add_entry = {
|
||||
cmd = cmd.insert,
|
||||
cmd_data = db_table.files
|
||||
},
|
||||
file_delete_entry = {
|
||||
cmd = cmd.delete,
|
||||
cmd_data = db_table.files
|
||||
},
|
||||
file_get_entries = {
|
||||
cmd = cmd.select,
|
||||
cmd_data = db_table.files
|
||||
},
|
||||
file_update_counter = {
|
||||
cmd = cmd.eval,
|
||||
cmd_data = "UPDATE files SET count = count + 1 WHERE path == :path;"
|
||||
},
|
||||
timestamp_add_entry = {
|
||||
cmd = cmd.eval,
|
||||
cmd_data = "INSERT INTO timestamps (file_id, timestamp) values(:file_id, julianday('now'));"
|
||||
},
|
||||
timestamp_delete_entry = {
|
||||
cmd = cmd.delete,
|
||||
cmd_data = db_table.timestamps
|
||||
},
|
||||
timestamp_get_all_entries = {
|
||||
cmd = cmd.select,
|
||||
cmd_data = db_table.timestamps,
|
||||
},
|
||||
timestamp_get_all_entry_ages = {
|
||||
cmd = cmd.eval,
|
||||
cmd_data = "SELECT id, file_id, CAST((julianday('now') - julianday(timestamp)) * 24 * 60 AS INTEGER) AS age FROM timestamps;"
|
||||
},
|
||||
timestamp_delete_before_id = {
|
||||
cmd = cmd.eval,
|
||||
cmd_data = "DELETE FROM timestamps WHERE id < :id and file_id == :file_id;"
|
||||
},
|
||||
}
|
||||
|
||||
M.queries = queries
|
||||
|
||||
--
|
||||
|
||||
local function row_id(entry)
|
||||
return (not vim.tbl_isempty(entry)) and entry[1].id or nil
|
||||
end
|
||||
|
||||
function M:update(filepath)
|
||||
local filestat = util.fs_stat(filepath)
|
||||
if (vim.tbl_isempty(filestat) or
|
||||
filestat.exists == false or
|
||||
filestat.isdirectory == true) then
|
||||
return end
|
||||
|
||||
-- create entry if it doesn't exist
|
||||
local file_id
|
||||
file_id = row_id(self:do_transaction(queries.file_get_entries, {where = {path = filepath}}))
|
||||
local has_added_entry
|
||||
if not file_id then
|
||||
self:do_transaction(queries.file_add_entry, {path = filepath, count = 1})
|
||||
file_id = row_id(self:do_transaction(queries.file_get_entries, {where = {path = filepath}}))
|
||||
has_added_entry = true
|
||||
else
|
||||
-- ..or update existing entry
|
||||
self:do_transaction(queries.file_update_counter, {path = filepath})
|
||||
end
|
||||
|
||||
-- register timestamp for this update
|
||||
self:do_transaction(queries.timestamp_add_entry, {file_id = file_id})
|
||||
|
||||
-- trim timestamps to MAX_TIMESTAMPS per file (there should be up to MAX_TS + 1 at this point)
|
||||
local timestamps = self:do_transaction(queries.timestamp_get_all_entries, {where = {file_id = file_id}})
|
||||
local trim_at = timestamps[(#timestamps - MAX_TIMESTAMPS) + 1]
|
||||
if trim_at then
|
||||
self:do_transaction(queries.timestamp_delete_before_id, {id = trim_at.id, file_id = file_id})
|
||||
end
|
||||
|
||||
return has_added_entry
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -1,44 +0,0 @@
|
||||
local uv = vim.loop
|
||||
|
||||
local util = {}
|
||||
|
||||
-- stolen from penlight
|
||||
|
||||
-- escape any Lua 'magic' characters in a string
|
||||
util.escape = function(str)
|
||||
return (str:gsub('[%-%.%+%[%]%(%)%$%^%%%?%*]','%%%1'))
|
||||
end
|
||||
|
||||
util.filemask = function(mask)
|
||||
mask = util.escape(mask)
|
||||
return '^'..mask:gsub('%%%*','.*'):gsub('%%%?','.')..'$'
|
||||
end
|
||||
|
||||
util.filename_match = function(filename, pattern)
|
||||
return filename:find(util.filemask(pattern)) ~= nil
|
||||
end
|
||||
|
||||
--
|
||||
|
||||
util.string_isempty = function(str)
|
||||
return str == nil or str == ''
|
||||
end
|
||||
|
||||
util.split = function(str, delimiter)
|
||||
local result = {}
|
||||
for match in str:gmatch("[^" .. delimiter .. "]+") do
|
||||
table.insert(result, match)
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
util.fs_stat = function(path)
|
||||
local stat = uv.fs_stat(path)
|
||||
local res = {}
|
||||
res.exists = stat and true or false -- TODO: this is silly
|
||||
res.isdirectory = (stat and stat.type == "directory") and true or false
|
||||
|
||||
return res
|
||||
end
|
||||
|
||||
return util
|
||||
Reference in New Issue
Block a user