Inital commit.

This commit is contained in:
Senghan Bright
2021-01-14 01:37:37 +01:00
commit 8d6b6cc48d
5 changed files with 385 additions and 0 deletions

View File

@@ -0,0 +1,84 @@
local has_telescope, telescope = pcall(require, "telescope")
-- TODO: make dependency errors occur in a better way
if not has_telescope then
error("This plugin requires telescope.nvim (https://github.com/nvim-telescope/telescope.nvim)")
end
-- start the database client
print("start")
local db_client = require("telescope._extensions.frecency.db_client")
vim.defer_fn(db_client.init, 100) -- TODO: this is a crappy attempt to lessen loadtime impact, use VimEnter?
-- finder code
-- local actions = require "telescope.actions"
local entry_display = require "telescope.pickers.entry_display"
local finders = require "telescope.finders"
local pickers = require "telescope.pickers"
local previewers = require "telescope.previewers"
local sorters = require "telescope.sorters"
local conf = require('telescope.config').values
local path = require('telescope.path')
local utils = require('telescope.utils')
local frecency = function(opts)
opts = opts or {}
local cwd = vim.fn.expand(opts.cwd or vim.fn.getcwd())
local results = db_client.get_file_scores()
-- print(vim.inspect(results))
local displayer = entry_display.create {
separator = "",
hl_chars = { ["/"] = "TelescopePathSeparator"},
items = {
{ width = 8 },
{ remaining = true },
},
}
-- TODO: look into why this gets called so much
local make_display = function(entry)
local filename = entry.name
if opts.tail_path then
filename = utils.path_tail(filename)
elseif opts.shorten_path then
filename = utils.path_shorten(filename)
end
filename = path.make_relative(filename, cwd)
-- TODO: remove score from display; only there for debug
return displayer {
{entry.score, "Directory"}, filename
}
end
pickers.new(opts, {
prompt_title = "Frecency files",
finder = finders.new_table {
results = results,
entry_maker = function(entry)
return {
value = entry.filename,
display = make_display,
ordinal = entry.filename,
name = entry.filename,
score = entry.score
}
end,
},
previewer = conf.file_previewer(opts),
sorter = sorters.get_generic_fuzzy_sorter(), -- TODO: do we have to have our own sorter? we only want filtering
}):find()
end
return telescope.register_extension {
exports = {
frecency = frecency,
},
}

View File

@@ -0,0 +1,91 @@
local sqlwrap = require("telescope._extensions.frecency.sql_wrapper")
local MAX_TIMESTAMPS = 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 sql_wrapper = nil
local function init()
if sql_wrapper then return end
sql_wrapper = sqlwrap:new()
sql_wrapper:bootstrap()
print("setup")
-- setup autocommands
vim.api.nvim_command("augroup TelescopeFrecency")
vim.api.nvim_command("autocmd!")
vim.api.nvim_command("autocmd BufEnter * lua require'telescope._extensions.frecency.db_client'.autocmd_handler(vim.fn.expand('<amatch>'))")
vim.api.nvim_command("augroup END")
end
-- TODO: move these to util.lua
local function string_isempty(s)
return s == nil or s == ''
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
local function get_file_scores()
if not sql_wrapper then return end
local files = sql_wrapper:do_transaction('get_all_filepaths')
local timestamp_ages = sql_wrapper:do_transaction('get_all_timestamp_ages')
local scores = {}
for _, file_entry in ipairs(files) do
table.insert(scores, {
filename = file_entry.path,
score = calculate_file_score(file_entry.count, filter_timestamps(timestamp_ages, file_entry.id))
})
end
return scores
end
local function autocmd_handler(filepath)
if not sql_wrapper or string_isempty(filepath) then return end
-- check if file is registered as loaded
if not vim.b.frecency_registered then
vim.b.frecency_registered = 1
sql_wrapper:update(filepath)
end
end
return {
init = init,
get_file_scores = get_file_scores,
autocmd_handler = autocmd_handler,
}

View File

@@ -0,0 +1,142 @@
local vim = vim
local uv = vim.loop
local has_sql, sql = pcall(require, "sql")
if not has_sql then
error("This plugin requires sql.nvim (https://github.com/tami5/sql.nvim)")
end
-- TODO: pass in max_timestamps from db.lua
local MAX_TIMESTAMPS = 10
-- TODO: prioritize files in project root!
local schemas = {[[
CREATE TABLE IF NOT EXISTS files (
id INTEGER PRIMARY KEY,
count INTEGER,
path TEXT
);
]],
[[
CREATE TABLE IF NOT EXISTS timestamps (
id INTEGER PRIMARY KEY,
file_id INTEGER,
timestamp REAL,
FOREIGN KEY(file_id) REFERENCES files(id)
);
]]}
local queries = {
["file_add_entry"] = "INSERT INTO files (path, count) values(:path, 1);",
["file_update_counter"] = "UPDATE files SET count = count + 1 WHERE path = :path;",
["timestamp_add_entry"] = "INSERT INTO timestamps (file_id, timestamp) values(:file_id, julianday('now'));",
["timestamp_delete_before_id"] = "DELETE FROM timestamps WHERE id < :id and file_id == :file_id;",
["get_all_filepaths"] = "SELECT * FROM files;",
["get_all_timestamp_ages"] = "SELECT id, file_id, CAST((julianday('now') - julianday(timestamp)) * 24 * 60 AS INTEGER) AS age FROM timestamps;",
["get_row"] = "SELECT * FROM files WHERE path == :path;",
["get_timestamp_ids_for_file"] = "SELECT id FROM timestamps WHERE file_id == :file_id;",
}
-- local ignore_patterns = {
-- }
--
local function fs_stat(path) -- TODO: move this to new file with M
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
local M = {}
M.queries = queries
function M:new()
local o = {}
setmetatable(o, self)
self.__index = self
self.db = nil
return o
end
function M:bootstrap(opts)
opts = opts or {}
if self.db then
print("sql wrapper already initialised")
return
end
self.max_entries = opts.max_entries or 2000
-- create the db if it doesn't exist
local db_root = opts.docs_root or "$XDG_DATA_HOME/nvim"
local db_filename = db_root .. "/file_frecency.sqlite3"
self.db = sql.open(db_filename)
if not self.db then
print("error")
return
end
-- create tables if they don't exist
for _, s in pairs(schemas) do
self.db:eval(s)
end
self.db:close()
end
function M:do_transaction(query, params)
if not queries[query] then
print("invalid query_preset: " .. query )
return
end
local res
self.db:with_open(function(db) res = db:eval(queries[query], params) end)
return res
end
function M:get_row_id(filepath)
local result = self:do_transaction('get_row', { path = filepath })
return type(result) == "table" and result[1].id or nil
end
function M:update(filepath)
local filestat = 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 = self:get_row_id(filepath)
if not file_id then
self:do_transaction('file_add_entry', { path = filepath })
file_id = self:get_row_id(filepath)
else
-- ..or update existing entry
self:do_transaction('file_update_counter', { path = filepath })
end
-- register timestamp for this update
self:do_transaction('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('get_timestamp_ids_for_file', { file_id = file_id })
local trim_at = timestamps[(#timestamps - MAX_TIMESTAMPS) + 1]
if trim_at then
self:do_transaction('timestamp_delete_before_id', { id = trim_at.id, file_id = file_id })
end
end
function M:validate()
end
return M