List up entries for workspace files by rg or fd (#156)

* feat: read workspace files by external commands

* fix: avoid errors when it manages invalid buffers

* feat: add workspace_scan_cmd to select a way

* docs: describe `workspace_scan_cmd` option
This commit is contained in:
JINNOUCHI Yasushi 2024-01-01 00:12:17 +09:00 committed by GitHub
parent de41070181
commit da7c724e3c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 194 additions and 57 deletions

View File

@ -79,12 +79,18 @@ directories provided by the language server.
- [telescope.nvim](https://github.com/nvim-telescope/telescope.nvim) (required) - [telescope.nvim](https://github.com/nvim-telescope/telescope.nvim) (required)
- [nvim-web-devicons](https://github.com/kyazdani42/nvim-web-devicons) (optional) - [nvim-web-devicons](https://github.com/kyazdani42/nvim-web-devicons) (optional)
- [ripgrep](https://github.com/BurntSushi/ripgrep) or [fd](https://github.com/sharkdp/fd) (optional)
**NOTE:** The former version of this plugin has used [SQLite3][] database to **NOTE:** The former version of this plugin has used [SQLite3][] database to
store timestamps and file records. But the current build uses Lua native code store timestamps and file records. But the current build uses Lua native code
to store them, so you can now remove [sqlite.lua][] from dependencies. See to store them, so you can now remove [sqlite.lua][] from dependencies. See
[*Remove dependency for sqlite.lua*][remove-sqlite] for the detail. [*Remove dependency for sqlite.lua*][remove-sqlite] for the detail.
**NOTE:** `ripgrep` or `fd` will be used to list up workspace files. They are
extremely faster than the native Lua logic. If you don't have them, it
fallbacks to Lua code automatically. See the detail for `workspace_scan_cmd`
option.
[SQLite3]: https://www.sqlite.org/index.html [SQLite3]: https://www.sqlite.org/index.html
[sqlite.lua]: https://github.com/kkharji/sqlite.lua [sqlite.lua]: https://github.com/kkharji/sqlite.lua
[remove-sqlite]: #user-content-remove-dependency-for-sqlitelua [remove-sqlite]: #user-content-remove-dependency-for-sqlitelua
@ -203,6 +209,23 @@ See [default configuration](https://github.com/nvim-telescope/telescope.nvim#tel
Use [sqlite.lua][] with `true` or native code with `false`. See [*Remove Use [sqlite.lua][] with `true` or native code with `false`. See [*Remove
dependency for sqlite.lua*][remove-sqlite] for the detail. dependency for sqlite.lua*][remove-sqlite] for the detail.
- `workspace_scan_cmd` (default: `nil`)
This option can be set values as `"LUA"|string[]|nil`. With the default
value: `nil`, it uses these way below to make entries for workspace files.
It tries in order until it works successfully.
1. `rg -0.g '!.git' --files`
2. `fdfind -0Htf`
3. `fd -0Htf`
4. Native Lua code (old way)
If you like another commands, set them to this option, like
`workspace_scan_cmd = { "find", ".", "-type", "f", "-print0" }`. This command
must use NUL characters for delimiters.
If you prefer Native Lua code, set `workspace_scan_cmd = "LUA"`.
- `workspaces` (default: `{}`) - `workspaces` (default: `{}`)
This table contains mappings of `workspace_tag` -> `workspace_directory`. The This table contains mappings of `workspace_tag` -> `workspace_directory`. The

View File

@ -5,20 +5,27 @@ local log = require "plenary.log"
---@field config FrecencyFinderConfig ---@field config FrecencyFinderConfig
---@field closed boolean ---@field closed boolean
---@field entries FrecencyEntry[] ---@field entries FrecencyEntry[]
---@field scanned_entries FrecencyEntry[]
---@field entry_maker FrecencyEntryMakerInstance ---@field entry_maker FrecencyEntryMakerInstance
---@field fs FrecencyFS ---@field fs FrecencyFS
---@field need_scandir boolean
---@field path string? ---@field path string?
---@field private database FrecencyDatabase ---@field private database FrecencyDatabase
---@field private recency FrecencyRecency
---@field private rx PlenaryAsyncControlChannelRx ---@field private rx PlenaryAsyncControlChannelRx
---@field private state FrecencyState
---@field private tx PlenaryAsyncControlChannelTx ---@field private tx PlenaryAsyncControlChannelTx
---@field private scan_rx PlenaryAsyncControlChannelRx
---@field private scan_tx PlenaryAsyncControlChannelTx
---@field private need_scan_db boolean
---@field private need_scan_dir boolean
---@field private seen table<string, boolean>
---@field private process VimSystemObj?
---@field private recency FrecencyRecency
---@field private state FrecencyState
local Finder = {} local Finder = {}
---@class FrecencyFinderConfig ---@class FrecencyFinderConfig
---@field chunk_size integer default: 1000 ---@field chunk_size integer? default: 1000
---@field sleep_interval integer default: 50 ---@field sleep_interval integer? default: 50
---@field workspace_scan_cmd "LUA"|string[]|nil default: nil
---@param database FrecencyDatabase ---@param database FrecencyDatabase
---@param entry_maker FrecencyEntryMakerInstance ---@param entry_maker FrecencyEntryMakerInstance
@ -31,19 +38,26 @@ local Finder = {}
---@return FrecencyFinder ---@return FrecencyFinder
Finder.new = function(database, entry_maker, fs, need_scandir, path, recency, state, config) Finder.new = function(database, entry_maker, fs, need_scandir, path, recency, state, config)
local tx, rx = async.control.channel.mpsc() local tx, rx = async.control.channel.mpsc()
local scan_tx, scan_rx = async.control.channel.mpsc()
return setmetatable({ return setmetatable({
config = vim.tbl_extend("force", { chunk_size = 1000, sleep_interval = 50 }, config or {}), config = vim.tbl_extend("force", { chunk_size = 1000, sleep_interval = 50 }, config or {}),
closed = false, closed = false,
database = database, database = database,
entries = {},
entry_maker = entry_maker, entry_maker = entry_maker,
fs = fs, fs = fs,
need_scandir = need_scandir,
path = path, path = path,
recency = recency, recency = recency,
rx = rx,
state = state, state = state,
seen = {},
entries = {},
scanned_entries = {},
need_scan_db = true,
need_scan_dir = need_scandir and path,
rx = rx,
tx = tx, tx = tx,
scan_rx = scan_rx,
scan_tx = scan_tx,
}, { }, {
__index = Finder, __index = Finder,
---@param self FrecencyFinder ---@param self FrecencyFinder
@ -56,80 +70,149 @@ end
---@param datetime string? ---@param datetime string?
---@return nil ---@return nil
function Finder:start(datetime) function Finder:start(datetime)
local cmd = self.config.workspace_scan_cmd
local ok
if cmd ~= "LUA" and self.need_scan_dir then
---@type string[][]
local cmds = cmd and { cmd } or { { "rg", "-0.g", "!.git", "--files" }, { "fdfind", "-0Htf" }, { "fd", "-0Htf" } }
for _, c in ipairs(cmds) do
ok = self:scan_dir_cmd(c)
if ok then
log.debug("scan_dir_cmd: " .. vim.inspect(c))
break
end
end
end
async.void(function() async.void(function()
-- NOTE: return to the main loop to show the main window -- NOTE: return to the main loop to show the main window
async.util.sleep(0) async.util.scheduler()
local seen = {} for _, file in ipairs(self:get_results(self.path, datetime)) do
for i, file in ipairs(self:get_results(self.path, datetime)) do
local entry = self.entry_maker(file) local entry = self.entry_maker(file)
seen[entry.filename] = true
entry.index = i
table.insert(self.entries, entry)
self.tx.send(entry) self.tx.send(entry)
end end
if self.need_scandir and self.path then
-- NOTE: return to the main loop to show results from DB
async.util.sleep(self.config.sleep_interval)
self:scan_dir(seen)
end
self:close()
self.tx.send(nil) self.tx.send(nil)
if self.need_scan_dir and not ok then
log.debug "scan_dir_lua"
async.util.scheduler()
self:scan_dir_lua()
end
end)() end)()
end end
---@param seen table<string, boolean> ---@param cmd string[]
---@return boolean
function Finder:scan_dir_cmd(cmd)
local ok
---@diagnostic disable-next-line: assign-type-mismatch
ok, self.process = pcall(vim.system, cmd, {
cwd = self.path,
stdout = function(err, chunk)
if not self.closed and not err and chunk then
for name in chunk:gmatch "[^%z]+" do
local cleaned = name:gsub("^%./", "")
local fullpath = self.fs.joinpath(self.path, cleaned)
local entry = self.entry_maker { id = 0, count = 0, path = fullpath, score = 0 }
self.scan_tx.send(entry)
end
end
end,
}, function()
self.process = nil
self:close()
self.scan_tx.send(nil)
end)
if not ok then
self.process = nil
end
return ok
end
---@async
---@return nil ---@return nil
function Finder:scan_dir(seen) function Finder:scan_dir_lua()
local count = 0 local count = 0
local index = #self.entries
for name in self.fs:scan_dir(self.path) do for name in self.fs:scan_dir(self.path) do
if self.closed then if self.closed then
break break
end end
local fullpath = self.fs.joinpath(self.path, name) local fullpath = self.fs.joinpath(self.path, name)
if not seen[fullpath] then local entry = self.entry_maker { id = 0, count = 0, path = fullpath, score = 0 }
seen[fullpath] = true self.scan_tx.send(entry)
count = count + 1 count = count + 1
local entry = self.entry_maker { id = 0, count = 0, path = fullpath, score = 0 } if count % self.config.chunk_size == 0 then
if entry then async.util.sleep(self.config.sleep_interval)
index = index + 1
entry.index = index
table.insert(self.entries, entry)
self.tx.send(entry)
if count % self.config.chunk_size == 0 then
self:reflow_results()
async.util.sleep(self.config.sleep_interval)
end
end
end end
end end
self.scan_tx.send(nil)
end end
---@async
---@param _ string ---@param _ string
---@param process_result fun(entry: FrecencyEntry): nil ---@param process_result fun(entry: FrecencyEntry): nil
---@param process_complete fun(): nil ---@param process_complete fun(): nil
---@return nil ---@return nil
function Finder:find(_, process_result, process_complete) function Finder:find(_, process_result, process_complete)
local index = 0 if self:process_table(process_result, self.entries) then
for _, entry in ipairs(self.entries) do return
index = index + 1
if process_result(entry) then
return
end
end end
local count = 0 if self.need_scan_db then
while not self.closed do if self:process_channel(process_result, self.entries, self.rx) then
count = count + 1
local entry = self.rx.recv()
if not entry then
break
elseif entry.index > index and process_result(entry) then
return return
end end
self.need_scan_db = false
end
if self:process_table(process_result, self.scanned_entries) then
return
end
if self.need_scan_dir then
if self:process_channel(process_result, self.scanned_entries, self.scan_rx, #self.entries) then
return
end
self.need_scan_dir = false
end end
process_complete() process_complete()
end end
---@param process_result fun(entry: FrecencyEntry): nil
---@param entries FrecencyEntry[]
---@return boolean?
function Finder:process_table(process_result, entries)
for _, entry in ipairs(entries) do
if process_result(entry) then
return true
end
end
end
---@async
---@param process_result fun(entry: FrecencyEntry): nil
---@param entries FrecencyEntry[]
---@param rx PlenaryAsyncControlChannelRx
---@param start_index integer?
---@return boolean?
function Finder:process_channel(process_result, entries, rx, start_index)
local index = #entries > 0 and entries[#entries].index or start_index or 0
local count = 0
while true do
local entry = rx.recv()
if not entry then
break
elseif not self.seen[entry.filename] then
self.seen[entry.filename] = true
index = index + 1
entry.index = index
table.insert(entries, entry)
if process_result(entry) then
return true
end
end
count = count + 1
if count % self.config.chunk_size == 0 then
self:reflow_results()
end
end
end
---@param workspace string? ---@param workspace string?
---@param datetime string? ---@param datetime string?
---@return FrecencyFile[] ---@return FrecencyFile[]
@ -159,16 +242,22 @@ end
function Finder:close() function Finder:close()
self.closed = true self.closed = true
if self.process then
self.process:kill(9)
end
end end
---@async
---@return nil
function Finder:reflow_results() function Finder:reflow_results()
local picker = self.state:get() local picker = self.state:get()
if not picker then if not picker then
return return
end end
async.util.scheduler()
local bufnr = picker.results_bufnr local bufnr = picker.results_bufnr
local win = picker.results_win local win = picker.results_win
if not bufnr or not win then if not bufnr or not win or not vim.api.nvim_buf_is_valid(bufnr) or not vim.api.nvim_win_is_valid(win) then
return return
end end
picker:clear_extra_rows(bufnr) picker:clear_extra_rows(bufnr)
@ -178,7 +267,6 @@ function Finder:reflow_results()
return return
end end
local worst_line = picker:get_row(manager:num_results()) local worst_line = picker:get_row(manager:num_results())
---@type WinInfo
local wininfo = vim.fn.getwininfo(win)[1] local wininfo = vim.fn.getwininfo(win)[1]
local bottom = vim.api.nvim_buf_line_count(bufnr) local bottom = vim.api.nvim_buf_line_count(bufnr)
if not self.reflowed or worst_line > wininfo.botline then if not self.reflowed or worst_line > wininfo.botline then

View File

@ -34,6 +34,7 @@ local Frecency = {}
---@field show_scores boolean? default: false ---@field show_scores boolean? default: false
---@field show_unindexed boolean? default: true ---@field show_unindexed boolean? default: true
---@field use_sqlite boolean? default: false ---@field use_sqlite boolean? default: false
---@field workspace_scan_cmd "LUA"|string[]|nil default: nil
---@field workspaces table<string, string>? default: {} ---@field workspaces table<string, string>? default: {}
---@param opts FrecencyConfig? ---@param opts FrecencyConfig?
@ -54,6 +55,7 @@ Frecency.new = function(opts)
show_scores = false, show_scores = false,
show_unindexed = true, show_unindexed = true,
use_sqlite = false, use_sqlite = false,
workspace_scan_cmd = nil,
workspaces = {}, workspaces = {},
}, opts or {}) }, opts or {})
local self = setmetatable({ buf_registered = {}, config = config }, { __index = Frecency })--[[@as Frecency]] local self = setmetatable({ buf_registered = {}, config = config }, { __index = Frecency })--[[@as Frecency]]
@ -133,6 +135,7 @@ function Frecency:start(opts)
filter_delimiter = self.config.filter_delimiter, filter_delimiter = self.config.filter_delimiter,
initial_workspace_tag = opts.workspace, initial_workspace_tag = opts.workspace,
show_unindexed = self.config.show_unindexed, show_unindexed = self.config.show_unindexed,
workspace_scan_cmd = self.config.workspace_scan_cmd,
workspaces = self.config.workspaces, workspaces = self.config.workspaces,
}) })
self.picker:start(vim.tbl_extend("force", self.config, opts)) self.picker:start(vim.tbl_extend("force", self.config, opts))

View File

@ -28,6 +28,7 @@ local Picker = {}
---@field filter_delimiter string ---@field filter_delimiter string
---@field initial_workspace_tag string? ---@field initial_workspace_tag string?
---@field show_unindexed boolean ---@field show_unindexed boolean
---@field workspace_scan_cmd "LUA"|string[]|nil
---@field workspaces table<string, string> ---@field workspaces table<string, string>
---@class FrecencyPickerEntry ---@class FrecencyPickerEntry
@ -77,7 +78,16 @@ function Picker:finder(opts, workspace, workspace_tag)
local filepath_formatter = self:filepath_formatter(opts) local filepath_formatter = self:filepath_formatter(opts)
local entry_maker = self.entry_maker:create(filepath_formatter, workspace, workspace_tag) local entry_maker = self.entry_maker:create(filepath_formatter, workspace, workspace_tag)
local need_scandir = not not (workspace and self.config.show_unindexed) local need_scandir = not not (workspace and self.config.show_unindexed)
return Finder.new(self.database, entry_maker, self.fs, need_scandir, workspace, self.recency, self.state) return Finder.new(
self.database,
entry_maker,
self.fs,
need_scandir,
workspace,
self.recency,
self.state,
{ workspace_scan_cmd = self.config.workspace_scan_cmd }
)
end end
---@param opts FrecencyPickerOptions? ---@param opts FrecencyPickerOptions?

View File

@ -166,6 +166,10 @@ function PlenaryAsyncUv.fs_close(fd) end
---@return nil ---@return nil
function PlenaryAsyncUtil.sleep(ms) end function PlenaryAsyncUtil.sleep(ms) end
---@async
---@return nil
function PlenaryAsyncUtil.scheduler() end
-- NOTE: types are for telescope.nvim -- NOTE: types are for telescope.nvim
---@alias TelescopeEntryDisplayer fun(items: string[]): table ---@alias TelescopeEntryDisplayer fun(items: string[]): table
@ -198,11 +202,20 @@ function PlenaryAsyncUtil.sleep(ms) end
-- NOTE: types for default functions -- NOTE: types for default functions
---@class WinInfo
---@field topline integer
---@field botline integer
---@class UvFsEventHandle ---@class UvFsEventHandle
---@field stop fun(self: UvFsEventHandle): nil ---@field stop fun(self: UvFsEventHandle): nil
---@field start fun(self: UvFsEventHandle, path: string, opts: { recursive: boolean }, cb: fun(err: string?, filename: string?, events: string[])): nil ---@field start fun(self: UvFsEventHandle, path: string, opts: { recursive: boolean }, cb: fun(err: string?, filename: string?, events: string[])): nil
---@field close fun(self: UvFsEventHandle): nil ---@field close fun(self: UvFsEventHandle): nil
--- @class VimSystemObj
--- @field pid integer
--- @field wait fun(self: VimSystemObj, timeout?: integer): VimSystemCompleted
--- @field kill fun(self: VimSystemObj, signal: integer|string)
--- @field write fun(self: VimSystemObj, data?: string|string[])
--- @field is_closing fun(self: VimSystemObj): boolean?
--- @class VimSystemCompleted
--- @field code integer
--- @field signal integer
--- @field stdout? string
--- @field stderr? string