diff options
Diffstat (limited to 'config/essentials')
| -rw-r--r-- | config/essentials/vis/Makefile | 9 | ||||
| -rw-r--r-- | config/essentials/vis/format.lua | 217 | ||||
| -rw-r--r-- | config/essentials/vis/fzf-mru.lua | 75 | ||||
| -rw-r--r-- | config/essentials/vis/fzf-open.lua | 81 | ||||
| -rw-r--r-- | config/essentials/vis/vis-go.lua | 102 | ||||
| -rw-r--r-- | config/essentials/vis/vis-ultisnips/init.lua | 149 | ||||
| -rw-r--r-- | config/essentials/vis/vis-ultisnips/snipmate-parser.lua | 128 | ||||
| -rw-r--r-- | config/essentials/vis/vis-ultisnips/testlpeg-snipmate.lua | 160 | ||||
| -rw-r--r-- | config/essentials/vis/vis-ultisnips/testlpeg-ultisnips.lua | 230 | ||||
| -rw-r--r-- | config/essentials/vis/vis-ultisnips/ultisnips-parser.lua | 211 | ||||
| -rw-r--r-- | config/essentials/vis/visrc.lua | 33 | ||||
| -rw-r--r-- | config/essentials/vis/yank-highlight.lua | 37 | 
12 files changed, 1311 insertions, 121 deletions
diff --git a/config/essentials/vis/Makefile b/config/essentials/vis/Makefile index 1599b58..f2d386b 100644 --- a/config/essentials/vis/Makefile +++ b/config/essentials/vis/Makefile @@ -1,11 +1,6 @@ -.PHONY: check format all +.PHONY: check  -LUA_FILES := $(shell find . -name "*.lua") - -all: format check +LUA_FILES := $(shell find . -type f -name "*.lua")  check:  	luacheck --no-color --globals=vis -- $(LUA_FILES) - -format: -	lua-format -i $(LUA_FILES) diff --git a/config/essentials/vis/format.lua b/config/essentials/vis/format.lua index 15488dd..e39320e 100644 --- a/config/essentials/vis/format.lua +++ b/config/essentials/vis/format.lua @@ -1,131 +1,134 @@ -local global_options = {check_same = true} +local global_options = { check_same = true }  local function stdio_formatter(cmd, options) -    local function apply(win, range, pos) -        local size = win.file.size -        local all = {start = 0, finish = size} -        if range == nil then range = all end -        local command = type(cmd) == 'function' and cmd(win, range, pos) or cmd -        local check_same = (options and options.check_same ~= nil) and -                               options.check_same or global_options.check_same -        local check = check_same == true or -                          (type(check_same) == 'number' and check_same >= size) -        local status, out, err = vis:pipe(win.file, all, command) -        if status ~= 0 then -            vis:message(err) -        elseif out == nil or out == '' then -            vis:info('No output from formatter') -        elseif not check or win.file:content(all) ~= out then -            local start, finish = range.start, range.finish -            win.file:delete(range) -            win.file:insert(start, -                            out:sub(start + 1, finish + (out:len() - size))) -        end -        return pos -    end -    return { -        apply = apply, -        options = options or {ranged = type(cmd) == 'function'} -    } +	local function apply(win, range, pos) +		local size = win.file.size +		local all = { start = 0, finish = size } +		if range == nil then +			range = all +		end +		local command = type(cmd) == "function" and cmd(win, range, pos) or cmd +		local check_same = (options and options.check_same ~= nil) and options.check_same or global_options.check_same +		local check = check_same == true or (type(check_same) == "number" and check_same >= size) +		local status, out, err = vis:pipe(win.file, all, command) +		if status ~= 0 then +			vis:message(err) +		elseif out == nil or out == "" then +			vis:info("No output from formatter") +		elseif not check or win.file:content(all) ~= out then +			local start, finish = range.start, range.finish +			win.file:delete(range) +			win.file:insert(start, out:sub(start + 1, finish + (out:len() - size))) +		end +		return pos +	end +	return { +		apply = apply, +		options = options or { ranged = type(cmd) == "function" }, +	}  end  local function with_filename(win, option) -    if win.file.path then -        return option .. "'" .. win.file.path:gsub("'", "\\'") .. "'" -    else -        return '' -    end +	if win.file.path then +		return option .. "'" .. win.file.path:gsub("'", "\\'") .. "'" +	else +		return "" +	end  end  local formatters = {}  formatters = { -    bash = stdio_formatter(function(win) -        return 'shfmt ' .. with_filename(win, '--filename ') .. ' -' -    end), -    csharp = stdio_formatter('dotnet csharpier'), -    go = stdio_formatter('gofmt'), -    lua = { -        pick = function(win) -            local _, out = vis:pipe(win.file, -                                    {start = 0, finish = win.file.size}, -                                    'test -e .lua-format && echo luaformatter || echo stylua') -            return formatters[out:gsub('\n$', '')] -        end -    }, -    luaformatter = stdio_formatter('lua-format'), -    markdown = stdio_formatter(function(win) -        if win.options and win.options.colorcolumn ~= 0 then -            return 'prettier --parser markdown --prose-wrap always ' .. -                       ('--print-width ' .. (win.options.colorcolumn - 1) .. ' ') .. -                       with_filename(win, '--stdin-filepath ') -        else -            return 'prettier --parser markdown ' .. -                       with_filename(win, '--stdin-filepath ') -        end -    end, {ranged = false}), -    powershell = stdio_formatter([[ +	bash = stdio_formatter(function(win) +		return "shfmt " .. with_filename(win, "--filename ") .. " -" +	end), +	csharp = stdio_formatter("dotnet csharpier"), +	go = stdio_formatter("gofmt"), +	lua = { +		pick = function(win) +			local _, out = vis:pipe( +				win.file, +				{ start = 0, finish = win.file.size }, +				"test -e .lua-format && echo luaformatter || echo stylua" +			) +			return formatters[out:gsub("\n$", "")] +		end, +	}, +	luaformatter = stdio_formatter("lua-format"), +	markdown = stdio_formatter(function(win) +		if win.options and win.options.colorcolumn ~= 0 then +			return "prettier --parser markdown --prose-wrap always " +				.. ("--print-width " .. (win.options.colorcolumn - 1) .. " ") +				.. with_filename(win, "--stdin-filepath ") +		else +			return "prettier --parser markdown " .. with_filename(win, "--stdin-filepath ") +		end +	end, { ranged = false }), +	powershell = stdio_formatter([[      "$( (command -v powershell.exe || command -v pwsh) 2>/dev/null )" -c '          Invoke-Formatter  -ScriptDefinition `            ([IO.StreamReader]::new([Console]::OpenStandardInput()).ReadToEnd())        ' | sed -e :a -e '/^[\r\n]*$/{$d;N;};/\n$/ba'    ]]), -    rust = stdio_formatter('rustfmt'), -    stylua = stdio_formatter(function(win, range) -        if range and (range.start ~= 0 or range.finish ~= win.file.size) then -            return -                'stylua -s --range-start ' .. range.start .. ' --range-end ' .. -                    range.finish .. with_filename(win, ' --stdin-filepath ') .. -                    ' -' -        else -            return 'stylua -s ' .. with_filename(win, '--stdin-filepath ') .. -                       ' -' -        end -    end), -    text = stdio_formatter(function(win) -        if win.options and win.options.colorcolumn ~= 0 then -            return 'fmt -w ' .. (win.options.colorcolumn - 1) -        else -            return 'fmt' -        end -    end, {ranged = false}) +	rust = stdio_formatter("rustfmt"), +	stylua = stdio_formatter(function(win, range) +		if range and (range.start ~= 0 or range.finish ~= win.file.size) then +			return "stylua -s --range-start " +				.. range.start +				.. " --range-end " +				.. range.finish +				.. with_filename(win, " --stdin-filepath ") +				.. " -" +		else +			return "stylua -s " .. with_filename(win, "--stdin-filepath ") .. " -" +		end +	end), +	text = stdio_formatter(function(win) +		if win.options and win.options.colorcolumn ~= 0 then +			return "fmt -w " .. (win.options.colorcolumn - 1) +		else +			return "fmt" +		end +	end, { ranged = false }),  }  local function getwinforfile(file) -    for win in vis:windows() do -        if win and win.file and win.file.path == file.path then -            return win -        end -    end +	for win in vis:windows() do +		if win and win.file and win.file.path == file.path then +			return win +		end +	end  end  local function apply(file_or_keys, range, pos) -    local win = -        type(file_or_keys) ~= 'string' and getwinforfile(file_or_keys) or -            vis.win -    local ret = type(file_or_keys) ~= 'string' and function() return pos end or -                    function() return 0 end -    pos = pos or win.selection.pos -    local formatter = formatters[win.syntax] -    if formatter and formatter.pick then formatter = formatter.pick(win) end -    if formatter == nil then -        vis:info('No formatter for ' .. win.syntax) -        return ret() -    end -    if range ~= nil and not formatter.options.ranged and range.start ~= 0 and -        range.finish ~= win.file.size then -        vis:info('Formatter for ' .. win.syntax .. ' does not support ranges') -        return ret() -    end -    pos = formatter.apply(win, range, pos) or pos -    vis:insert('') -- redraw and friends don't work -    win.selection.pos = pos -    return ret() +	local win = type(file_or_keys) ~= "string" and getwinforfile(file_or_keys) or vis.win +	local ret = type(file_or_keys) ~= "string" and function() +		return pos +	end or function() +		return 0 +	end +	pos = pos or win.selection.pos +	local formatter = formatters[win.syntax] +	if formatter and formatter.pick then +		formatter = formatter.pick(win) +	end +	if formatter == nil then +		vis:info("No formatter for " .. win.syntax) +		return ret() +	end +	if range ~= nil and not formatter.options.ranged and range.start ~= 0 and range.finish ~= win.file.size then +		vis:info("Formatter for " .. win.syntax .. " does not support ranges") +		return ret() +	end +	pos = formatter.apply(win, range, pos) or pos +	vis:redraw() +	win.selection.pos = pos +	return ret()  end  return { -    formatters = formatters, -    options = global_options, -    apply = apply, -    stdio_formatter = stdio_formatter, -    with_filename = with_filename +	formatters = formatters, +	options = global_options, +	apply = apply, +	stdio_formatter = stdio_formatter, +	with_filename = with_filename,  } diff --git a/config/essentials/vis/fzf-mru.lua b/config/essentials/vis/fzf-mru.lua new file mode 100644 index 0000000..6c2510d --- /dev/null +++ b/config/essentials/vis/fzf-mru.lua @@ -0,0 +1,75 @@ +local module = {} +module.fzfmru_filepath = os.getenv("XDG_CACHE_HOME") .. "/vis-fzf-mru" +module.fzfmru_path = "fzf" +module.fzfmru_args = "--height=40%" +module.fzfmru_history = 20 + +local function read_mru() +    local mru = {} +    local f = io.open(module.fzfmru_filepath) +    if f == nil then return end +    for line in f:lines() do table.insert(mru, line) end +    f:close() + +    return mru +end + +local function write_mru(win) +    local file_path = win.file.path +    local mru = read_mru() + +    -- check if mru data exists +    if mru == nil then mru = {} end +    -- check if we opened any file +    if file_path == nil then return end +    -- check duplicate +    if file_path == mru[1] then return end + +    local f = io.open(module.fzfmru_filepath, "w+") +    if f == nil then return end + +    table.insert(mru, 1, file_path) + +    for i, k in ipairs(mru) do +        if i > module.fzfmru_history then break end +        if i == 1 or k ~= file_path then +            f:write(string.format("%s\n", k)) +        end +    end + +    f:close() +end + +vis.events.subscribe(vis.events.WIN_OPEN, write_mru) + +vis:command_register("fzfmru", function(argv) +    local command = "cat " .. module.fzfmru_filepath .. " | " .. +                        module.fzfmru_path .. " " .. module.fzfmru_args .. " " .. +                        table.concat(argv, " ") + +    local file = io.popen(command) +    local output = file:read() +    local _, _, status = file:close() + +    if status == 0 then +        vis:command(string.format("e '%s'", output)) +    elseif status == 1 then +        vis:info(string.format( +                     "fzf-open: No match. Command %s exited with return value %i.", +                     command, status)) +    elseif status == 2 then +        vis:info(string.format( +                     "fzf-open: Error. Command %s exited with return value %i.", +                     command, status)) +    elseif status ~= 130 then +        vis:info(string.format( +                     "fzf-open: Unknown exit status %i. command %s exited with return value %i", +                     status, command, status, status)) +    end + +    vis:redraw() + +    return true +end) + +return module diff --git a/config/essentials/vis/fzf-open.lua b/config/essentials/vis/fzf-open.lua new file mode 100644 index 0000000..1c9d1e6 --- /dev/null +++ b/config/essentials/vis/fzf-open.lua @@ -0,0 +1,81 @@ +-- Copyright (C) 2017  Guillaume Chérel +-- Copyright (C) 2023  Matěj Cepl +-- +-- This program is free software: you can redistribute it and/or modify +-- it under the terms of the GNU Lesser General Public License as +-- published by the Free Software Foundation, either version 3 of the +-- License, or (at your option) any later version. +-- +-- This program is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +-- GNU Lesser General Public License for more details. +-- +-- You should have received a copy of the GNU Lesser General Public License +-- along with this program.  If not, see <https://www.gnu.org/licenses/>. +local M = {} + +M.fzf_path = "fzf" +M.fzf_args = "--height=40%" + +vis:command_register("fzf", function(argv) +	local fzf_path = M.fzf_path +	if argv[1] == "--search-path" then +		table.remove(argv, 1) +		local dir = table.remove(argv, 1) +		fzf_path = ([[FZF_DEFAULT_COMMAND="$FZF_DEFAULT_COMMAND --search-path ]] .. dir .. [[" fzf]]) +	end + +	local command = string.gsub( +		[[ +            $fzf_path \ +                --header="Enter:edit,^s:split,^v:vsplit" \ +                --expect="ctrl-s,ctrl-v" \ +                $fzf_args $args +        ]], +		"%$([%w_]+)", +		{ +			fzf_path = fzf_path, +			fzf_args = M.fzf_args, +			args = table.concat(argv, " "), +		} +	) + +	local file = io.popen(command) +	local output = {} +	for line in file:lines() do +		table.insert(output, line) +	end +	local _, _, status = file:close() + +	if status == 0 then +		local action = "e" + +		if output[1] == "ctrl-s" then +			action = "split" +		elseif output[1] == "ctrl-v" then +			action = "vsplit" +		end + +		vis:feedkeys(string.format(":%s '%s'<Enter>", action, output[2])) +	elseif status == 1 then +		vis:info(string.format("fzf-open: No match. Command %s exited with return value %i.", command, status)) +	elseif status == 2 then +		vis:info(string.format("fzf-open: Error. Command %s exited with return value %i.", command, status)) +	elseif status ~= 130 then +		vis:info( +			string.format( +				"fzf-open: Unknown exit status %i. command %s exited with return value %i", +				status, +				command, +				status +			) +		) +	end + +	vis:redraw() + +	return true +end, "Select file to open with fzf") + +return M diff --git a/config/essentials/vis/vis-go.lua b/config/essentials/vis/vis-go.lua new file mode 100644 index 0000000..b0b383f --- /dev/null +++ b/config/essentials/vis/vis-go.lua @@ -0,0 +1,102 @@ +local function jump_to(path, line, col) +	if path then +		vis:command(string.format("e %s", path)) +	end +	vis.win.selection:to(line, col) +end + +local Gostack = { s = {}, i = 1 } + +function Gostack:push(v) +	self.s[self.i] = v +	self.i = self.i + 1 +end + +function Gostack:pop() +	if self.i == 1 then +		return nil +	end +	self.i = self.i - 1 +	return self.s[self.i] +end + +local function godef() +	local win = vis.win +	if win.syntax ~= "go" then +		return 0 +	end + +	local file = win.file +	local pos = win.selection.pos +	local cmd = string.format("godef -i -o %d", pos) +	local status, out, err = vis:pipe(file, { start = 0, finish = file.size }, cmd) +	if status ~= 0 or not out then +		if err then +			vis:info(err) +		end +		return status +	end + +	Gostack:push({ path = file.path, line = win.selection.line, col = win.selection.col }) + +	local path, line, col = string.match(out, "([^:]+):([^:]+):([^:]+)") +	if not path then +		-- same file +		line, col = string.match(out, "([^:]+):([^:]+)") +	end +	jump_to(path, line, col) +end + +local function godef_back() +	if vis.win.syntax ~= "go" then +		return 0 +	end + +	local pos = Gostack:pop() +	if pos then +		jump_to(pos.path, pos.line, pos.col) +	end +end + +vis:map(vis.modes.NORMAL, "gd", godef, "Jump to Go symbol/definition") +vis:map(vis.modes.NORMAL, "gD", godef_back, "Jump back to previous Go symbol/definition") + +local function gorename(argv, force, win, selection) +	if win.syntax ~= "go" then +		return true +	end + +	local name = argv[1] +	if not name then +		vis:info("empty new name provided") +		return false +	end + +	local forceFlag = "" +	if force then +		forceFlag = "-force" +	end + +	local pos = selection.pos +	local f = +		io.popen(string.format("gorename -offset %s:#%d -to %s %s 2>&1", win.file.path, pos, name, forceFlag), "r") +	local out = f:read("*all") +	local success, _, _ = f:close() +	if not success then +		vis:message(out) +		return false +	end + +	-- refresh current file +	vis:command("e") +	win.selection.pos = pos + +	vis:info(out) +	return true +end + +vis:command_register( +	"gorename", +	gorename, +	"Perform precise type-safe renaming of identifiers in Go source code: :gorename newName" +) diff --git a/config/essentials/vis/vis-ultisnips/init.lua b/config/essentials/vis/vis-ultisnips/init.lua new file mode 100644 index 0000000..52faa55 --- /dev/null +++ b/config/essentials/vis/vis-ultisnips/init.lua @@ -0,0 +1,149 @@ +-------------------------------------------------------------------------------- +-- Modules + +local M = {} +local cwd = ... +local SnipMate  = require(cwd .. '.snipmate-parser') +local UltiSnips = require(cwd .. '.ultisnips-parser') + + + +-------------------------------------------------------------------------------- +-- Config + +M.snipmate  = '' +M.ultisnips = '' + + + +-------------------------------------------------------------------------------- +-- Helper functions + +-- Takes list of snippets and concatenates them into the string suitable +-- for passing to dmenu (or, very probably, vis-menu) +local function snippetslist(snippets) +  local list = '' + +  for k,v in pairs(snippets) do +    if not v.description then +      list = list .. k .. '\n' +    else +      list = list .. k .. ' - ' .. v.description .. '\n' +    end +  end + +  return list +end + + + +local function load_ultisnips() +  local snippetfile = M.ultisnips .. vis.win.syntax .. '.snippets' +  local snippets, success = UltiSnips.load_snippets(snippetfile) +  if not success then +    vis:info('Failed to load a correct UltiSnip: ' .. snippetfile) +  end +  return snippets, success +end + + + +local function load_snipmate() +  local snippetfile = M.snipmate .. vis.win.syntax .. '.snippets' +  local snippets, success = SnipMate.load_snippets(snippetfile) +  if not success then +    vis:info('Failed to load a correct SnipMate: ' .. snippetfile) +  end +  return snippets, success +end + + + +-- Second will append to first using suffix for distinguishing +local function merge_and_override(snips1, snips2, suffix) +  for k,v in pairs(snips2) do +    snips1[k .. suffix] = v +  end +  return snips1 +end + + + +-------------------------------------------------------------------------------- +-- Plugging it all in + +vis:map(vis.modes.INSERT, "<C-x><C-j>", function() +  local snippets = merge_and_override(load_snipmate(), load_ultisnips(), '_us') + +  local win = vis.win +  local file = win.file +  local pos = win.selection.pos + +  if not pos then +    return +  end +  -- TODO do something clever here + +  -- Use prefix W if exists +  local initial = ' ' +  local range = file:text_object_longword(pos > 0 and pos - 1 or pos) +  if range then +      initial = initial .. file:content(range) +  end + +  -- Note, for one reason or another, using vis-menu corrupts my terminal +  -- (urxvt) for exact amount of lines that vis-menu takes +  -- dmenu has no such problems, but can't take initial input :-\ +  --local stdout = io.popen("echo '" .. snippetslist(snippets) .. "' | dmenu -l 5", "r") +  local stdout = io.popen("echo '" .. snippetslist(snippets) .. "' | vis-menu " .. initial, "r") +  local chosen = stdout:lines()() +  local _, msg, status = stdout:close() +  if status ~= 0 or not chosen then +    vis:message(msg) +    return +  end + +  local trigger = chosen:gmatch('[^ ]+')() +  local snipcontent = snippets[trigger].content +  if range then +    file:delete(range) +    -- Update position after deleting the range +    pos = pos - (range.finish - range.start) +    vis:redraw() +  end + +  vis:insert(snipcontent.str) + + +  if #snipcontent.tags > 0 then +    vis:info("Use 'g>' and 'g<' to navigate between anchors.") + +    -- Create selections iteratively using `:#n,#n2 p` command and `gs` to +    -- save it in the jumplist +    for _,v in ipairs(snipcontent.tags) do +      -- Can't use 'x' command because it'd select stuff across +      -- whole file +      vis:command('#' .. pos + v.selstart ..',#' .. pos + v.selend .. ' p') +      --vis:feedkeys('gs') -- Tested, works without this too, but just in case +      --vis:message('Command: ' .. cmd) +    end + +    -- Backtrack through all selections we've made first +    -- (so that we can use g> to move us forward)... +    for _ in ipairs(snipcontent.tags) do +      vis:feedkeys('g<') +    end + +    -- ... then set us on the first selection +    vis:feedkeys('g>') +  else +    win.selection.pos = pos + #snipcontent.str +  end +end, "Insert a snippet") + + + +-------------------------------------------------------------------------------- +-- End module + +return M
\ No newline at end of file diff --git a/config/essentials/vis/vis-ultisnips/snipmate-parser.lua b/config/essentials/vis/vis-ultisnips/snipmate-parser.lua new file mode 100644 index 0000000..9d735f1 --- /dev/null +++ b/config/essentials/vis/vis-ultisnips/snipmate-parser.lua @@ -0,0 +1,128 @@ +-------------------------------------------------------------------------------- +-- Module table + +local M = {} + +local lpeg = require('lpeg') + + + +-------------------------------------------------------------------------------- +-- lpeg rules + +-- Base definitions +-- local tws                = lpeg.S' ' ^ 1 +local tnewline           = lpeg.S'\n' +-- local tlowcasedword      = lpeg.R'az' ^ 1 +local tdigit             = lpeg.locale()['digit'] +local talphanum          = lpeg.locale()['alnum'] +local tanyprintable      = lpeg.locale()['print'] +-- local tcontrol           = lpeg.locale()['cntrl'] +local ttabtrigger        = tanyprintable ^ 1 +local ttag               = lpeg.Cg(lpeg.Cp(), 'selstart') +                           * lpeg.P'${' +                           * lpeg.Cg(tdigit^1, 'tag-order') +                           * ( +                               (lpeg.S':' * lpeg.Cg(talphanum^1, 'default-value') * lpeg.S'}') +                               + lpeg.S'}' +                             ) +                           * lpeg.Cg(lpeg.Cp(), 'selend') +local tsnippetdecl       = lpeg.P'snippet' * lpeg.S' ' * lpeg.Cg(ttabtrigger, 'tabtrigger') * tnewline +local tsnippetcontent    = lpeg.C( +                             lpeg.Cp() * +                             (lpeg.S'\t '^1 +                              * (lpeg.Ct(ttag) + tanyprintable)^1 +                              * tnewline +                             )^1 +                           ) + +-- Constructs +local tsnippet = tsnippetdecl * tsnippetcontent +local tcomment = lpeg.S'#' * tanyprintable^0 * tnewline + +-- The way grammar captures: +-- Every snippet gets its own table, and every table has: +-- 'tabtrigger' - the tabtrigger +-- [1]          - full content +-- [2]          - start of snippet content (need to subtract from selstart/selend +-- [3..n]       - tags +local tsnippetsfile = lpeg.Ct((tcomment + lpeg.Ct(tsnippet) + tnewline) ^1) + +-------------------------------------------------------------------------------- +-- Functions + +local function trim_tabs(content) +  local trim = function (s) +    return (string.gsub(s, "^\t(.-)$", "%1")) +  end + +  local ret='' +  for str in string.gmatch(content, '([^\n]+)') do +    ret = ret .. trim(str) .. '\n' +  end +  return ret +end + +-- Tags are on the top level of th table, +-- defined starting with index '3' +-- Index '2' is start of the content +-- Structure: +-- { tag-order: int +-- , selstart: int +-- , selend: int +-- , default-value: str +-- } +local function extract_tags(tableau) +  local tags = {} +  for k,v in ipairs(tableau) do +    if k >= 3 then -- Only process starting with ix 2 +      tags[k - 2] = { selstart = v.selstart - tableau[2] - 1 +                    , selend   = v.selend   - tableau[2] - 1 +                    , default  = v['default-value'] +                    , order    = v['tag-order'] +                    } +-- vis:message('snippet ' .. tableau.tabtrigger .. ' tag ' .. +--                 tostring(tags[k - 1].order) .. ' has start/end: ' .. +--                 tostring(tags[k - 1].selstart) .. '/' .. +--                 tostring(tags[k - 1].selend)) +    end +  end +  return tags +end + +M.load_snippets = function(snippetfile) +  local snippets = {} + +  local f = io.open(snippetfile, 'r') +  if f then +    local content = f:read("*all") + +    -- TODO hmmm, this'll make whole file unsuable, when it could +    --      in fact have usable snippets +    local m = tsnippetsfile:match(content) +    if not m then +      vis:info('Failed to parse SnipMate file: '.. snippetfile) +      return nil +    else +      -- k is index of snippet definition, v is table of snippet def +      for _,v in pairs(m) do +        snippets[v.tabtrigger] = { description = nil +                                 , options     = {} +                                 , content = { str  = trim_tabs(v[1]) +                                             , tags = extract_tags(v) +                                             } +                                 } +      end +    end + +    f:close() +    return snippets, true +  else +    return snippets, false +  end +end + +-------------------------------------------------------------------------------- +-- End module + +return M diff --git a/config/essentials/vis/vis-ultisnips/testlpeg-snipmate.lua b/config/essentials/vis/vis-ultisnips/testlpeg-snipmate.lua new file mode 100644 index 0000000..997365f --- /dev/null +++ b/config/essentials/vis/vis-ultisnips/testlpeg-snipmate.lua @@ -0,0 +1,160 @@ +local lpeg = require("lpeg") + +-------------------------------------------------------------------------------- + +-- Base definitions +-- local tws = lpeg.S(" ") ^ 1 +local tnewline = lpeg.S("\n") +-- local tlowcasedword = lpeg.R("az") ^ 1 +local tdigit = lpeg.locale()["digit"] +local talphanum = lpeg.locale()["alnum"] +local tanyprintable = lpeg.locale()["print"] +-- local tcontrol = lpeg.locale()["cntrl"] +local ttabtrigger = tanyprintable ^ 1 +local ttag = lpeg.Cg(lpeg.Cp(), "selstart") +	* lpeg.P("${") +	* lpeg.Cg(tdigit ^ 1, "tag-order") +	* ((lpeg.S(":") * lpeg.Cg(talphanum ^ 1, "default-value") * lpeg.S("}")) + lpeg.S("}")) +	* lpeg.Cg(lpeg.Cp(), "selend") +local tsnippetdecl = lpeg.P("snippet") * lpeg.S(" ") * lpeg.Cg(ttabtrigger, "tabtrigger") * tnewline +local tsnippetcontent = lpeg.C(lpeg.Cp() * (lpeg.S("\t ") ^ 1 * (lpeg.Ct(ttag) + tanyprintable) ^ 1 * tnewline) ^ 1) + +-- Constructs +local tsnippet = tsnippetdecl * tsnippetcontent +local tcomment = lpeg.S("#") * tanyprintable ^ 0 * tnewline + +-- The way grammar captures: +-- Every snippet gets its own table, and every table has: +-- 'tabtrigger' - the tabtrigger +-- [1]          - full content +-- [2..n]       - tags +local tsnippetsfile = lpeg.Ct((tcomment + lpeg.Ct(tsnippet) + tnewline) ^ 1) +-------------------------------------------------------------------------------- + +-- local testsingle = [[ +-- snippet sim +--         ${1:public} static int Main(string[] args) +--         { +--                 ${0} +--                 return 0; +--         } +-- ]] + +-- local testmulti = [[ +-- snippet sim +--         ${1:public} static int Main(string[] args) +--         { +--                 ${0} +--                 return 0; +--         } +-- snippet simc +--         public class Application +--         { +--                 ${1:public} static int Main(string[] args) +--                 { +--                         ${0} +--                         return 0; +--                 } +--         } +-- snippet svm +--         ${1:public} static void Main(string[] args) +--         { +--                 ${0} +--         } +-- ]] + +local testfile = [[ +# I'll most propably add more stuff in here like +# * List/Array constructio +# * Mostly used generics +# * Linq +# * Funcs, Actions, Predicates +# * Lambda +# * Events +# +# Feedback is welcome! +# +# Main +snippet sim +        ${1:public} static int Main(string[] args) +        { +                ${0} +                return 0; +        } +snippet simc +        public class Application +        { +                ${1:public} static int Main(string[] args) +                { +                        ${0} +                        return 0; +                } +        } +snippet svm +        ${1:public} static void Main(string[] args) +        { +                ${0} +        } +# if condition +snippet if +        if (${1:true}) +        { +                ${0:${VISUAL}} +        } +snippet el +        else +        { +                ${0:${VISUAL}} +        } +]] + +-------------------------------------------------------------------------------- +-- Test + +local function print_table(tableau, tabwidth) +	if tabwidth == nil then +		tabwidth = 0 +	end + +	-- Iterate +	for k, v in pairs(tableau) do +		local tabs = ("\t"):rep(tabwidth) + +		print(tabs .. k .. ':"' .. tostring(v) .. '"') +		if type(v) == "table" then +			print_table(v, tabwidth + 1) +		end +	end +end + +--print("------------ header ------------------------------------") +--p = lpeg.Ct(tsnippetdecl) +--t = p:match([[ +--snippet classy +--]]) +--print_table(t) +--print("--------------------------------------------------------------") + +--print("------------ tag ------------------------------------") +--print_table( +--  lpeg.Ct(ttag):match('${0:VISUAL}') +--) +--print_table( +--  lpeg.Ct(ttag):match('${12:Badonkadong}') +--) +--print_table( +--  lpeg.Ct(ttag):match('${1}') +--) +--print("--------------------------------------------------------------") + +--print("------------ single snippet test ------------------------------------") +--print_table(lpeg.Ct(tsnippet):match(testsingle)) +--print("--------------------------------------------------------------") + +--print("------------ multi snippet test ------------------------------------") +--print_table(lpeg.Ct(tsnippetsfile):match(testmulti)) +--print("--------------------------------------------------------------") + +print("------------ file with comments -------------------------------------") +print_table(tsnippetsfile:match(testfile)) +print("--------------------------------------------------------------") diff --git a/config/essentials/vis/vis-ultisnips/testlpeg-ultisnips.lua b/config/essentials/vis/vis-ultisnips/testlpeg-ultisnips.lua new file mode 100644 index 0000000..79df900 --- /dev/null +++ b/config/essentials/vis/vis-ultisnips/testlpeg-ultisnips.lua @@ -0,0 +1,230 @@ +local lpeg = require("lpeg") + +-------------------------------------------------------------------------------- + +local tsep = lpeg.S(" ") +local tws = tsep ^ 1 +local tnewline = lpeg.S("\n") +local tlowcasedword = lpeg.R("az") ^ 1 +local tdigit = lpeg.locale()["digit"] +-- local talphanum          = lpeg.locale()['alnum'] +local tanyprintable = lpeg.locale()["print"] +local tcontrol = lpeg.locale()["cntrl"] +local function quoted(p) +	return lpeg.S('"') * p * lpeg.S('"') +end +local function anythingbut(ch) +	return (tanyprintable + tcontrol) - lpeg.S(ch) +end + +local ttabtriggercomplex = quoted(tlowcasedword * lpeg.S("()[]?0123456789-") ^ 1) +-- TODO This is just retarded +local ttabtriggerweird = lpeg.S("!") * (lpeg.R("az") + lpeg.S("?()")) ^ 1 * lpeg.S("!") +local ttabtriggerweird2 = lpeg.P("#!") +local ttabtrigger = ttabtriggercomplex + ttabtriggerweird + ttabtriggerweird2 + tlowcasedword +local tdescription = quoted(lpeg.Cg((tanyprintable - lpeg.S('"')) ^ 1, "description")) +local toption = lpeg.R("az") + +local tstartsnippet = lpeg.P("snippet") +	* tws +	* lpeg.Cg(ttabtrigger, "tabtrigger") +	* tws +	* tdescription +	* tws ^ 0 +	* lpeg.Cg(toption ^ 0, "options") +local tendsnippet = lpeg.P("endsnippet") + +-- The content parsing needs cleanup, its really convoluted due to me learning +-- lpeg while using it +--tcontent      = ((tanyprintable + tcontrol)^1 - tendsnippet) * tnewline +local tcontent = ((lpeg.S(" \t") + tanyprintable) ^ 1 - tendsnippet) * tnewline +local tsnippet = tstartsnippet * tnewline * ((tendsnippet * tnewline) + lpeg.Cg(tcontent ^ 1, "content")) + +local tcomment = lpeg.S("#") * tanyprintable ^ 0 * tnewline +local tpriority = lpeg.P("priority") * tws * lpeg.Cg(lpeg.S("-") ^ 0 * tdigit ^ 1, "priority") + +-- TODO doesn't work +local tsnippetsfile = (lpeg.Ct(tsnippet) + tpriority + tcomment + tnewline) ^ 1 + +-- TODO does parse values correctly, but parsing out nested tags will +--      require recursion at the callsite since I have no clue how to do it +local ttag = { +	"T", +	Expr = lpeg.C((lpeg.V("T") + anythingbut("}")) ^ 1), +	Tnum = lpeg.Cg(tdigit ^ 1, "tagnum"), +	Ps = lpeg.Cg(lpeg.Cp(), "selstart"), +	Pe = lpeg.Cg(lpeg.Cp(), "selend"), +	Tc = lpeg.V("Ps") +		* lpeg.P("${") +		* lpeg.V("Tnum") +		* lpeg.S(":") +		* lpeg.Cg(lpeg.V("Expr"), "expr") +		* lpeg.V("Pe") +		* lpeg.S("}"), +	Ts = lpeg.V("Ps") * lpeg.S("$") * lpeg.V("Pe") * lpeg.V("Tnum"), +	T = lpeg.V("Tc") + lpeg.V("Ts"), +} + +-------------------------------------------------------------------------------- + +-- local testheader = [[ +-- snippet #! "#!/usr/bin/env lua" b +-- ]] + +local testcontent = [[ +for ${1:idx},${2:val} in ipairs(${3:table_name}) do +	$0 +end +]] + +local testsnippet = [[ +snippet fori "ipair for foop" b +for ${1:idx},${2:val} in ipairs(${3:table_name}) do +	$0 +end +endsnippet +]] + +local luasnippetfile = [[ +priority -50 + +################################# +# Snippets for the Lua language # +################################# +snippet #! "#!/usr/bin/env lua" b +#!/usr/bin/env lua +$0 +endsnippet + +snippet !fun(ction)?! "New function" br +local function ${1:new_function}(${2:args}) +	$0 +end +endsnippet + +snippet forp "pair for loop" b +for ${1:name},${2:val} in pairs(${3:table_name}) do +	$0 +end +endsnippet + +snippet fori "ipair for foop" b +for ${1:idx},${2:val} in ipairs(${3:table_name}) do +	$0 +end +endsnippet + +snippet for "numeric for loop" b +for ${1:i}=${2:first},${3:last}${4/^..*/(?0:,:)/}${4:step} do +	$0 +end +endsnippet + +snippet do "do block" +do +	$0 +end +endsnippet + +snippet repeat "repeat loop" b +repeat +	$1 +until $0 +endsnippet + +snippet while "while loop" b +while $1 do +	$0 +end +endsnippet + +snippet if "if statement" b +if $1 then +	$0 +end +endsnippet + +snippet ife "if/else statement" b +if $1 then +	$2 +else +	$0 +end +endsnippet + +snippet eif "if/elseif statement" b +if $1 then +	$2 +elseif $3 then +	$0 +end +endsnippet + +snippet eife "if/elseif/else statement" b +if $1 then +	$2 +elseif $3 then +	$4 +else +	$0 +end +endsnippet + +snippet pcall "pcall statement" b +local ok, err = pcall(${1:your_function}) +if not ok then +	handler(${2:ok, err}) +${3:else +	success(${4:ok, err}) +}end +endsnippet + +snippet local "local x = 1" +local ${1:x} = ${0:1} +endsnippet + +# vim:ft=snippets: +]] + +-------------------------------------------------------------------------------- +-- Test + +local function print_table(tableau, tabwidth) +	if tabwidth == nil then +		tabwidth = 0 +	end + +	-- Iterate +	for k, v in pairs(tableau) do +		local tabs = ("\t"):rep(tabwidth) + +		print(tabs .. k .. ': "' .. tostring(v) .. '"') +		if type(v) == "table" then +			print_table(v, tabwidth + 1) +		end +	end +end + +do +	print("------------ snippet test ------------------------------------") +	local p = lpeg.Ct(tsnippet) +	local t = p:match(testsnippet) +	print_table(t) +	print("--------------------------------------------------------------") +end + +do +	print("------------ snippetfile test ------------------------------------") +	local p = lpeg.Ct(tsnippetsfile) +	local t = p:match(luasnippetfile) +	print_table(t) +	print("--------------------------------------------------------------") +end + +do +	print("------------ tags test -------------------------------------") +	local p = lpeg.Ct((lpeg.Ct(ttag) + tanyprintable + tcontrol) ^ 1) +	local t = p:match(testcontent) +	print_table(t) +	print("--------------------------------------------------------------") +end diff --git a/config/essentials/vis/vis-ultisnips/ultisnips-parser.lua b/config/essentials/vis/vis-ultisnips/ultisnips-parser.lua new file mode 100644 index 0000000..a4240b8 --- /dev/null +++ b/config/essentials/vis/vis-ultisnips/ultisnips-parser.lua @@ -0,0 +1,211 @@ +-------------------------------------------------------------------------------- +-- Module table + +local M = {} + +local lpeg = require('lpeg') + + + +-------------------------------------------------------------------------------- +-- lpeg rules + +local tsep               = lpeg.S' \t' +local tws                = tsep ^ 1 +local tnewline           = lpeg.S'\n' +local tlowcasedword      = lpeg.R'az' ^ 1 +local tdigit             = lpeg.locale()['digit'] +-- local talphanum          = lpeg.locale()['alnum'] +local tanyprintable      = lpeg.locale()['print'] +local tcontrol           = lpeg.locale()['cntrl'] +local function surrounded(ch, p) return lpeg.S(ch) * p * lpeg.S(ch) end +local function anythingbut(ch) return (tanyprintable + tcontrol) - lpeg.S(ch) end + +local ttabtriggercomplex = surrounded ('"', +                              tlowcasedword * lpeg.S'()[]?0123456789-'^1 +                           ) +-- TODO This is just retarded +--      Check the actual grammar and see what special starting chars are +--      then relax the grammar a bit +local ttabtriggerweird   = surrounded('!', +                             (lpeg.R'az' + lpeg.S'?()') ^ 1 +                           ) +local ttabtriggerweird2  = lpeg.P'#!' +local ttabtriggerweird3  = surrounded('/', +                             (anythingbut'/') ^1 +                           ) +local ttabtrigger        = ttabtriggercomplex +                         + ttabtriggerweird +                         + ttabtriggerweird2 +                         + ttabtriggerweird3 +                         + (tlowcasedword + lpeg.S'.') +local tdescription       = surrounded ('"', +                              lpeg.Cg( (tanyprintable - lpeg.S'"')^1, 'description') +                           ) +local toption            = lpeg.R'az' + +local tstartsnippet = lpeg.P'snippet' +                    * tws +                    * lpeg.Cg(ttabtrigger, 'tabtrigger') +                    * tws +                    * tdescription +                    * tws ^ 0 +                    * lpeg.Cg(toption^0, 'options') +local tendsnippet   = lpeg.P'endsnippet' + +-- The content parsing needs cleanup, its really convoluted due to me learning +-- lpeg while using it +--tcontent      = ((tanyprintable + tcontrol)^1 - tendsnippet) * tnewline +local tcontent = ((lpeg.S' \t' + tanyprintable)^1 - tendsnippet) +               * tnewline +local tsnippet = tstartsnippet +               * tnewline +               * ((tendsnippet * tnewline) + lpeg.Cg(tcontent ^ 1, 'content')) + +-- local tcomment  = lpeg.S'#' +--                 * tanyprintable^0 +--                 * tnewline +-- local tpriority = lpeg.P'priority' +--                 * tws +--                 * lpeg.Cg(lpeg.S('-')^0 * tdigit^1, 'priority') + +-- TODO doesn't work +-- local tsnippetsfile = (lpeg.Ct(tsnippet) + tpriority + tcomment + tnewline) ^ 1 + + +-- TODO does parse values correctly, but parsing out nested tags will +--      require recursion at the callsite since I have no clue how to do it +local ttag = { 'T' +       ; Expr = lpeg.C((lpeg.V'T' + ((tanyprintable + tcontrol) - lpeg.S'}'))^1) +       , Tnum = lpeg.Cg(tdigit ^ 1, 'tagnum') +       , Ps   = lpeg.Cg(lpeg.Cp(), 'selstart') +       , Pe   = lpeg.Cg(lpeg.Cp(), 'selend') +       , Tc   = lpeg.V'Ps' +                * lpeg.P'${' +                * lpeg.V'Tnum' +                * lpeg.S(':') +                * lpeg.Cg(lpeg.V'Expr', 'expr') +                * lpeg.V'Pe' +                * lpeg.S'}' +       , Ts   = lpeg.V'Ps' * lpeg.S'$' * lpeg.V'Pe' * lpeg.V'Tnum' +       , T    = lpeg.V'Tc' + lpeg.V'Ts' +       } + + + +-------------------------------------------------------------------------------- +-- Functions + +-- Parses the snippet's content to create a table we later use +-- to corrently insert the text, the selections, and the default values +local function create_content(str) +  local content = {} +  content.str   = str +  content.tags  = {} + +  local p = vis.lpeg.Ct((lpeg.Ct(ttag) + tanyprintable + tcontrol) ^ 1) +  local m = p:match(str) + +  local s = 1 -- We start from 1 to adjust position from $^0 to ^$0 +  for k,v in ipairs(m) do +    content.tags[k] = v +    -- TODO recurse over tag.expr to extract nested tags +    --      Of course this will actually have to be used later on, depending +    --      on whether the tag is added or not + +    -- We need to keep track of how much we remove, and readjust all +    -- subsequent selection points +    -- Note to self, I hate all this bookkeeping +    local tagtext = string.sub(str, v.selstart, v.selend) +    if v.expr ~= nil then +      content.str = string.gsub(content.str, tagtext, v.expr) +      content.tags[k].selstart = content.tags[k].selstart - s +      content.tags[k].selend   = content.tags[k].selstart + #v.expr +      s = s + #'${' + #tostring(k) + #':' + 1 +    else +      content.str = string.gsub(content.str, tagtext, '') +      content.tags[k].selstart = content.tags[k].selstart - s +      content.tags[k].selend   = content.tags[k].selstart +      s = s + #'$' + 1 +    end +  end + +  return content +end + + + +-- Takes a line starting with 'snippet' and a lines iterator, and creates +-- a 'snippet' table to be used +-- If it fails it returns nil, otherwise returns two values, a tabtrigger +-- and a snippet +local function create_snippet(start_line, linesit) +  local snippetstr = start_line .. '\n' +  -- Read content into list of lines until we hit `endsnippet` +  for line in linesit do +    local s, _ = string.find(line, 'endsnippet') +    if s == 1 then +      snippetstr = snippetstr .. 'endsnippet' .. '\n' +      break +    else +      snippetstr = snippetstr .. line .. '\n' +    end +  end + +  local p = vis.lpeg.Ct(tsnippet) +  local m = p:match(snippetstr) + +  if not m then +    -- Enable this when debugging, otherwise it nukes whole app +    vis:info('Failed to parse some snippets!') +    -- vis:message('Failed to parse snippet: ' .. snippetstr) +    return nil +  else +    local tabtrigger = m.tabtrigger +    local snippet = {} +    snippet.description = m.description +    snippet.options = m.options +    snippet.content = create_content(m.content) +    return tabtrigger, snippet +  end +end + + + +-- Loads all snippets from passed '.snippets' file. Should probably be +-- triggered when new file is loaded or when syntax is set/changed +M.load_snippets = function(snippetfile) +  local snippets = {} + +  local f = io.open(snippetfile, 'r') +  if f then +    io.input(f) +    local linesit = io.lines() + +    for line in linesit do +      -- TODO read whole file, then apply lpeg grammar that parses all +      -- snippets out rather than being pedestrian about it like this +      local s, _ = string.find(line, 'snippet') +      -- Find lines that start with 'snippet' and enter +      -- snippet reading loop +      if s == 1 then +        local tabtrigger, snippet = create_snippet(line, linesit) +        if tabtrigger then +          snippets[tabtrigger] = snippet +        end +      end +    end + +    io.close(f) +    return snippets, true +  else +    return snippets, false +  end +end + + + +-------------------------------------------------------------------------------- +-- End module + +return M
\ No newline at end of file diff --git a/config/essentials/vis/visrc.lua b/config/essentials/vis/visrc.lua index 60162bc..7847784 100644 --- a/config/essentials/vis/visrc.lua +++ b/config/essentials/vis/visrc.lua @@ -1,25 +1,45 @@  ------------------------------------ ---- LIBRARIES +--- REQUIRES  ------------------------------------  require("vis")  -- plugins  require("build") +-- use Trash directory instead, remove set_dir function  require("backup")  require("cursors")  require("title")  require("commentary")  require("complete-line") +-- removed formatting because already fulfilled by format.lua +require("vis-go") +-- set height to 40% +require("fzf-open") +require("vis-ultisnips") +-- TODO: doesn't work when using with 'e|b' +-- require("yank-highlight") + +-- save position before formatting, use vis:redraw  local format = require("format") +-- set height to 40% +local fzfmru = require("fzf-mru") +fzfmru.fzfmru_path = 'grep "^' .. os.getenv("PWD") .. '" | fzf' + +  -- todo:  -- c-scope  -- c-tags +-- ... +-- vis-goto, favor open-file-under-cursor +-- ... +-- ultisnips +-- ... +-- vis-yank-highlight  ------------------------------------  --- VARIABLES  ------------------------------------ -  local m = vis.modes  ------------------------------------ @@ -56,13 +76,16 @@ vis:command_register("Q", function()  	vis:command("qa!")  end, "Quit all")  vis:command_register("delws", function() -	vis:command("x/[ \t]+$|^[ \t]+$/d") +	vis:command(",x/[ \t]+$|^[ \t]+$/d")  end, "Remove trailing whitespace")  -------------------------------------  --- MAPPINGS  ------------------------------------- +vis:map(m.NORMAL, "<C-p>", function() vis:command("fzf") end, "Open file with fzf") + +  vis:map(m.NORMAL, " r", function()  	wrap_restore(vis.command, vis, "e $vis_filepath")  end, "Reload active file") @@ -107,8 +130,4 @@ vis.events.subscribe(vis.events.WIN_OPEN, function(win) -- luacheck: no unused a  			"Print variable"  		)  	end - -	vis:command_register("pipe", function() -		vis:pipe(win.file, nil, "sed 's/.*/- &/'") -	end, "pipe test")  end) diff --git a/config/essentials/vis/yank-highlight.lua b/config/essentials/vis/yank-highlight.lua new file mode 100644 index 0000000..37a9578 --- /dev/null +++ b/config/essentials/vis/yank-highlight.lua @@ -0,0 +1,37 @@ +require("vis") + +local M = { +	style = "reverse", -- Style used for highlighting +	duration = 0.2,    -- [s] Time to remain highlighted (10 ms precision) +} + +vis.events.subscribe(vis.events.INIT, function() +	local yank = vis:action_register("highlighted-yank", function() +		vis.win:style_define(vis.win.STYLE_SELECTION, M.style) +		vis:redraw() +		local tstamp = os.clock() +		while os.clock() - tstamp < M.duration do end +		vis.win:style_define(vis.win.STYLE_SELECTION, vis.lexers.STYLE_SELECTION) +		vis:redraw() +		vis:feedkeys("<vis-operator-yank>") +	end, "Yank operator with highlighting") +	vis:map(vis.modes.OPERATOR_PENDING, "y", yank) +	vis:map(vis.modes.VISUAL, "y", yank) +	vis:map(vis.modes.VISUAL_LINE, "y", yank) + +	vis:map(vis.modes.NORMAL, "y", function(keys) +		local sel_end_chrs = "$%^{}()wp" +		if #keys < 1 or sel_end_chrs:find(keys:sub(-1), 1, true) == nil then +			if keys:find("<Escape>") then +				return #keys +			end +			return -1 +		end +		vis:feedkeys("<vis-mode-visual-charwise>") +		vis:feedkeys(keys) +		vis:feedkeys("y<Escape>") +		return #keys +	end) +end) + +return M  | 
