nvim-config

Log | Files | Refs | Submodules | README

commit 9957a6d6be583b376e9d20cf85b26a416aec79a4
parent 323820748648176be9be96a068649ac2e9cfbbbf
Author: Thomas Vigouroux <me@vigoux.eu>
Date:   Wed,  6 Mar 2024 12:26:43 +0100

feat(latex): better ai/surround textobject

Diffstat:
Mafter/ftplugin/tex.lua | 321++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
Mafter/queries/latex/textobjects.scm | 30++++++++++++------------------
2 files changed, 208 insertions(+), 143 deletions(-)

diff --git a/after/ftplugin/tex.lua b/after/ftplugin/tex.lua @@ -111,144 +111,215 @@ do end), { buffer = true }) end -local spec_treesitter = require('mini.ai').gen_spec.treesitter +---@param captures string[] +local function well_defined(captures, capture_to_id, matches) + for _, capture in ipairs(captures) do + if not matches[capture_to_id[capture:sub(2)]] then + return false + end + end + return true +end + +local function extract_range(spec, match, capture_to_id) + local ret = {} + if type(spec) == "string" then + local start_row, start_col, end_row, end_col = match[capture_to_id[spec:sub(2)]]:range() + ret.from = { line = start_row, col = start_col } + ret.to = { line = end_row, col = end_col } + else + local start_row, start_col = match[capture_to_id[spec.from:sub(2)]]:start() + ret.from = { line = start_row, col = start_col } + + local end_row, end_col = match[capture_to_id[spec.to:sub(2)]]:end_() + ret.to = { line = end_row, col = end_col } + end + + return ret +end + +function captures_inout(spec) + vim.validate { + spec = { + spec, + 'table' + } + } + + vim.validate { + ["spec.inner"] = { + spec.inner, + { 'string', 'table' }, + }, + ["spec.outer"] = { + spec.outer, + { 'string', 'table' }, + } + } + + local all_captures = {} + + if type(spec.inner) == 'table' then + vim.validate { + ["spec.inner.from"] = { + spec.inner.from, + "string", + }, + ["spec.inner.to"] = { + spec.inner.to, + "string", + } + } + all_captures[#all_captures + 1] = spec.inner.from + all_captures[#all_captures + 1] = spec.inner.to + else + all_captures[#all_captures + 1] = spec.inner + end + + if type(spec.outer) == 'table' then + vim.validate { + ["spec.outer.from"] = { + spec.outer.from, + "string", + }, + ["spec.outer.to"] = { + spec.outer.to, + "string" + } + } + all_captures[#all_captures + 1] = spec.outer.from + all_captures[#all_captures + 1] = spec.outer.to + else + all_captures[#all_captures + 1] = spec.outer + end + + local lang = require 'nvim-treesitter.parsers'.get_buf_lang() + local query = vim.treesitter.query.get(lang, 'textobjects') + if query == nil then error("Could not get query") end + + local capture_to_id = {} + for idx, val in ipairs(query.captures) do + capture_to_id[val] = idx + end + + + + local ok, parser = pcall(vim.treesitter.get_parser, 0, lang) + + -- Compute matched captures + local res = {} + for _, tree in ipairs(parser:trees()) do + for _, match, _ in query:iter_matches(tree:root(), 0) do + if well_defined(all_captures, capture_to_id, match) then + res[#res + 1] = { + outer = extract_range(spec.outer, match, capture_to_id), + inner = extract_range(spec.inner, match, capture_to_id) + } + end + end + end + return res +end + +local function ai_treesitter(captures) + return function(ai_type) + local tgt_captures = ai_type == "a" and "outer" or "inner" + + local matches = captures_inout(captures) + local res = {} + for _, match in ipairs(matches) do + local tgt = match[tgt_captures] + res[#res + 1] = { + from = { + line = tgt.from.line + 1, + col = tgt.from.col + 1 + }, + to = { + line = tgt.to.line + 1, + col = tgt.to.col + } + } + end + + return res + end +end + +-- local spec_treesitter = require('mini.ai').gen_spec.treesitter vim.b.miniai_config = { custom_textobjects = { - m = spec_treesitter { a = "@math.outer", i = "@math.inner" }, - e = spec_treesitter { a = "@env.outer", i = "@env.inner" }, - s = spec_treesitter { a = "@section.outer", i = "@section.inner" }, - i = spec_treesitter { a = "@item.outer", i = "@item.inner" } + m = ai_treesitter { outer = "@math.outer", inner = { from = "@math.inner.from", to = "@math.inner.to" } }, + e = ai_treesitter { outer = "@env.outer", inner = { from = "@env.inner.from", to = "@env.inner.to" } }, + s = ai_treesitter { outer = "@section.outer", inner = { from = "@section.inner.from", to = "@section.inner.to" } }, + i = ai_treesitter { outer = "@item.outer", inner = { from = "@item.inner.from", to = "@item.inner.to" } } } } local surround = require 'mini.surround' + +---@param prompt string +---@param leftfmt string +---@param rightfmt string +---@return function() +local function output_ask(prompt, leftfmt, rightfmt) + return function() + local name = surround.user_input(prompt) + if not name then return end + local ret = { left = string.format(leftfmt, name), right = string.format(rightfmt, name) } + vim.print(leftfmt, rightfmt, ret) + return ret + end +end + + +local pos_to_left = function(pos) + if pos.line == 1 and pos.col == 1 then return { line = pos.line, col = pos.col } end + if pos.col == 1 then return { line = pos.line - 1, col = H.get_line_cols(pos.line - 1) } end + return { line = pos.line, col = pos.col - 1 } +end + +local pos_to_right = function(pos) + local n_cols = vim.api.nvim_buf_get_lines(0, pos.line - 1, pos.line, true)[1]:len() + -- Using `>` and not `>=` helps with removing '\n' and in the last line + if pos.line == vim.api.nvim_buf_line_count(0) and pos.col > n_cols then return { line = pos.line, col = n_cols } end + if pos.col > n_cols then return { line = pos.line + 1, col = 1 } end + return { line = pos.line, col = pos.col + 1 } +end + +local function surround_treesitter(captures) + return function() + local matches = captures_inout(captures) + local res = {} + for _, match in ipairs(matches) do + local left_from = { line = match.outer.from.line + 1, col = match.outer.from.col + 1 } + local right_to = { line = match.outer.to.line + 1, col = match.outer.to.col } + + local left_to = pos_to_left { line = match.inner.from.line + 1, col = match.inner.from.col + 1 } + local right_from = pos_to_right { line = match.inner.to.line + 1, col = match.inner.to.col } + + res[#res + 1] = { + left = { from = left_from, to = left_to }, + right = { from = right_from, to = right_to } + } + end + + return res + end +end + vim.b.minisurround_config = { custom_surroundings = { f = { - input = surround.gen_spec.input.treesitter { outer = "@call.outer", inner = "@call.inner" }, - output = function() - local name = surround.user_input("Function name") - if not name then return end - - return { left = string.format("\\%s{", name), right = "}" } - end + input = surround_treesitter { outer = "@call.outer", inner = "@call.inner" }, + output = output_ask("Function name", "\\%s{", "}") }, e = { - input = surround.gen_spec.input.treesitter { outer = "@env.outer", inner = "@env.inner" }, - output = function() - local name = surround.user_input("Env name") - if not name then return end - - return { left = string.format("\\begin{%s}\n", name), right = string.format("\n\\end{%s}", name)} - end + input = surround_treesitter { outer = "@env.outer", inner = { from = "@env.inner.from", to = "@env.inner.to" } }, + output = output_ask("Env name", "\\begin{%s}\n", "\n\\end{%s}") }, c = { - input = surround.gen_spec.input.treesitter { outer = "@color.outer", inner = "@color.inner" }, - output = function() - local name = surround.user_input("Color name") - if not name then return end - - return { left = string.format("\\textcolor{%s}{", name), right = "}"} - end + input = surround_treesitter { outer = { from = "@color.outer.from", to = "@color.outer.to" }, inner = "@color.inner" }, + output = output_ask("Color name", "{\\textcolor{%s}{", "}}") } } } - --- -- Wrap the selection in a color block --- vis_input_change('<LocalLeader>c', 'Color name: ', preffix_suffix_change(function(input) --- return string.format('{\\color{%s}', input), '}' --- end)) --- --- -- Wrap into an environment --- vis_input_change('<LocalLeader>e', 'Environment name: ', preffix_suffix_change(function(input) --- return string.format('\\begin{%s}', input), string.format('\\end{%s}', input) --- end)) --- --- local function toggle_thing(query, left, right) --- return function(buf, win, cursor) --- local match, node = utls.find_smallest_match(cursor[1] - 1, cursor[2], query, buf) --- --- local innode = utls.index_by_name(query, match or {}, "in") --- if not innode then --- if node then --- local sline, scol, eline, ecol = vim.treesitter.get_node_range(node) --- vim.api.nvim_buf_set_text(buf, sline, scol, eline, ecol, {}) --- else --- vim.api.nvim_buf_set_text(buf, cursor[1] - 1, cursor[2], cursor[1] - 1, cursor[2], { left .. right }) --- vim.api.nvim_win_set_cursor(win, { cursor[1], cursor[2] + #left }) --- end --- else --- local _, _, _, ecol = vim.treesitter.get_node_range(innode) --- if ecol == cursor[2] then --- -- At the end of the node, move out --- vim.api.nvim_win_set_cursor(win, { cursor[1], cursor[2] + #right }) --- end --- end --- end --- end --- --- do --- local inline_fml_query = lazy_query("latex", [[ --- (inline_formula . (_)? @in) @_root --- ]]) --- vim.keymap.set('i', '<C-F>', with_current_position(toggle_thing(inline_fml_query, "\\(", "\\)"))) --- --- local italics_query = lazy_query("latex", [[ --- ((generic_command --- command: (command_name) @_name --- arg: (curly_group (_)? @in)) --- (#match? @_name "^(\\\\textit|\\\\mathit)$")) @_root --- ]]) --- vim.keymap.set('i', '<C-T>', with_current_position(toggle_thing(italics_query, "\\textit{", "}"))) --- --- local bold_query = lazy_query("latex", [[ --- ((generic_command --- command: (command_name) @_name --- arg: (curly_group (_)? @in)) --- (#match? @_name "^(\\\\textbf|\\\\mathbf)$")) @_root --- ]]) --- vim.keymap.set('i', '<C-B>', with_current_position(toggle_thing(bold_query, "\\textbf{", "}"))) --- end --- --- -- Delete surrounding environment / function call --- do --- local function_query = lazy_query("latex", [[ --- (generic_command --- command: (command_name) @_name --- arg: (curly_group (_) @in)) @_root --- ]]) --- --- vim.keymap.set('n', '<LocalLeader>df', with_current_position(function(curbuf, curwin, cursor) --- local match, node = utls.find_smallest_match(cursor[1] - 1, cursor[2], function_query, curbuf) --- --- if not match then --- print("Not in an function") --- return --- end --- --- local start_row, _, end_row, _ = node:range() --- --- local innode = utls.index_by_name(function_query, match, "in") --- --- -- Correct the cursor position (this is at best a guesstimation) --- local cname = utls.index_by_name(function_query, match, "_name") --- local cstartline = cname:start() --- local srow, scol, _, ecol = innode:range() --- if cstartline == cursor[1] - 1 then --- cursor[2] = math.min(math.max(scol, cursor[2]), ecol - 1) --- local offset = #vim.treesitter.get_node_text(cname, curbuf) + 1 -- the bracket --- vim.api.nvim_win_set_cursor(curwin, { cursor[1], cursor[2] - offset }) --- elseif srow == cursor[1] - 1 then --- vim.api.nvim_win_set_cursor(curwin, { cursor[1] - 1, math.max(scol, cursor[2]) - scol }) --- else --- vim.api.nvim_win_set_cursor(curwin, { cursor[1] - 1, cursor[2] }) --- end --- --- local current = vim.treesitter.get_node_text(innode, curbuf) --- edit.edit_match(curbuf, match, function_query, { _root = current }, start_row, end_row) --- end), { buffer = true }) --- end --- --- --- Tiny mapping to select the node type at cursor and input it --- do --- end diff --git a/after/queries/latex/textobjects.scm b/after/queries/latex/textobjects.scm @@ -1,27 +1,21 @@ -((displayed_equation . (_) @_ibegin (_)? @_iend .) @math.outer - (#make-range! "math.inner" @_ibegin @_iend)) +((displayed_equation . (_) @math.inner.from (_)? @math.inner.to .) @math.outer) -((inline_formula . (_) @_ibegin (_)? @_iend .) @math.outer - (#make-range! "math.inner" @_ibegin @_iend)) +((inline_formula . (_) @math.inner.from (_)? @math.inner.to .) @math.outer) -((math_environment begin: (_) . (_) @_ibegin (_)? @_iend . end: (_)) @math.outer @env.outer - (#make-range! "math.inner" @_ibegin @_iend) - (#make-range! "env.inner" @_ibegin @_iend)) +((math_environment + begin: (_) . (_) @math.inner.from @env.inner.from + (_)? @math.inner.to @env.inner.to . end: (_)) @math.outer @env.outer) -((generic_environment begin: (_) . (_) @_ibegin (_)? @_iend . end: (_)) @env.outer - (#make-range! "env.inner" @_ibegin @_iend)) +((generic_environment + begin: (_) . (_) @env.inner.from + (_)? @env.inner.to . end: (_)) @env.outer) -((section text: _ . (_) @_ibegin) @section.outer - (#make-range! "section.inner" @_ibegin @section.outer)) -((subsection text: _ . (_) @_ibegin) @section.outer - (#make-range! "section.inner" @_ibegin @section.outer)) +((section text: _ . (_) @section.inner.from) @section.outer @section.inner.to) +((subsection text: _ . (_) @section.inner.from) @section.outer @section.inner.to) -((enum_item . (_) @_ibegin) @item.outer - (#make-range! "item.inner" @_ibegin @item.outer)) +((enum_item . (_) @item.inner.from) @item.outer @item.inner.to) (generic_command arg: (curly_group (_) @call.inner)) @call.outer ;; Colored text -((_ (color_reference) @_obegin) - . (curly_group (_) @color.inner) @_oend - (#make-range! "color.outer" @_obegin @_oend)) +((color_reference) @color.outer.from . (curly_group (_) @color.inner) @color.outer.to)