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:
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)