tsql.nvim/lua/tsql/buf_select.lua

240 lines
6.0 KiB
Lua

---@module 'buf_select'
---@class mod_buf_select
local M = {}
---@alias MatchPredicate fun(nodes: QBuf): boolean
---@class QBuf
---@field bufnr number representing the vim runtime's bufnr, 0 is current buf
---@field path string the absolute path to the buffer. This uses
---`vim.api.nvim_buf_get_name(bufnr: number) -> string`
---Assume [""] if it's erroneous (like a terminal buffer)
---@field filetype string the associated filetypes gotten from. This uses
---`vim.api.nvim_buf_get_option(bufnr: number, 'filetype')`
---@field lang string | nil The language of the treesitter parser. This is gotten
---from `pcall of vim.treesitter.get_parser(bufnr: number):lang() -> string | nil`
---@field is_loaded boolean whether it is loaded (`vim.api.nvim_buf_is_loaded`)
M.QBuf = {}
M.QBuf.__index = M.QBuf
---@param bufnr number
---@param path string
---@param filetype string
---@param lang string | nil
---@param is_loaded boolean
---@return QBuf
function M.QBuf:new(bufnr, path, filetype, lang, is_loaded)
assert(type(bufnr) == "number", "bufnr must be a number")
assert(type(path) == "string", "path must be a string")
assert(type(filetype) == "string", "filetype must be a string")
local qbuf = {
bufnr = bufnr,
path = path,
filetype = filetype,
lang = lang,
is_loaded = is_loaded
}
setmetatable(qbuf, self)
qbuf.__index = self
return qbuf
end
---@return string language
---NOTE: may fail with string
local function get_lang(bufnr)
local status, lang = pcall(function()
return vim.treesitter.get_parser(bufnr):lang()
end)
-- PURPOSE: enriches the message
if not status then
local path = vim.api.nvim_buf_get_name(bufnr)
local err = lang
error(string.format("Error determining language for buffer %d: %s\n%s", bufnr, path, err))
end
return lang
end
---@param bufnr number
---@return QBuf | nil
function M.QBuf.from_nvim_bufnr(bufnr)
local path = vim.api.nvim_buf_get_name(bufnr)
local filetype = vim.api.nvim_buf_get_option(bufnr, 'filetype')
assert(type(filetype) == "string")
if #filetype == 0 then
return nil
end
local sts, _lang = pcall(get_lang, bufnr)
---@type nil | string
local lang = nil
if sts then
lang = _lang
end
local is_loaded = vim.api.nvim_buf_is_loaded(bufnr)
return M.QBuf:new(
bufnr,
path,
filetype,
lang,
is_loaded
)
end
M.buf_match = {}
---@class BufMatch
---@field not_ fun(self): BufMatch
---@field or_ fun(self, q: BufMatch): BufMatch
---@field then_ fun(self, q: BufMatch): BufMatch
---@field filter_on fun(self, q: QBuf[]): QBuf[]
M.BufMatch = {}
M.BufMatch.__index = M.BufMatch
function M.buf_match.is_loaded()
return M.BufMatch.new(function(buf)
return buf.is_loaded
end)
end
---@param match_fn fun(buf: QBuf): boolean
---@return BufMatch
---NOTE: assigns `match_fn` private field
function M.BufMatch.new(match_fn)
local self = setmetatable({}, M.BufMatch)
self.__index = M.BufMatch
self.match_fn = match_fn
return self
end
---@vararg string OR for filetypes. It doesn't make a lot of sense for AND filetypes
---@return BufMatch
function M.buf_match.filetype(...)
local filetypes = { ... }
return M.BufMatch.new(function(buf)
for _, filetype in ipairs(filetypes) do
if buf.filetype == filetype then
return true
end
end
return false
end)
end
function M.buf_match.any()
return M.BufMatch.new(function(_) return true end)
end
---@vararg string OR for path
---@return BufMatch
function M.buf_match.path(...)
local paths = { ... }
return M.BufMatch.new(function(buf)
for _, path in ipairs(paths) do
if string.find(buf.path, path) ~= nil then
return true
end
end
return false
end)
end
---@vararg string OR for path
---@return BufMatch _ f
function M.buf_match.path_or(...)
return M.buf_match.path(...)
end
---@vararg string AND for path
---@return BufMatch
function M.buf_match.path_and(...)
local paths = { ... }
return M.BufMatch.new(function(buf)
for _, path in ipairs(paths) do
if string.find(buf.path, path) == nil then
return false
end
end
return true
end)
end
---@vararg string
---@return BufMatch
function M.buf_match.ext(...)
local exts = { ... }
return M.BufMatch.new(function(buf)
for _, ext in ipairs(exts) do
if buf.path:sub(- #ext) == ext then
return true
end
end
return false
end)
end
---@param q BufMatch
---@return BufMatch
function M.BufMatch:or_(q)
return M.BufMatch.new(function(buf)
return self.matched_fn --[[@as MatchPredicate]](buf)
or q.matched_fn --[[@as MatchPredicate]](buf)
end)
end
---@param q BufMatch
---@return BufMatch
function M.BufMatch:then_(q)
return M.BufMatch.new(function(buf)
return self.matched_fn --[[@as MatchPredicate]](buf)
and q.matched_fn --[[@as MatchPredicate]](buf)
end)
end
---@return BufMatch
function M.BufMatch:not_()
return M.BufMatch.new(function(buf)
return not self.matched_fn --[[@as MatchPredicate]](buf)
end)
end
---@param itr QBuf[]
---@return QBuf[]
function M.BufMatch:filter_on(itr)
---@type QBuf[]
local matched = {}
for _, buf in ipairs(itr) do
if (self.match_fn --[[@as MatchPredicate]])(buf) then
table.insert(matched, buf)
end
end
return matched
end
---@return number[] bufnrs that can be loaded or not loaded
local function list_bufs()
return vim.api.nvim_list_bufs()
end
---@return QBuf[] qbufs
function M.nvim_get_qbufs()
local bufnrs = list_bufs()
local qbufs = {}
for _, bufnr in ipairs(bufnrs) do
local qbuf = M.QBuf.from_nvim_bufnr(bufnr)
if qbuf ~= nil then
table.insert(qbufs, qbuf)
end
end
return qbufs
end
return M