feat: call init process before telescope loading (#234)

* feat: call init process before telescope loading

Fix #231

This changes enable to load frecency without telescope's loading itself.
This is needed when you want to load telescope lazily, but want to start
registering process as soon as Neovim has started.

```lua
{
  "nvim-telescope/telescope-frecency.nvim",
  main = "frecency",
  ---@type FrecencyOpts
  opts = {
    db_safe_mode = false,
  },
},

{
  "nvim-telescope/telescope.nvim",
  -- `cmd` opts makes lazy.nvim load telescope.nvim lazily.
  cmd = { "Telescope" },
  config = function()
    local telescope = require "telescope"
    telescope.setup {
      extensions = {
        other_extension = {
          foo_bar = true,
        },
        -- Here you need no configuration opts for frecency because
        -- you've already done.
      }
    }
    -- This is still needed.
    telescope.load_extension "frecency"
  end,
},
```

* docs: add note for loading telescope.nvim lazily
This commit is contained in:
JINNOUCHI Yasushi 2024-08-01 17:12:43 +09:00 committed by GitHub
parent cef01dee8b
commit 87ccbae5d2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 380 additions and 319 deletions

View File

@ -134,6 +134,9 @@ If no database is found when running Neovim with the plugin installed, a new
one is created and entries from |shada| and |v:oldfiles| are automatically one is created and entries from |shada| and |v:oldfiles| are automatically
imported. imported.
NOTE: Even if you want to load |telescope.nvim| lazily, you should NOT load
telescope-frecency.nvim lazily. See |telescope-frecency-function-setup|.
============================================================================== ==============================================================================
USAGE *telescope-frecency-usage* USAGE *telescope-frecency-usage*
@ -315,6 +318,46 @@ Options: *telescope-frecency-function-query-options*
exist below the directory specified this value. See also exist below the directory specified this value. See also
|telescope-frecency-usage|. |telescope-frecency-usage|.
*telescope-frecency-function-setup*
setup() ~
This will be called by |telescope.nvim| for its initialization. You can also
call this to initialize this plugin separated from |telescope.nvim|'s
initialization phase.
This is useful when you want to load |telescope.nvim| lazily, but want to
register opened files as soon as Neovim has started. Example configuration for
|lazy.nvim| is below.
>lua
{
"nvim-telescope/telescope-frecency.nvim",
main = "frecency",
---@type FrecencyOpts
opts = {
db_safe_mode = false,
},
},
{
"nvim-telescope/telescope.nvim",
-- `cmd` opts makes lazy.nvim load telescope.nvim lazily.
cmd = { "Telescope" },
config = function()
local telescope = require "telescope"
telescope.setup {
extensions = {
other_extension = {
foo_bar = true,
},
-- Here you need no configuration opts for frecency because
-- you've already done.
}
}
-- This is still needed.
telescope.load_extension "frecency"
end,
},
============================================================================== ==============================================================================
CONFIGURATION *telescope-frecency-configuration* CONFIGURATION *telescope-frecency-configuration*

View File

@ -1,261 +1,81 @@
local Database = require "frecency.database" ---This object is intended to be used as a singleton, and is lazily loaded.
local EntryMaker = require "frecency.entry_maker" ---When methods are called at the first time, it calls the constructor and
local FS = require "frecency.fs" ---setup() to be initialized.
local Picker = require "frecency.picker" ---@class FrecencyInstance
local Recency = require "frecency.recency" ---@field complete fun(findstart: 1|0, base: string): integer|''|string[]
local config = require "frecency.config" ---@field delete fun(path: string): nil
local log = require "frecency.log" ---@field query fun(opts?: FrecencyQueryOpts): FrecencyQueryEntry[]|string[]
---@field register fun(bufnr: integer, datetime: string?): nil
---@class Frecency ---@field start fun(opts: FrecencyPickerOptions?): nil
---@field private buf_registered table<integer, boolean> flag to indicate the buffer is registered to the database. ---@field validate_database fun(force: boolean?): nil
---@field private database FrecencyDatabase local frecency = setmetatable({}, {
---@field private entry_maker FrecencyEntryMaker ---@param self FrecencyInstance
---@field private fs FrecencyFS ---@param key "complete"|"delete"|"register"|"start"|"validate_database"
---@field private picker FrecencyPicker ---@return function
---@field private recency FrecencyRecency __index = function(self, key)
local Frecency = {}
---@return Frecency ---@return Frecency
Frecency.new = function() local function instance()
local self = setmetatable({ buf_registered = {} }, { __index = Frecency }) --[[@as Frecency]] return rawget(self, "instance")
self.fs = FS.new()
self.database = Database.new(self.fs)
self.entry_maker = EntryMaker.new(self.fs)
self.recency = Recency.new()
return self
end end
---This is called when `:Telescope frecency` is called at the first time. return function(...)
---@return nil if not instance() then
function Frecency:setup() rawset(self, "instance", require("frecency.klass").new())
-- HACK: Wihout this wrapping, it spoils background color detection. instance():setup()
-- See https://github.com/nvim-telescope/telescope-frecency.nvim/issues/210
vim.defer_fn(function()
self:assert_db_entries()
if config.auto_validate then
self:validate_database()
end end
end, 0) return instance()[key](instance(), ...)
end end
---This can be calledBy `require("telescope").extensions.frecency.frecency`.
---@param opts? FrecencyPickerOptions
---@return nil
function Frecency:start(opts)
local start = os.clock()
log.debug "Frecency:start"
opts = opts or {}
if opts.cwd then
opts.cwd = vim.fn.expand(opts.cwd)
end
local ignore_filenames
if opts.hide_current_buffer or config.hide_current_buffer then
ignore_filenames = { vim.api.nvim_buf_get_name(0) }
end
self.picker = Picker.new(self.database, self.entry_maker, self.fs, self.recency, {
editing_bufnr = vim.api.nvim_get_current_buf(),
ignore_filenames = ignore_filenames,
initial_workspace_tag = opts.workspace,
})
self.picker:start(vim.tbl_extend("force", config.get(), opts))
log.debug(("Frecency:start picker:start takes %f seconds"):format(os.clock() - start))
end
---This can be calledBy `require("telescope").extensions.frecency.complete`.
---@param findstart 1|0
---@param base string
---@return integer|''|string[]
function Frecency:complete(findstart, base)
return self.picker:complete(findstart, base)
end
---@private
---@return nil
function Frecency:assert_db_entries()
if not self.database:has_entry() then
self.database:insert_files(vim.v.oldfiles)
self:notify("Imported %d entries from oldfiles.", #vim.v.oldfiles)
end
end
---@private
---@param force? boolean
---@return nil
function Frecency:validate_database(force)
local unlinked = self.database:unlinked_entries()
if #unlinked == 0 or (not force and #unlinked < config.db_validate_threshold) then
return
end
local function remove_entries()
self.database:remove_files(unlinked)
self:notify("removed %d missing entries.", #unlinked)
end
if not config.db_safe_mode then
remove_entries()
return
end
vim.ui.select({ "y", "n" }, {
prompt = self:message("remove %d entries from database?", #unlinked),
---@param item "y"|"n"
---@return string
format_item = function(item)
return item == "y" and "Yes. Remove them." or "No. Do nothing."
end, end,
}, function(item) })
if item == "y" then
remove_entries()
else
self:notify "validation aborted"
end
end)
end
---@param bufnr integer local setup_done = false
---@param epoch? integer
function Frecency:register(bufnr, epoch)
if config.ignore_register and config.ignore_register(bufnr) then
return
end
local path = vim.api.nvim_buf_get_name(bufnr)
if self.buf_registered[bufnr] or not self.fs:is_valid_path(path) then
return
end
self.database:update(path, epoch)
self.buf_registered[bufnr] = true
end
---@param path string ---When this func is called, Frecency instance is NOT created but only
---configuration is done.
---@param ext_config? FrecencyOpts
---@return nil ---@return nil
function Frecency:delete(path) local function setup(ext_config)
if self.database:remove_entry(path) then if setup_done then
self:notify("successfully deleted: %s", path) return
else
self:warn("failed to delete: %s", path)
end
end end
---@alias FrecencyQueryOrder "count"|"path"|"score"|"timestamps" require("frecency.config").setup(ext_config)
---@alias FrecencyQueryDirection "asc"|"desc"
---@class FrecencyQueryOpts vim.api.nvim_set_hl(0, "TelescopeBufferLoaded", { link = "String", default = true })
---@field direction? "asc"|"desc" default: "desc" vim.api.nvim_set_hl(0, "TelescopePathSeparator", { link = "Directory", default = true })
---@field limit? integer default: 100 vim.api.nvim_set_hl(0, "TelescopeFrecencyScores", { link = "Number", default = true })
---@field order? FrecencyQueryOrder default: "score" vim.api.nvim_set_hl(0, "TelescopeQueryFilter", { link = "WildMenu", default = true })
---@field record? boolean default: false
---@field workspace? string default: nil
---@class FrecencyQueryEntry ---@param cmd_info { bang: boolean }
---@field count integer vim.api.nvim_create_user_command("FrecencyValidate", function(cmd_info)
---@field path string frecency.validate_database(cmd_info.bang)
---@field score number end, { bang = true, desc = "Clean up DB for telescope-frecency" })
---@field timestamps integer[]
vim.api.nvim_create_user_command("FrecencyDelete", function(info)
local path_string = info.args == "" and "%:p" or info.args
local path = vim.fn.expand(path_string) --[[@as string]]
frecency.delete(path)
end, { nargs = "?", complete = "file", desc = "Delete entry from telescope-frecency" })
local group = vim.api.nvim_create_augroup("TelescopeFrecency", {})
vim.api.nvim_create_autocmd({ "BufWinEnter", "BufWritePost" }, {
desc = "Update database for telescope-frecency",
group = group,
---@param args { buf: integer }
callback = function(args)
local is_floatwin = vim.api.nvim_win_get_config(0).relative ~= ""
if not is_floatwin then
frecency.register(args.buf)
end
end,
})
setup_done = true
end
---@param opts? FrecencyQueryOpts
---@param epoch? integer
---@return FrecencyQueryEntry[]|string[]
function Frecency:query(opts, epoch)
opts = vim.tbl_extend("force", {
direction = "desc",
limit = 100,
order = "score",
record = false,
}, opts or {})
---@param entry FrecencyDatabaseEntry
local entries = vim.tbl_map(function(entry)
return { return {
count = entry.count, start = frecency.start,
path = entry.path, complete = frecency.complete,
score = entry.ages and self.recency:calculate(entry.count, entry.ages) or 0, query = frecency.query,
timestamps = entry.timestamps, setup = setup,
} }
end, self.database:get_entries(opts.workspace, epoch))
table.sort(entries, self:query_sorter(opts.order, opts.direction))
local results = opts.record and entries or vim.tbl_map(function(entry)
return entry.path
end, entries)
if #results > opts.limit then
return vim.list_slice(results, 1, opts.limit)
end
return results
end
---@private
---@param order FrecencyQueryOrder
---@param direction FrecencyQueryDirection
---@return fun(a: FrecencyQueryEntry, b: FrecencyQueryEntry): boolean
function Frecency:query_sorter(order, direction)
local is_asc = direction == "asc"
if order == "count" then
if is_asc then
return function(a, b)
return a.count < b.count or (a.count == b.count and a.path < b.path)
end
end
return function(a, b)
return a.count > b.count or (a.count == b.count and a.path < b.path)
end
elseif order == "path" then
if is_asc then
return function(a, b)
return a.path < b.path
end
end
return function(a, b)
return a.path > b.path
end
elseif order == "score" then
if is_asc then
return function(a, b)
return a.score < b.score or (a.score == b.score and a.path < b.path)
end
end
return function(a, b)
return a.score > b.score or (a.score == b.score and a.path < b.path)
end
elseif is_asc then
return function(a, b)
local a_timestamp = a.timestamps[1] or 0
local b_timestamp = b.timestamps[1] or 0
return a_timestamp < b_timestamp or (a_timestamp == b_timestamp and a.path < b.path)
end
end
return function(a, b)
local a_timestamp = a.timestamps[1] or 0
local b_timestamp = b.timestamps[1] or 0
return a_timestamp > b_timestamp or (a_timestamp == b_timestamp and a.path < b.path)
end
end
---@private
---@param fmt string
---@param ...? any
---@return string
function Frecency:message(fmt, ...)
return ("[Telescope-Frecency] " .. fmt):format(unpack { ... })
end
---@private
---@param fmt string
---@param ...? any
---@return nil
function Frecency:notify(fmt, ...)
vim.notify(self:message(fmt, ...))
end
---@private
---@param fmt string
---@param ...? any
---@return nil
function Frecency:warn(fmt, ...)
vim.notify(self:message(fmt, ...), vim.log.levels.WARN)
end
---@private
---@param fmt string
---@param ...? any
---@return nil
function Frecency:error(fmt, ...)
vim.notify(self:message(fmt, ...), vim.log.levels.ERROR)
end
return Frecency

261
lua/frecency/klass.lua Normal file
View File

@ -0,0 +1,261 @@
local Database = require "frecency.database"
local EntryMaker = require "frecency.entry_maker"
local FS = require "frecency.fs"
local Picker = require "frecency.picker"
local Recency = require "frecency.recency"
local config = require "frecency.config"
local log = require "frecency.log"
---@class Frecency
---@field private buf_registered table<integer, boolean> flag to indicate the buffer is registered to the database.
---@field private database FrecencyDatabase
---@field private entry_maker FrecencyEntryMaker
---@field private fs FrecencyFS
---@field private picker FrecencyPicker
---@field private recency FrecencyRecency
local Frecency = {}
---@return Frecency
Frecency.new = function()
local self = setmetatable({ buf_registered = {} }, { __index = Frecency }) --[[@as Frecency]]
self.fs = FS.new()
self.database = Database.new(self.fs)
self.entry_maker = EntryMaker.new(self.fs)
self.recency = Recency.new()
return self
end
---This is called when `:Telescope frecency` is called at the first time.
---@return nil
function Frecency:setup()
-- HACK: Wihout this wrapping, it spoils background color detection.
-- See https://github.com/nvim-telescope/telescope-frecency.nvim/issues/210
vim.defer_fn(function()
self:assert_db_entries()
if config.auto_validate then
self:validate_database()
end
end, 0)
end
---This can be calledBy `require("telescope").extensions.frecency.frecency`.
---@param opts? FrecencyPickerOptions
---@return nil
function Frecency:start(opts)
local start = os.clock()
log.debug "Frecency:start"
opts = opts or {}
if opts.cwd then
opts.cwd = vim.fn.expand(opts.cwd)
end
local ignore_filenames
if opts.hide_current_buffer or config.hide_current_buffer then
ignore_filenames = { vim.api.nvim_buf_get_name(0) }
end
self.picker = Picker.new(self.database, self.entry_maker, self.fs, self.recency, {
editing_bufnr = vim.api.nvim_get_current_buf(),
ignore_filenames = ignore_filenames,
initial_workspace_tag = opts.workspace,
})
self.picker:start(vim.tbl_extend("force", config.get(), opts))
log.debug(("Frecency:start picker:start takes %f seconds"):format(os.clock() - start))
end
---This can be calledBy `require("telescope").extensions.frecency.complete`.
---@param findstart 1|0
---@param base string
---@return integer|''|string[]
function Frecency:complete(findstart, base)
return self.picker:complete(findstart, base)
end
---@private
---@return nil
function Frecency:assert_db_entries()
if not self.database:has_entry() then
self.database:insert_files(vim.v.oldfiles)
self:notify("Imported %d entries from oldfiles.", #vim.v.oldfiles)
end
end
---@private
---@param force? boolean
---@return nil
function Frecency:validate_database(force)
local unlinked = self.database:unlinked_entries()
if #unlinked == 0 or (not force and #unlinked < config.db_validate_threshold) then
return
end
local function remove_entries()
self.database:remove_files(unlinked)
self:notify("removed %d missing entries.", #unlinked)
end
if not config.db_safe_mode then
remove_entries()
return
end
vim.ui.select({ "y", "n" }, {
prompt = self:message("remove %d entries from database?", #unlinked),
---@param item "y"|"n"
---@return string
format_item = function(item)
return item == "y" and "Yes. Remove them." or "No. Do nothing."
end,
}, function(item)
if item == "y" then
remove_entries()
else
self:notify "validation aborted"
end
end)
end
---@param bufnr integer
---@param epoch? integer
function Frecency:register(bufnr, epoch)
if config.ignore_register and config.ignore_register(bufnr) then
return
end
local path = vim.api.nvim_buf_get_name(bufnr)
if self.buf_registered[bufnr] or not self.fs:is_valid_path(path) then
return
end
self.database:update(path, epoch)
self.buf_registered[bufnr] = true
end
---@param path string
---@return nil
function Frecency:delete(path)
if self.database:remove_entry(path) then
self:notify("successfully deleted: %s", path)
else
self:warn("failed to delete: %s", path)
end
end
---@alias FrecencyQueryOrder "count"|"path"|"score"|"timestamps"
---@alias FrecencyQueryDirection "asc"|"desc"
---@class FrecencyQueryOpts
---@field direction? "asc"|"desc" default: "desc"
---@field limit? integer default: 100
---@field order? FrecencyQueryOrder default: "score"
---@field record? boolean default: false
---@field workspace? string default: nil
---@class FrecencyQueryEntry
---@field count integer
---@field path string
---@field score number
---@field timestamps integer[]
---@param opts? FrecencyQueryOpts
---@param epoch? integer
---@return FrecencyQueryEntry[]|string[]
function Frecency:query(opts, epoch)
opts = vim.tbl_extend("force", {
direction = "desc",
limit = 100,
order = "score",
record = false,
}, opts or {})
---@param entry FrecencyDatabaseEntry
local entries = vim.tbl_map(function(entry)
return {
count = entry.count,
path = entry.path,
score = entry.ages and self.recency:calculate(entry.count, entry.ages) or 0,
timestamps = entry.timestamps,
}
end, self.database:get_entries(opts.workspace, epoch))
table.sort(entries, self:query_sorter(opts.order, opts.direction))
local results = opts.record and entries or vim.tbl_map(function(entry)
return entry.path
end, entries)
if #results > opts.limit then
return vim.list_slice(results, 1, opts.limit)
end
return results
end
---@private
---@param order FrecencyQueryOrder
---@param direction FrecencyQueryDirection
---@return fun(a: FrecencyQueryEntry, b: FrecencyQueryEntry): boolean
function Frecency:query_sorter(order, direction)
local is_asc = direction == "asc"
if order == "count" then
if is_asc then
return function(a, b)
return a.count < b.count or (a.count == b.count and a.path < b.path)
end
end
return function(a, b)
return a.count > b.count or (a.count == b.count and a.path < b.path)
end
elseif order == "path" then
if is_asc then
return function(a, b)
return a.path < b.path
end
end
return function(a, b)
return a.path > b.path
end
elseif order == "score" then
if is_asc then
return function(a, b)
return a.score < b.score or (a.score == b.score and a.path < b.path)
end
end
return function(a, b)
return a.score > b.score or (a.score == b.score and a.path < b.path)
end
elseif is_asc then
return function(a, b)
local a_timestamp = a.timestamps[1] or 0
local b_timestamp = b.timestamps[1] or 0
return a_timestamp < b_timestamp or (a_timestamp == b_timestamp and a.path < b.path)
end
end
return function(a, b)
local a_timestamp = a.timestamps[1] or 0
local b_timestamp = b.timestamps[1] or 0
return a_timestamp > b_timestamp or (a_timestamp == b_timestamp and a.path < b.path)
end
end
---@private
---@param fmt string
---@param ...? any
---@return string
function Frecency:message(fmt, ...)
return ("[Telescope-Frecency] " .. fmt):format(unpack { ... })
end
---@private
---@param fmt string
---@param ...? any
---@return nil
function Frecency:notify(fmt, ...)
vim.notify(self:message(fmt, ...))
end
---@private
---@param fmt string
---@param ...? any
---@return nil
function Frecency:warn(fmt, ...)
vim.notify(self:message(fmt, ...), vim.log.levels.WARN)
end
---@private
---@param fmt string
---@param ...? any
---@return nil
function Frecency:error(fmt, ...)
vim.notify(self:message(fmt, ...), vim.log.levels.ERROR)
end
return Frecency

View File

@ -3,7 +3,7 @@
vim.opt.runtimepath:append(vim.env.TELESCOPE_PATH) vim.opt.runtimepath:append(vim.env.TELESCOPE_PATH)
---@diagnostic disable: invisible, undefined-field ---@diagnostic disable: invisible, undefined-field
local Frecency = require "frecency" local Frecency = require "frecency.klass"
local Picker = require "frecency.picker" local Picker = require "frecency.picker"
local util = require "frecency.tests.util" local util = require "frecency.tests.util"
local log = require "plenary.log" local log = require "plenary.log"

View File

@ -1,32 +1,4 @@
---This object is intended to be used as a singleton, and is lazily loaded. local frecency = require "frecency"
---When methods are called at the first time, it calls the constructor and
---setup() to be initialized.
---@class FrecencyInstance
---@field complete fun(findstart: 1|0, base: string): integer|''|string[]
---@field delete fun(path: string): nil
---@field query fun(opts?: FrecencyQueryOpts): FrecencyQueryEntry[]|string[]
---@field register fun(bufnr: integer, datetime: string?): nil
---@field start fun(opts: FrecencyPickerOptions?): nil
---@field validate_database fun(force: boolean?): nil
local frecency = setmetatable({}, {
---@param self FrecencyInstance
---@param key "complete"|"delete"|"register"|"start"|"validate_database"
---@return function
__index = function(self, key)
---@return Frecency
local function instance()
return rawget(self, "instance")
end
return function(...)
if not instance() then
rawset(self, "instance", require("frecency").new())
instance():setup()
end
return instance()[key](instance(), ...)
end
end,
})
return require("telescope").register_extension { return require("telescope").register_extension {
exports = { exports = {
@ -34,42 +6,7 @@ return require("telescope").register_extension {
complete = frecency.complete, complete = frecency.complete,
query = frecency.query, query = frecency.query,
}, },
setup = frecency.setup,
---When this func is called, Frecency instance is NOT created but only
---configuration is done.
setup = function(ext_config)
require("frecency.config").setup(ext_config)
vim.api.nvim_set_hl(0, "TelescopeBufferLoaded", { link = "String", default = true })
vim.api.nvim_set_hl(0, "TelescopePathSeparator", { link = "Directory", default = true })
vim.api.nvim_set_hl(0, "TelescopeFrecencyScores", { link = "Number", default = true })
vim.api.nvim_set_hl(0, "TelescopeQueryFilter", { link = "WildMenu", default = true })
---@param cmd_info { bang: boolean }
vim.api.nvim_create_user_command("FrecencyValidate", function(cmd_info)
frecency.validate_database(cmd_info.bang)
end, { bang = true, desc = "Clean up DB for telescope-frecency" })
vim.api.nvim_create_user_command("FrecencyDelete", function(info)
local path_string = info.args == "" and "%:p" or info.args
local path = vim.fn.expand(path_string) --[[@as string]]
frecency.delete(path)
end, { nargs = "?", complete = "file", desc = "Delete entry from telescope-frecency" })
local group = vim.api.nvim_create_augroup("TelescopeFrecency", {})
vim.api.nvim_create_autocmd({ "BufWinEnter", "BufWritePost" }, {
desc = "Update database for telescope-frecency",
group = group,
---@param args { buf: integer }
callback = function(args)
local is_floatwin = vim.api.nvim_win_get_config(0).relative ~= ""
if not is_floatwin then
frecency.register(args.buf)
end
end,
})
end,
health = function() health = function()
if vim.F.npcall(require, "nvim-web-devicons") then if vim.F.npcall(require, "nvim-web-devicons") then
vim.health.ok "nvim-web-devicons installed." vim.health.ok "nvim-web-devicons installed."