-- SPDX-License-Identifier: GPL-3.0-or-later
-- © 2020 Georgi Kirilov
require("vis")
local vis = vis
local l = require("lpeg")
local M
local builtin_textobjects = {
["["] = { "[" , "]" },
["{"] = { "{" , "}" },
["<"] = { "<" , ">" },
["("] = { "(" , ")" },
['"'] = { '"' , '"' },
["'"] = { "'" , "'" },
["`"] = { "`" , "`" },
}
local builtin_motions = {
["["] = { ["("] = true, ["{"] = true },
["]"] = { [")"] = true, ["}"] = true },
}
local aliases = {}
for key, pair in pairs(builtin_textobjects) do aliases[pair[2]] = key ~= pair[2] and pair or nil end
for alias, pair in pairs(aliases) do builtin_textobjects[alias] = pair end
for alias, key in pairs({
B = "{",
b = "(",
}) do builtin_textobjects[alias] = builtin_textobjects[key] end
local function get_pair(key)
return M.map[vis.win.syntax] and M.map[vis.win.syntax][key]
or M.map[1] and M.map[1][key]
or builtin_textobjects[key]
or not key:match("%w") and {key, key}
end
local function asymmetric(d, at_pos)
local p
local I = l.Cp()
if #d == 1 then
p = (d - l.B"\\") * I * ("\\" * l.P(1) + (l.P(1) - d))^0 * I * d
else
p = d * I * (l.P(1) - d)^0 * I * d
end
return l.Ct(I * p * I) / at_pos
end
local function symmetric(d1, d2, at_pos)
local I = l.Cp()
return l.P{l.Ct(I * d1 * I * ((l.P(1) - d1 - d2) + l.V(1))^0 * I * d2 * I) / at_pos}
end
local function nth(t)
if type(t) == "table" then
local start, finish, c = 0, 0, vis.count or 1
if #t == 5 then
start, finish, c = nth(t[3])
end
if c then
return {t[1], t[2]}, {t[#t - 1], t[#t]}, c > 1 and c - 1 or nil
end
return start, finish
end
end
local function any_captures(_, position, t)
if type(t) == "table" then
return position, t
end
end
local done_once
local function get_range(key, file, pos)
if vis.count and vis.count > 1 then
if done_once then
done_once = nil
return
else
done_once = true
end
end
local d = get_pair(key)
if not d then return end
local function at_pos(t)
if pos + 1 >= t[1] and pos + 1 < t[#t] then return t end
end
local p = d[1] ~= d[2] and symmetric(d[1], d[2], at_pos) or asymmetric(d[1], at_pos)
local pattern = l.P{l.Cmt(p, any_captures) + 1 * l.V(1)}
local offsets = {nth(l.match(pattern, file:content(0, file.size)))}
offsets[3] = nil
for _, o in ipairs(offsets) do
for i, v in ipairs(o) do
o[i] = v - 1
end
end
return unpack(offsets)
end
local function get_delimiters(key, pos)
local d = get_pair(key)
if not d or type(d[1]) == "string" and type(d[2]) == "string" then return d end
local start, finish = get_range(key, vis.win.file, pos)
if not (start and finish) then return end
return {vis.win.file:content(start[1], start[2] - start[1]), vis.win.file:content(finish[1], finish[2] - finish[1])}
end
local function outer(win, pos)
local start, finish = get_range(M.key, win.file, pos)
if not (start and finish) then return end
return start[1], finish[2]
end
local function inner(win, pos)
local start, finish = get_range(M.key, win.file, pos)
if not (start and finish) then return end
return start[2], finish[1]
end
local function opening(win, pos)
local start, _ = get_range(M.key, win.file, pos)
if start then
if pos == start[2] - 1 then
start, _ = get_range(M.key, win.file, start[1] - 1)
end
if start then
return start[2] - 1
end
end
return pos
end
local function closing(win, pos)
local _, finish = get_range(M.key, win.file, pos)
if finish then
if pos == finish[1] then
_, finish = get_range(M.key, win.file, finish[2])
end
if finish then
return finish[1]
end
end
return pos
end
local function new(execute, register, prefix, handler, help)
local id = register(vis, handler)
if id < 0 then
return false
end
if prefix then
local binding = function(keys)
if #keys < 1 then return -1 end
if #keys == 1 then
M.key = keys
execute(vis, id)
end
return #keys
end
if execute ~= vis.textobject then
vis:map(vis.modes.NORMAL, prefix, binding, help)
end
vis:map(vis.modes.VISUAL, prefix, binding, help)
vis:map(vis.modes.OPERATOR_PENDING, prefix, binding, help)
local builtin = execute == vis.motion and builtin_motions[prefix] or builtin_textobjects
for key, _ in pairs(builtin) do
if execute ~= vis.textobject then
vis:unmap(vis.modes.NORMAL, prefix..key)
end
vis:unmap(vis.modes.VISUAL, prefix..key)
vis:unmap(vis.modes.OPERATOR_PENDING, prefix..key)
end
end
return id
end
vis.events.subscribe(vis.events.INIT, function()
M.motion = {
opening = new(vis.motion, vis.motion_register, M.prefix.opening, opening, "Move cursor to the beginning of a delimited block"),
closing = new(vis.motion, vis.motion_register, M.prefix.closing, closing, "Move cursor to the end of a delimited block"),
}
M.textobject = {
inner = new(vis.textobject, vis.textobject_register, M.prefix.inner, inner, "Delimited block (inner variant)"),
outer = new(vis.textobject, vis.textobject_register, M.prefix.outer, outer, "Delimited block (outer variant)"),
}
local tag = {"<" * l.Cg(l.C((l.S"_:" + l.R("az", "AZ")) * (l.R("az", "AZ", "09") + l.S"_:.-")^0), "t") * (1 - l.S"><")^0 * (">" - l.B"/"), l.Cmt("</" * l.Cb("t") * l.C((1 - l.P">")^1) * ">", function(_, _, c1, c2) return c1 == c2 end)}
local any_bracket = {l.Cg(l.C(l.S"([{"), "s"), l.Cmt(l.Cb("s") * l.C(1), function(_, _, c1, c2) return builtin_textobjects[c1][2] == c2 end)}
local presets = {
html = {t = tag},
xml = {t = tag},
scheme = {b = any_bracket},
lisp = {b = any_bracket},
clojure = {b = any_bracket},
}
for syntax, bindings in pairs(presets) do
if not M.map[syntax] then
M.map[syntax] = bindings
else
for key, pattern in pairs(bindings) do
if not M.map[syntax][key] then M.map[syntax][key] = pattern end
end
end
end
end)
M = {
map = {},
get_pair = get_delimiters,
prefix = {outer = "a", inner = "i", opening = "[", closing = "]"},
}
return M