From bd52772bf2e8d3e83f1575a018cf4a0e8c3c09a3 Mon Sep 17 00:00:00 2001 From: JINNOUCHI Yasushi Date: Thu, 14 Mar 2024 20:28:06 +0900 Subject: [PATCH] fix!: change timing for initialization (#179) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix!: change timing for initialization fix #109 fix #59 This fixes problems below. * Auto-validation feature is called at Neovim starting. - → Now it starts at `:Telescope frecency` called at the first time. * `frecency.setup()` is called every when `:Telescope frecency` is called. - → Now it is called only once. * `telescope.setup()` calls `frecency.new()` and it reads the database. This causes time in executing `init.lua`. - → Now it reads the database lazily. It reads at the first time when needed. * test: change logic to initialize config * test: make Neovim version newer in CI --- .github/workflows/ci.yml | 4 +- lua/frecency/config.lua | 100 +++++++++++ lua/frecency/frecency.lua | 237 ------------------------- lua/frecency/init.lua | 197 +++++++++++++++++--- lua/frecency/tests/frecency_spec.lua | 79 +++++---- lua/telescope/_extensions/frecency.lua | 65 ++++++- 6 files changed, 377 insertions(+), 305 deletions(-) create mode 100644 lua/frecency/config.lua delete mode 100644 lua/frecency/frecency.lua diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a4f4098..7e14f37 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,9 +12,9 @@ jobs: - macos-latest - windows-latest version: + - v0.9.5 + - v0.9.4 - v0.9.2 - - v0.9.1 - - v0.9.0 - nightly runs-on: ${{ matrix.os }} timeout-minutes: 15 diff --git a/lua/frecency/config.lua b/lua/frecency/config.lua new file mode 100644 index 0000000..8910303 --- /dev/null +++ b/lua/frecency/config.lua @@ -0,0 +1,100 @@ +local os_util = require "frecency.os_util" + +---@class FrecencyConfig: FrecencyRawConfig +---@field private values FrecencyRawConfig +local Config = {} + +---@class FrecencyRawConfig +---@field auto_validate boolean default: true +---@field db_root string default: vim.fn.stdpath "data" +---@field db_safe_mode boolean default: true +---@field db_validate_threshold integer default: 10 +---@field default_workspace string default: nil +---@field disable_devicons boolean default: false +---@field filter_delimiter string default: ":" +---@field hide_current_buffer boolean default: false +---@field ignore_patterns string[] default: { "*.git/*", "*/tmp/*", "term://*" } +---@field max_timestamps integer default: 10 +---@field show_filter_column boolean|string[]|nil default: true +---@field show_scores boolean default: false +---@field show_unindexed boolean default: true +---@field workspace_scan_cmd "LUA"|string[]|nil default: nil +---@field workspaces table default: {} + +---@return FrecencyConfig +Config.new = function() + local default_values = { + auto_validate = true, + db_root = vim.fn.stdpath "data", + db_safe_mode = true, + db_validate_threshold = 10, + default_workspace = nil, + disable_devicons = false, + filter_delimiter = ":", + hide_current_buffer = false, + ignore_patterns = os_util.is_windows and { [[*.git\*]], [[*\tmp\*]], "term://*" } + or { "*.git/*", "*/tmp/*", "term://*" }, + max_timestamps = 10, + show_filter_column = true, + show_scores = false, + show_unindexed = true, + workspace_scan_cmd = nil, + workspaces = {}, + } + ---@type table + local keys = {} + for k, _ in pairs(default_values) do + keys[k] = true + end + return setmetatable({ + values = default_values, + }, { + __index = function(self, key) + if key == "values" then + return rawget(self, key) + elseif keys[key] then + return rawget(rawget(self, "values"), key) + end + return rawget(Config, key) + end, + }) +end + +local config = Config.new() + +---@return FrecencyRawConfig +Config.get = function() + return config.values +end + +---@param ext_config any +---@return nil +Config.setup = function(ext_config) + local opts = vim.tbl_extend("force", config.values, ext_config or {}) + vim.validate { + auto_validate = { opts.auto_validate, "b" }, + db_root = { opts.db_root, "s" }, + db_safe_mode = { opts.db_safe_mode, "b" }, + db_validate_threshold = { opts.db_validate_threshold, "n" }, + default_workspace = { opts.default_workspace, "s", true }, + disable_devicons = { opts.disable_devicons, "b" }, + filter_delimiter = { opts.filter_delimiter, "s" }, + hide_current_buffer = { opts.hide_current_buffer, "b" }, + ignore_patterns = { opts.ignore_patterns, "t" }, + max_timestamps = { + opts.max_timestamps, + function(v) + return type(v) == "number" and v > 0 + end, + "positive number", + }, + show_filter_column = { opts.show_filter_column, { "b", "t" }, true }, + show_scores = { opts.show_scores, "b" }, + show_unindexed = { opts.show_unindexed, "b" }, + workspace_scan_cmd = { opts.workspace_scan_cmd, { "s", "t" }, true }, + workspaces = { opts.workspaces, "t" }, + } + config.values = opts +end + +return config diff --git a/lua/frecency/frecency.lua b/lua/frecency/frecency.lua deleted file mode 100644 index f337930..0000000 --- a/lua/frecency/frecency.lua +++ /dev/null @@ -1,237 +0,0 @@ -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 WebDevicons = require "frecency.web_devicons" -local os_util = require "frecency.os_util" -local log = require "plenary.log" - ----@class Frecency ----@field config FrecencyConfig ----@field private buf_registered table 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 = {} - ----@class FrecencyConfig ----@field auto_validate boolean? default: true ----@field db_root string? default: vim.fn.stdpath "data" ----@field db_safe_mode boolean? default: true ----@field db_validate_threshold? integer default: 10 ----@field default_workspace string? default: nil ----@field disable_devicons boolean? default: false ----@field filter_delimiter string? default: ":" ----@field hide_current_buffer boolean default: false ----@field ignore_patterns string[]? default: { "*.git/*", "*/tmp/*", "term://*" } ----@field max_timestamps integer? default: 10 ----@field show_filter_column boolean|string[]|nil default: true ----@field show_scores boolean? default: false ----@field show_unindexed boolean? default: true ----@field workspace_scan_cmd "LUA"|string[]|nil default: nil ----@field workspaces table? default: {} - ----@param opts FrecencyConfig? ----@return Frecency -Frecency.new = function(opts) - ---@type FrecencyConfig - local config = vim.tbl_extend("force", { - auto_validate = true, - db_root = vim.fn.stdpath "data", - db_safe_mode = true, - db_validate_threshold = 10, - default_workspace = nil, - disable_devicons = false, - filter_delimiter = ":", - hide_current_buffer = false, - ignore_patterns = os_util.is_windows and { [[*.git\*]], [[*\tmp\*]], "term://*" } - or { "*.git/*", "*/tmp/*", "term://*" }, - max_timestamps = 10, - show_filter_column = true, - show_scores = false, - show_unindexed = true, - workspace_scan_cmd = nil, - workspaces = {}, - }, opts or {}) - local self = setmetatable({ buf_registered = {}, config = config }, { __index = Frecency })--[[@as Frecency]] - self.fs = FS.new { ignore_patterns = config.ignore_patterns } - - self.database = Database.new(self.fs, { root = config.db_root }) - local web_devicons = WebDevicons.new(not config.disable_devicons) - self.entry_maker = EntryMaker.new(self.fs, web_devicons, { - show_filter_column = config.show_filter_column, - show_scores = config.show_scores, - }) - local max_count = config.max_timestamps > 0 and config.max_timestamps or 10 - self.recency = Recency.new { max_count = max_count } - return self -end - ----@return nil -function Frecency:setup() - 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 }) - - -- TODO: Should we schedule this after loading shada? - self:assert_db_entries() - - ---@param cmd_info { bang: boolean } - vim.api.nvim_create_user_command("FrecencyValidate", function(cmd_info) - self:validate_database(cmd_info.bang) - end, { bang = true, desc = "Clean up DB for telescope-frecency" }) - - if self.config.auto_validate then - self:validate_database() - end - - 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]] - self: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) - self:register(args.buf) - end, - }) -end - ----@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 self.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, { - default_workspace_tag = self.config.default_workspace, - editing_bufnr = vim.api.nvim_get_current_buf(), - filter_delimiter = self.config.filter_delimiter, - ignore_filenames = ignore_filenames, - initial_workspace_tag = opts.workspace, - show_unindexed = self.config.show_unindexed, - workspace_scan_cmd = self.config.workspace_scan_cmd, - workspaces = self.config.workspaces, - }) - self.picker:start(vim.tbl_extend("force", self.config, opts)) - log.debug(("Frecency:start picker:start takes %f seconds"):format(os.clock() - start)) -end - ----@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 < self.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 self.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 datetime string? ISO8601 format string -function Frecency:register(bufnr, datetime) - 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, self.recency.config.max_count, datetime) - 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 - ----@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 diff --git a/lua/frecency/init.lua b/lua/frecency/init.lua index 98c587a..79dafd4 100644 --- a/lua/frecency/init.lua +++ b/lua/frecency/init.lua @@ -1,29 +1,172 @@ ----@type Frecency? -local frecency +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 WebDevicons = require "frecency.web_devicons" +local config = require "frecency.config" +local log = require "plenary.log" -return { - ---@param opts FrecencyConfig? - setup = function(opts) - frecency = require("frecency.frecency").new(opts) - frecency:setup() - end, - ---@param opts FrecencyPickerOptions - start = function(opts) - if frecency then - frecency:start(opts) +---@class Frecency +---@field private buf_registered table 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 { ignore_patterns = config.ignore_patterns } + self.database = Database.new(self.fs, { root = config.db_root }) + local web_devicons = WebDevicons.new(not config.disable_devicons) + self.entry_maker = EntryMaker.new(self.fs, web_devicons, { + show_filter_column = config.show_filter_column, + show_scores = config.show_scores, + }) + self.recency = Recency.new { max_count = config.max_timestamps } + return self +end + +---This is called when `:Telescope frecency` is called at the first time. +---@return nil +function Frecency:setup() + self:assert_db_entries() + if config.auto_validate then + self:validate_database() + 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, { + default_workspace_tag = config.default_workspace, + editing_bufnr = vim.api.nvim_get_current_buf(), + filter_delimiter = config.filter_delimiter, + ignore_filenames = ignore_filenames, + initial_workspace_tag = opts.workspace, + show_unindexed = config.show_unindexed, + workspace_scan_cmd = config.workspace_scan_cmd, + workspaces = config.workspaces, + }) + 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, - ---@param findstart 1|0 - ---@param base string - ---@return integer|''|string[] - complete = function(findstart, base) - if frecency then - return frecency:complete(findstart, base) - end - return "" - end, - ---@return Frecency - frecency = function() - return assert(frecency) - end, -} + end) +end + +---@param bufnr integer +---@param datetime string? ISO8601 format string +function Frecency:register(bufnr, datetime) + 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, self.recency.config.max_count, datetime) + 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 + +---@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 diff --git a/lua/frecency/tests/frecency_spec.lua b/lua/frecency/tests/frecency_spec.lua index a5463c6..a571540 100644 --- a/lua/frecency/tests/frecency_spec.lua +++ b/lua/frecency/tests/frecency_spec.lua @@ -3,19 +3,30 @@ vim.opt.runtimepath:append(vim.env.TELESCOPE_PATH) ---@diagnostic disable: invisible, undefined-field -local Frecency = require "frecency.frecency" +local Frecency = require "frecency" local Picker = require "frecency.picker" local util = require "frecency.tests.util" local log = require "plenary.log" local Path = require "plenary.path" +local config = require "frecency.config" ---@param files string[] ----@param callback fun(frecency: Frecency, finder: FrecencyFinder, dir: PlenaryPath): nil +---@param cb_or_config table|fun(frecency: Frecency, finder: FrecencyFinder, dir: PlenaryPath): nil +---@param callback? fun(frecency: Frecency, finder: FrecencyFinder, dir: PlenaryPath): nil ---@return nil -local function with_files(files, callback) +local function with_files(files, cb_or_config, callback) local dir, close = util.make_tree(files) - log.debug { db_root = dir.filename } - local frecency = Frecency.new { db_root = dir.filename } + local cfg + if type(cb_or_config) == "table" then + cfg = vim.tbl_extend("force", { db_root = dir.filename }, cb_or_config) + else + cfg = { db_root = dir.filename } + callback = cb_or_config + end + assert(callback) + log.debug(cfg) + config.setup(cfg) + local frecency = Frecency.new() frecency.picker = Picker.new( frecency.database, frecency.entry_maker, @@ -232,38 +243,42 @@ describe("frecency", function() describe("when with not force", function() describe("when files are unlinked but it is less than threshold", function() - with_files({ "hoge1.txt", "hoge2.txt", "hoge3.txt", "hoge4.txt", "hoge5.txt" }, function(frecency, finder, dir) - local register = make_register(frecency, dir) - register("hoge1.txt", "2023-07-29T00:00:00+09:00") - register("hoge2.txt", "2023-07-29T00:01:00+09:00") - register("hoge3.txt", "2023-07-29T00:02:00+09:00") - register("hoge4.txt", "2023-07-29T00:03:00+09:00") - register("hoge5.txt", "2023-07-29T00:04:00+09:00") - frecency.config.db_validate_threshold = 3 - dir:joinpath("hoge1.txt"):rm() - dir:joinpath("hoge2.txt"):rm() - frecency:validate_database() + with_files( + { "hoge1.txt", "hoge2.txt", "hoge3.txt", "hoge4.txt", "hoge5.txt" }, + { db_validate_threshold = 3 }, + function(frecency, finder, dir) + local register = make_register(frecency, dir) + register("hoge1.txt", "2023-07-29T00:00:00+09:00") + register("hoge2.txt", "2023-07-29T00:01:00+09:00") + register("hoge3.txt", "2023-07-29T00:02:00+09:00") + register("hoge4.txt", "2023-07-29T00:03:00+09:00") + register("hoge5.txt", "2023-07-29T00:04:00+09:00") + dir:joinpath("hoge1.txt"):rm() + dir:joinpath("hoge2.txt"):rm() + frecency:validate_database() - it("removes no entries", function() - local results = finder:get_results(nil, "2023-07-29T02:00:00+09:00") - table.sort(results, function(a, b) - return a.path < b.path + it("removes no entries", function() + local results = finder:get_results(nil, "2023-07-29T02:00:00+09:00") + table.sort(results, function(a, b) + return a.path < b.path + end) + assert.are.same({ + { count = 1, path = filepath(dir, "hoge1.txt"), score = 10 }, + { count = 1, path = filepath(dir, "hoge2.txt"), score = 10 }, + { count = 1, path = filepath(dir, "hoge3.txt"), score = 10 }, + { count = 1, path = filepath(dir, "hoge4.txt"), score = 10 }, + { count = 1, path = filepath(dir, "hoge5.txt"), score = 10 }, + }, results) end) - assert.are.same({ - { count = 1, path = filepath(dir, "hoge1.txt"), score = 10 }, - { count = 1, path = filepath(dir, "hoge2.txt"), score = 10 }, - { count = 1, path = filepath(dir, "hoge3.txt"), score = 10 }, - { count = 1, path = filepath(dir, "hoge4.txt"), score = 10 }, - { count = 1, path = filepath(dir, "hoge5.txt"), score = 10 }, - }, results) - end) - end) + end + ) end) describe("when files are unlinked and it is more than threshold", function() describe('when the user response "yes"', function() with_files( { "hoge1.txt", "hoge2.txt", "hoge3.txt", "hoge4.txt", "hoge5.txt" }, + { db_validate_threshold = 3 }, function(frecency, finder, dir) local register = make_register(frecency, dir) register("hoge1.txt", "2023-07-29T00:00:00+09:00") @@ -271,7 +286,6 @@ describe("frecency", function() register("hoge3.txt", "2023-07-29T00:02:00+09:00") register("hoge4.txt", "2023-07-29T00:03:00+09:00") register("hoge5.txt", "2023-07-29T00:04:00+09:00") - frecency.config.db_validate_threshold = 3 dir:joinpath("hoge1.txt"):rm() dir:joinpath("hoge2.txt"):rm() dir:joinpath("hoge3.txt"):rm() @@ -301,6 +315,7 @@ describe("frecency", function() describe('when the user response "no"', function() with_files( { "hoge1.txt", "hoge2.txt", "hoge3.txt", "hoge4.txt", "hoge5.txt" }, + { db_validate_threshold = 3 }, function(frecency, finder, dir) local register = make_register(frecency, dir) register("hoge1.txt", "2023-07-29T00:00:00+09:00") @@ -308,7 +323,6 @@ describe("frecency", function() register("hoge3.txt", "2023-07-29T00:02:00+09:00") register("hoge4.txt", "2023-07-29T00:03:00+09:00") register("hoge5.txt", "2023-07-29T00:04:00+09:00") - frecency.config.db_validate_threshold = 3 dir:joinpath("hoge1.txt"):rm() dir:joinpath("hoge2.txt"):rm() dir:joinpath("hoge3.txt"):rm() @@ -366,14 +380,13 @@ describe("frecency", function() end) describe("when db_safe_mode is false", function() - with_files({ "hoge1.txt", "hoge2.txt" }, function(frecency, finder, dir) + with_files({ "hoge1.txt", "hoge2.txt" }, { db_safe_mode = false }, function(frecency, finder, dir) local register = make_register(frecency, dir) register("hoge1.txt", "2023-07-29T00:00:00+09:00") register("hoge2.txt", "2023-07-29T00:01:00+09:00") dir:joinpath("hoge1.txt"):rm() with_fake_vim_ui_select("y", function(called) - frecency.config.db_safe_mode = false frecency:validate_database(true) it("did not call vim.ui.select()", function() diff --git a/lua/telescope/_extensions/frecency.lua b/lua/telescope/_extensions/frecency.lua index 83a76ce..2b99259 100644 --- a/lua/telescope/_extensions/frecency.lua +++ b/lua/telescope/_extensions/frecency.lua @@ -1,7 +1,64 @@ -local frecency = require "frecency" +---This object is intended to be used as a singleton, and is lazily loaded. +---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 register fun(bufnr: integer, datetime: string?): nil +---@field start fun(opts: FrecencyPickerOptions?): nil +---@field validate_database fun(force: boolean?): nil +local frecency = setmetatable({}, { + __index = function(self, key) + return function(...) + local instance = rawget(self, "instance") --[[@as Frecency?]] + if not instance then + instance = require("frecency").new() + instance:setup() + rawset(self, "instance", instance) + end + return instance[key](instance, ...) + end + end, +}) return require("telescope").register_extension { - setup = frecency.setup, + exports = { + frecency = frecency.start, + complete = frecency.complete, + }, + + ---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) + frecency.register(args.buf) + end, + }) + end, + health = function() if vim.F.npcall(require, "nvim-web-devicons") then vim.health.ok "nvim-web-devicons installed." @@ -18,8 +75,4 @@ return require("telescope").register_extension { vim.health.info "No suitable find executable found." end end, - exports = { - frecency = frecency.start, - complete = frecency.complete, - }, }