diff --git a/lua/tsql.lua b/lua/tsql.lua index caa63ce..e9b9d27 100644 --- a/lua/tsql.lua +++ b/lua/tsql.lua @@ -1,5 +1,4 @@ ----TODO: how does this work with changing texts? ----TODO: add reducer as formatter +---TODO: how does this work with changing texts? TODO: add reducer as formatter ---TODO: Add reducer as buf_select predicate ---@type mod_buf_select @@ -96,12 +95,11 @@ function M.sink_by.print(format) if format == nil then format = M.format.default end - return setmetatable({ - ---@type fun(nodes: QNode[]) - sink = function(nodes) + return M.sink_by.pure_fn( + function(nodes) print(format(nodes)) end - }, M.Sink) + ) end ---@param format Format @@ -110,13 +108,12 @@ function M.sink_by.nvim_yank_buf(format) if format == nil then format = M.format.default end - return setmetatable({ - ---@type fun(nodes: QNode[]) - sink = function(nodes) + return M.sink.pure_fn( + function(nodes) local text = format(nodes) vim.fn.setreg('"', text) end - }, M.Sink) + ) end ---NOTE: re-export with implementation @@ -155,9 +152,11 @@ end ---NOTE: requires nvim runtime function M.Tsql:q_nodes() - return self.codeql:find_nodes( - self.buf_match.filter_on(self.buf_match, M.nvim_get_qbufs()) - ) + 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 @@ -220,7 +219,7 @@ end function M.Store:new() local o = { highlighting = {} } setmetatable(o, self) - self.__index = self + o.__index = self return o end @@ -240,7 +239,6 @@ function M.setup(config) end, { nargs = "?", desc = "tsql DSL invocation" }) M.store = M.Store:new() - print("tsql v0.0.1") end return M diff --git a/lua/tsql/buf_select.lua b/lua/tsql/buf_select.lua index e3812da..6a74c2d 100644 --- a/lua/tsql/buf_select.lua +++ b/lua/tsql/buf_select.lua @@ -11,9 +11,9 @@ local M = {} ---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 The language of the treesitter parser. This is gotten ----from `vim.treesitter.get_parser(bufnr: number):lang() -> string [may fail]` ----@field is_loaded boolean whether it is loaded +---@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 @@ -24,6 +24,7 @@ local function get_lang(bufnr) 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) error(string.format("Error determining language for buffer %d: %s", bufnr, path)) @@ -35,15 +36,13 @@ end ---@param bufnr number ---@param path string ---@param filetype string ----@param lang 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") - assert(type(lang) == "string", "lang must be a string") - assert(type(is_loaded) == "boolean", "is_loaded must be a boolean") local qbuf = { bufnr = bufnr, @@ -58,14 +57,33 @@ function M.QBuf:new(bufnr, path, filetype, lang, is_loaded) 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') - local status, lang = pcall(get_lang, bufnr) - local is_loaded = status + assert(type(filetype) == "string") - return M.QBuf:new(bufnr, path, filetype, lang, is_loaded) + 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 = {} @@ -75,6 +93,7 @@ M.buf_match = {} ---@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) @@ -201,27 +220,16 @@ local function list_bufs() return vim.api.nvim_list_bufs() end ----@return QBuf[] +---@return QBuf[] qbufs function M.nvim_get_qbufs() local bufnrs = list_bufs() local qbufs = {} for _, bufnr in ipairs(bufnrs) do - local path = vim.api.nvim_buf_get_name(bufnr) - local filetype = vim.api.nvim_buf_get_option(bufnr, 'filetype') - - local status, lang = pcall(get_lang, bufnr) - local is_loaded = status - - local qbuf = { - bufnr = bufnr, - path = path, - filetype = filetype, - lang = lang, - is_loaded = is_loaded - } - - table.insert(qbufs, qbuf) + local qbuf = M.QBuf.from_nvim_bufnr(bufnr) + if qbuf ~= nil then + table.insert(qbufs, qbuf) + end end return qbufs diff --git a/lua/tsql/token_select.lua b/lua/tsql/token_select.lua index ef19eed..37ad6ba 100644 --- a/lua/tsql/token_select.lua +++ b/lua/tsql/token_select.lua @@ -7,6 +7,15 @@ M.ts_query = {} ---@class TSQuery ---@field query string the passthru for treesitter language parser M.TSQuery = {} +M.TSQuery.__index = M.TSQuery + +---@type Ctor<{query: string}, TSQuery> +function M.TSQuery.new(q) + local self = q + setmetatable(self, M.TSQuery) + self.__index = M.TSQuery + return self +end ---@class FileLoc ---@field row_0 number 0-index row location @@ -14,6 +23,15 @@ M.TSQuery = {} M.FileLoc = {} M.FileLoc.__index = M.FileLoc +---@type Ctor<{row_0: number, col_0: number}, FileLoc> +function M.FileLoc.new(file_loc) + assert(file_loc ~= nil) + local self = file_loc + setmetatable(self, M.FileLoc) + self.__index = M.FileLoc + return self +end + ---@class QNode ---@field start FileLoc ---@field end_ex_col FileLoc @@ -21,11 +39,20 @@ M.FileLoc.__index = M.FileLoc M.QNode = {} M.QNode.__index = M.QNode +---@type Ctor<{start: FileLoc, end_ex_col: FileLoc, buf: QBuf}, QNode> +function M.QNode.new(qnode) + assert(qnode ~= nil) + local self = qnode + setmetatable(self, M.QNode) + self.__index = M.QNode + return self +end + ---@param treesitter_query string the passthru for treesitter language ---parser ---@return TSQuery function M.ts_query.from_scm(treesitter_query) - return { + return M.TSQuery.new { query = treesitter_query } end @@ -35,23 +62,35 @@ end function M.TSQuery:find_nodes(files) local result = {} for _, file in ipairs(files) do - local parser = vim.treesitter.get_parser(file.bufnr, file.filetype) + if file.lang == nil then + goto continue + end + local sts, parser = pcall(vim.treesitter.get_parser, file.bufnr, file.filetype) + if not sts then + -- NOTE: no parser for language + goto continue + end local tree = parser:parse()[1] local root = tree:root() ---@type Query - local query = vim.treesitter.query.parse_query(file.lang, self.query) + local sts, query = pcall(vim.treesitter.query.parse, file.lang, self.query) + if not sts then + error("Error parsing query \"" .. self.query .. "\" on lang " .. file.lang .. " for file " .. file.path) + end + for _, match, _ in query:iter_matches(root, file.bufnr, 0, -1) do for id, node in pairs(match) do local start_row, start_col, end_row, end_col = node:range(false) - local start = { row_0 = start_row, col_0 = start_col } + local start = M.FileLoc.new { row_0 = start_row, col_0 = start_col } -- CORRECTNESS: -- :lua local parser = vim.treesitter.get_parser(0, 'lua'); local tree = parser:parse()[1]; local query = vim.treesitter.parse_query('lua', '(identifier) @name'); for id, node in query:iter_captures(tree:root(), 0) do local name = query.captures[id]; if name == 'name' and vim.treesitter.get_node_text(node, 0) == 'TSQuery' then local sr, sc, er, ec = node:range(); print(string.format("TSQuery Start: (%d, %d), End: (%d, %d)", sr, sc, er, ec)); end; end - local end_ex_col = { row_0 = end_row, col_0 = end_col } - local qnode = { buf = file, start = start, end_ex_col = end_ex_col } - setmetatable(qnode, M.QNode) + local end_ex_col = M.FileLoc.new { row_0 = end_row, col_0 = end_col } + local qnode = M.QNode.new { buf = file, start = start, end_ex_col = end_ex_col } + table.insert(result, qnode) end end + ::continue:: end return result end diff --git a/repl.md b/repl.md index 5068a1c..93923d9 100644 --- a/repl.md +++ b/repl.md @@ -1,13 +1,30 @@ # Simple location for me to use as a neovim repl ```lua -:lua local ts = require('tsql'); ts.t(ts.buf_match.any(), ts.ts_query.from_scm("string"), ts.sink_by.print()):do_nvim(); - -:lua local ts = require('tsql'); local tsql = ts.t(ts.buf_match.any(), ts.ts_query.from_scm("string"), ts.sink_by.print()); print(vim.inspect(tsql.buf_match)); - --- print found bufs -:lua local t = require('tsql'); print(vim.inspect(t.nvim_get_qbufs())) - --- check that buf filter works -:lua local t = require('tsql'); print(vim.inspect(t.buf_match.any():filter_on(t.nvim_get_qbufs()))) +:lua local ts = require('tsql'); ts.t(ts.buf_match.filetype("lua"), ts.ts_query.from_scm("(string) @_"), ts.sink_by.print()):do_nvim(ts.store); +:lua local ts = require('tsql'); ts.t(ts.buf_match.filetype("lua"), ts.ts_query.from_scm("(string) @_"), ts.sink_by.highlight()):do_nvim(ts.store); +:Noh +-- QNodes from current buffer +:lua local ts = require('tsql'); print(vim.inspect(ts.ts_query.from_scm("(string) @_"):find_nodes({ts.QBuf.from_nvim_bufnr(0)}))) +``` + +Non-tsql poc +```lua +:lua local parser = vim.treesitter.get_parser(0, 'lua'); local tree = parser:parse()[1]; local query = vim.treesitter.parse_query('lua', '(identifier) @name'); for id, node in query:iter_captures(tree:root(), 0) do local name = query.captures[id]; if name == 'name' and vim.treesitter.get_node_text(node, 0) == 'TSQuery' then local sr, sc, er, ec = node:range(); print(string.format("TSQuery Start: (%d, %d), End: (%d, %d)", sr, sc, er, ec)); end; end + +``` + + +```lua +:lua local ts = require('tsql'); ts.t(ts.buf_match.filetype("lua"), ts.ts_query.from_scm("(string) @_"), ts.sink_by.highlight()):do_nvim(ts.store); +:lua local ts = require('tsql'); ts.t(ts.buf_match.filetype("lua"), ts.ts_query.from_scm("(identifier) @_"), ts.sink_by.highlight()):do_nvim(ts.store); +-- Highlight all function calls `hello.world("what the heck!")` +:lua local ts = require('tsql'); ts.t(ts.buf_match.filetype("lua"), ts.ts_query.from_scm("(function_call) @_"), ts.sink_by.highlight()):do_nvim(ts.store); +-- Highlight only function calls that are "pure": Not `hello.world` but `world` +:lua local ts = require('tsql'); ts.t(ts.buf_match.filetype("lua"), ts.ts_query.from_scm("(function_call (identifier) @_)"), ts.sink_by.highlight()):do_nvim(ts.store); +-- Highlight function calls, but only the function identifier +:lua local ts = require('tsql'); ts.t(ts.buf_match.filetype("lua"), ts.ts_query.from_scm("[(function_call name: (dot_index_expression) @_) (function_call name: (identifier) @_) (function_call name: (method_index_expression) @_)]"), ts.sink_by.highlight()):do_nvim(ts.store); + +:Noh + ```