tsql.nvim/lua/tsql.lua

251 lines
6.4 KiB
Lua

---TODO: how does this work with changing texts? (extmarks, but can look at `TextChanged` and `TextChangedI` events, along with some buf events to re-run when needs to)
-- TODO: Better `ts_query` construction experience
-- TODO: injection tree-sitter query
---TODO: add reducer as formatter
---TODO: Add reducer as buf_select predicate
---@type mod_buf_select
local buf_select = require('tsql.buf_select')
---@type mod_token_select
local token_select = require('tsql.token_select')
---@module 'tsql'
local M = {}
---@class mod_sink_by
M.sink_by = {}
---@class mod_format
---@alias Format fun(nodes: QNode[]): string
M.format = {}
---@class Sink
---@field sink fun(self, nodes: QNode[])
M.Sink = {}
M.Sink.__index = M.Sink
---@type Ctor<fun(nodes: QNode[]), Sink>
function M.sink_by.pure_fn(func)
local self = setmetatable({
sink = function(_, nodes)
return func(nodes)
end
}, M.Sink)
self.__index = M.Sink
return self
end
---@return Sink
function M.sink_by.highlight()
-- NOTE: magic! Neovim has extmarks, so in a very general case, you don't need to
-- re-run this sink to have the highlights be up-to-date
return M.sink_by.pure_fn(function(nodes)
for _, node in ipairs(nodes) do
vim.highlight.range(
node.buf.bufnr,
M.config.nvim_ns,
M.config.nvim_hl_group,
{ node.start.row_0, node.start.col_0 },
{ node.end_ex_col.row_0, node.end_ex_col.col_0 },
{ inclusive = false }
)
end
end)
end
---@type Format
---Something that can be represented in string in a concise/DSL format.
---In the context of QNode, it should just be the text content. If it's multiline,
---just join by newline
---
---`vim.api.nvim_buf_get_text(
--- bufnr: number,
--- start_row: number,
--- start_col: number,
--- end_row: number,
--- end_col_exclusive: number,
--- opt: {}
---) -> string[]` return value is array of lines, empty array for unloaded buffer
function M.format.display(nodes)
local texts = {}
for _, node in ipairs(nodes) do
local text = vim.api.nvim_buf_get_text(
node.buf.bufnr,
node.start.row_0,
node.start.col_0,
node.end_ex_col.row_0,
node.end_ex_col.col_0,
{}
)
table.insert(texts, table.concat(text, '\n'))
end
return table.concat(texts, '\n\n')
end
---@type Format
---Something like a JSON if natively possible, or RON for Rust for clarity
---Basically return a string that is pretty-printed that represents
---a Lua table onto string
function M.format.dump(nodes)
return vim.inspect(nodes, { newline = '\n', indent = ' ' })
end
M.format.debug = M.format.dump
M.format.default = M.format.display
---@param format Format | nil
---@return Sink
function M.sink_by.print(format)
if format == nil then
format = M.format.default
end
return M.sink_by.pure_fn(
function(nodes)
print(format(nodes))
end
)
end
---@param format Format
---@return Sink
function M.sink_by.nvim_yank_buf(format)
if format == nil then
format = M.format.default
end
return M.sink.pure_fn(
function(nodes)
local text = format(nodes)
vim.fn.setreg('"', text)
end
)
end
---NOTE: re-export with implementation
M.buf_match = buf_select.buf_match
M.BufMatch = buf_select.BufMatch
M.QBuf = buf_select.QBuf
M.nvim_get_qbufs = buf_select.nvim_get_qbufs
M.ts_query = token_select.ts_query
M.TSQuery = token_select.TSQuery
---@class Tsql
---@field buf_match BufMatch
---@field codeql TSQuery
---@field sink Sink
M.Tsql = {}
M.Tsql.__index = M.Tsql
---@return Tsql
---@param external_dsl string
function M.s(external_dsl)
-- TODO: implement
end
---@return Tsql
---@param buf_match BufMatch
---@param codeql TSQuery
---@param sink Sink
function M.t(buf_match, codeql, sink)
return setmetatable({
buf_match = buf_match,
codeql = codeql,
sink = sink
}, M.Tsql)
end
---NOTE: requires nvim runtime
function M.Tsql:q_nodes()
return self.codeql:find_nodes(self:qbufs())
end
function M.Tsql:qbufs()
return self.buf_match.filter_on(self.buf_match, M.nvim_get_qbufs())
end
---NOTE: This is now exiting the functional core and entering
--- imperative shell
--- @param store StaticStore
function M.Tsql:do_nvim(store)
self.sink:sink(self:q_nodes())
-- FIXME: this should be limited to only highlight sink
store:add_highlight(self)
end
---@param config RtConfig
---@param store StaticStore
function M._delete_all_highlights(config, store)
---@type table<integer, QBuf>
local bufs = {}
for _, highlight_q in pairs(store.highlighting) do
for _, qnode in ipairs(highlight_q:q_nodes()) do
table.insert(bufs, qnode.buf.bufnr, qnode.buf)
end
end
for _, buf in pairs(bufs) do
vim.api.nvim_buf_clear_namespace(buf.bufnr, config.nvim_ns, 0, -1)
end
store:clear_highlights()
end
---NOTE: Collocated with `M.setup`
---@class Config
---@field nvim_hl_group string
local Config = {}
---@type Config
M.config_default = {
nvim_hl_group = "Search"
}
---@class RtConfig: Config
---@field nvim_ns number
M.RtConfig = {}
---@type RtConfig
M.config = {}
---@class StaticStore
---@field highlighting Tsql[]
---This is needed to undo the highlighting done by this plugin and potentially
---subscribe to the changing buffers to re-highlight if necessary
M.Store = {}
M.Store.__index = M.Store
---@param tsql Tsql
function M.Store:add_highlight(tsql)
table.insert(self.highlighting, tsql)
end
function M.Store:clear_highlights()
self.highlighting = {}
end
---@return StaticStore
function M.Store:new()
local o = { highlighting = {} }
setmetatable(o, self)
o.__index = self
return o
end
function M.clear_highlights()
return M._delete_all_highlights(M.config, M.store)
end
---@param config Config | nil
function M.setup(config)
M.config = vim.tbl_deep_extend("force", M.config, config or M.config_default)
M.config.nvim_ns = vim.api.nvim_create_namespace("tsql")
vim.api.nvim_create_user_command("Noh", function(_) M.clear_highlights() end,
{ nargs = 0, desc = "Clear tsql highlights" })
vim.api.nvim_create_user_command("Tdsl", function(cmd)
M.s(cmd.args):do_nvim(M.store)
end, { nargs = "?", desc = "tsql DSL invocation" })
M.store = M.Store:new()
end
return M