linux-qubasis
linux oasis port as a qubes template
git clone https://9o.is/git/linux-qubasis.git
commit 157cd95425f7e2a8212e3b6b8016fd6fe2e33084 parent f168a45f4a6ed2985b15b1e29712ecaecb2fc433 Author: Jul <jul@9o.is> Date: Tue, 29 Jul 2025 10:35:14 +0800 add initial vis user configs Diffstat:
| M | pkg/vis/build | | | 4 | ++++ |
| A | pkg/vis/config/config/navigation.lua | | | 34 | ++++++++++++++++++++++++++++++++++ |
| A | pkg/vis/config/config/status.lua | | | 36 | ++++++++++++++++++++++++++++++++++++ |
| A | pkg/vis/config/plugins/commentary.lua | | | 147 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | pkg/vis/config/plugins/pairs.lua | | | 477 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | pkg/vis/config/plugins/surround.lua | | | 192 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | pkg/vis/config/themes/default-16.lua | | | 59 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | pkg/vis/config/visrc.lua | | | 16 | ++++++++++++++++ |
8 files changed, 965 insertions(+), 0 deletions(-)
diff --git a/pkg/vis/build b/pkg/vis/build @@ -6,6 +6,7 @@ set -euo pipefail # - lua-devel repodir="$srcdir/repo" +configdir="$outdir/home/user/.config/vis" ( cd $repodir @@ -20,7 +21,10 @@ make -C "$repodir" if [ "$local_install" == "true" ]; then sudo make -C "$repodir" install + configdir="/home/user/.config/vis" else make DESTDIR="$outdir" -C "$repodir" install fi +mkdir -p "$configdir" +cp -r "$srcdir"/config/* "$configdir" diff --git a/pkg/vis/config/config/navigation.lua b/pkg/vis/config/config/navigation.lua @@ -0,0 +1,34 @@ +require('vis') + +local function count_windows() + local count = 0 + for _, _ in vis:windows() do + count = count + 1 + end + return count +end + +vis.events.subscribe(vis.events.INIT, function() + vis:map(vis.modes.NORMAL, '<M-Up>', '<C-w>k') + vis:map(vis.modes.NORMAL, '<M-Down>', '<C-w>j') + vis:map(vis.modes.NORMAL, '<M-Left>', '<C-w>h') + vis:map(vis.modes.NORMAL, '<M-Right>', '<C-w>l') + + vis:map(vis.modes.NORMAL, '<M-q>', function() + local closed = vis.win:close(false) + local modified = vis.win.file.modified + + if not closed then + if modified then + vis:info('Save changes before closing window') + elseif count_windows() == 1 then + vis:exit(0) + else + vis:info('Failed to close window') + end + end + end, 'Close current window') + + vis:map(vis.modes.NORMAL, '<C-t>', ':!tmux new-window -a vis<Enter>') +end) + diff --git a/pkg/vis/config/config/status.lua b/pkg/vis/config/config/status.lua @@ -0,0 +1,36 @@ +local mode_names = { + [vis.modes.NORMAL] = 'n', + [vis.modes.OPERATOR_PENDING] = 'o', + [vis.modes.INSERT] = 'i', + [vis.modes.REPLACE] = 'r', + [vis.modes.VISUAL] = 'v', + [vis.modes.VISUAL_LINE] = 'V', +} + +vis.events.subscribe(vis.events.WIN_STATUS, function(win) + local filename = win.file.name or '[No Name]' + local modified = win.file.modified and '+' or '' + local recording = vis.recording and '@' or '' + local mode = mode_names[vis.mode] + local line = win.selection.line + local total_lines = #win.file.lines + local scroll_percent = 0 + + if total_lines > 0 then + scroll_percent = math.floor((line / total_lines) * 100) + end + + win:status( + string.format("%s", filename), + string.format("%-2s %s%s%s %3d,%-4d %3d%%", + vis.input_queue, + modified, + recording, + mode, + win.selection.line, + win.selection.col, + scroll_percent + ) + ) +end) + diff --git a/pkg/vis/config/plugins/commentary.lua b/pkg/vis/config/plugins/commentary.lua @@ -0,0 +1,147 @@ +-- +-- vis-commentary +-- + +local vis = _G.vis + +local comment_string = { + actionscript='//', ada='--', ansi_c='/*|*/', antlr='//', apdl='!', apl='#', + applescript='--', asp='\'', autoit=';', awk='#', b_lang='//', bash='#', + batch=':', bibtex='%', boo='#', chuck='//', cmake='#', coffeescript='#', + context='%', cpp='//', crystal='#', csharp='//', css='/*|*/', cuda='//', + dart='//', desktop='#', django='{#|#}', dmd='//', dockerfile='#', dot='//', + eiffel='--', elixir='#', erlang='%', faust='//', fennel=';;', fish='#', + forth='|\\', fortran='!', fsharp='//', gap='#', gettext='#', gherkin='#', + glsl='//', gnuplot='#', go='//', groovy='//', gtkrc='#', haskell='--', + html='<!--|-->', icon='#', idl='//', inform='!', ini='#', Io='#', + java='//', javascript='//', json='/*|*/', jsp='//', latex='%', ledger='#', + less='//', lilypond='%', lisp=';', logtalk='%', lua='--', makefile='#', + markdown='<!--|-->', matlab='#', moonscript='--', myrddin='//', + nemerle='//', nsis='#', objective_c='//', pascal='//', perl='#', php='//', + pico8='//', pike='//', pkgbuild='#', prolog='%', props='#', protobuf='//', + ps='%', pure='//', python='#', rails='#', rc='#', rebol=';', rest='.. ', + rexx='--', rhtml='<!--|-->', rstats='#', ruby='#', rust='//', sass='//', + scala='//', scheme=';', smalltalk='"|"', sml='(*)', snobol4='#', sql='#', + tcl='#', tex='%', text='', toml='#', vala='//', vb='\'', vbscript='\'', + verilog='//', vhdl='--', wsf='<!--|-->', xml='<!--|-->', yaml='#', zig='//', + nim='#', julia='#', rpmspec='#', caml='(*|*)' +} + +-- escape all magic characters with a '%' +local function esc(str) + if not str then return "" end + return (str:gsub('[[.+*?$^()%%%]-]', '%%%0')) +end + +-- escape '%' +local function pesc(str) + if not str then return "" end + return str:gsub('%%', '%%%%') +end + +local function rtrim(s) + local n = #s + while n > 0 and s:find("^%s", n) do n = n - 1 end + return s:sub(1, n) + end + +local function comment_line(lines, lnum, prefix, suffix) + if suffix ~= "" then suffix = " " .. suffix end + lines[lnum] = string.gsub(lines[lnum], + "(%s*)(.*)", + "%1" .. pesc(prefix) .. " %2" .. pesc(suffix)) +end + +local function uncomment_line(lines, lnum, prefix, suffix) + local match_str = "^(%s*)" .. esc(prefix) .. "%s?(.*)" .. esc(suffix) + local m = table.pack(lines[lnum]:match(match_str)) + lines[lnum] = m[1] .. rtrim(m[2]) +end + +local function is_comment(line, prefix) + return (line:match("^%s*(.+)"):sub(0, #prefix) == prefix) +end + +local function toggle_line_comment(lines, lnum, prefix, suffix) + if not lines or not lines[lnum] then return end + if not lines[lnum]:match("^%s*(.+)") then return end -- ignore empty lines + if is_comment(lines[lnum], prefix) then + uncomment_line(lines, lnum, prefix, suffix) + else + comment_line(lines, lnum, prefix, suffix) + end +end + +-- if one line inside the block is not a comment, comment the block. +-- only uncomment, if every single line is comment. +local function block_comment(lines, a, b, prefix, suffix) + local uncomment = true + for i=a,b do + if lines[i]:match("^%s*(.+)") and not is_comment(lines[i], prefix) then + uncomment = false + end + end + + if uncomment then + for i=a,b do + if lines[i]:match("^%s*(.+)") then + uncomment_line(lines, i, prefix, suffix) + end + end + else + for i=a,b do + if lines[i]:match("^%s*(.+)") then + comment_line(lines, i, prefix, suffix) + end + end + end +end + +vis:operator_new("gc", function(file, range, pos) + local comment = comment_string[vis.win.syntax] + local prefix, suffix = comment:match('^([^|]+)|?([^|]*)$') + if not prefix then return end + + local c = 0 + local i = 1 + local a = -1 + local b = -1 + for line in file:lines_iterator() do + local line_start = c + local line_finish = c + #line + 1 + if line_start < range.finish and line_finish > range.start then + if a == -1 then + a = i + b = i + else + b = i + end + end + c = line_finish + if c > range.finish then break end + i = i + 1 + end + block_comment(file.lines, a, b, prefix, suffix) + + return range.start +end, "Toggle comment on selected lines") + +vis:map(vis.modes.NORMAL, "gcc", function() + local win = vis.win + local lines = win.file.lines + local comment = comment_string[win.syntax] + if not comment then return end + local prefix, suffix = comment:match('^([^|]+)|?([^|]*)$') + if not prefix then return end + + for sel in win:selections_iterator() do + local lnum = sel.line + local col = sel.col + + toggle_line_comment(lines, lnum, prefix, suffix) + sel:to(lnum, col) -- restore cursor position + end + + win:draw() +end, "Toggle comment on a the current line") + diff --git a/pkg/vis/config/plugins/pairs.lua b/pkg/vis/config/plugins/pairs.lua @@ -0,0 +1,477 @@ +-- SPDX-License-Identifier: GPL-3.0-or-later +-- © 2020 Georgi Kirilov + +require("vis") +local vis = vis + +local l = require("lpeg") + +local progname = ... + +-- XXX: in Lua 5.2 unpack() was moved into table +local unpack = table.unpack or unpack + +local M + +local builtin_textobjects = { + ["["] = { "[" , "]" }, + ["{"] = { "{" , "}" }, + ["<"] = { "<" , ">" }, + ["("] = { "(" , ")" }, + ['"'] = { '"' , '"', name = "A quoted string" }, + ["'"] = { "'" , "'", name = "A single quoted string" }, + ["`"] = { "`" , "`", name = "A backtick delimited string" }, +} + +local builtin_motions = { + ["["] = { ["("] = builtin_textobjects["("], ["{"] = builtin_textobjects["{"] }, + ["]"] = { [")"] = builtin_textobjects["("], ["}"] = builtin_textobjects["{"] }, +} + +local alias = { + ["]"] = "[", + ["}"] = "{", + [">"] = "<", + [")"] = "(", + B = "{", + b = "(", +} + +local function get_pair(key, win) + return M.map[win.syntax] and M.map[win.syntax][key] + or M.map[1] and M.map[1][key] + or builtin_textobjects[key] + or builtin_textobjects[alias[key]] + or not key:match("%w") and {key, key} +end + +local function at_pos(t, pos) + if pos.start + 1 >= t[1] and pos.finish < t[#t] then return t end +end + +local function asymmetric(d, escaped, pos) + local p + local I = l.Cp() + local skip = escaped and escaped + l.P(1) or l.P(1) + if #d == 1 then + p = (d - l.B"\\") * I * ("\\" * l.P(1) + (skip - d))^0 * I * d + else + p = d * I * (skip - d)^0 * I * d + end + return l.Ct(I * p * I) * l.Cc(pos) / at_pos +end + +local function symmetric(d1, d2, escaped, pos) + local I = l.Cp() + local skip = escaped and escaped + l.P(1) or l.P(1) + return l.P{l.Ct(I * d1 * I * ((skip - d1 - d2) + l.V(1))^0 * I * d2 * I) * l.Cc(pos) / at_pos} +end + +local function nth_innermost(t, count) + local start, finish, c = 0, 0, count + if #t == 5 then + start, finish, c = nth_innermost(t[3], count) + 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 + +local precedence = { + [vis.lexers.COMMENT] = {vis.lexers.STRING}, + [vis.lexers.STRING] = {}, +} + +local function selection_range(win, pos) + for selection in win:selections_iterator() do + if selection.pos == pos then + return selection.range + end + end +end + +local prev_match + +local function any_captures(_, position, t) + if type(t) == "table" then + return position, t + end + if t then + prev_match = position - t + end +end + +local function not_past(_, position, pos) + local newpos = prev_match > position and prev_match or position + return newpos <= pos and newpos or false +end + +local function match_at(str, pattern, pos) + prev_match = 0 + local I = l.Cp() + local p = l.P{l.Cmt(l.Ct(I * (pattern/0) * I) * l.Cc(pos) / at_pos * l.Cc(0), any_captures) + 1 * l.Cmt(l.Cc(pos.start + 1), not_past) * l.V(1)} + local t = p:match(str) + if t then return t[1] - 1, t[#t] - 1 end +end + +--- Returns a unique grammar rule name for the given lexer's rule name. +local function rule_id(lexer, name) return lexer._name .. '.' .. name end + +-- get_rule that doesn't assert on me: +local function get_rule(lexer, id) + if lexer._lexer then lexer = lexer._lexer end -- proxy; get true parent + if id == 'whitespace' then return l.V(rule_id(lexer, id)) end -- special case + return (lexer._RULES or lexer._rules)[id] +end + +local function escaping_context(lexer, range, data) + local p + for _, name in ipairs({vis.lexers.COMMENT, vis.lexers.STRING}) do + local rule = get_rule(lexer, name) + if rule then + p = p and p + rule / 0 or rule / 0 + end + end + if not p then return {} end + if not range then return {escape = p} end -- means we are retrying with a "fake" pos + local e1, e2 = match_at(data, p, range) + if not (e1 and e2) then return {escape = p} end + p = nil + local escaped_range = {e1 + 1, e2} + local escaped_data = data:sub(e1 + 1, e2) + for _, level in ipairs({vis.lexers.COMMENT, vis.lexers.STRING}) do + if l.match(get_rule(lexer, level) / 0 * -1, escaped_data) then + for _, name in ipairs(precedence[level]) do + local rule = get_rule(lexer, name) + if rule then + p = p and p + rule / 0 or rule / 0 + end + end + return {escape = p, range = escaped_range} + end + end +end + +local function get_range(key, win, pos, file_data, count) + if not win.syntax then return end + local d = get_pair(key, win) + if not d then return end + local lexer = vis.lexers.load(win.syntax) + repeat + local sel_range = selection_range(win, pos) + local c = escaping_context(lexer, sel_range, file_data) + local range = c.range or {1, #file_data} + local correction = range[1] - 1 + pos = pos - correction + if sel_range then + sel_range.start = sel_range.start - correction + sel_range.finish = sel_range.finish - correction + else + sel_range = {start = pos + 1, finish = pos + 2} + end + local p = d[1] ~= d[2] and symmetric(d[1], d[2], c.escape, sel_range) or asymmetric(d[1], c.escape, sel_range) + local can_abut = d[1] == d[2] and #d[1] == 1 and not (builtin_textobjects[key] or M.map[1][key] or M.map[win.syntax] and M.map[win.syntax][key]) + local skip = c.escape and c.escape + 1 or 1 + local data = c.range and file_data:sub(unpack(c.range)) or file_data + local pattern = l.P{l.Cmt(p * l.Cc(can_abut and 1 or 0), any_captures) + skip * l.Cmt(l.Cc(pos + 1), not_past) * l.V(1)} + prev_match = 0 + local hierarchy = pattern:match(data) + if hierarchy then + local offsets = {nth_innermost(hierarchy, count or 1)} + offsets[3] = nil -- a leftover from calling nth_innermost() with count higher than the hierarchy depth. + for _, o in ipairs(offsets) do + for i, v in ipairs(o) do + o[i] = v - 1 + correction + end + end + return unpack(offsets) + else + pos = correction - 1 + end + until hierarchy or pos < 0 +end + +local function keep_last(acc, cur) + if #acc == 0 then + acc[1] = cur + else + acc[2] = cur + end + return acc +end + +local function barf_linewise(win, content, start, finish) + if vis.mode == vis.modes.VISUAL_LINE then + local skip + if win.syntax then + local rules = vis.lexers.load(win.syntax)._RULES + for _, name in ipairs({vis.lexers.COMMENT, vis.lexers.STRING}) do + if rules[name] then + skip = skip and skip + rules[name] / 0 or rules[name] / 0 + end + end + end + skip = skip and skip + 1 or 1 + start, finish = unpack(l.match(l.Cf(l.Cc({}) * (l.Cp() * l.P"\n" + skip * l.Cmt(l.Cc(finish), not_past))^0, keep_last), content, start + 1)) + end + return start, finish +end + +local function get_delimiters(key, win, pos, count) + local d = get_pair(key, win) + if not d or type(d[1]) == "string" and type(d[2]) == "string" then return d end + local content = win.file:content(0, win.file.size) + local start, finish = get_range(key, win, pos, content, count or vis.count) + if start and finish then + return {win.file:content(start[1], start[2] - start[1]), win.file:content(finish[1], finish[2] - finish[1]), d[3], d.prompt} + elseif #d > 2 then + return {nil, nil, d[3], d.prompt} + end +end + +local function outer(win, pos, content, count) + local start, finish = get_range(M.key, win, pos, content, count) + if start and finish then return start[1], finish[2] end +end + +local function inner(win, pos, content, count) + local start, finish = get_range(M.key, win, pos, content, count) + if start and finish then return barf_linewise(win, content, start[2], finish[1]) end +end + +local function opening(win, pos, content, count) + local start, _ = get_range(M.key, win, pos, content, count) + if not start then return pos end + local exclusive = vis.mode == vis.modes.OPERATOR_PENDING and pos >= start[2] or vis.mode == vis.modes.VISUAL and pos < start[2] - 1 + return start[2] - 1 + (exclusive and 1 or 0), vis.mode == vis.modes.OPERATOR_PENDING and pos >= start[2] +end + +local function closing(win, pos, content, count) + local _, finish = get_range(M.key, win, pos, content, count) + if not finish then return pos end + local exclusive = vis.mode == vis.modes.VISUAL and pos > finish[1] + return finish[1] - (exclusive and 1 or 0) +end + +local done_once + +local function bail_early() + if vis.count and vis.count > 1 then + if done_once then + done_once = nil + return true + else + done_once = true + end + end + return false +end + +local function win_map(textobject, prefix, binding, help) + return function(win) + if not textobject then + win:map(vis.modes.NORMAL, prefix, binding, help) + end + win:map(vis.modes.VISUAL, prefix, binding, help) + win:map(vis.modes.OPERATOR_PENDING, prefix, binding, help) + end +end + +local function bind_builtin(key, execute, id) + return function() + M.key = key + execute(vis, id) + end +end + +local function prep(func) + return function(win, pos) + if bail_early() then return pos end + local content = win.file:content(0, win.file.size) + local start, finish = func(win, pos, content, vis.count) + if not vis.count and vis.mode == vis.modes.VISUAL or start and not finish then + local old = selection_range(win, pos) + local same_or_smaller = finish and start >= old.start and finish <= old.finish + local didnt_move = not finish and start == pos + if same_or_smaller or didnt_move then + start, finish = func(win, pos, content, 2) + end + end + return start, finish + end +end + +local function h(msg) + return string.format("|@%s| %s", progname, msg) +end + +local mappings = {} + +local function new(execute, register, prefix, handler, help) + local id = register(vis, prep(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 + table.insert(mappings, win_map(execute == vis.textobject, prefix, binding, help)) + local builtin = execute == vis.motion and builtin_motions[prefix] or builtin_textobjects + for key, _ in pairs(builtin) do + local d = builtin[key] + local simple = type(d[1]) == "string" and type(d[2]) == "string" and d[1] .. d[2] + local hlp = (execute == vis.motion and help or "") .. (d.name or (simple or "pattern-delimited") .. " block") + if execute ~= vis.textobject then + vis:map(vis.modes.NORMAL, prefix .. key, bind_builtin(key, execute, id), h(hlp)) + end + local variant = prefix == M.prefix.outer and " (outer variant)" or prefix == M.prefix.inner and " (inner variant)" or "" + vis:map(vis.modes.VISUAL, prefix .. key, bind_builtin(key, execute, id), h(hlp and hlp .. variant or help)) + vis:map(vis.modes.OPERATOR_PENDING, prefix .. key, bind_builtin(key, execute, id), h(hlp and hlp .. variant or help)) + end + end + return id +end + +vis.events.subscribe(vis.events.WIN_OPEN, function(win) + for _, map_keys in ipairs(mappings) do + map_keys(win) + end + local function delete_pair(direction, do_delete) + return function() + local locations = {} + for selection in win:selections_iterator() do + local pos = selection.pos + if pos - direction < 0 then return end + local key = win.file:content(pos - direction, 1) + local p = M.map[win.syntax] and M.map[win.syntax][key] + or M.map[1] and M.map[1][key] + or builtin_textobjects[key] + or builtin_textobjects[alias[key]] + local left, len = pos - direction, #key + if p and (key == p[1] or key == p[2]) then + M.key = p[1] + local start, finish = inner(win, pos, win.file:content(0, win.file.size)) + if start and start == finish and pos == start then + left = start - #p[1] + len = #p[1] + #p[2] + end + end + locations[selection.number] = len - 1 + if do_delete then + win.file:delete(left, len) + selection.pos = left + end + end + return locations + end + end + M.unpair[win] = delete_pair(1) + if M.autopairs and (not vis_parkour or vis_parkour(win)) then + win:map(vis.modes.INSERT, "<Backspace>", delete_pair(1, true)) + win:map(vis.modes.INSERT, "<Delete>", delete_pair(0, true)) + end +end) + +vis.events.subscribe(vis.events.WIN_CLOSE, function(win) + M.unpair[win] = nil +end) + +vis.events.subscribe(vis.events.INIT, function() + local function cmp(_, _, c1, c2) return c1 == c2 end + local function casecmp(_, _, c1, c2) return c1:lower() == c2:lower() end + local function end_tag(s1, s2, cmpfunc) return l.Cmt(s1 * l.Cb("t") * l.C((1 - l.P(s2))^1) * s2, cmpfunc) end + local tex_environment = {"\\begin{" * l.Cg(l.R("az", "AZ")^1, "t") * "}", end_tag("\\end{", "}", cmp), {"\\begin{\xef\xbf\xbd}", "\\end{\xef\xbf\xbd}"}, prompt = "environment name"} + local tag_name = (l.S"_:" + l.R("az", "AZ")) * (l.R("az", "AZ", "09") + l.S"_:.-")^0 + local noslash = {--[[implicit:]] p=1, dt=1, dd=1, li=1, --[[void:]] area=1, base=1, br=1, col=1, embed=1, hr=1, img=1, input=1, link=1, meta=1, param=1, source=1, track=1, wbr=1} + local function is_not(_, _, v) return v ~= 1 end + local html_tag = {"<" * l.Cg(l.Cmt(tag_name / string.lower / noslash, is_not), "t") * (1 - l.S"><")^0 * (">" - l.B"/"), end_tag("</", ">", casecmp), {"<\xef\xbf\xbd>", "</\xef\xbf\xbd>"}, prompt = "tag name"} + local xml_tag = {"<" * l.Cg(tag_name, "t") * (1 - l.S"><")^0 * (">" - l.B"/"), end_tag("</", ">", cmp), {"<\xef\xbf\xbd>", "</\xef\xbf\xbd>"}, prompt = "tag name", name = "<tag></tag> block"} + local function any_pair(set, default) return {l.Cg(l.S(set), "s"), l.Cmt(l.Cb("s") * l.C(1), function(_, _, c1, c2) return builtin_textobjects[c1][2] == c2 end), builtin_textobjects[default]} end + local any_bracket = any_pair("({[", "(") + local presets = { + {t = xml_tag}, + xml = {t = xml_tag}, + html = {t = html_tag}, + markdown = {t = html_tag, ["_"] = {"_", "_"}, ["*"] = {"*", "*"}}, + asp = {t = html_tag}, + jsp = {t = html_tag}, + php = {t = html_tag}, + rhtml = {t = html_tag}, + scheme = {b = any_bracket}, + clojure = {b = any_bracket}, + fennel = {b = any_bracket}, + latex = {e = tex_environment}, + } + 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 + for key, d in pairs(M.map[1]) do + builtin_textobjects[key] = {d[1], d[2], name = d.name} + builtin_motions[M.prefix.opening][key] = builtin_textobjects[key] + builtin_motions[M.prefix.closing][key] = builtin_textobjects[key] + end + + M.motion = { + opening = new(vis.motion, vis.motion_register, M.prefix.opening, opening, "Move cursor to the beginning of a "), + closing = new(vis.motion, vis.motion_register, M.prefix.closing, closing, "Move cursor to the end of a "), + } + 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)"), + } + + if M.autopairs then + vis.events.subscribe(vis.events.INPUT, function(key) + if vis.mode == vis.modes.REPLACE then return end + local win = vis.win + if vis_parkour and vis_parkour(win) then return end + local p = M.map[win.syntax] and M.map[win.syntax][key] + or M.map[1] and M.map[1][key] + or builtin_textobjects[key] + or builtin_textobjects[alias[key]] + if not p then return end + if M.no_autopairs[key] and M.no_autopairs[key][win.syntax or ""] then return end + for selection in win:selections_iterator() do + local pos = selection.pos + M.key = key + local _, finish = outer(win, pos, win.file:content(0, win.file.size)) + if key == p[1] and p[1] ~= p[2] or p[1] == p[2] and pos + 1 ~= finish then + win.file:insert(pos, p[2]) + selection.pos = pos + elseif key == p[2] and pos + 1 == finish then + win.file:delete(pos, #p[2]) + selection.pos = pos + end + end + end) + end + +end) + +M = { + map = {}, + get_pair = get_delimiters, + get_range_inner = inner, + get_range_outer = outer, + prefix = {outer = "a", inner = "i", opening = "[", closing = "]"}, + autopairs = true, + no_autopairs = {["'"] = {markdown = true, [""] = true}}, + unpair = {} +} + +vis_pairs = M + +return M diff --git a/pkg/vis/config/plugins/surround.lua b/pkg/vis/config/plugins/surround.lua @@ -0,0 +1,192 @@ +-- SPDX-License-Identifier: GPL-3.0-or-later +-- © 2020 Georgi Kirilov + +require("vis") +local vis = vis + +local progname = ... + +local M = { + prefix = {add = {"ys", "S"}, change = {"cs", "C"}, delete = {"ds", "D"}}, +} + +local builtin_textobjects = { + ["["] = {{ "[" , "]" }, id = 7}, -- +/VIS_TEXTOBJECT_OUTER_SQUARE_BRACKET vis.h + ["{"] = {{ "{" , "}" }, id = 9}, -- +/VIS_TEXTOBJECT_OUTER_CURLY_BRACKET vis.h + ["<"] = {{ "<" , ">" }, id = 11}, -- +/VIS_TEXTOBJECT_OUTER_ANGLE_BRACKET vis.h + ["("] = {{ "(" , ")" }, id = 13}, -- +/VIS_TEXTOBJECT_OUTER_PARENTHESIS vis.h + ['"'] = {{ '"' , '"' }, id = 15}, -- +/VIS_TEXTOBJECT_OUTER_QUOTE vis.h + ["'"] = {{ "'" , "'" }, id = 17}, -- +/VIS_TEXTOBJECT_OUTER_SINGLE_QUOTE vis.h + ["`"] = {{ "`" , "`" }, id = 19}, -- +/VIS_TEXTOBJECT_OUTER_BACKTICK vis.h + {{ "" , "" }, id = 28}, -- +/VIS_TEXTOBJECT_INVALID vis.h +} + +local aliases = {} +for key, data in pairs(builtin_textobjects) do + local pair = data[1] aliases[pair[2]] = key ~= pair[2] and data or nil +end +for alias, data in pairs(aliases) do + builtin_textobjects[alias] = data +end +for alias, key in pairs({ + B = "{", + b = "(", +}) do builtin_textobjects[alias] = builtin_textobjects[key] end + +local function get_pair(key) return builtin_textobjects[key] and builtin_textobjects[key][1] end + +local function take_param(_, d) + if d and type(d[3]) == "table" then + if #d[3] == 2 then + if table.concat(d[3]):find("\xef\xbf\xbd", 1, true) then + local status, out = vis:pipe(nil, nil, "vis-menu" .. (d[4] and " -p '" .. d[4] .. ":'" or "")) + if status == 0 then + local param = out:sub(1, -2) + return {d[3][1]:gsub("\xef\xbf\xbd", param), d[3][2]:gsub("\xef\xbf\xbd", param)} + end + else + return d[3] + end + end + else + return d + end +end + +local function adjust_spacing(file, range, d) + local padding = "" + if vis.mode == vis.modes.VISUAL_LINE then + padding = d[1] ~= "\n" and "\n" or padding + elseif vis.mode ~= vis.modes.VISUAL then + local trailing = file:content(range):match("(%s*)$") + if #trailing > 0 then + range.finish = range.finish - #trailing + end + end + return padding +end + +local function add(file, range, pos) + if range.finish <= range.start then return pos end + local d = take_param(vis.win, get_pair(M.key[1], pos)) + if not d then return pos end + local padding = adjust_spacing(file, range, d) + file:insert(range.finish, d[2] .. padding) + file:insert(range.start, d[1] .. padding) + return range.start +end + +local function escape(text) + return text:gsub("[][^$)(%%.*+?-]", "%%%0") +end + +local function delimiters_in_place(file, range, pos, key, get_padding) + local start, slen, finish, flen + if vis.mode == vis.modes.VISUAL_LINE then + local block = file:content(range) + vis.count = nil + local d = get_pair(key, range.start + block:find("\n", 1, true)) + if not (d and d[1] and d[2]) then return end + local d1, d2 = escape(d[1]), escape(d[2]) + local sl = table.pack(block:match("^()[ \t]*()" .. d1 .. "[ \t]-\n()")) + if #sl == 0 then + sl = table.pack(block:match("()[ \t]*()" .. d1 .. "[ \t]-()\n")) + end + local el = table.pack(block:match("()\n[ \t]*()" .. d2 .. "()[ \t]*\n$")) + if #el == 0 then + el = table.pack(block:match("\n[ \t]*()()" .. d2 .. "[ \t]*()[^\n]-\n$")) + end + if not (#sl > 0 and #el > 0) then return end + start = range.start + sl[get_padding and 1 or 2] - 1 + slen = get_padding and sl[3] - sl[1] or #d[1] + finish = range.start + el[get_padding and 1 or 2] - 1 + flen = get_padding and el[3] - el[1] or #d[2] + else + local d = get_pair(key, pos) + if not (d and d[1] and d[2]) then return end + if file:content(range.start, #d[1]):find(d[1], 1, true) + and file:content(range.finish - #d[2], #d[2]):find(d[2], 1, true) then + start, slen, finish, flen = range.start, #d[1], range.finish - #d[2], #d[2] + end + end + return start, slen, finish, flen +end + +local function change(file, range, pos) + if range.finish <= range.start then return pos end + local start, slen, finish, flen = delimiters_in_place(file, range, pos, M.key[1]) + if not start then return pos end + local n = take_param(vis.win, get_pair(M.key[2], pos)) + if not n then return pos end + file:delete(finish, flen) + file:insert(finish, n[2]) + file:delete(start, slen) + file:insert(start, n[1]) + if pos < range.start + slen then + return (pos < range.start + #n[1] and pos < range.start + slen - 1 or slen == 1) and pos or range.start + #n[1] - 1 + elseif pos >= range.finish - flen then + return (pos < range.finish - flen + #n[2] and pos < range.finish - 1) and pos - slen + #n[1] or range.finish - slen - flen + #n[1] + #n[2] - 1 + else + return pos - slen + #n[1] + end +end + +local function delete(file, range, pos) + if range.finish <= range.start then return pos end + local start, slen, finish, flen = delimiters_in_place(file, range, pos, M.key[1], true) + if not start then return pos end + file:delete(finish, flen) + file:delete(start, slen) + return range.start +end + +local function outer(key) + return builtin_textobjects[key] and builtin_textobjects[key].id or builtin_textobjects[1].id +end + +local function va_call(id, nargs, needs_range) + return function(keys) + if #keys < nargs then return -1 end + if #keys == nargs then + M.key = {} + for key in keys:gmatch(".") do table.insert(M.key, key) end + vis:operator(id) + if needs_range then + vis:textobject(outer(M.key[1])) + end + end + return #keys + end +end + +local function h(msg) + return string.format("|@%s| %s", progname, msg) +end + +local function operator_new(prefix, handler, nargs, help) + local id = vis:operator_register(handler) + if id < 0 then + return false + end + if type(prefix) == "table" then + local needs_range = ({[change] = true, [delete] = true})[handler] + if prefix[1] then vis:map(vis.modes.NORMAL, prefix[1], va_call(id, nargs, needs_range), h(help)) end + if prefix[2] then vis:map(vis.modes.VISUAL, prefix[2], va_call(id, nargs), h(help)) end + end + return id +end + +vis.events.subscribe(vis.events.INIT, function() + M.operator = { + add = operator_new(M.prefix.add, add, 1, "Add delimiters at range boundaries"), + change = operator_new(M.prefix.change, change, 2, "Change delimiters at range boundaries"), + delete = operator_new(M.prefix.delete, delete, 1, "Delete delimiters at range boundaries"), + } + local vis_pairs = package.loaded["pairs"] or package.loaded["vis-pairs"] + if vis_pairs then + get_pair = function(key, pos) return vis_pairs.get_pair(key, vis.win, pos) end + outer = function(key) vis_pairs.key = key return vis_pairs.textobject.outer end + end +end) + +return M diff --git a/pkg/vis/config/themes/default-16.lua b/pkg/vis/config/themes/default-16.lua @@ -0,0 +1,59 @@ +local lexers = vis.lexers + +local colors = { + ['base00'] = '#181825', + ['base01'] = '#1e1e2e ', + ['base02'] = '#313244', + ['base03'] = '#6c7086', + ['base04'] = '#a6adc8', + ['base05'] = '#cdd6f4', + ['base06'] = '#f5e0dc', + ['base07'] = '#b4befe', + ['base08'] = '#f38ba8', + ['base09'] = '#fab387', + ['base0A'] = '#f9e2af', + ['base0B'] = '#a6e3a1', + ['base0C'] = '#94e2d5', + ['base0D'] = '#89b4fa', + ['base0E'] = '#cba6f7', + ['base0F'] = '#f2cdcd', +} + +lexers.colors = colors + +local fg = ',fore:'..colors.base05..',' +local bg = ',back:'..colors.base01..',' +local cmnt = ',fore:'..colors.base03..',' + +lexers.STYLE_DEFAULT = bg..fg +lexers.STYLE_NOTHING = bg +lexers.STYLE_CLASS = 'fore:'..colors.base0A +lexers.STYLE_COMMENT = cmnt +lexers.STYLE_CONSTANT = fg +lexers.STYLE_DEFINITION = fg +lexers.STYLE_ERROR = 'fore:'..colors.base08 +lexers.STYLE_FUNCTION = fg +lexers.STYLE_KEYWORD = 'fore:'..colors.base0D +lexers.STYLE_LABEL = fg +lexers.STYLE_NUMBER = fg +lexers.STYLE_OPERATOR = fg +lexers.STYLE_REGEX = fg +lexers.STYLE_STRING = 'fore:'..colors.base0B +lexers.STYLE_PREPROCESSOR = fg +lexers.STYLE_TAG = fg +lexers.STYLE_TYPE = fg +lexers.STYLE_VARIABLE = fg +lexers.STYLE_WHITESPACE = fg +lexers.STYLE_EMBEDDED = fg +lexers.STYLE_IDENTIFIER = fg + +lexers.STYLE_LINENUMBER = cmnt +lexers.STYLE_CURSOR = 'reverse' +lexers.STYLE_CURSOR_PRIMARY = bg..fg +lexers.STYLE_CURSOR_LINE = bg +lexers.STYLE_COLOR_COLUMN = bg +lexers.STYLE_SELECTION = 'reverse' +lexers.STYLE_STATUS = cmnt..bg +lexers.STYLE_STATUS_FOCUSED = fg..bg +lexers.STYLE_SEPARATOR = cmnt +lexers.STYLE_EOF = cmnt diff --git a/pkg/vis/config/visrc.lua b/pkg/vis/config/visrc.lua @@ -0,0 +1,16 @@ +require('vis') +require('plugins/commentary') +require('plugins/pairs') +require('plugins/surround') +require('config/status') +require('config/navigation') + +vis.events.subscribe(vis.events.INIT, function() + vis:command('set theme default-16') + vis:command('set autoindent on') +end) + +vis.events.subscribe(vis.events.WIN_OPEN, function(win) + vis:command('set tabwidth 4') + vis:command('set expandtab on') +end)