#!/usr/bin/env lua5.4 ver = "950be44 (2026-06-01)" package.preload["neo-ed.apidoc"] = assert(load( [================================================================================[ local apidoc = setmetatable({}, {__call = function(t, k) return function(v) t[k] = v end end}) apidoc["/api/apidoc"] = { name = "apidoc(path)(data)", type = "function(string) -> function(table)", descr = "add an api documentation node", -- TODO: see details = [=[ The help for `path` is made available under `about:`. The following fields are available: - `data.name`: full name - `data.type`: type specification - `data.descr`: short description, required - `data.details`: extended description string - `data.see`: list of references to other help pages ]=], } return apidoc ]================================================================================] , "neo-ed.apidoc")) package.preload["neo-ed.body"] = assert(load( [================================================================================[ local as = require "neo-ed.lib.as" local lib = require "neo-ed.lib" local mt = {__index = {}, __name = "body"} local function new(curr) as ({nprev = "number", nnext = "number"}) lib.assert(curr.nprev == -1 or curr.prev, "missing prev field") lib.assert(curr.nnext == 0 or curr.next, "missing next field") return setmetatable({ _curr = curr, pos_key = {}, text_key = {}, }, mt) end function mt:__len() as ("body") return self._curr.nprev + 1 + self._curr.nnext end function mt.__index:check_pos(pos, allow0) as ("body", "number", "*") if not ((allow0 and 0 or 1) <= pos and pos <= #self) then lib.error(#self == 0 and "buffer is empty" or (pos .. " not in range [1, " .. #self .. "]")) end end function mt.__index:get(first, last) as ("body", "number?", "number?") first = first or 1 last = last or #self local ret = {} self:inspect(function(_, l) local tmp = lib.dup(l) tmp.prev = nil tmp.next = nil tmp.nprev = nil tmp.nnext = nil table.insert(ret, tmp) end, first, last) return ret end function mt.__index:inspect(f, first, last) as ("body", "function", "number?", "number?") return self:_walk(f, first, last) end function mt.__index:inspect_r(f, first, last) as ("body", "function", "number?", "number?") return self:_walk(f, last, first, {rev = true}) end function mt.__index:pos() as ("body") return self._curr.nprev + 1 end function mt.__index:scan(f, first, last, wrap) as ("body", "function", "number?", "number?", "*") first = first or 1 last = last or #self if wrap and first >= last then local ret = self:scan(f, first, #self) if ret ~= nil then return ret end return self:scan(f, 1, last) end local ret = nil self:inspect(function(n, l) ret = f(n, l); return ret ~= nil end, first, last) return ret end function mt.__index:scan_r(f, first, last, wrap) as ("body", "function", "number?", "number?", "*") first = first or #self last = last or 1 if wrap and first <= last then local ret = self:scan_r(f, first, 1) if ret ~= nil then return ret end return self:scan_r(f, #self, last) end local ret = nil self:inspect_r(function(n, l) ret = f(n, l); return ret ~= nil end, first, last) return ret end function mt.__index:sel_first() as ("body") return self:scan(function(n, l) return not l.hide and n or nil end) or 1 end function mt.__index:sel_last() as ("body") return self:scan_r(function(n, l) return not l.hide and n or nil end) or #self end -- vvv --- CoW Mutation --- vvv --- function mt.__index:copy_append(line) as ("body", {text = "string"}) local newcurr = lib.dup(line) local newprev = lib.dup(self._curr) newcurr.prev = newprev newcurr.next = newprev.next newcurr.nprev = newprev.nprev + 1 newcurr.nnext = newprev.nnext newprev.next = nil newprev.nnext = nil return new(newcurr) end function mt.__index:copy_delete() as ("body") lib.assert(self._curr.nprev > -1, "cannot delete index 0") if self._curr.next then local newcurr = lib.dup(self._curr.next) newcurr.prev = self._curr.prev newcurr.nprev = self._curr.nprev return new(newcurr) end if self._curr.prev then local newcurr = lib.dup(self._curr.prev) newcurr.next = self._curr.next newcurr.nnext = self._curr.nnext return new(newcurr) end lib.error("copy_delete underflow") end function mt.__index:copy_drop(first, last) as ("body", "number?", "number?") first = first or 1 last = last or #self self:check_pos(last) self = self:copy_seek(first) for _ = first, last do self = self:copy_delete() end return self end function mt.__index:copy_map(f, first, last, keep_text) as ("body", "function", "number?", "number?", "*") local ret = self:_walk(f, first, last, {change = true}) if keep_text then ret.text_key = self.text_key end return ret end function mt.__index:copy_put(lines, pos) as ("body", {{text = "string"}}, "number?") if pos then self = self:copy_seek(pos) end for _, l in ipairs(lines) do self = self:copy_append(l) end return self end function mt.__index:copy_seek(n) as ("body", "number") self:check_pos(n, true) if n == self:pos() then return self end local curr = self._curr while curr.nprev + 1 > n and curr.prev do local newcurr = lib.dup(curr.prev) local newnext = lib.dup(curr ) newcurr.next = newnext newcurr.nnext = newnext.nnext + 1 newcurr.hide = nil newnext.prev = nil newnext.nprev = nil curr = newcurr end while curr.nprev + 1 < n and curr.next do local newcurr = lib.dup(curr.next) local newprev = lib.dup(curr ) newcurr.prev = newprev newcurr.nprev = newprev.nprev + 1 newcurr.hide = nil newprev.next = nil newprev.nnext = nil curr = newcurr end local ret = new(curr) ret.text_key = self.text_key return ret end function mt.__index:copy_select(first, last) as ("body", "number", "number") local oldfirst = self:sel_first() local oldlast = self:sel_last () local ret = self :copy_seek(last) :copy_map( function(n, l) l.hide = not (first <= n and n <= last) end, math.min(oldfirst, first), math.max(oldlast , last ) ) ret.text_key = self.text_key return ret end function mt.__index:copy_replace(lines, first, last) as ("body", {{text = "string"}}, "number?", "number?") return self:copy_drop(first, last):copy_put(lines, first - 1) end function mt.__index:_print() as ("body") local function show(line) print(tostring(line.nprev) .. (line.prev and "^" or ""), tostring(line.nnext) .. (line.next and "v" or ""), line.text) end local function head(line) if line.prev then head(line.prev) end; show(line) end local function tail(line) show(line); if line.next then tail(line.next) end end print("---") if self._curr.prev then head(self._curr.prev) end show(self._curr) if self._curr.next then tail(self._curr.next) end print("---") return self end function mt.__index:_walk(f, first, last, opts) as ("body", "function", "number?", "number?", "table?") first = first or 1 last = last or #self opts = opts or {} local abort = false local function head(line) if not line then return end local n = line.nprev + 1 if n < first or abort then return line end if opts.change then line = lib.dup(line) end if not opts.rev then line.prev = head(line.prev) end if n <= last then abort = abort or f(n, line) end if opts.rev then line.prev = head(line.prev) end return line end local function tail(line, n) if not line then return end if n > last or abort then return line end if opts.change then line = lib.dup(line) end if opts.rev then line.next = tail(line.next, n + 1) end if n >= first then abort = abort or f(n, line) end if not opts.rev then line.next = tail(line.next, n + 1) end return line end local pos = self:pos() local curr = opts.change and lib.dup(self._curr) or self._curr if opts.rev then curr.next = tail(curr.next, pos + 1) else curr.prev = head(curr.prev) end if first <= pos and pos <= last then abort = abort or f(pos, curr) end if opts.rev then curr.prev = head(curr.prev) else curr.next = tail(curr.next, pos + 1) end return opts.change and new(curr) or self end return function(text) as ("string?") local ret = new{nprev = -1, nnext = 0} if not text then return ret end return ret:copy_put(lib.lines_split(text)) end ]================================================================================] , "neo-ed.body")) package.preload["neo-ed.buffer"] = assert(load( [================================================================================[ local as = require "neo-ed.lib.as" local lib = require "neo-ed.lib" local parser = require "neo-ed.parser" local term = require "neo-ed.term" local posix = require "posix" local regex = require "neo-ed.regex" local mt = {__index = {}, __name = "buffer"} function mt.__index:change(f, ...) as ("buffer", "function") lib.assert(not self._changing, "recursive call of buffer:change()") self:change_group(function(...) self._changing = true self.body = f(self.body, ...) or self.body self._changing = nil end, ...) return self end function mt.__index:change_group(f, ...) as ("buffer", "function") if self._change_group then f(...) else self._change_group = true local _ = lib.defer(function() self._change_group, self._changing = nil, nil end) local before = self.body local undo_before = self._undo_body lib.try{ fn = function(...) self._undo_body = before f(...) if self.body.text_key ~= before.text_key then self:history_commit() else self._undo_body = undo_before end end, catch = function() self.body = before self._undo_body = undo_before return false end, n = select("#", ...), ... } end return self end function mt.__index:change_weak(f, ...) as ("buffer", "function") self.state:warn("call of deprecated function buffer:change_weak(): " .. lib.fninfo(2)) return self:change(f, ...) end function mt.__index:check_for_writes() as ("buffer") local p = self:get_path() if not p then return end if not self.cookie then return end local now = lib.path_cookie(p) if now ~= self.cookie then self.state:warn("file was modified since last read/write") self.state:info("old: " .. self.cookie) self.state:info("new: " .. now ) end return self end function mt.__index:close(force) as ("buffer", "*") if self:is_modified() and not force then lib.error("buffer modified: " .. self:label()) end self.state:hook(self.state.hooks.buffer.close, self) self.state:unregister(self) end function mt.__index:cmd(s) as ("buffer", "string") local cmd_before = self.state._cmd self.state._cmd = cmd_before or s local _ = lib.defer(function() self.state._cmd = cmd_before end) if lib.prof then self.cmd_prof = lib.profiler("command " .. s) end if self.cmd_prof then self.cmd_prof:start("parse") end local f = parser.cmd(self.state, s) if self.cmd_prof then self.cmd_prof:stop() end if self.cmd_prof then self.cmd_prof:start("exec") end f(self) if self.cmd_prof then self.cmd_prof:stop() end if self.cmd_prof then self.cmd_prof:print() end self.cmd_prof = nil return self end function mt.__index:cmd_list(s) as ("buffer", "string") local ls = {} for l in s:gmatch("[^\n]*") do table.insert(ls, l) end table.insert(self._preloaded_lines, ls) local _ = lib.defer(function() table.remove(self._preloaded_lines) end) while self._preloaded_lines[#self._preloaded_lines][1] do self:cmd(self:get_cmd()) end return self end local conf_key = {} function mt.__index:conf_get(k) as ("buffer", "string") local cache = lib.cache[self.state.world_key][self.conf_key][conf_key] if not cache.conf then local conf = {} for k, v in pairs(self.state.conf_defs) do conf[k] = {value = v.def, origin = "default value"} end conf = self.state:filter("conf", conf, self:get_vpath(), self, function(conf, k, v) local c = lib.assert(self.state.conf_defs[k], "unknown config option: " .. k) lib.assert(type(v) == c.type, ("not a %s: %s=%q"):format(c.type, k, v)) conf[k] = {value = v, origin = lib.fninfo(2)} end) cache.conf = conf end if self._conf[k] then return self._conf[k].value, self._conf[k].origin end if cache.conf[k] then return cache.conf[k].value, cache.conf[k].origin end lib.error("unknown config option: " .. k) end function mt.__index:conf_read(k, s) as ("buffer", "string", "string") local c = lib.assert(self.state.conf_defs[k], "unknown config option: " .. k) if c.type == "boolean" then if s == "y" then return true end if s == "Y" then return true end if s == "n" then return false end if s == "N" then return false end if s == "1" then return true end if s == "0" then return false end lib.error("could not parse boolean: " .. s) elseif c.type == "number" then return lib.assert(tonumber(s), "could not parse number: " .. s) elseif c.type == "string" then return s end end function mt.__index:conf_reset(k) as ("buffer", "string") local c = lib.assert(self.state.conf_defs[k], "unknown config option: " .. k) self._conf[k] = nil if c.drop_cache then self:drop_cache() end return self end function mt.__index:conf_set(k, v, origin) as ("buffer", "string", "*", "string") local c = lib.assert(self.state.conf_defs[k], "unknown config option: " .. k) lib.assert(type(v) == c.type, ("not a %s: %s=%q"):format(c.type, k, v)) self._conf[k] = {value = v, origin = origin} if c.drop_cache then self:drop_cache() end return self end function mt.__index:conf_show(v) as ("buffer", "*") if type(v) == "boolean" then return v and "y" or "n" end if type(v) == "number" then return tostring(v) end if type(v) == "string" then return v end lib.error("cannot show setting of type " .. type(v)) end function mt.__index:diff(fst, snd, lfst, lsnd) as ("buffer", "body?", "body?", "string?", "string?") if not fst then if snd then fst, lfst = self.body, lfst or "current" else fst, lfst = lib.assert(self._undo_body, "no undo point to compare to"), lfst or "previous" end end if not snd then snd, lsnd = self.body, lsnd or "current" end local fst_lines = fst:get() local snd_lines = snd:get() self:diff_lines(fst_lines, snd_lines, lfst, lsnd) return self end function mt.__index:diff_lines(a, b, la, lb) as ("buffer", "table?", "table?", "string?", "string?") a = a or self.body:get() b = b or self.body:get() la = la or "old" lb = lb or "new" lib.pager(lib.diff(self.state.impl.diff[#self.state.impl.diff], a, b, la, lb)) return self end function mt.__index:display_height(first, last, extra) as ("buffer", "number?", "number?", "number?") first = first or 1 last = last or #self.body extra = extra or 0 local lines = self:print_body() local w = term.cols local ret = 0 for i = first, last do for _, t in ipairs(lines[i].lead) do ret = ret + math.max(math.ceil((t.width + extra) / w), 1) end ret = ret + math.max(math.ceil((lines[i].width + extra) / w), 1) for _, t in ipairs(lines[i].trail) do ret = ret + math.max(math.ceil((t.width + extra) / w), 1) end end return ret end function mt.__index:drop_cache() as ("buffer") self.conf_key = {} return self end function mt.__index:focus() as ("buffer") self.state:focus(self) return self end local function fancy_tab(width) return term:sgr"weak" .. ("─"):rep(width - 1):gsub("^─", "╶") .. "┤" .. term:sgr"reset" end function mt.__index:get_cmd() as ("buffer") term.tab = fancy_tab(self:conf_get("tabs")) local prof = lib.prof and lib.profiler("autocomp") or nil local function line(first) local pl = self._preloaded_lines pl = pl[#pl] if pl then return table.remove(pl, 1) end table.insert(self.state.history, "") return term:getline{ prompt = first and "* " or "+ ", history = self.state.history, autocomp = function(comps, pre) local _, _, rest = pcall(parser.addrs, self.state, pre) pre = rest or pre for _, c in ipairs(self.state.impl.autocomp.cmd) do local s = pre:match(c.pat) if s then local p = prof and prof:start(lib.fninfo(c.fn)) or nil c.fn(comps, s, self) end end end, state = self.state, } end local first = true local ret = line(true) if prof then prof:print() end if not ret then return ret end while ret:find("\\$") do ret = ret:gsub("\\$", "\n" .. line(false)) end return ret end function mt.__index:get_indent_text() as ("buffer") return self:conf_get("tab2spc") and (" "):rep((self:conf_get("indent"))) or "\t" end function mt.__index:get_input(history, lno) as ("buffer", {"string"}, "number?") local pl = self._preloaded_lines pl = pl[#pl] if pl then return table.remove(pl, 1) end local prof = lib.prof and lib.profiler("autocomp") or nil local padded_lno = ("%" .. math.max(3, #tostring(#self.body)) .. "d"):format(lno or 0) term.tab = fancy_tab(self:conf_get("tabs")) local ret = term:getline{ state = self.state, prompt = term:sgr"note rev" .. padded_lno .. term:sgr"reset accent" .. term.box.vline .. term:sgr"reset", prompt_done = term:sgr"note" .. padded_lno .. term:sgr"reset accent" .. term.box.vline .. term:sgr"reset", history = history, on_tab = self:conf_get("tab2spc") and (" "):rep((self:conf_get("indent"))) or nil, autocomp = function(comps, pre) for _, c in ipairs(self.state.impl.autocomp.src) do local s = pre:match(c.pat) if s then local p = prof and prof:start(lib.fninfo(c.fn)) or nil c.fn(comps, s, self) end end local words = self:get_words(pre, {suf = "$"}) for _, c in ipairs(self.state.impl.autocomp.dict) do local p = prof and prof:start(lib.fninfo(c)) or nil for _, w in lib.opairs(words) do c(comps, w, self) end end end, } if prof then prof:print() end return ret end function mt.__index:get_loc() as ("buffer") return self._loc end function mt.__index:get_path() as ("buffer") return self._loc and self._loc:get_path() end function mt.__index:get_words(s, opts) as ("buffer", "string", "table?") opts = opts or {} as("*", "*", {pre = "string?", suf = "string?", into = "table?"}) local delim, rest = self:conf_get("words"):match("^(.)(.*)$") if not delim then return {} end local ret = opts.into or {} while rest ~= "" do local r, _, rest_ = regex.curr.parse(delim, rest, false, true) for w in regex.curr.gmatch(s, (opts.pre or "") .. r .. (opts.suf or "")) do ret[w] = true end rest = rest_ end return ret end function mt.__index:get_vpath() as ("buffer") return self._vpath or self:get_path() or ("buffer_%d.txt"):format(self.id) end function mt.__index:history_checkout(id) as ("buffer", "number") local pt = lib.assert(self.history[id], "undo point not found: " .. id) self._undo_body = self.body self.body = pt.body self.history_id = pt.id return self end function mt.__index:history_commit() as ("buffer") local pt = { id = #self.history + 1, body = self.body, pred = self.history_id, cmd = self.state._cmd, } table.insert(self.history, pt) self.history_id = pt.id return self end function mt.__index:is_modified() as ("buffer") return self._modified_key ~= self.body.text_key end function mt.__index:label(rich) as ("buffer", "*") local n = self.name or ("#%d"):format(self.id) local l = self._loc and self._loc:label(rich) if rich then n = term:sgr"accent" .. n .. term:sgr"reset" end local ret = ("[%s]%s"):format(n, l and " " .. l or "") if self:is_modified() then ret = ("%s%s*%s"):format(ret, rich and term:sgr"note bold" or " ", rich and term:sgr"reset" or "") end return ret end function mt.__index:load(loc, force) as ("buffer", "loc?", "*") if self:is_modified() and not force then lib.error("buffer modified: " .. self:label()) end if loc then self:set_loc(loc) end loc = lib.assert(loc or self._loc, "no location to load from") local s = loc:read(self) self.state:info(("read %d bytes from %s"):format(#s, tostring(loc))) if loc == self._loc then s = self.state:filter("read", s, self) end self:load_str(s, true) self:history_commit() self.cookie = self:get_path() and lib.path_cookie(self:get_path()) or nil return self end function mt.__index:load_str(s, force) as ("buffer", "string", "*") if self:is_modified() and not force then lib.error("buffer modified: " .. self:label()) end self.state:hook(self.state.hooks.buffer.load_pre, self) self.body = require "neo-ed.body" (s) self:set_modified(false) self:drop_cache() self.state:hook(self.state.hooks.buffer.load_post, self) return self end function mt.__index:print(first, last) as ("buffer", "number?", "number?") first = first or 1 last = last or #self.body self.state:hook(self.state.hooks.buffer.print_pre, self) local printed = self:print_body() local w = #tostring(#printed) self.body:inspect(function(i, l) local fmt = term:sgr(i == self.body:pos() and "note rev" or l.hide and "weak" or "note") for _, t in ipairs(printed[i].lead or {}) do io.stdout:write(("%s%" .. tostring(w) .. "s%s%s%s%s%s\n"):format( fmt, "", term:sgr"reset accent", term.box.vline, term:sgr"reset", t.text, term:sgr"reset" )) end io.stdout:write(("%s%" .. tostring(w) .. "d%s%s%s%s%s\n"):format( fmt, i, term:sgr"reset accent", term.box.vline, term:sgr"reset", printed[i].text, term:sgr"reset" )) for _, t in ipairs(printed[i].trail or {}) do io.stdout:write(("%s%" .. tostring(w) .. "s%s%s%s%s%s\n"):format( fmt, "", term:sgr"reset accent", term.box.vline, term:sgr"reset", t.text, term:sgr"reset" )) end end, first, last) self.state:hook(self.state.hooks.buffer.print_post, self) return self end local print_body_key = {} function mt.__index:print_body(body) as ("buffer", "body?") body = body or self.body local cache = lib.cache[body.text_key][self.conf_key][term.key][print_body_key] if not cache.printed then local ret = body:get() self:print_lines(ret) cache.printed = ret end return cache.printed end local print_line_mt = {__index = function(t, k) if k == "stripped" then t.stripped = lib.strip_fmt(t.text) end if k == "width" then t.width = term:display_width(t.stripped) end return rawget(t, k) end} function mt.__index:print_lines(lines) as ("buffer", {{text = "string"}}) local prof = lib.prof and lib.profiler("print pipeline") or nil local function go(f) local p = prof and prof:start(lib.fninfo(f)) or nil local ok, err = lib.pcall(f, lines, self) if not ok then self.state:warn("print function failed: " .. lib.fninfo(f) .. ": " .. err) end end local ovr = self:conf_get("highlighter") local hfn = nil if ovr ~= "" then local found = false for _, v in ipairs(self.state.print.highlight) do if v.name == ovr then hfn = v.fn; break end end if not hfn then self.state:err("unknown highlighter: " .. ovr) hfn = function(l) return l end end else hfn = self.state.print.highlight[#self.state.print.highlight].fn end if prof then prof:start("initialization") end for _, l in ipairs(lines) do l.lead, l.raw, l.trail = {}, l.text, {} end if prof then prof:stop() end for _, f in ipairs(self.state.print.pre ) do go(f) end go(hfn) for _, f in ipairs(self.state.print.post) do go(f) end if prof then prof:start("width analysis") end for _, l in ipairs(lines) do for _, t in ipairs(l.lead ) do setmetatable(t, print_line_mt) end setmetatable(l, print_line_mt) for _, t in ipairs(l.trail) do setmetatable(t, print_line_mt) end end if prof then prof:stop() end if prof then prof:print() end return self end function mt.__index:print_width() as ("buffer") return term.cols - #tostring(#self.body) - 1 end function mt.__index:save(loc, first, last, append) as ("buffer", "loc?", "number?", "number?", "*") if loc and loc:get_path() and not self._loc and not append then self:set_loc(loc) end first = first or 1 last = last or #self.body local default_write = not loc and first == 1 and last == #self.body and not append loc = lib.assert(loc or self._loc, "no location to save to") if default_write then self.state:hook(self.state.hooks.buffer.save_pre, self) end local s = lib.lines_join(self.body:get(first, last)) if default_write then s = self.state:filter("write", s, self) end self.state:info(("%s %d bytes to %s"):format(append and "append" or "write", #s, tostring(loc))) if append then loc:append(s, self) else loc:write(s, self) end if default_write then self:set_modified(false) self.cookie = self:get_path() and lib.path_cookie(self:get_path()) or nil end if default_write then self.state:hook(self.state.hooks.buffer.save_post, self) end return self end function mt.__index:screen_range(first, last, opts) as ("buffer", "number?", "number?", "table?") if opts then as ("*", "*", "*", {nup = "number?", ndn = "number?", size = "number?"}) end last = last or first opts = opts or {} opts.nup = opts.nup or 1 opts.ndn = opts.ndn or 1 opts.size = opts.size or 1 local height = math.floor((term.rows - 3) * opts.size + 0.5) local extra = #tostring(#self.body) + 1 repeat local stop = true for _ = 1, opts.ndn do if last < #self.body then if self:display_height(first, last + 1, extra) <= height then last = last + 1 stop = false end end end for _ = 1, opts.nup do if first > 1 then if self:display_height(first - 1, last, extra) <= height then first = first - 1 stop = false end end end until stop return first, last end function mt.__index:set_loc(loc) as ("buffer", "loc") local b = loc:get_path() and self.state:get_buffer_for(loc:get_path()) if b and b ~= self then lib.error("already opened: " .. tostring(loc)) end local old_loc = self._loc self._loc = loc self:set_modified(true) self:drop_cache() self.cookie = nil self.state:hook(self.state.hooks.buffer.loc_post, self, old_loc) return self end function mt.__index:set_modified(b) as ("buffer", "*") self._modified_key = b and {} or self.body.text_key return self end function mt.__index:status_line() as ("buffer") local ret = {} for _, w in ipairs(self.state.widgets) do local ok, s = lib.pcall(w, self) if ok then table.insert(ret, s) else self.state:err(s) end end return table.concat(ret, " ") end function mt.__index:set_vpath(p) as ("buffer", "string?") self._vpath = p self:drop_cache() return self end function mt.__index:undo() as ("buffer") local tmp = self.body self.body = lib.assert(self._undo_body, "no undo point to restore found") self._undo_body = tmp return self end local next_id = 1 return function(state, loc) as ("state", "loc?") local ret = setmetatable({ _cmd = false, _conf = {}, _preloaded_lines = {}, _undo_body = nil, body = require "neo-ed.body" (), conf_key = {}, history = {}, history_id = 0, name = false, state = state, }, mt) ret._modified_key = ret.body.text_key state:register(ret) if loc then ret:load(loc) else ret:history_commit() end return ret end ]================================================================================] , "neo-ed.buffer")) package.preload["neo-ed.lib"] = assert(load( [================================================================================[ local m = {} local apidoc = require "neo-ed.apidoc" local posix = require "posix" local as = require "neo-ed.lib.as" local json = require "neo-ed.lib.json" apidoc "/api/lib/prof" {name = "lib.prof", type = "boolean", def = "`false`", descr = "enable profiling of various code paths"} m.prof = false apidoc "/api/lib/trace" {name = "lib.trace", type = "boolean", def = "`false`", descr = "enable stack traces for errors"} m.trace = false apidoc "/api/lib/assert" { name = "lib.assert(test, msg)", type = "function(*, string?) -> *", descr = "behaves like the standard `assert`, but respects ", see = {"/api/lib/error", "/api/lib/pcall"}, } function m.assert(pred, msg) if not pred then m.error(msg or "assertion failed") end return pred end apidoc "/api/lib/autocomp_dict" { name = "lib.autocomp_dict(dict, f)", type = "function(table, (function(string) -> string, string?)?) -> function(table, string) -> table", descr = "build an autocomplete function from table", see = {"/api/lib/autocomp_path", "/api/term/getline"}, details = [=[ A word is either a string key, or a string value to a number key. If `f` is given, it is called with each word, and expected to return the possibly modified word, as well as an optional info text. The returned function completes any prefix (even an empty string) to all matching words, with the info text as given. Choosing a suitable autocompletion activation pattern is up to the caller. ]=], } function m.autocomp_dict(dict, f) as ("table", "function?") f = f or function(s) return s end local cache = {} for k, v in pairs(dict) do local w = type(k) == "string" and k or type(k) == "number" and type(v) == "string" and v if w then local k_, v_ = f(w) for i = 0, #k_ do local pre = k_:sub(1, i) local suf = k_:sub(i + 1) cache[pre] = cache[pre] or {} cache[pre][suf] = v_ or true end end end return function(comps, pre) for k, v in pairs(cache[pre] or {}) do if type(comps[k]) == "string" and type(v) == "string" then comps[k] = comps[k] .. "|" .. v else comps[k] = v end end end end apidoc "/api/lib/autocomp_path" { name = "lib.autocomp_path", type = "function(table, string) -> table", descr = "autocompletion function for paths", see = {"/api/lib/autocomp_dict", "/api/term/getline"}, details = [=[ Autocompletes paths using the contents of the parent directory. The entries `.` and `..` are skipped, everything else can be completed to. Paths to complete can be: - absolute, i.e. starting with `/`, - relative to the home directory, starting with `~/`, - relative to the current working directory, if none of the above. Shell escaping is currently not handled. Choosing a suitable autocompletion activation pattern is up to the caller. ]=], } function m.autocomp_path(comps, pre) as ("table", "string") pre = pre:gsub("^~", os.getenv("HOME")) local function stat_comp(pre, suf) local path = pre .. suf local ty = m.path_type(path) if ty then local st = posix.sys.stat.stat(path) local comp = ("%s %.1fK %d:%d %#o"):format(ty, st.st_size / 1024, st.st_uid, st.st_gid, st.st_mode & ~posix.sys.stat.S_IFMT) if ty == "dir" and not path:find("/$") then comps[suf .. "/"] = comp else comps[suf] = comp end end end stat_comp(pre, "") local dir, base = pre:match("^(.*/+)([^/]*)$") if not base then dir, base = ".", pre end if base == "" then stat_comp(dir, "") end local ok, d = m.pcall(posix.dirent.dir, dir) if not ok then d = {} end for _, v in ipairs(d) do if v ~= "." and v ~= ".." and v:sub(1, base:len()) == base then stat_comp(dir .. "/" .. base, v:sub(base:len() + 1)) end end end apidoc "/api/lib/basename" { name = "lib.basename(path)", type = "function(string) -> string", descr = "returns the path without leading directories", see = {"/api/lib/dirname", "/api/state/realpath"}, details = [=[ Alias for `posix.libgen.basename`. ]=], } function m.basename(path) as ("string") return posix.libgen.basename(path) end apidoc "/api/lib/cache" { name = "lib.cache", type = "table", descr = "caching infrastructure", see = { "/api/body/pos_key", "/api/body/text_key", "/api/buffer/conf_key", "/api/state/frame_key", "/api/state/world_key", }, details = [=[ ## Synopsis Has weak keys, initially empty. When indexed with a table, another cache table with the same properties as `lib.cache` is created, associated with the key, and returned. Keys of other types have no special behavior. ## Detailed Explanation The cache operates on the principle of **cache keys**. It forms an infinite maze of empty, lazily expanding, automatically collapsing sub-caches. The keys are breadcrumbs that uniquely define a path to a sub-cache. Any table can be used as a cache key, but usually a dedicted empty table is employed. When a cache key is no longer reachable, any associated sub-cache tree is eventually collected as well. Various objects hold public cache keys tied to their internal state. When their state changes in a specific way, the corresponding cache key is swapped out for a new one. A unique, auto-invalidating cache table reference can be obtained with chained indexing operations on `lib.cache`. Typically, `lib.cache` is indexed with the first public cache key, the result is indexed with the next public cache key, etc. Finally, the result of this is indexed with a local, persistent cache key that determines the identity of the sub-cache. For example, if we wanted to track the total amount of leading indentation per buffer, we could do something like: ```lua local lib = require "neo-ed.lib" local my_key = {} local function get_wsp_count(buffer) local cache = lib.cache[buffer.conf_key][buffer.body.text_key][my_key] if not cache.wsp then local t2s = buffer:conf_get("tab2spc") local ind = buffer:conf_get("indent") cache.wsp = ... end return cache.wsp end ``` This function performs the calculation once and caches the result. If the buffer config or contents change, the cache keys are replaced. The updated list of keys traces a different path through the cache maze, leading to a fresh sub-cache. Consequently, the value is recalculated on the next call to `get_wsp_count`. ## Recommendations - By idly accessing cached properties at opportune moments, costly recalculations during more timing-sensitive situations can be avoided. For example, autocompletion dictionaries can be accessed in the `buffer.prompt_pre` hook to avoid a latency spike between two key presses as the partial input first satisfies the corresponding activation pattern. ]=], } local cache_mt; cache_mt = { __index = function(t, k) if type(k) == "table" then t[k] = setmetatable({}, cache_mt); return rawget(t, k) end end, __mode = "k", } m.cache = setmetatable({}, cache_mt) apidoc "/api/lib/const" { name = "lib.const(value)", type = "function(*) -> function() -> *", descr = "returns a function that always returns `value`", see = {"/api/lib/const"}, } function m.const(val) return function() return val end end apidoc "/api/lib/defer" { name = "local _ = lib.defer(f)", type = "function(function) -> table", descr = "wrap a function into a to-be-closed table", } function m.defer(f) return setmetatable({}, {__close = f}) end apidoc "/api/lib/diff" { name = "lib.diff(cmdf, sa, sb, la, lb)", type = "function((function(string, string, string?, string?) -> cmd)?, function|string|table, function|string|table, string?, string?) -> string", descr = "run diff command", -- TODO: see details = [=[ `cmdf` is a function that takes two file names and two optional "display" file names, and returns a with an adequately configured `diff` program. It defaults to a function that uses the system `diff`. `sa` and `sb` are strings to diff against each other (not paths!). They can each be given a display file name using `la` and `lb`. The diff command is run, and its output is captured and returned as a string. ]=], } function m.diff(cmdf, sa, sb, la, lb) as ("function?", "function|string|table", "function|string|table", "string?", "string?") cmdf = cmdf or function(a, b, la, lb) return lib.cmd "diff" "-u" "-L" (la or a) "-L" (lb or b) "--" (a) (b) end return cmdf("/proc/self/fd/3", "/proc/self/fd/4", la, lb):load(3, sa):load(4, sb):read("a!") end apidoc "/api/lib/dirname" { name = "lib.dirname(path)", type = "function(string) -> string", descr = "returns the directory prefix of `path`", see = {"/api/lib/basename", "/api/state/realpath"}, details = [=[ Alias for `posix.libgen.dirname`. ]=], } function m.dirname(path) as ("string") return posix.libgen.dirname(path) end apidoc "/api/lib/dup" { name = "lib.dup(t, n)", type = "function(table, number?) -> table", descr = "returns a recursive deep copy of `t`, up to depth `n`, defaulting to 1", see = {"/api/lib/subset_key_prefix"}, } function m.dup(t, n) as ("table", "number?") n = n or 1 local ret = {} for k, v in pairs(t) do if type(v) == "table" and n > 1 then ret[k] = m.dup(v, n - 1) else ret[k] = v end end return ret end apidoc "/api/lib/error" { name = "lib.error(msg)", type = "function(string)", descr = "behaves like the standard `error`, but respects ", see = {"/api/lib/assert", "/api/lib/pcall"}, } function m.error(msg) as ("string") error(msg, m.trace and 2 or 0) end apidoc "/api/lib/fninfo" { name = "lib.fninfo(f)", type = "function(function|number) -> string", descr = "identifies the function `f` in human-readable way", details = [=[ If `f` is a number, it refers to a stack level. `0` is `lib.fninfo`, `1` is its caller, ... ]=], } function m.fninfo(f) as ("function|number") local info = debug.getinfo(type(f) == "number" and f + 1 or f, "lnS") local lines = info.currentline > 0 and ("%d"):format(info.currentline) or ("%d-%d"):format(info.linedefined, info.lastlinedefined) local desc = info.name and (" (%s %s)"):format(info.namewhat == "" and "function" or info.namewhat, info.name) or "" return info.source .. ":" .. lines .. desc end apidoc "/api/lib/have_executable" { name = "lib.have_executable(name)", type = "function(string) -> boolean", descr = "check whether `name` is available in `$PATH`", } function m.have_executable(name) as ("string") return m.cmd "which" "--" (name) :hush() :ok() end apidoc "/api/lib/heredoc" { name = "lib.heredoc(s)", type = "function(string) -> string", descr = "removes space from documentation strings", -- TODO: see details = [=[ The leading tabs of the first line are removed from it, as well as every subsequent line. Any trailing space is also removed. ]=], } function m.heredoc(s) as ("string") local lead, s = s:match("^(\t*)(.*)$") s = s:gsub("\n" .. lead, "\n"):gsub("%s*$", "") return s end apidoc "/api/lib/id" { name = "lib.id", type = "function(...) -> ...", descr = "returns its arguments", see = {"/api/lib/const"}, } function m.id(...) return ... end apidoc "/api/lib/json_api" { name = "lib.json(method, endpoint, headers, data)", type = "function(string, string, table, table?) -> table", descr = "helper function to interact with JSON APIs", details = [=[ `method` is the request method, e.g. `GET`. `url` defines the URL to query. `headers` is a table of headers to add. `data` is an optional table to send as a JSON body. The function returns a table, decoded from the JSON response. ]=], } function m.json_api(method, endpoint, headers, data) as ("string", "string", "table", "table?") local cmd = m.cmd "curl" "--silent" "-X" (method) (endpoint) for _, k, v in m.opairs(headers) do cmd "--header" (("%s:%s"):format(k, v)) end if data then cmd "--data" (json.encode(data)) end return json.decode(cmd:read()) end apidoc "/api/lib/keys" { name = "lib.keys(t)", type = "function(table) -> table", descr = "returns a sorted list with the keys from `t`", see = {"/api/lib/opairs"}, } function m.keys(t) as ("table") local ret = {} for _, k in m.opairs(t) do table.insert(ret, k) end return ret end apidoc "/api/lib/lines_join" { name = "lib.lines_join(l, first, last)", type = 'function({{text = "string"}}, number?, number?) -> string', descr = "joins a list of lines to a single string", see = {"/api/lib/lines_split"}, -- TODO: see body details = [=[ Each line is expected to be a table, with `.text` containing the text of the line. All other fields are ignored. `text` may not contain newline characters. `first` and `last` optionally select the first and last line of the range to extract. They default to the first and last line of the list respectively. The result has each line terminated by a newline character, i.e. it is either an empty string, or ends with a newline character. ]=], } function m.lines_join(l, first, last) as ({{text = "string"}}, "number?", "number?") first = first or 1 last = last or #l local tmp = {} for k = first, last do table.insert(tmp, l[k].text) end table.insert(tmp, "") return table.concat(tmp, "\n") end apidoc "/api/lib/lines_split" { name = "lib.lines_split(s)", type = 'function(string) -> {{text = "string"}}', descr = "split a string into lines", see = {"/api/lib/lines_join"}, -- TODO: see body details = [=[ The result is a list of tables, each containing the line text in `.text`. Newline characters are never present in the result. Newlines are interpreted as terminators, not separators. The last non-empty line is still recognized if it is not newline terminated, but if the string ends with a newline character, it is interpreted as the terminator of the preceding line, not as a separator to a final empty line. ]=], } function m.lines_split(s) as ("string") local ret = {} for l in s:gmatch("[^\n]*") do table.insert(ret, {text = l}) end if ret[#ret].text == "" then table.remove(ret) end return ret end apidoc "/api/lib/match" { name = "lib.match(args)", type = "function(table) -> *", descr = "control flow based on string pattern matching", details = [=[ `args.s` is the string to match. `args.choose` is the list of alternatives to choose from. Each element is a table with at least the `.pat` and `.fn` present. `.pat` is the pattern to match against `args.s`. If it matches, `.fn` is called with a table containing all captured patterns, and the unpacked contents of `args.args`, if present, up to `args.args.n`, if present. The return values of `.fn` can be examined by the optional function `args.fallthrough`. If it returns `true` given the return values of `.fn`, the successful match of `.pat` is ignored, and processing continues with the next element in `args.choose`. Otherwise, `lib.match` returns the values returned by `.fn`. If processing reaches the end of `args.choose`, `args.def` is called with `s`, as well as the unpacked contents of `args.args`, if present, up to `args.args.n`, if present. Its return values are returned from `lib.match`. `args.def` defaults to a function that raises an error. ]=], } function m.match(args) as { s = "string", choose = {"table"}, args = "table?", def = "function?", fallthrough = "function?", } args.args = args.args or {} args.def = args.def or function(s) m.error("could not parse: " .. s) end for _, v in ipairs(args.choose) do if not v.pat or not v.fn then error("command " .. (v.syntax or v.descr or v[1] or "???") .. " is not defined correctly") end local r = {args.s:match(v.pat)} if r[1] then local r_ = table.pack(v.fn(r, table.unpack(args.args, 1, args.args.n or #args.args))) if not args.fallthrough or not args.fallthrough(table.unpack(r_, 1, r_.n)) then return table.unpack(r_, 1, r_.n) end end end return args.def(args.s, table.unpack(args.args, 1, args.args.n or #args.args)) end apidoc "/api/lib/mkdir" { name = "lib.mkdir(path)", type = "function(string)", descr = "creates the directory `path`, including parent directories, if missing", details = [=[ Equivalent to `mkdir -p`. ]=], } function m.mkdir(path) as ("string") m.cmd "mkdir" "-p" "--" (path) :err() end apidoc "/api/lib/not_impl" { name = "lib.not_impl(what)", type = "function(string) -> function()", descr = "specialized error function for missing implementations", see = {"/api/lib/error"}, details = [=[ Returns a function that, when called, raises an error indicating that `what` is not implemented. ]=], } function m.not_impl(what) return function() m.error("not implemented: " .. what) end end apidoc "/api/lib/opairs" { name = "lib.opairs(t)", type = "function(table) -> (function(table, number) -> (number, *, *, number)?), table, number", descr = "deterministically ordered iteration over table pairs", details = [=[ Conceptually similar to `pairs`. All pairs of `t` are visited, but in the following consistent, ascending order: - booleans, with `false` < `true` - functions, based on `tostring` - numbers - strings - tables, based on `tostring` - threads, based on `tostring` - userdata, based on `tostring` Unlike with `pairs`, the iterator returns: - the iteration count, starting at 1 - the key - the value - the total number of pairs Note that the deterministic equivalent to for k, v in pairs(t) do ... end is for _, k, v in lib.opairs(t) do ... end ]=], } local function ofn() local cache = {} local function as_string(v) if not cache[v] then cache[v] = tostring(v) end return cache[v] end local ofns = { boolean = function(a, b) return (a and 1 or 0) < (b and 1 or 0) end, ["function"] = function(a, b) return as_string(a) < as_string(b) end, number = function(a, b) return a < b end, string = function(a, b) return a < b end, table = function(a, b) return as_string(a) < as_string(b) end, thread = function(a, b) return as_string(a) < as_string(b) end, userdata = function(a, b) return as_string(a) < as_string(b) end, } local type_order = { boolean = 1, ["function"] = 2, number = 3, string = 4, table = 5, thread = 6, userdata = 7, } return function(a, b) local ta = type(a) local tb = type(b) if ta ~= tb then return type_order[ta] < type_order[tb] end return ofns[ta](a, b) end end local function iter(data, i) i = i + 1 if data.ks[i] ~= nil then return i, data.ks[i], data.t[data.ks[i]], #data.ks end end function m.opairs(t) as ("table") local ks = {} for k in pairs(t) do table.insert(ks, k) end table.sort(ks, ofn()) return iter, {ks = ks, t = t}, 0 end apidoc "/api/lib/pad" { name = "lib.pad(s, l, padRight)", type = "function(string, number, boolean?) -> string", descr = "[deprecated] pad a string to a given width", } local space_lookup = {" "} for i = 2, 10 do space_lookup[i] = space_lookup[i - 1] .. space_lookup[i - 1] end function m.pad(s, l, padRight) as ("string", "number", "*") for i = #space_lookup, 1, -1 do if utf8.len(s) + utf8.len(space_lookup[i]) <= l then s = padRight and s .. space_lookup[i] or space_lookup[i] .. s end end return s end apidoc "/api/lib/pager" { name = "lib.pager(text)", type = "function(string)", descr = "show text via pager", details = [=[ Uses `less` with processing of ANSI color codes enabled. The pager is only started if the text is longer than one screen. ]=], } function m.pager(text) as ("string") if posix.unistd.isatty(0) and m.have_executable("less") then m.cmd "less" "--quit-if-one-screen" "-R" :load(text) :ok() else print(text) end end apidoc "/api/lib/patesc" { name = "lib.patesc(s)", type = "function(string) -> string", descr = "escape string for use in Lua patterns", see = {"/api/lib/shellesc"}, } function m.patesc(s) as ("string") return s:gsub("[%^$()%%.[%]*+%-?]", "%%%1") end apidoc "/api/lib/path_cookie" { name = "lib.path_cookie(path)", type = "function(string) -> string", descr = "pretty-print inode information, use the result to check if a path has changed", details = [=[ Tries to `stat(3p)` the given `path`. If it fails, the error message is incorporated in the return value. Otherwise, the following information is present: - inode type: `st_mode` (without permissions) - inode identity: `st_dev`, `st_ino` - file size: `st_size` - modification time: `st_mtim` ]=], } local function stat_type(st) as ("table?") if not st then return nil end if posix.sys.stat.S_ISBLK (st.st_mode) ~= 0 then return "block" end if posix.sys.stat.S_ISCHR (st.st_mode) ~= 0 then return "char" end if posix.sys.stat.S_ISDIR (st.st_mode) ~= 0 then return "dir" end if posix.sys.stat.S_ISFIFO(st.st_mode) ~= 0 then return "fifo" end if posix.sys.stat.S_ISLNK (st.st_mode) ~= 0 then return "link" end if posix.sys.stat.S_ISREG (st.st_mode) ~= 0 then return "file" end if posix.sys.stat.S_ISSOCK(st.st_mode) ~= 0 then return "socket" end return "???" end function m.path_cookie(path) as ("string") local st, msg = posix.sys.stat.stat(path) if not st then return path .. ": " .. msg end return ("%s: %s inode %#x:%#x %dB @ %s"):format( path, stat_type(st), st.st_dev, st.st_ino, st.st_size, os.date("%Y-%m-%d %H:%M:%S", st.st_mtime) ) end apidoc "/api/lib/path_type" { name = "lib.path_type(path)", type = "function(string) -> string?", descr = "classify `path` by filesystem type", details = [=[ Possible return values: - `nil` - does not exist - `"block"` - block device - `"char"` - character device - `"dir"` - directory - `"fifo"` - named pipe - `"link"` - dead symbolic link - `"file"` - regular file - `"socket"` - unix domain socket ]=], } function m.path_type(path) as ("string") return stat_type(posix.sys.stat.stat(path)) end apidoc "/api/lib/pcall" { name = "lib.pcall(f, ...)", type = "function(function, ...) -> ...", descr = "behaves like `pcall`, but adds a stack trace to errors if is enabled", see = {"/api/lib/assert", "/api/lib/error"}, } function m.pcall(f, ...) as ("function") return xpcall(f, m.traceback, ...) end apidoc "/api/lib/profiler" { name = "lib.profiler(name)", type = "function(string) -> profiler", descr = "returns a named profiler", } apidoc "/api/lib/profiler/start" { name = "profiler:start(name)", type = "function(profiler, string) -> table", descr = "start a profiling section", details = [=[ The section can be terminated using . Alternatively, the return value can be assigned to a to-be-closed variable, so the profiling section is tied to the lexical scope. If a section has already been started, it is stopped first. Statistics from multiple sections with the same name are added together and reported as one. ]=], } apidoc "/api/lib/profiler/stop" { name = "profiler:stop()", type = "function(profiler)", descr = "end a profiling section started by ", details = [=[ This function does nothing if no profiling section is currently active. ]=], } apidoc "/api/lib/profiler/print" { name = "profiler:print()", type = "function(profiler)", descr = "print profiling results", details = [=[ If a section is still running, it is stopped first. ]=], } local profmt = { __name = "profiler", __index = { start = function(self, name) as ("profiler", "string") self:stop() local step = self.steps[name] if not step then step = {name = name, time = 0, calls = 0} table.insert(self.steps, step) self.steps[name] = step end self.active = step self.active.calls = self.active.calls + 1 self.active.started = m.timestamp() return setmetatable({stop = function() self:stop() end}, {__close = function() self:stop(n) end}) end, stop = function(self) as ("profiler") if not self.active then return end self.active.time = self.active.time + (m.timestamp() - self.active.started) self.active.started = nil self.active = nil end, print = function(self) as ("profiler") self:stop() print("\tSequence for " .. self.name .. ":") for _, s in ipairs(self.steps) do print(("\t%10d %7.1f ms %s"):format(s.calls, s.time * 1000, s.name)) end print(("\t %7.1f ms total"):format((m.timestamp() - self.created) * 1000)) local cnt = collectgarbage("count") print(("\tMemory: %.1fK -> %.1fK (%+.1fK)"):format(self.mem, cnt, cnt - self.mem)) end, } } function m.profiler(name) as ("string") return setmetatable({ name = name, steps = {}, created = m.timestamp(), mem = collectgarbage("count"), }, profmt) end apidoc "/api/lib/require" { name = "lib.require(modname, ...)", type = "function(string, string...) -> * | nil, string", descr = "`require` wrapper that returns `nil` on failure, optionally with search path overriding", details = [=[ All paths given after `modname` are searched before the system paths. As usual, module `a` matches `a.lua` or `a/init.lua`. If the module is not found, `nil` and the error message are returned. ]=], } function m.require(modname, ...) as ("string") local add_paths = {...} local old_path = package.path local dirsep, sep, subst = package.config:match("^([^\n]*)\n([^\n]*)\n([^\n]*)\n") if add_paths[1] then local t = {} for _, p in ipairs(add_paths) do table.insert(t, p .. dirsep .. subst .. ".lua") table.insert(t, p .. dirsep .. subst .. dirsep .. "init.lua") end table.insert(t, package.path) package.path = table.concat(t, sep) end local ok, ret = pcall(require, modname) package.path = old_path if not ok then return nil, ret end return ret end apidoc "/api/lib/sigdesc" { name = "lib.signame(signal)", type = "function(number) -> string", descr = "find a best-effort human-readable description for a signal number", } local sigdescs = {} for k, v in pairs(posix.signal) do if type(k) == "string" and type(v) == "number" and k:find("^SIG") then sigdescs[v] = ("%s (%d)"):format(k, v) end end function m.sigdesc(sig) as ("number") return sigdescs[sig] or ("signal %d"):format(sig) end apidoc "/api/lib/subset_key_prefix" { name = "lib.subset_key_prefix(t, prefix, proper)", type = "function(table, string, boolean?) -> table", descr = "filter table keys by prefix", see = {"/lib/api/dup"}, details = [=[ Returns a partial copy of `t`, containing only pairs where the `prefix` is a prefix of the key. If `proper` is `true`, a proper prefix match is enforced, i.e. `prefix` cannot be a key. ]=], } function m.subset_key_prefix(t, p, proper) as ("table", "string", "*") local p_ = "^" .. m.patesc(p) local ret = {} for k, v in pairs(t) do if k:find(p_) then ret[k] = v end end if proper then ret[p] = nil end return ret end apidoc "/api/lib/shellesc" { name = "lib.shellesc(s)", type = "function(string) -> string", descr = "escape string for use in shell commands", details = [=[ This is a low-level function. In many cases, the class is more convenient and safer to use. ]=], } function m.shellesc(s) as ("string") if s:find("^[-+,./0-9=A-Z_a-z]*$") then return s end return "'" .. s:gsub("'", "'\\''") .. "'" end apidoc "/api/lib/strip_fmt" { name = "lib.strip_fmt(s)", type = "function(string) -> string", descr = "strips SGR sequences", -- TODO: see: term } function m.strip_fmt(s) as ("string") return (s:gsub("\x01.-\x02", ""):gsub("\x1b%[[0-9;]-m", "")) end apidoc "/api/lib/timestamp" { name = "lib.timestamp()", type = "function() -> number", descr = "fractional unix timestamp with sub-second resolution", } function m.timestamp() local t = posix.sys.time.gettimeofday() return t.tv_sec + t.tv_usec / 1000 / 1000 end apidoc "/api/lib/traceback" { name = "lib.traceback(s)", type = "function(string) -> string", descr = "appends a stack trace if is enabled", see = {"/api/lib/pcall"}, } function m.traceback(s) as ("string") if m.trace then return debug.traceback(s, 2) else return s end end apidoc "/api/lib/try" { name = "lib.try(args)", type = "function(table) -> ...", descr = "general error handling wrapper", details = [=[ `args.fn` is `pcall`ed with the unpacked table contents of `args`, up to `args.n`, if present. If `args.fn` does not raise an error, its return values are returned. Otherwise, if `args.catch` is present, it is called with the error. If it returns `true`, the function returns without return values, if it returns `false` the error is re-raised. In any case, before returning or re-raising, `args.finally` is called, if present. It receives either `true` plus the return values of `args.fn`, or `false` plus the error. Its return value is ignored. ]=], } function m.try(tbl) as { fn = "function", catch = "function?", finally = "function?", n = "number?", } local tr = table.pack(m.pcall(tbl.fn, table.unpack(tbl, 1, tbl.n or #tbl))) if tr[1] then if tbl.finally then tbl.finally(table.unpack(tr, 1, tr.n)) end return table.unpack(tr, 2, tr.n) end if tbl.catch then local ok = tbl.catch(table.unpack(tr, 2, tr.n)) if tbl.finally then tbl.finally(table.unpack(tr, 1, tr.n)) end if not ok then m.error(tr[2], 0) end return end if tbl.finally then tbl.finally(table.unpack(tr, 1, tr.n)) end error(tr[2], 0) end -- Some lazy loading shenanigans to avoid circular imports apidoc "/api/lib/xdg/cache_home" {name = "lib.xdg.cache_home" , type = "string" , descr = "XDG cache directory" } apidoc "/api/lib/xdg/config_home" {name = "lib.xdg.config_home", type = "string" , descr = "XDG config directory" } apidoc "/api/lib/xdg/data_home" {name = "lib.xdg.data_home" , type = "string" , descr = "XDG data directory" } apidoc "/api/lib/xdg/runtime_dir" {name = "lib.xdg.runtime_dir", type = "string?", descr = "XDG runtime directory"} apidoc "/api/lib/xdg/state_home" {name = "lib.xdg.state_home" , type = "string" , descr = "XDG state directory" } local lazy = { cmd = function() return require "neo-ed.lib.cmd" end, xdg = function() local function get_xdg_dir(name, fallback) local ret = os.getenv("XDG_" .. name) if ret == "" then ret = nil end ret = ret or fallback and (os.getenv("HOME") .. "/" .. fallback) return ret end return { cache_home = get_xdg_dir("CACHE_HOME" , ".cache" ), config_home = get_xdg_dir("CONFIG_HOME", ".config" ), data_home = get_xdg_dir("DATA_HOME" , ".local/share"), runtime_dir = get_xdg_dir("RUNTIME_DIR" ), state_home = get_xdg_dir("STATE_HOME" , ".local/state"), } end, } setmetatable(m, { __index = function(m, k) if not lazy[k] then return end m[k] = lazy[k]() return rawget(m, k) end, }) return m ]================================================================================] , "neo-ed.lib")) package.preload["neo-ed.lib.as"] = assert(load( [================================================================================[ local cache = {} local primitive = { boolean = true, ["function"] = true, ["nil"] = true, number = true, string = true, table = true, thread = true, userdata = true, } local function compile(spec) if not cache[spec] then local pdict, mdict = {}, {} for s in spec:gsub("%?$", "|nil"):gmatch("[^|]+") do if primitive[s] then pdict[s] = true else mdict[s] = true end end cache[spec] = function(v) if pdict[type(v)] then return end local m = getmetatable(v) m = m and m.__name if m and mdict[m] then return end return "", ("expected %s, got %s%s%s%s"):format(spec, m or "", m and " (" or "", type(v), m and ")" or "") end end return cache[spec] end local function tc(spec, v) if spec == "*" then return end if type(spec) == "string" then return compile(spec)(v) end if type(spec) == "table" then local path, msg = tc("table", v) if path then return path, msg end if spec[1] then for i, v_ in ipairs(v) do local path, msg = tc(spec[1], v_) if path then return ("[%d]%s"):format(i, path), msg end end end for field, spec_ in pairs(spec) do if type(field) ~= "number" then local path, msg = tc(spec_, v[field]) if path then if type(field) == "string" and field:find("^[a-zA-Z_][a-zA-Z0-9_]*$") then return (".%s%s"):format(field, path), msg end return ("[%q]%s"):format(field, path), msg end end end end end return function(...) for i, spec in ipairs{...} do local n, v = debug.getlocal(2, i) local path, msg = tc(spec, v) if path then local fname = debug.getinfo(2, "n").name or "???" error( ("%s: %s (arg #%d)%s: %s"):format(fname, n, i, path, msg), (not package.loaded["neo-ed.lib"] or package.loaded["neo-ed.lib"].trace) and 3 or 0 ) end end end ]================================================================================] , "neo-ed.lib.as")) package.preload["neo-ed.lib.cmd"] = assert(load( [================================================================================[ local apidoc = require "neo-ed.apidoc" local apidoc_low_level = [=[ This is a low-level function, mostly for internal purposes. ]=] local apidoc_only_before_start = [=[ This function can only be used before the command is started. ]=] local apidoc_only_running = [=[ This function can only be used while the command is running. ]=] local apidoc_only_after_exit = [=[ This function can only be used after the command has exited. ]=] local apidoc_autostart = [=[ If the command has not been started, `cmd:fork()` is called first. ]=] local apidoc_autowait = [=[ If the command has not exited, `cmd:wait()` is called first. ]=] local apidoc_returns_self = [=[ The call returns the object for easy operation chaining. Note that this operation modifies the object, the return value is the object, not a new copy. ]=] local as = require "neo-ed.lib.as" local lib = require "neo-ed.lib" local posix = require "posix" posix.signal.signal(posix.signal.SIGCHLD, function() end) posix.signal.signal(posix.signal.SIGPIPE, function() end) local FD_MAX = 20 local expected_errors = { [posix.errno.EAGAIN ] = true, [posix.errno.EWOULDBLOCK] = true, [posix.errno.EINTR ] = true, } local function get_mode(fd) as ("number") local n = posix.fcntl.fcntl(fd, posix.fcntl.F_GETFL) if not n then return end local ret = {} if n & posix.fcntl.O_RDWR ~= 0 then return "rw" end if n & posix.fcntl.O_WRONLY ~= 0 then return "w" end return "r" end local function get_safe_fd() local fd = FD_MAX + 1 while get_mode(fd) do fd = fd + 1 end return fd end local function make_safe(fd) as ("number") local ret = lib.assert(posix.unistd.dup2(fd, get_safe_fd())) posix.unistd.close(fd) return ret end local cmd_mt = {__index = {}, __name = "cmd"} apidoc "/api/lib/cmd/trace" { name = "cmd:trace(fmt, ...)", type = "function(cmd, string?, ...) -> cmd", descr = "execution tracing", details = [=[ With no arguments, enable execution tracing for this command. With arguments, use `string.format` to construct a message from `fmt` and `...`, and print it to stderr if tracing is enabled. ]=] .. apidoc_returns_self, } function cmd_mt.__index:trace(fmt, ...) as ("cmd", "string?") if fmt then if self._trace then io.stderr:write("\x1b[33m", self.path, "\x1b[0m ", fmt:format(...), "\n") end return self end self._trace = true return self end apidoc "/api/lib/cmd/meta/add" { name = "cmd_a + cmd_b", type = "function(cmd, cmd) -> cmd", descr = "append the second command to the first as separate arguments", see = {"/api/lib/cmd/meta/call", "/api/lib/cmd/meta/concat"}, details = [=[ The executable as well as all arguments from `cmd_b` are appended to `cmd_a` as separate arguments. ]=] .. apidoc_only_before_start .. apidoc_returns_self, } function cmd_mt.__add(self, other) as ("cmd", "cmd") lib.assert(not self.pid , "already running") lib.assert(not self.exit, "already exited" ) self(other.path) for _, p in ipairs(other.args) do self(p) end return self end apidoc "/api/lib/cmd/meta/bor" { name = "cmd_a | cmd_b", type = "function(string|cmd, string|cmd) -> string", descr = "create a pipe command from two commands", see = {"/api/lib/cmd/meta/tostring", "/api/lib/cmd/meta/add", "/api/lib/cmd/meta/concat"}, details = [=[ `cmd` type arguments are converted using `tostring`, which produces POSIX shell escaped command strings. String type arguments are used as-is. The result is a concatenation of both strings, separated by ` | `. It can be executed by a POSIX shell, e.g. using , `os.execute`, or `io.popen`. The operator is designed be chainable, to assemble a pipeline from a number of `cmd`s. ]=], } function cmd_mt.__bor(a, b) as ("string|cmd", "string|cmd") return a .. " | " .. b end apidoc "/api/lib/cmd/meta/concat" { name = "cmd_a .. cmd_b", type = "function(string|cmd, string|cmd) -> string", descr = "string conversion + concatenation", see = {"/api/lib/cmd/meta/add", "/api/lib/cmd/meta/bor", "/api/lib/cmd/meta/call", "/api/lib/cmd/meta/tostring"}, details = [=[ `cmd` type arguments are converted using `tostring`, which produces POSIX shell escaped command strings. String type arguments are used as-is. The result is a concatenation of both strings. It can be executed by a POSIX shell, e.g. using , `os.execute`, or `io.popen`. ]=], } function cmd_mt.__concat(a, b) as ("string|cmd", "string|cmd") return tostring(a) .. tostring(b) end apidoc "/api/lib/cmd/meta/call" { name = "cmd(s)", type = "function(cmd, *) -> cmd", descr = "add an argument to the command", see = {"/api/lib/cmd/arg0", "/api/lib/cmd/meta/add"}, details = [=[ `s` is converted using `tostring`, then added to the argument list of the call. ]=] .. apidoc_only_before_start .. apidoc_returns_self, } function cmd_mt.__call(self, s) as ("cmd", "*") lib.assert(not self.pid , "already running") lib.assert(not self.exit, "already exited" ) if s then table.insert(self.args, tostring(s)) end return self end apidoc "/api/lib/cmd/meta/close" { name = "local c = lib.cmd(...)", type = "function(cmd)", descr = "close fds, send `SIGKILL`, `wait(3p)` for child process", see = {"/api/lib/cmd/meta/gc"}, } apidoc "/api/lib/cmd/meta/gc" { name = "do lib.cmd(...) end", type = "function(cmd)", descr = "close fds, send `SIGKILL`, `wait(3p)` for child process", see = {"/api/lib/cmd/meta/close"}, } function cmd_mt:__gc() self:_close_child_fds():_close_parent_fds() if self.pid then self:kill(posix.signal.SIGKILL) self:wait() end end cmd_mt.__close = cmd_mt.__gc apidoc "/api/lib/cmd/meta/tostring" { name = "tostring(cmd)", type = "function(cmd) -> string", descr = "escape command for POSIX shell use", see = {"/api/lib/cmd/shell", "/api/lib/shellesc"}, details = [=[ The result is a string suitable for execution using a POSIX shell. It contains the command path as well as all arguments. All other `cmd` object settings, such as `arg0` or redirections, are ignored. ]=], } function cmd_mt.__tostring(self) as ("cmd") local ret = {lib.shellesc(self.path)} for _, s in ipairs(self.args) do table.insert(ret, lib.shellesc(s)) end return table.concat(ret, " ") end apidoc "/api/lib/cmd/close" { name = "cmd:close(fd)", type = "function(cmd, number) -> cmd", descr = "`close(3p)` the given file descriptor, and deregister it from the object", details = apidoc_low_level .. [=[ The `cmd` object automatically closes all of its file descriptors, this function is only needed if a file descriptor explicitly needs to be closed early. A file descriptor managed by a `cmd` object may only be closed by using this function, otherwise undefined behavior will occur. `fd` refers to the number of a real file descriptor, which is currently open in this process, and could be passed directly to `close(3p)`. ]=] .. apidoc_returns_self, } function cmd_mt.__index:close(fd) as ("cmd", "number") posix.unistd.close(fd) for nom, real in pairs(self._child_fds) do if real == fd then self._child_fds[nom] = nil return self end end for nom, real in pairs(self._parent_fds) do if real == fd then self:trace("\x1b[30;44mclose pipe\x1b[0m for fd \x1b[36m%d\x1b[0m", nom) self._parent_fds[nom], self._istreams[real], self._ostreams[real] = nil, nil, nil return self end end lib.error(self.path .. ": close " .. fd .. ": not found (double close?)") end function cmd_mt.__index:_close_child_fds() as ("cmd") for _, fd in pairs(self._child_fds) do self:close(fd) end return self end function cmd_mt.__index:_close_parent_fds() as ("cmd") for _, fd in pairs(self._parent_fds) do self:close(fd) end return self end apidoc "/api/lib/cmd/arg0" { name = "cmd:arg0(s)", type = "function(cmd, *) -> cmd", descr = "set a different `argv[0]` for the command", see = {"/api/lib/cmd/meta/call"}, details = [=[ `s` is converted using `tostring`, then used as the 0-th argument for the command. By default, the 0-th argument is the name/path through which the executable is called. It is filled by , but can be overridden using this function. The effects of this depend on the called executable. ]=] .. apidoc_only_before_start .. apidoc_returns_self, } function cmd_mt.__index:arg0(s) as ("cmd", "*") lib.assert(not self.pid , "already running") lib.assert(not self.exit, "already exited" ) self.args[0] = tostring(s) return self end apidoc "/api/lib/cmd/pipe" { name = "cmd:pipe(fd, mode)", type = "function(cmd, number, string) -> number", descr = "prepare a `pipe(3p)` to be attached to the process", see = {"/api/lib/cmd/load", "/api/lib/cmd/capture"}, details = apidoc_low_level .. [=[ A `pipe(3p)` is opened, and will be attached to file descriptor `fd` when the command is started. The parent side file descriptor is set to nonblocking mode, and then returned. `mode` can be `"r"` to read from the command's output, or `"w"` to write to the command's input. ]=] .. apidoc_only_before_start, } function cmd_mt.__index:pipe(fd, mode) as ("cmd", "number", "string") lib.assert(not self.pid , "already running") lib.assert(not self.exit, "already exited" ) self:trace("\x1b[30;44mopen pipe\x1b[0m %s fd \x1b[36m%d\x1b[0m", mode == "w" and "to" or "from", fd) local r0, w0 = posix.unistd.pipe() lib.assert(r0, w0) local rfd = make_safe(r0) local wfd = make_safe(w0) if mode == "r" then posix.fcntl.fcntl(rfd, posix.fcntl.F_SETFL, posix.fcntl.O_NONBLOCK) self._parent_fds[fd] = rfd self._child_fds [fd] = wfd return rfd end if mode == "w" then posix.fcntl.fcntl(wfd, posix.fcntl.F_SETFL, posix.fcntl.O_NONBLOCK) self._parent_fds[fd] = wfd self._child_fds [fd] = rfd return wfd end lib.error("invalid pipe mode: " .. mode) end apidoc "/api/lib/cmd/load" { name = "cmd:load([fd = 0,] input)", type = "function(cmd, number, string|function|table) -> cmd", descr = "prepare data to be written to the command", see = {"/api/lib/cmd/capture", "/api/lib/cmd/pipe", "/api/lib/cmd/r"}, details = [=[ Input chunks will be fed to the process until the pipe is full. This is interleaved with other process I/O operations. `fd` can be left out, it defaults to the standard input of the command, i.e. `0`. - If `input` is a string, it is used as-is. - If `input` is a function, it will be called repeatedly with a number, starting at `1`, and increment by `1` for each successive call. It is expected to return the next bit of data as a string of arbitrary length (including 0), or `nil` if no more data is available. The function must be able to handle further calls after the first `nil` return, and continue returning `nil`. - If `input` is a table, the contents are used as chunks. - If `input[i]` is a string, it is used as-is. - If `input[i]` is a table, `input[i].text` is used, after a trailing `\n` is appended. ]=] .. apidoc_only_before_start .. apidoc_returns_self, } function cmd_mt.__index:load(fd, input) if not input then fd, input = 0, fd end as ("cmd", "number", "string|function|table") local chunkfn = nil if type(input) == "string" then chunkfn = function(i) return i == 1 and input or nil end elseif type(input) == "function" then chunkfn = input elseif type(input) == "table" then chunkfn = function(i) return type(input[i]) == "table" and (input[i].text .. "\n") or input[i] end else lib.error("invalid input type: " .. type(input)) end local wfd = self:pipe(fd, "w") local chunk = nil local ctr = 0 local off = 0 self._istreams[wfd] = function() while true do if not chunk or off >= #chunk then local tmp = {} local tsz = 0 while tsz < 256 do local chunk_ repeat chunk_, ctr = chunkfn(ctr + 1), ctr + 1, 0 until chunk_ ~= "" self:trace("got chunk #%d: \x1b[35m%dB\x1b[0m", ctr, chunk_ and #chunk_ or -1) if chunk_ then table.insert(tmp, chunk_); tsz = tsz + #chunk_ else break end end chunk, off = tmp[1] and table.concat(tmp) or nil, 0 end if chunk then while off < #chunk do local r, msg, errno = posix.unistd.write(wfd, chunk, math.min(512, #chunk - off), off) if r then off = off + r self:trace( "write \x1b[35m%dB\x1b[0m to \x1b[36m%d\x1b[0m, now at \x1b[35m%dB\x1b[0m/\x1b[35m%dB\x1b[0m, #%d", r, fd, off, #chunk, ctr ) elseif expected_errors[errno] then self:trace("\x1b[34msuspend\x1b[0m writing to \x1b[36m%d\x1b[0m: %s", fd, msg) return else self:trace("\x1b[30;41merror\x1b[0m writing to \x1b[36m%d\x1b[0m: %s", fd, msg) self:close(wfd) return end end else self:trace("\x1b[30;42mdone\x1b[0m writing to \x1b[36m%d\x1b[0m", fd) self:close(wfd) return end end end self:trace("prepared input to fd \x1b[36m%d\x1b[0m", fd) return self end apidoc "/api/lib/cmd/capture" { name = "cmd:capture([fd = 1])", type = "function(cmd, number?) -> cmd", descr = "request command output to be captured", see = {"/api/lib/cmd/load", "/api/lib/cmd/pipe", "/api/lib/cmd/w", "/api/lib/cmd/a", "/api/lib/cmd/read"}, details = [=[ Output chunks are read as available. This is interleaved with other process I/O operations. After execution, the collected data is available at `cmd.outputs[fd]`. `fd` can be left out, it defaults to the standard output of the command, i.e. `1`. ]=] .. apidoc_only_before_start .. apidoc_returns_self, } function cmd_mt.__index:capture(fd) as ("cmd", "number?") fd = fd or 1 local rfd = self:pipe(fd, "r") local ret = {} self._ostreams[rfd] = function() while true do local r, msg, errno = posix.unistd.read(rfd, 512) if r and r ~= "" then table.insert(ret, r) self:trace("read \x1b[35m%dB\x1b[0m from \x1b[36m%d\x1b[0m", #r, fd) elseif r == "" or not r and not expected_errors[errno] then if r then self:trace("\x1b[30;42mdone\x1b[0m reading from \x1b[36m%d\x1b[0m", fd) else self:trace("\x1b[30;41merror\x1b[0m reading from \x1b[36m%d\x1b[0m: %s", fd, msg) end self:close(rfd) self.outputs[fd] = table.concat(ret) return else self:trace("\x1b[34msuspend\x1b[0m reading from \x1b[36m%d\x1b[0m: %s", fd, msg) self.outputs[fd] = table.concat(ret) return end end end self:trace("set up capture from fd \x1b[36m%d\x1b[0m", fd) return self end apidoc "/api/lib/cmd/open" { name = "cmd:open(fd, arg, mode)", type = "function(cmd, number, number|string, number) -> cmd", descr = "open file or file descriptor for the command", see = {"/api/lib/cmd/pipe", "/api/lib/cmd/r", "/api/lib/cmd/w", "api/lib/cmd/a"}, details = apidoc_low_level .. [=[ If `arg` is a string, it is interpreted as a path. The file is opened in POSIX numeric mode `mode` (see ). If `arg` is a number, the given child file descriptor is duplicated. In this case, `mode` is ignored. ]=] .. apidoc_only_before_start .. apidoc_returns_self, } function cmd_mt.__index:open(fd, arg, mode) as ("cmd", "number", "number|string", "number") lib.assert(not self.pid , "already running") lib.assert(not self.exit, "already exited" ) if type(arg) == "number" then local fd_from = self._child_fds[arg] if not fd_from and 0 <= arg and arg <= 2 then fd_from = arg end lib.assert(fd_from, "fd not open: " .. arg) self:trace("copy fd \x1b[36m%d\x1b[0m to \x1b[36m%d\x1b[0m", arg, fd) self._child_fds[fd] = posix.unistd.dup2(fd_from, get_safe_fd()) return self end if type(arg) == "string" then self._child_fds[fd] = make_safe(lib.assert(posix.fcntl.open(arg, mode))) self:trace("open %s on fd \x1b[36m%d\x1b[0m", arg, fd) return self end end apidoc "/api/lib/cmd/r" { name = "cmd:r([fd = 0,] arg)", type = "function(cmd, number, number|string) -> cmd", descr = "open file or file descriptor for the command to read from", see = {"/api/lib/cmd/load", "/api/lib/cmd/w", "/api/lib/cmd/a", "/api/lib/cmd/open"}, details = [=[ `arg` can either be a path, or another file descriptor number, in which case that file descriptor is duplicated. `fd` can be left out, it defaults to the standard input of the command, i.e. `0`. ]=] .. apidoc_only_before_start .. apidoc_returns_self, } function cmd_mt.__index:r(fd, arg) if not arg then fd, arg = 0, fd end as ("cmd", "number", "number|string") return self:open(fd, arg, posix.fcntl.O_RDONLY) end apidoc "/api/lib/cmd/w" { name = "cmd:w([fd = 1,] arg)", type = "function(cmd, number, number|string) -> cmd", descr = "open file or file descriptor for the command to write to", see = {"/api/lib/cmd/capture", "/api/lib/cmd/r", "/api/lib/cmd/a", "/api/lib/cmd/open"}, details = [=[ `arg` can either be a path, or another file descriptor number, in which case that file descriptor is duplicated. `fd` can be left out, it defaults to the standard output of the command, i.e. `1`. ]=] .. apidoc_only_before_start .. apidoc_returns_self, } function cmd_mt.__index:w(fd, arg) if not arg then fd, arg = 1, fd end as ("cmd", "number", "number|string") return self:open(fd, arg, posix.fcntl.O_WRONLY | posix.fcntl.O_CREAT | posix.fcntl.O_TRUNC) end apidoc "/api/lib/cmd/a" { name = "cmd:a([fd = 1,] arg)", type = "function(cmd, number, number|string) -> cmd", descr = "open file or file descriptor for the command to append to", see = {"/api/lib/cmd/capture", "/api/lib/cmd/r", "/api/lib/cmd/w", "/api/lib/cmd/open"}, details = [=[ `arg` can either be a path, or another file descriptor number, in which case that file descriptor is duplicated. `fd` can be left out, it defaults to the standard output of the command, i.e. `1`. ]=] .. apidoc_only_before_start .. apidoc_returns_self, } function cmd_mt.__index:a(fd, arg) if not arg then fd, arg = 1, fd end as ("cmd", "number", "number|string") return self:open(fd, arg, posix.fcntl.O_WRONLY | posix.fcntl.O_CREAT | posix.fcntl.O_APPEND) end apidoc "/api/lib/cmd/hush" { name = "cmd:hush()", type = "function(cmd) -> cmd", descr = "silence all command output by redirecting stdin and stderr to `/dev/null`", see = {"/api/lib/cmd/capture", "/api/lib/cmd/w", "/api/lib/cmd/a"}, details = [=[ This is equivalent to `cmd:w("/dev/null"):w(2, 1)`. ]=] .. apidoc_only_before_start .. apidoc_returns_self, } function cmd_mt.__index:hush() as ("cmd") return self:w("/dev/null"):w(2, 1) end apidoc "/api/lib/cmd/setenv" { name = "cmd:setenv(k [, v])", type = "function(cmd, string, string?) -> cmd", descr = "(un)set environment variable for the command", details = [=[ The variable `k` is set to `v`, or unset if `v` is `nil`. ]=] .. apidoc_only_before_start .. apidoc_returns_self, } function cmd_mt.__index:setenv(k, v) as ("cmd", "string", "string?") self._env[k] = v return self end apidoc "/api/lib/cmd/fork" { name = "cmd:fork()", type = "function(cmd) -> cmd", descr = "start the command", details = apidoc_low_level .. [=[ The command is started as specified by preceding operations on `cmd`. ]=] .. apidoc_only_before_start .. apidoc_returns_self, } function cmd_mt.__index:fork() as ("cmd") lib.assert(not self.pid , "already running") lib.assert(not self.exit, "already exited" ) local p = posix.unistd.fork() if p == 0 then posix.signal.signal(posix.signal.SIGCHLD, nil) posix.signal.signal(posix.signal.SIGHUP , nil) posix.signal.signal(posix.signal.SIGPIPE, nil) self._trace = nil self:_close_parent_fds() for i = 0, FD_MAX - 1 do local fd = self._child_fds[i] if fd and fd ~= i then posix.unistd.dup2 (fd, i) posix.unistd.close(fd ) self._child_fds[i] = i end end for k, v in pairs(self._env) do posix.stdlib.setenv(k, v) end lib.assert(posix.unistd.execp(self.path, self.args)) else self:trace("\x1b[30;44mfork + exec\x1b[0m") for i = 0, #self.args do self:trace("arg[%d]: %s", i, self.args[i]) end self.pid = p self:_close_child_fds() end return self end apidoc "/api/lib/cmd/kill" { name = "cmd:kill([sig = posix.signal.SIGTERM])", type = "function(number?) -> 0 | nil, string, number", descr = "send a signal to the running command", details = [=[ The function returns 0 if the signal was successfully delivered, or `nil`, an error message, and the `errno`. ]=] .. apidoc_only_running, } function cmd_mt.__index:kill(s) as ("cmd", "number") return posix.signal.kill(self.pid, s or posix.signal.SIGTERM) end apidoc "/api/lib/cmd/wait" { name = "cmd:wait()", type = "function(cmd) -> cmd", descr = "(start the command,) handle I/O, and wait for the command to finish", see = {"/api/lib/cmd/exit"}, details = apidoc_low_level .. [=[ The command's exit status is made available in `cmd.exit`. The call fails if `cmd:wait()` has already been called. ]=] .. apidoc_autostart .. apidoc_returns_self, } function cmd_mt.__index:wait() as ("cmd") lib.assert(not self.exit, "already exited") if not self.pid then self:fork() end local function do_wait(block) if not self.pid then return end if block then self:trace("\x1b[34mwait\x1b[0m for termination") end while true do local o, what, status = posix.sys.wait.wait( self.pid, block and not coroutine.isyieldable() and 0 or posix.sys.wait.WNOHANG ) if o and what ~= "running" then self.exit = { ok = what == "exited" and status == 0, code = what == "exited" and status or nil, signal = what == "killed" and status or nil, } self:trace("\x1b[%smprocess %s %d\x1b[0m", self.exit.ok and "30;42" or "30;41", what, status) self.pid = nil return elseif not block then return end require "neo-ed.lib.sched" .poll() end end while self.pid and (next(self._istreams) or next(self._ostreams)) do do_wait(false) local poll = {} for fd in pairs(self._istreams) do poll[fd] = {events = {OUT = true}} end for fd in pairs(self._ostreams) do poll[fd] = {events = {IN = true}} end self:trace("\x1b[34mwait\x1b[0m until i/o ready") if self._trace then local t = {"requested:"} for _, fd, p in lib.opairs(poll) do for _, k, v in lib.opairs(p.events) do if v then table.insert(t, ("\x1b[34m%d:%s\x1b[0m"):format(fd, k)) end end end self:trace(table.concat(t, " ")) end poll = require "neo-ed.lib.sched" .poll(poll) if self._trace then local t = {"ready:"} for _, fd, p in lib.opairs(poll) do for _, k, v in lib.opairs(p.revents) do if v then table.insert(t, ("\x1b[32m%d:%s\x1b[0m"):format(fd, k)) end end end self:trace(table.concat(t, " ")) end for fd, f in pairs(self._istreams) do f() end for fd, f in pairs(self._ostreams) do f() end end do_wait(true) return self end apidoc "/api/lib/cmd/ok" { name = "cmd:ok()", type = "function(cmd) -> bool", descr = "(run the command and) check if the command exited successfully", see = {"/api/lib/cmd/fork", "/api/lib/cmd/wait", "/api/lib/cmd/err", "/api/lib/cmd/read"}, details = [=[ - If the command exited with code 0, the return value is `true`. - Otherwise, the return value is `false`. ]=] .. apidoc_autostart .. apidoc_autowait, } function cmd_mt.__index:ok() as ("cmd") if not self.exit then self:wait() end return self.exit.ok end apidoc "/api/lib/cmd/err" { name = "cmd:err([msg])", type = "function(string?) -> cmd", descr = "(run the command and) raise an error unless the command exited successfully", see = {"/api/lib/cmd/fork", "/api/lib/cmd/wait", "/api/lib/cmd/ok", "/api/lib/cmd/read"}, details = [=[ - If the command exited with code 0, `cmd` is returned. - Otherwise, if `msg` is not `nil`, it is raised as an error. - Otherwise, an error is raised, mentioning the command and its exit status. ]=] .. apidoc_autostart .. apidoc_autowait, } function cmd_mt.__index:err(msg) as ("cmd", "string?") if not self:ok() then if msg then lib.error(msg) end lib.error(("%s %s %s"):format( self, self.exit.signal and "terminated by" or self.exit.code and "exited with code" or "failed", self.exit.signal and lib.sigdesc(self.exit.signal) or ("%d"):format(self.exit.code or -1) )) end return self end apidoc "/api/lib/cmd/read" { name = 'cmd:read([what = "a"])', type = "function(cmd, string?) -> nil | string | (function() -> string)", descr = "run the command and retrieve (part of) its output", see = {"/api/lib/cmd/capture", "/api/lib/cmd/wait", "/api/lib/cmd/ok", "/api/lib/cmd/err"}, details = [=[ The standard output of the command is captured. It is started, and its termination awaited. Further actions depend on `what`, which must consist of one letter specifying the desired kind of return value, and an optional suffix specifying how to handle unsuccessful command execution (`cmd:ok() = false`). - If the command failed and no suffix is given, an error is raised. - If the command failed and the suffix is `?`, `nil` is returned. - If the command failed and the suffix is `!`, the failure is ignored, and the return value is determined as specified below. If the command exited successfully, or if a `!` suffix was given, the return value is determined as follows: - If `what` starts with `a`, the entire command output is returned as one string. - If `what` starts with `l`, the first line, excluding the `\n`, is returned as a string. - If `what` starts with `L`, an iterator function that successively returns each line (without trailing `\n`) is returned. - If `what` starts with `0`, an iterator function that successively returns each null-byte-delimited strings (without the null byte) is returned. Both `L` and `0` expect the output to end with the respective delimiter. If the trailing delimiter is missing, the last, unterminated record is handled as if it was properly terminated. ]=] .. apidoc_only_before_start, } function cmd_mt.__index:read(mode) as ("cmd", "string?") if not mode then mode = "a" end local what, strict = mode:match("^([0alL])([?!]?)$") lib.assert(what, "invalid read mode: " .. mode) self:capture():wait() if strict == "" then self:err() end if strict == "?" and not self:ok() then return end if what == "a" then return self.outputs[1] end if what == "l" then return self.outputs[1]:match("^[^\n]*") end local delim = what == "L" and "\n" or what == "0" and "\0" or lib.error("???") local o = self.outputs[1] if o:find("[^" .. delim .. "]$") then o = o .. delim end return o:gmatch("([^" .. delim .. "]*)" .. delim) end apidoc "/api/lib/cmd" { name = "lib.cmd(path)", type = "function(string) -> cmd", descr = "create a new `cmd` object from the name of an executable to call", details = [=[ ## Common Usage Important operations for modifying command execution include: - `cmd(arg)` adds a single argument to the call - `cmd:load(fd, str)` prepares command input - `cmd:r(fd, src)`, `cmd:w(fd, dst)`, and `cmd:a(fd, dst)` redirect input and output Each `cmd` object can only be executed once. The highest level interface for this is: - `cmd:ok()` runs `cmd` and returns whether it exited successfully - `cmd:err([msg])` runs `cmd` and raises an error if it did not exit successfully - `cmd:read([what])` runs `cmd`, returns its output and/or handles errors Alternatively, `tostring(cmd)` is the command, properly escaped for POSIX shells. Similarly, `cmd1 | cmd2` can be used to build POSIX shell pipelines, which can be executed using `lib.cmd.shell(str)`. ]=], } local m = setmetatable({}, { __call = function(_, path) as ("*", "string") return setmetatable({ _child_fds = {}, _parent_fds = {}, _istreams = {}, _ostreams = {}, _env = {}, args = {[0] = path}, outputs = {}, path = path, }, cmd_mt) end, }) apidoc "/api/lib/cmd/outputs" { name = "cmd.outputs", type = "{[number] = string}", descr = "contains the captured output for each file descriptor, after the command has exited", see = {"/api/lib/cmd/capture"}, } apidoc "/api/lib/cmd/shell" { name = "lib.cmd.shell([str])", type = "function(string?) -> cmd", descr = "create `cmd` object with the user's shell", see = {"/api/lib/cmd", "/api/lib/cmd/meta/tostring", "/api/lib/cmd/meta/bor"}, details = [=[ The executable is determined as follows: - If the environment variable `SHELL` is set, it is used. - Otherwise, if the user has a shell in the `/etc/passwd` database, it is used. - Otherwise, `sh` is used. A `cmd` is constructed using this executable, and returned. If `str` is given, the arguments `-c` and `str` are added to the `cmd` beforehand. The return value can then be further modified and executed using the regular `cmd` interface. ]=], } function m.shell(s) as ("string?") local path = os.getenv("SHELL") if not path then local passwd = posix.pwd.getpwuid(posix.unistd.geteuid()) if passwd then path = passwd.pw_shell end path = path or 'sh' end local ret = m(path) if s then ret "-c" (s) end return ret end return m ]================================================================================] , "neo-ed.lib.cmd")) package.preload["neo-ed.lib.json"] = assert(load( [================================================================================[ -- -- json.lua -- -- Copyright (c) 2020 rxi -- -- Permission is hereby granted, free of charge, to any person obtaining a copy of -- this software and associated documentation files (the "Software"), to deal in -- the Software without restriction, including without limitation the rights to -- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -- of the Software, and to permit persons to whom the Software is furnished to do -- so, subject to the following conditions: -- -- The above copyright notice and this permission notice shall be included in all -- copies or substantial portions of the Software. -- -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -- SOFTWARE. -- local json = { _version = "0.1.2" } ------------------------------------------------------------------------------- -- Encode ------------------------------------------------------------------------------- local encode local escape_char_map = { [ "\\" ] = "\\", [ "\"" ] = "\"", [ "\b" ] = "b", [ "\f" ] = "f", [ "\n" ] = "n", [ "\r" ] = "r", [ "\t" ] = "t", } local escape_char_map_inv = { [ "/" ] = "/" } for k, v in pairs(escape_char_map) do escape_char_map_inv[v] = k end local function escape_char(c) return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) end local function encode_nil(val) return "null" end local function encode_table(val, stack) local res = {} stack = stack or {} -- Circular reference? if stack[val] then error("circular reference") end stack[val] = true if rawget(val, 1) ~= nil or next(val) == nil then -- Treat as array -- check keys are valid and it is not sparse local n = 0 for k in pairs(val) do if type(k) ~= "number" then error("invalid table: mixed or invalid key types") end n = n + 1 end if n ~= #val then error("invalid table: sparse array") end -- Encode for i, v in ipairs(val) do table.insert(res, encode(v, stack)) end stack[val] = nil return "[" .. table.concat(res, ",") .. "]" else -- Treat as an object for k, v in pairs(val) do if type(k) ~= "string" then error("invalid table: mixed or invalid key types") end table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) end stack[val] = nil return "{" .. table.concat(res, ",") .. "}" end end local function encode_string(val) return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' end local function encode_number(val) -- Check for NaN, -inf and inf if val ~= val or val <= -math.huge or val >= math.huge then error("unexpected number value '" .. tostring(val) .. "'") end return string.format("%.14g", val) end local type_func_map = { [ "nil" ] = encode_nil, [ "table" ] = encode_table, [ "string" ] = encode_string, [ "number" ] = encode_number, [ "boolean" ] = tostring, } encode = function(val, stack) local t = type(val) local f = type_func_map[t] if f then return f(val, stack) end error("unexpected type '" .. t .. "'") end function json.encode(val) return ( encode(val) ) end ------------------------------------------------------------------------------- -- Decode ------------------------------------------------------------------------------- local parse local function create_set(...) local res = {} for i = 1, select("#", ...) do res[ select(i, ...) ] = true end return res end local space_chars = create_set(" ", "\t", "\r", "\n") local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") local literals = create_set("true", "false", "null") local literal_map = { [ "true" ] = true, [ "false" ] = false, [ "null" ] = nil, } local function next_char(str, idx, set, negate) for i = idx, #str do if set[str:sub(i, i)] ~= negate then return i end end return #str + 1 end local function decode_error(str, idx, msg) local line_count = 1 local col_count = 1 for i = 1, idx - 1 do col_count = col_count + 1 if str:sub(i, i) == "\n" then line_count = line_count + 1 col_count = 1 end end error( string.format("%s at line %d col %d", msg, line_count, col_count) ) end local function codepoint_to_utf8(n) -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa local f = math.floor if n <= 0x7f then return string.char(n) elseif n <= 0x7ff then return string.char(f(n / 64) + 192, n % 64 + 128) elseif n <= 0xffff then return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) elseif n <= 0x10ffff then return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, f(n % 4096 / 64) + 128, n % 64 + 128) end error( string.format("invalid unicode codepoint '%x'", n) ) end local function parse_unicode_escape(s) local n1 = tonumber( s:sub(1, 4), 16 ) local n2 = tonumber( s:sub(7, 10), 16 ) -- Surrogate pair? if n2 then return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) else return codepoint_to_utf8(n1) end end local function parse_string(str, i) local res = "" local j = i + 1 local k = j while j <= #str do local x = str:byte(j) if x < 32 then decode_error(str, j, "control character in string") elseif x == 92 then -- `\`: Escape res = res .. str:sub(k, j - 1) j = j + 1 local c = str:sub(j, j) if c == "u" then local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) or str:match("^%x%x%x%x", j + 1) or decode_error(str, j - 1, "invalid unicode escape in string") res = res .. parse_unicode_escape(hex) j = j + #hex else if not escape_chars[c] then decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") end res = res .. escape_char_map_inv[c] end k = j + 1 elseif x == 34 then -- `"`: End of string res = res .. str:sub(k, j - 1) return res, j + 1 end j = j + 1 end decode_error(str, i, "expected closing quote for string") end local function parse_number(str, i) local x = next_char(str, i, delim_chars) local s = str:sub(i, x - 1) local n = tonumber(s) if not n then decode_error(str, i, "invalid number '" .. s .. "'") end return n, x end local function parse_literal(str, i) local x = next_char(str, i, delim_chars) local word = str:sub(i, x - 1) if not literals[word] then decode_error(str, i, "invalid literal '" .. word .. "'") end return literal_map[word], x end local function parse_array(str, i) local res = {} local n = 1 i = i + 1 while 1 do local x i = next_char(str, i, space_chars, true) -- Empty / end of array? if str:sub(i, i) == "]" then i = i + 1 break end -- Read token x, i = parse(str, i) res[n] = x n = n + 1 -- Next token i = next_char(str, i, space_chars, true) local chr = str:sub(i, i) i = i + 1 if chr == "]" then break end if chr ~= "," then decode_error(str, i, "expected ']' or ','") end end return res, i end local function parse_object(str, i) local res = {} i = i + 1 while 1 do local key, val i = next_char(str, i, space_chars, true) -- Empty / end of object? if str:sub(i, i) == "}" then i = i + 1 break end -- Read key if str:sub(i, i) ~= '"' then decode_error(str, i, "expected string for key") end key, i = parse(str, i) -- Read ':' delimiter i = next_char(str, i, space_chars, true) if str:sub(i, i) ~= ":" then decode_error(str, i, "expected ':' after key") end i = next_char(str, i + 1, space_chars, true) -- Read value val, i = parse(str, i) -- Set res[key] = val -- Next token i = next_char(str, i, space_chars, true) local chr = str:sub(i, i) i = i + 1 if chr == "}" then break end if chr ~= "," then decode_error(str, i, "expected '}' or ','") end end return res, i end local char_func_map = { [ '"' ] = parse_string, [ "0" ] = parse_number, [ "1" ] = parse_number, [ "2" ] = parse_number, [ "3" ] = parse_number, [ "4" ] = parse_number, [ "5" ] = parse_number, [ "6" ] = parse_number, [ "7" ] = parse_number, [ "8" ] = parse_number, [ "9" ] = parse_number, [ "-" ] = parse_number, [ "t" ] = parse_literal, [ "f" ] = parse_literal, [ "n" ] = parse_literal, [ "[" ] = parse_array, [ "{" ] = parse_object, } parse = function(str, idx) local chr = str:sub(idx, idx) local f = char_func_map[chr] if f then return f(str, idx) end decode_error(str, idx, "unexpected character '" .. chr .. "'") end function json.decode(str) if type(str) ~= "string" then error("expected argument of type string, got " .. type(str)) end local res, idx = parse(str, next_char(str, 1, space_chars, true)) idx = next_char(str, idx, space_chars, true) if idx <= #str then decode_error(str, idx, "trailing garbage") end return res end return json ]================================================================================] , "neo-ed.lib.json")) package.preload["neo-ed.lib.sched"] = assert(load( [================================================================================[ local as = require "neo-ed.lib.as" local lib = require "neo-ed.lib" local posix = require "posix" local expected_errors = { [posix.errno.EAGAIN ] = true, [posix.errno.EWOULDBLOCK] = true, [posix.errno.EINTR ] = true, } local mt = {__index = {}, __name = "sched"} function mt.__index:add(fn) as ("sched", "function") table.insert(self.queue, coroutine.create(fn)) return self end local nproc_key = {} function mt.__index:run(jobs) as ("sched", "number?") local cache = lib.cache[nproc_key] cache.nproc = cache.nproc or tonumber(lib.cmd "nproc" :read("l")) jobs = math.floor((jobs > 0 and jobs or tonumber(cache.nproc) * (jobs < 0 and -jobs or 1)) + 0.5) local term = require "neo-ed.term" local started = lib.timestamp() local progress = false local total = #self.queue local active = {} local errors = {} while self.queue[1] or active[1] do if self.queue[1] and (jobs < 0 or #active < jobs) then table.insert(active, {thread = table.remove(self.queue), fds = {}}) end progress = progress or (not term.in_getline and lib.timestamp() - started > 1) if progress then term:progress(total - #self.queue - #active, total, #active) end local resume = nil local arg = nil local poll = {} for i, a in ipairs(active) do if not next(a.fds) then resume = i; break end for fd, events in pairs(a.fds) do poll[fd] = {events = events, idx = i} end end if not resume and next(poll) then posix.poll.poll(poll, 100) end local ready = {} for i, a in ipairs(active) do if not next(a.fds) then table.insert(ready, a) else local found = false for fd, events in pairs(a.fds) do for ev, v in pairs(events) do if v and poll[fd] and poll[fd].revents and poll[fd].revents[ev] then found = true break end end if found == true then break end end if found then table.insert(ready, a) end end end for _, a in ipairs(ready[1] and ready or active) do local ok, newfds = coroutine.resume(a.thread, poll) if ok then a.fds = newfds or {} else table.insert(errors, newfds) end end local active_ = {} for _, a in ipairs(active) do if coroutine.status(a.thread) ~= "dead" then table.insert(active_, a) end end active = active_ end if progress then term:progress() end return not errors[1], errors[1] and errors or nil end return setmetatable({ poll = function(fds) as ("table?") if coroutine.isyieldable() then return coroutine.yield(fds) else posix.poll.poll(fds or {}, 1000) return fds end end, }, { __call = function() return setmetatable({queue = {}}, mt) end, }) ]================================================================================] , "neo-ed.lib.sched")) package.preload["neo-ed.lib.ucd"] = assert(load( [================================================================================[ -- generated from ucd-gen.lua, do not edit manually local ucd = {} ucd.replace = {} for i = 0x0000, 0x0008 do ucd.replace[i] = true end for i = 0x000a, 0x001a do ucd.replace[i] = true end for i = 0x001c, 0x001f do ucd.replace[i] = true end ucd.replace[0x007f] = true ucd.replace[0x00a0] = false ucd.replace[0x00ad] = true ucd.replace[0x00b4] = false ucd.replace[0x00b8] = false ucd.replace[0x00d7] = false ucd.replace[0x00fe] = false ucd.replace[0x0131] = false ucd.replace[0x017f] = false ucd.replace[0x0184] = false ucd.replace[0x018d] = false ucd.replace[0x0192] = false ucd.replace[0x0196] = false for i = 0x01a6, 0x01a7 do ucd.replace[i] = false end ucd.replace[0x01b7] = false for i = 0x01bc, 0x01bd do ucd.replace[i] = false end for i = 0x01bf, 0x01c0 do ucd.replace[i] = false end ucd.replace[0x01c3] = false ucd.replace[0x021c] = false for i = 0x0222, 0x0223 do ucd.replace[i] = false end ucd.replace[0x0241] = false ucd.replace[0x0251] = false ucd.replace[0x0261] = false ucd.replace[0x0263] = false for i = 0x0269, 0x026a do ucd.replace[i] = false end ucd.replace[0x026f] = false ucd.replace[0x028b] = false ucd.replace[0x028f] = false ucd.replace[0x0294] = false ucd.replace[0x02b9] = false for i = 0x02bb, 0x02be do ucd.replace[i] = false end for i = 0x02c2, 0x02c4 do ucd.replace[i] = false end ucd.replace[0x02c6] = false ucd.replace[0x02c8] = false for i = 0x02ca, 0x02cb do ucd.replace[i] = false end ucd.replace[0x02d0] = false ucd.replace[0x02d7] = false for i = 0x02db, 0x02dc do ucd.replace[i] = false end ucd.replace[0x02f4] = false ucd.replace[0x02f8] = false ucd.replace[0x034f] = true ucd.replace[0x0374] = false ucd.replace[0x037a] = false for i = 0x037e, 0x037f do ucd.replace[i] = false end ucd.replace[0x0384] = false for i = 0x0391, 0x0392 do ucd.replace[i] = false end for i = 0x0395, 0x0397 do ucd.replace[i] = false end for i = 0x0399, 0x039a do ucd.replace[i] = false end for i = 0x039c, 0x039d do ucd.replace[i] = false end ucd.replace[0x039f] = false ucd.replace[0x03a1] = false for i = 0x03a4, 0x03a5 do ucd.replace[i] = false end ucd.replace[0x03a7] = false ucd.replace[0x03b1] = false ucd.replace[0x03b3] = false ucd.replace[0x03b9] = false ucd.replace[0x03bd] = false ucd.replace[0x03bf] = false ucd.replace[0x03c1] = false ucd.replace[0x03c3] = false ucd.replace[0x03c5] = false ucd.replace[0x03d2] = false ucd.replace[0x03dc] = false ucd.replace[0x03e8] = false for i = 0x03ec, 0x03ed do ucd.replace[i] = false end for i = 0x03f1, 0x03f3 do ucd.replace[i] = false end for i = 0x03f8, 0x03fa do ucd.replace[i] = false end for i = 0x0405, 0x0406 do ucd.replace[i] = false end ucd.replace[0x0408] = false ucd.replace[0x0410] = false ucd.replace[0x0412] = false ucd.replace[0x0415] = false ucd.replace[0x0417] = false ucd.replace[0x041a] = false for i = 0x041c, 0x041e do ucd.replace[i] = false end for i = 0x0420, 0x0423 do ucd.replace[i] = false end ucd.replace[0x0425] = false ucd.replace[0x042c] = false for i = 0x0430, 0x0431 do ucd.replace[i] = false end ucd.replace[0x0433] = false ucd.replace[0x0435] = false ucd.replace[0x043e] = false for i = 0x0440, 0x0441 do ucd.replace[i] = false end ucd.replace[0x0443] = false ucd.replace[0x0445] = false ucd.replace[0x0448] = false for i = 0x0455, 0x0456 do ucd.replace[i] = false end ucd.replace[0x0458] = false ucd.replace[0x0461] = false for i = 0x0474, 0x0475 do ucd.replace[i] = false end for i = 0x04ae, 0x04af do ucd.replace[i] = false end ucd.replace[0x04bb] = false ucd.replace[0x04bd] = false ucd.replace[0x04c0] = false ucd.replace[0x04cf] = false ucd.replace[0x04e0] = false ucd.replace[0x0501] = false ucd.replace[0x050c] = false for i = 0x051b, 0x051d do ucd.replace[i] = false end ucd.replace[0x054d] = false ucd.replace[0x054f] = false ucd.replace[0x0555] = false ucd.replace[0x055a] = false ucd.replace[0x055d] = false ucd.replace[0x0561] = false ucd.replace[0x0563] = false ucd.replace[0x0566] = false ucd.replace[0x0570] = false ucd.replace[0x0578] = false for i = 0x057c, 0x057d do ucd.replace[i] = false end for i = 0x0581, 0x0582 do ucd.replace[i] = false end for i = 0x0584, 0x0585 do ucd.replace[i] = false end ucd.replace[0x0589] = false ucd.replace[0x05c0] = false ucd.replace[0x05c3] = false ucd.replace[0x05d5] = false for i = 0x05d8, 0x05d9 do ucd.replace[i] = false end ucd.replace[0x05df] = false ucd.replace[0x05e1] = false ucd.replace[0x05f3] = false ucd.replace[0x060d] = false ucd.replace[0x061c] = true ucd.replace[0x0627] = false ucd.replace[0x0647] = false for i = 0x0660, 0x0661 do ucd.replace[i] = false end ucd.replace[0x0665] = false ucd.replace[0x0667] = false ucd.replace[0x066b] = false ucd.replace[0x066d] = false ucd.replace[0x06be] = false ucd.replace[0x06c1] = false for i = 0x06d4, 0x06d5 do ucd.replace[i] = false end for i = 0x06f0, 0x06f1 do ucd.replace[i] = false end ucd.replace[0x06f5] = false ucd.replace[0x06f7] = false for i = 0x0701, 0x0704 do ucd.replace[i] = false end ucd.replace[0x07c0] = false ucd.replace[0x07ca] = false for i = 0x07f4, 0x07f5 do ucd.replace[i] = false end ucd.replace[0x07fa] = false ucd.replace[0x0903] = false ucd.replace[0x0966] = false ucd.replace[0x0969] = false ucd.replace[0x097d] = false ucd.replace[0x09e6] = false ucd.replace[0x09ea] = false ucd.replace[0x09ed] = false for i = 0x0a66, 0x0a67 do ucd.replace[i] = false end ucd.replace[0x0a6a] = false ucd.replace[0x0a83] = false ucd.replace[0x0ae6] = false ucd.replace[0x0ae9] = false ucd.replace[0x0b03] = false ucd.replace[0x0b20] = false ucd.replace[0x0b66] = false ucd.replace[0x0b68] = false ucd.replace[0x0be6] = false ucd.replace[0x0c02] = false ucd.replace[0x0c66] = false ucd.replace[0x0c82] = false ucd.replace[0x0ce6] = false ucd.replace[0x0d02] = false for i = 0x0d1f, 0x0d20 do ucd.replace[i] = false end ucd.replace[0x0d66] = false ucd.replace[0x0d6d] = false ucd.replace[0x0d82] = false ucd.replace[0x0e50] = false ucd.replace[0x0ed0] = false ucd.replace[0x1004] = false ucd.replace[0x101d] = false ucd.replace[0x1040] = false ucd.replace[0x105a] = false ucd.replace[0x10e7] = false ucd.replace[0x10ff] = false for i = 0x115f, 0x1160 do ucd.replace[i] = true end ucd.replace[0x1200] = false ucd.replace[0x12d0] = false for i = 0x13a0, 0x13a2 do ucd.replace[i] = false end ucd.replace[0x13a5] = false for i = 0x13a9, 0x13ac do ucd.replace[i] = false end ucd.replace[0x13ae] = false ucd.replace[0x13b3] = false ucd.replace[0x13b7] = false ucd.replace[0x13bb] = false ucd.replace[0x13bd] = false ucd.replace[0x13c0] = false for i = 0x13c2, 0x13c3 do ucd.replace[i] = false end for i = 0x13ce, 0x13cf do ucd.replace[i] = false end ucd.replace[0x13d2] = false for i = 0x13d4, 0x13d5 do ucd.replace[i] = false end for i = 0x13d9, 0x13da do ucd.replace[i] = false end for i = 0x13de, 0x13df do ucd.replace[i] = false end ucd.replace[0x13e2] = false for i = 0x13e6, 0x13e7 do ucd.replace[i] = false end ucd.replace[0x13ee] = false for i = 0x13f3, 0x13f4 do ucd.replace[i] = false end ucd.replace[0x1400] = false ucd.replace[0x142f] = false ucd.replace[0x1433] = false ucd.replace[0x1438] = false ucd.replace[0x144a] = false ucd.replace[0x144c] = false ucd.replace[0x146d] = false ucd.replace[0x146f] = false ucd.replace[0x1472] = false ucd.replace[0x148d] = false ucd.replace[0x14aa] = false ucd.replace[0x14bf] = false ucd.replace[0x1541] = false for i = 0x157c, 0x157d do ucd.replace[i] = false end ucd.replace[0x1587] = false ucd.replace[0x15af] = false ucd.replace[0x15b4] = false ucd.replace[0x15c5] = false ucd.replace[0x15de] = false ucd.replace[0x15ea] = false ucd.replace[0x15f0] = false ucd.replace[0x15f7] = false for i = 0x166d, 0x166e do ucd.replace[i] = false end ucd.replace[0x1680] = false ucd.replace[0x16b2] = false ucd.replace[0x16b7] = false ucd.replace[0x16c1] = false ucd.replace[0x16cc] = false for i = 0x16d5, 0x16d6 do ucd.replace[i] = false end for i = 0x16ec, 0x16ed do ucd.replace[i] = false end ucd.replace[0x1735] = false for i = 0x17b4, 0x17b5 do ucd.replace[i] = true end ucd.replace[0x17e0] = false ucd.replace[0x1803] = false ucd.replace[0x1809] = false for i = 0x180b, 0x180f do ucd.replace[i] = true end ucd.replace[0x1d04] = false ucd.replace[0x1d0f] = false ucd.replace[0x1d11] = false ucd.replace[0x1d1c] = false for i = 0x1d20, 0x1d22 do ucd.replace[i] = false end ucd.replace[0x1d26] = false ucd.replace[0x1d83] = false ucd.replace[0x1d8c] = false ucd.replace[0x1e9d] = false ucd.replace[0x1eff] = false for i = 0x1fbd, 0x1fc0 do ucd.replace[i] = false end ucd.replace[0x1fef] = false for i = 0x1ffd, 0x1ffe do ucd.replace[i] = false end for i = 0x2000, 0x200a do ucd.replace[i] = false end ucd.replace[0x200b] = "[zero width space]" ucd.replace[0x200c] = "[zero width non-joiner]" ucd.replace[0x200d] = "[zero width joiner]" for i = 0x200e, 0x200f do ucd.replace[i] = true end for i = 0x2010, 0x2013 do ucd.replace[i] = false end for i = 0x2018, 0x201b do ucd.replace[i] = false end ucd.replace[0x2024] = false for i = 0x2028, 0x2029 do ucd.replace[i] = false end ucd.replace[0x202a] = "[BIDI:LRE]" ucd.replace[0x202b] = "[BIDI:RLE]" ucd.replace[0x202c] = "[BIDI:PDF]" ucd.replace[0x202d] = "[BIDI:LRO]" ucd.replace[0x202e] = "[BIDI:RLO]" ucd.replace[0x202f] = false ucd.replace[0x2032] = false ucd.replace[0x2035] = false for i = 0x2039, 0x203a do ucd.replace[i] = false end ucd.replace[0x2041] = false for i = 0x2043, 0x2044 do ucd.replace[i] = false end ucd.replace[0x204e] = false ucd.replace[0x2053] = false ucd.replace[0x205a] = false ucd.replace[0x205f] = false for i = 0x2060, 0x2065 do ucd.replace[i] = true end ucd.replace[0x2066] = "[BIDI:LRI]" ucd.replace[0x2067] = "[BIDI:RLI]" ucd.replace[0x2068] = "[BIDI:FSI]" ucd.replace[0x2069] = "[BIDI:PDI]" for i = 0x206a, 0x206f do ucd.replace[i] = true end ucd.replace[0x2102] = false for i = 0x210a, 0x210e do ucd.replace[i] = false end for i = 0x2110, 0x2113 do ucd.replace[i] = false end ucd.replace[0x2115] = false for i = 0x2119, 0x211d do ucd.replace[i] = false end ucd.replace[0x2124] = false ucd.replace[0x2128] = false ucd.replace[0x212a] = false for i = 0x212c, 0x2131 do ucd.replace[i] = false end for i = 0x2133, 0x2134 do ucd.replace[i] = false end ucd.replace[0x2139] = false ucd.replace[0x213d] = false for i = 0x2145, 0x2149 do ucd.replace[i] = false end ucd.replace[0x2160] = false ucd.replace[0x2164] = false ucd.replace[0x2169] = false for i = 0x216c, 0x2170 do ucd.replace[i] = false end ucd.replace[0x2174] = false ucd.replace[0x2179] = false for i = 0x217c, 0x217e do ucd.replace[i] = false end ucd.replace[0x2212] = false for i = 0x2215, 0x2217 do ucd.replace[i] = false end ucd.replace[0x2223] = false ucd.replace[0x2228] = false ucd.replace[0x222a] = false ucd.replace[0x2236] = false ucd.replace[0x223c] = false ucd.replace[0x22a4] = false ucd.replace[0x22c1] = false ucd.replace[0x22c3] = false ucd.replace[0x22ff] = false for i = 0x2373, 0x2374 do ucd.replace[i] = false end ucd.replace[0x237a] = false ucd.replace[0x23fd] = false ucd.replace[0x2571] = false ucd.replace[0x2573] = false for i = 0x2768, 0x2769 do ucd.replace[i] = false end for i = 0x276e, 0x276f do ucd.replace[i] = false end for i = 0x2772, 0x2775 do ucd.replace[i] = false end for i = 0x2795, 0x2796 do ucd.replace[i] = false end ucd.replace[0x27cb] = false ucd.replace[0x27cd] = false ucd.replace[0x27d9] = false for i = 0x292b, 0x292c do ucd.replace[i] = false end ucd.replace[0x29f5] = false for i = 0x29f8, 0x29f9 do ucd.replace[i] = false end ucd.replace[0x2a2f] = false ucd.replace[0x2c82] = false ucd.replace[0x2c85] = false ucd.replace[0x2c8e] = false for i = 0x2c92, 0x2c94 do ucd.replace[i] = false end ucd.replace[0x2c98] = false ucd.replace[0x2c9a] = false ucd.replace[0x2c9c] = false for i = 0x2c9e, 0x2c9f do ucd.replace[i] = false end for i = 0x2ca2, 0x2ca6 do ucd.replace[i] = false end for i = 0x2ca8, 0x2ca9 do ucd.replace[i] = false end ucd.replace[0x2cac] = false for i = 0x2cba, 0x2cbb do ucd.replace[i] = false end ucd.replace[0x2cbd] = false ucd.replace[0x2cc4] = false for i = 0x2cc6, 0x2cc7 do ucd.replace[i] = false end for i = 0x2cca, 0x2ccc do ucd.replace[i] = false end for i = 0x2cce, 0x2cd0 do ucd.replace[i] = false end for i = 0x2cd2, 0x2cd3 do ucd.replace[i] = false end ucd.replace[0x2cdc] = false for i = 0x2d38, 0x2d39 do ucd.replace[i] = false end ucd.replace[0x2d4f] = false ucd.replace[0x2d51] = false for i = 0x2d54, 0x2d55 do ucd.replace[i] = false end ucd.replace[0x2d5d] = false ucd.replace[0x2e40] = false for i = 0x2f02, 0x2f03 do ucd.replace[i] = false end ucd.replace[0x3007] = false for i = 0x3014, 0x3015 do ucd.replace[i] = false end ucd.replace[0x3033] = false ucd.replace[0x30a0] = false ucd.replace[0x30ce] = false ucd.replace[0x3164] = true for i = 0x31d3, 0x31d4 do ucd.replace[i] = false end ucd.replace[0x4e36] = false ucd.replace[0x4e3f] = false for i = 0xa4d0, 0xa4d4 do ucd.replace[i] = false end for i = 0xa4d6, 0xa4d7 do ucd.replace[i] = false end for i = 0xa4d9, 0xa4da do ucd.replace[i] = false end for i = 0xa4dc, 0xa4dd do ucd.replace[i] = false end for i = 0xa4df, 0xa4e3 do ucd.replace[i] = false end for i = 0xa4e6, 0xa4e7 do ucd.replace[i] = false end for i = 0xa4ea, 0xa4ec do ucd.replace[i] = false end ucd.replace[0xa4ee] = false ucd.replace[0xa4f0] = false for i = 0xa4f2, 0xa4f4 do ucd.replace[i] = false end for i = 0xa4f8, 0xa4f9 do ucd.replace[i] = false end ucd.replace[0xa4fd] = false ucd.replace[0xa4ff] = false ucd.replace[0xa60e] = false ucd.replace[0xa644] = false ucd.replace[0xa647] = false ucd.replace[0xa6df] = false ucd.replace[0xa6eb] = false ucd.replace[0xa6ef] = false ucd.replace[0xa731] = false ucd.replace[0xa75a] = false ucd.replace[0xa76a] = false ucd.replace[0xa76e] = false ucd.replace[0xa778] = false ucd.replace[0xa789] = false ucd.replace[0xa78c] = false for i = 0xa798, 0xa799 do ucd.replace[i] = false end ucd.replace[0xa79f] = false ucd.replace[0xa7ab] = false for i = 0xa7b2, 0xa7b4 do ucd.replace[i] = false end ucd.replace[0xab32] = false ucd.replace[0xab35] = false ucd.replace[0xab3d] = false for i = 0xab47, 0xab48 do ucd.replace[i] = false end ucd.replace[0xab4e] = false ucd.replace[0xab52] = false ucd.replace[0xab5a] = false ucd.replace[0xab75] = false ucd.replace[0xab81] = false ucd.replace[0xab83] = false ucd.replace[0xab93] = false for i = 0xaba9, 0xabaa do ucd.replace[i] = false end ucd.replace[0xabaf] = false for i = 0xfba6, 0xfbad do ucd.replace[i] = false end for i = 0xfd3e, 0xfd3f do ucd.replace[i] = false end for i = 0xfdd0, 0xfdef do ucd.replace[i] = true end for i = 0xfe00, 0xfe0f do ucd.replace[i] = true end ucd.replace[0xfe30] = false for i = 0xfe4d, 0xfe4f do ucd.replace[i] = false end ucd.replace[0xfe58] = false ucd.replace[0xfe68] = false for i = 0xfe8d, 0xfe8e do ucd.replace[i] = false end for i = 0xfee9, 0xfeec do ucd.replace[i] = false end ucd.replace[0xfeff] = "[BOM]" ucd.replace[0xff01] = false ucd.replace[0xff07] = false ucd.replace[0xff1a] = false for i = 0xff21, 0xff23 do ucd.replace[i] = false end ucd.replace[0xff25] = false for i = 0xff28, 0xff2b do ucd.replace[i] = false end for i = 0xff2d, 0xff30 do ucd.replace[i] = false end for i = 0xff33, 0xff34 do ucd.replace[i] = false end for i = 0xff38, 0xff3d do ucd.replace[i] = false end for i = 0xff40, 0xff41 do ucd.replace[i] = false end ucd.replace[0xff43] = false ucd.replace[0xff45] = false for i = 0xff47, 0xff4a do ucd.replace[i] = false end ucd.replace[0xff4c] = false for i = 0xff4f, 0xff50 do ucd.replace[i] = false end ucd.replace[0xff53] = false ucd.replace[0xff56] = false for i = 0xff58, 0xff59 do ucd.replace[i] = false end ucd.replace[0xffa0] = true ucd.replace[0xffe8] = false for i = 0xfff0, 0xfff8 do ucd.replace[i] = true end ucd.replace[0xfffe] = "[BOM:rev]" ucd.replace[0xffff] = true ucd.replace[0x10282] = false for i = 0x10286, 0x10287 do ucd.replace[i] = false end ucd.replace[0x1028a] = false ucd.replace[0x10290] = false ucd.replace[0x10292] = false for i = 0x10295, 0x10297 do ucd.replace[i] = false end ucd.replace[0x1029b] = false for i = 0x102a0, 0x102a2 do ucd.replace[i] = false end ucd.replace[0x102a5] = false ucd.replace[0x102ab] = false for i = 0x102b0, 0x102b2 do ucd.replace[i] = false end ucd.replace[0x102b4] = false ucd.replace[0x102cf] = false ucd.replace[0x102f5] = false for i = 0x10301, 0x10302 do ucd.replace[i] = false end ucd.replace[0x10309] = false ucd.replace[0x10311] = false ucd.replace[0x10315] = false ucd.replace[0x10317] = false ucd.replace[0x1031a] = false for i = 0x1031f, 0x10320 do ucd.replace[i] = false end ucd.replace[0x10322] = false ucd.replace[0x10404] = false ucd.replace[0x10415] = false ucd.replace[0x1041b] = false ucd.replace[0x10420] = false ucd.replace[0x1042c] = false ucd.replace[0x1043d] = false ucd.replace[0x10448] = false ucd.replace[0x104b4] = false ucd.replace[0x104c2] = false ucd.replace[0x104ce] = false ucd.replace[0x104d2] = false ucd.replace[0x104ea] = false ucd.replace[0x104f6] = false ucd.replace[0x10513] = false ucd.replace[0x10516] = false ucd.replace[0x10518] = false for i = 0x1051c, 0x1051d do ucd.replace[i] = false end for i = 0x10525, 0x10527 do ucd.replace[i] = false end ucd.replace[0x10a50] = false ucd.replace[0x114d0] = false ucd.replace[0x11706] = false ucd.replace[0x1170a] = false for i = 0x1170e, 0x1170f do ucd.replace[i] = false end ucd.replace[0x118a0] = false for i = 0x118a2, 0x118a4 do ucd.replace[i] = false end ucd.replace[0x118a6] = false ucd.replace[0x118a9] = false ucd.replace[0x118ac] = false for i = 0x118ae, 0x118af do ucd.replace[i] = false end ucd.replace[0x118b2] = false ucd.replace[0x118b5] = false ucd.replace[0x118b8] = false for i = 0x118bb, 0x118bc do ucd.replace[i] = false end for i = 0x118c0, 0x118c4 do ucd.replace[i] = false end ucd.replace[0x118c6] = false ucd.replace[0x118c8] = false ucd.replace[0x118ca] = false ucd.replace[0x118cc] = false for i = 0x118d5, 0x118d8 do ucd.replace[i] = false end ucd.replace[0x118dc] = false ucd.replace[0x118e0] = false for i = 0x118e5, 0x118e6 do ucd.replace[i] = false end ucd.replace[0x118e9] = false ucd.replace[0x118ec] = false ucd.replace[0x118ef] = false ucd.replace[0x118f2] = false for i = 0x11dd9, 0x11dda do ucd.replace[i] = false end for i = 0x11de0, 0x11de1 do ucd.replace[i] = false end ucd.replace[0x16eaa] = false ucd.replace[0x16eb6] = false ucd.replace[0x16f08] = false ucd.replace[0x16f0a] = false ucd.replace[0x16f16] = false ucd.replace[0x16f28] = false ucd.replace[0x16f35] = false for i = 0x16f3a, 0x16f3b do ucd.replace[i] = false end for i = 0x16f3f, 0x16f40 do ucd.replace[i] = false end for i = 0x16f42, 0x16f43 do ucd.replace[i] = false end for i = 0x16f51, 0x16f52 do ucd.replace[i] = false end for i = 0x1bca0, 0x1bca3 do ucd.replace[i] = true end for i = 0x1ccd6, 0x1ccf9 do ucd.replace[i] = false end ucd.replace[0x1d114] = false ucd.replace[0x1d16d] = false for i = 0x1d173, 0x1d17a do ucd.replace[i] = true end ucd.replace[0x1d206] = false ucd.replace[0x1d20d] = false ucd.replace[0x1d20f] = false for i = 0x1d212, 0x1d213 do ucd.replace[i] = false end ucd.replace[0x1d216] = false ucd.replace[0x1d22a] = false for i = 0x1d236, 0x1d237 do ucd.replace[i] = false end for i = 0x1d23a, 0x1d23b do ucd.replace[i] = false end for i = 0x1d400, 0x1d425 do ucd.replace[i] = false end for i = 0x1d427, 0x1d454 do ucd.replace[i] = false end for i = 0x1d456, 0x1d459 do ucd.replace[i] = false end for i = 0x1d45b, 0x1d48d do ucd.replace[i] = false end for i = 0x1d48f, 0x1d49c do ucd.replace[i] = false end for i = 0x1d49e, 0x1d49f do ucd.replace[i] = false end ucd.replace[0x1d4a2] = false for i = 0x1d4a5, 0x1d4a6 do ucd.replace[i] = false end for i = 0x1d4a9, 0x1d4ac do ucd.replace[i] = false end for i = 0x1d4ae, 0x1d4b9 do ucd.replace[i] = false end ucd.replace[0x1d4bb] = false for i = 0x1d4bd, 0x1d4c1 do ucd.replace[i] = false end ucd.replace[0x1d4c3] = false for i = 0x1d4c5, 0x1d4f5 do ucd.replace[i] = false end for i = 0x1d4f7, 0x1d505 do ucd.replace[i] = false end for i = 0x1d507, 0x1d50a do ucd.replace[i] = false end for i = 0x1d50d, 0x1d514 do ucd.replace[i] = false end for i = 0x1d516, 0x1d51c do ucd.replace[i] = false end for i = 0x1d51e, 0x1d529 do ucd.replace[i] = false end for i = 0x1d52b, 0x1d539 do ucd.replace[i] = false end for i = 0x1d53b, 0x1d53e do ucd.replace[i] = false end for i = 0x1d540, 0x1d544 do ucd.replace[i] = false end ucd.replace[0x1d546] = false for i = 0x1d54a, 0x1d550 do ucd.replace[i] = false end for i = 0x1d552, 0x1d55d do ucd.replace[i] = false end for i = 0x1d55f, 0x1d591 do ucd.replace[i] = false end for i = 0x1d593, 0x1d5c5 do ucd.replace[i] = false end for i = 0x1d5c7, 0x1d5f9 do ucd.replace[i] = false end for i = 0x1d5fb, 0x1d62d do ucd.replace[i] = false end for i = 0x1d62f, 0x1d661 do ucd.replace[i] = false end for i = 0x1d663, 0x1d695 do ucd.replace[i] = false end for i = 0x1d697, 0x1d6a4 do ucd.replace[i] = false end for i = 0x1d6a8, 0x1d6a9 do ucd.replace[i] = false end for i = 0x1d6ac, 0x1d6ae do ucd.replace[i] = false end for i = 0x1d6b0, 0x1d6b1 do ucd.replace[i] = false end for i = 0x1d6b3, 0x1d6b4 do ucd.replace[i] = false end ucd.replace[0x1d6b6] = false ucd.replace[0x1d6b8] = false for i = 0x1d6bb, 0x1d6bc do ucd.replace[i] = false end ucd.replace[0x1d6be] = false ucd.replace[0x1d6c2] = false ucd.replace[0x1d6c4] = false ucd.replace[0x1d6ca] = false ucd.replace[0x1d6ce] = false ucd.replace[0x1d6d0] = false ucd.replace[0x1d6d2] = false ucd.replace[0x1d6d4] = false ucd.replace[0x1d6d6] = false ucd.replace[0x1d6e0] = false for i = 0x1d6e2, 0x1d6e3 do ucd.replace[i] = false end for i = 0x1d6e6, 0x1d6e8 do ucd.replace[i] = false end for i = 0x1d6ea, 0x1d6eb do ucd.replace[i] = false end for i = 0x1d6ed, 0x1d6ee do ucd.replace[i] = false end ucd.replace[0x1d6f0] = false ucd.replace[0x1d6f2] = false for i = 0x1d6f5, 0x1d6f6 do ucd.replace[i] = false end ucd.replace[0x1d6f8] = false ucd.replace[0x1d6fc] = false ucd.replace[0x1d6fe] = false ucd.replace[0x1d704] = false ucd.replace[0x1d708] = false ucd.replace[0x1d70a] = false ucd.replace[0x1d70c] = false ucd.replace[0x1d70e] = false ucd.replace[0x1d710] = false ucd.replace[0x1d71a] = false for i = 0x1d71c, 0x1d71d do ucd.replace[i] = false end for i = 0x1d720, 0x1d722 do ucd.replace[i] = false end for i = 0x1d724, 0x1d725 do ucd.replace[i] = false end for i = 0x1d727, 0x1d728 do ucd.replace[i] = false end ucd.replace[0x1d72a] = false ucd.replace[0x1d72c] = false for i = 0x1d72f, 0x1d730 do ucd.replace[i] = false end ucd.replace[0x1d732] = false ucd.replace[0x1d736] = false ucd.replace[0x1d738] = false ucd.replace[0x1d73e] = false ucd.replace[0x1d742] = false ucd.replace[0x1d744] = false ucd.replace[0x1d746] = false ucd.replace[0x1d748] = false ucd.replace[0x1d74a] = false ucd.replace[0x1d754] = false for i = 0x1d756, 0x1d757 do ucd.replace[i] = false end for i = 0x1d75a, 0x1d75c do ucd.replace[i] = false end for i = 0x1d75e, 0x1d75f do ucd.replace[i] = false end for i = 0x1d761, 0x1d762 do ucd.replace[i] = false end ucd.replace[0x1d764] = false ucd.replace[0x1d766] = false for i = 0x1d769, 0x1d76a do ucd.replace[i] = false end ucd.replace[0x1d76c] = false ucd.replace[0x1d770] = false ucd.replace[0x1d772] = false ucd.replace[0x1d778] = false ucd.replace[0x1d77c] = false ucd.replace[0x1d77e] = false ucd.replace[0x1d780] = false ucd.replace[0x1d782] = false ucd.replace[0x1d784] = false ucd.replace[0x1d78e] = false for i = 0x1d790, 0x1d791 do ucd.replace[i] = false end for i = 0x1d794, 0x1d796 do ucd.replace[i] = false end for i = 0x1d798, 0x1d799 do ucd.replace[i] = false end for i = 0x1d79b, 0x1d79c do ucd.replace[i] = false end ucd.replace[0x1d79e] = false ucd.replace[0x1d7a0] = false for i = 0x1d7a3, 0x1d7a4 do ucd.replace[i] = false end ucd.replace[0x1d7a6] = false ucd.replace[0x1d7aa] = false ucd.replace[0x1d7ac] = false ucd.replace[0x1d7b2] = false ucd.replace[0x1d7b6] = false ucd.replace[0x1d7b8] = false ucd.replace[0x1d7ba] = false ucd.replace[0x1d7bc] = false ucd.replace[0x1d7be] = false ucd.replace[0x1d7c8] = false ucd.replace[0x1d7ca] = false for i = 0x1d7ce, 0x1d7ff do ucd.replace[i] = false end ucd.replace[0x1e6e9] = false ucd.replace[0x1e8c7] = false ucd.replace[0x1e8cb] = false ucd.replace[0x1ee00] = false ucd.replace[0x1ee24] = false ucd.replace[0x1ee64] = false ucd.replace[0x1ee80] = false ucd.replace[0x1ee84] = false ucd.replace[0x1f74c] = false ucd.replace[0x1f768] = false for i = 0x1fbf0, 0x1fbf9 do ucd.replace[i] = false end for i = 0x1fffe, 0x1ffff do ucd.replace[i] = true end for i = 0x2fffe, 0x2ffff do ucd.replace[i] = true end for i = 0x3fffe, 0x3ffff do ucd.replace[i] = true end for i = 0x4fffe, 0x4ffff do ucd.replace[i] = true end for i = 0x5fffe, 0x5ffff do ucd.replace[i] = true end for i = 0x6fffe, 0x6ffff do ucd.replace[i] = true end for i = 0x7fffe, 0x7ffff do ucd.replace[i] = true end for i = 0x8fffe, 0x8ffff do ucd.replace[i] = true end for i = 0x9fffe, 0x9ffff do ucd.replace[i] = true end for i = 0xafffe, 0xaffff do ucd.replace[i] = true end for i = 0xbfffe, 0xbffff do ucd.replace[i] = true end for i = 0xcfffe, 0xcffff do ucd.replace[i] = true end for i = 0xdfffe, 0xe0fff do ucd.replace[i] = true end for i = 0xefffe, 0xeffff do ucd.replace[i] = true end for i = 0xffffe, 0xfffff do ucd.replace[i] = true end for i = 0x10fffe, 0x10ffff do ucd.replace[i] = true end ucd.width = {} for i = 0x0300, 0x036f do ucd.width[i] = 0 end for i = 0x0483, 0x0489 do ucd.width[i] = 0 end for i = 0x0591, 0x05bd do ucd.width[i] = 0 end ucd.width[0x05bf] = 0 for i = 0x05c1, 0x05c2 do ucd.width[i] = 0 end for i = 0x05c4, 0x05c5 do ucd.width[i] = 0 end ucd.width[0x05c7] = 0 for i = 0x0610, 0x061a do ucd.width[i] = 0 end for i = 0x064b, 0x065f do ucd.width[i] = 0 end ucd.width[0x0670] = 0 for i = 0x06d6, 0x06dc do ucd.width[i] = 0 end for i = 0x06df, 0x06e4 do ucd.width[i] = 0 end for i = 0x06e7, 0x06e8 do ucd.width[i] = 0 end for i = 0x06ea, 0x06ed do ucd.width[i] = 0 end ucd.width[0x0711] = 0 for i = 0x0730, 0x074a do ucd.width[i] = 0 end for i = 0x07a6, 0x07b0 do ucd.width[i] = 0 end for i = 0x07eb, 0x07f3 do ucd.width[i] = 0 end ucd.width[0x07fd] = 0 for i = 0x0816, 0x0819 do ucd.width[i] = 0 end for i = 0x081b, 0x0823 do ucd.width[i] = 0 end for i = 0x0825, 0x0827 do ucd.width[i] = 0 end for i = 0x0829, 0x082d do ucd.width[i] = 0 end for i = 0x0859, 0x085b do ucd.width[i] = 0 end for i = 0x0897, 0x089f do ucd.width[i] = 0 end for i = 0x08ca, 0x08e1 do ucd.width[i] = 0 end for i = 0x08e3, 0x0902 do ucd.width[i] = 0 end ucd.width[0x093a] = 0 ucd.width[0x093c] = 0 for i = 0x0941, 0x0948 do ucd.width[i] = 0 end ucd.width[0x094d] = 0 for i = 0x0951, 0x0957 do ucd.width[i] = 0 end for i = 0x0962, 0x0963 do ucd.width[i] = 0 end ucd.width[0x0981] = 0 ucd.width[0x09bc] = 0 for i = 0x09c1, 0x09c4 do ucd.width[i] = 0 end ucd.width[0x09cd] = 0 for i = 0x09e2, 0x09e3 do ucd.width[i] = 0 end ucd.width[0x09fe] = 0 for i = 0x0a01, 0x0a02 do ucd.width[i] = 0 end ucd.width[0x0a3c] = 0 for i = 0x0a41, 0x0a42 do ucd.width[i] = 0 end for i = 0x0a47, 0x0a48 do ucd.width[i] = 0 end for i = 0x0a4b, 0x0a4d do ucd.width[i] = 0 end ucd.width[0x0a51] = 0 for i = 0x0a70, 0x0a71 do ucd.width[i] = 0 end ucd.width[0x0a75] = 0 for i = 0x0a81, 0x0a82 do ucd.width[i] = 0 end ucd.width[0x0abc] = 0 for i = 0x0ac1, 0x0ac5 do ucd.width[i] = 0 end for i = 0x0ac7, 0x0ac8 do ucd.width[i] = 0 end ucd.width[0x0acd] = 0 for i = 0x0ae2, 0x0ae3 do ucd.width[i] = 0 end for i = 0x0afa, 0x0aff do ucd.width[i] = 0 end ucd.width[0x0b01] = 0 ucd.width[0x0b3c] = 0 ucd.width[0x0b3f] = 0 for i = 0x0b41, 0x0b44 do ucd.width[i] = 0 end ucd.width[0x0b4d] = 0 for i = 0x0b55, 0x0b56 do ucd.width[i] = 0 end for i = 0x0b62, 0x0b63 do ucd.width[i] = 0 end ucd.width[0x0b82] = 0 ucd.width[0x0bc0] = 0 ucd.width[0x0bcd] = 0 ucd.width[0x0c00] = 0 ucd.width[0x0c04] = 0 ucd.width[0x0c3c] = 0 for i = 0x0c3e, 0x0c40 do ucd.width[i] = 0 end for i = 0x0c46, 0x0c48 do ucd.width[i] = 0 end for i = 0x0c4a, 0x0c4d do ucd.width[i] = 0 end for i = 0x0c55, 0x0c56 do ucd.width[i] = 0 end for i = 0x0c62, 0x0c63 do ucd.width[i] = 0 end ucd.width[0x0c81] = 0 ucd.width[0x0cbc] = 0 ucd.width[0x0cbf] = 0 ucd.width[0x0cc6] = 0 for i = 0x0ccc, 0x0ccd do ucd.width[i] = 0 end for i = 0x0ce2, 0x0ce3 do ucd.width[i] = 0 end for i = 0x0d00, 0x0d01 do ucd.width[i] = 0 end for i = 0x0d3b, 0x0d3c do ucd.width[i] = 0 end for i = 0x0d41, 0x0d44 do ucd.width[i] = 0 end ucd.width[0x0d4d] = 0 for i = 0x0d62, 0x0d63 do ucd.width[i] = 0 end ucd.width[0x0d81] = 0 ucd.width[0x0dca] = 0 for i = 0x0dd2, 0x0dd4 do ucd.width[i] = 0 end ucd.width[0x0dd6] = 0 ucd.width[0x0e31] = 0 for i = 0x0e34, 0x0e3a do ucd.width[i] = 0 end for i = 0x0e47, 0x0e4e do ucd.width[i] = 0 end ucd.width[0x0eb1] = 0 for i = 0x0eb4, 0x0ebc do ucd.width[i] = 0 end for i = 0x0ec8, 0x0ece do ucd.width[i] = 0 end for i = 0x0f18, 0x0f19 do ucd.width[i] = 0 end ucd.width[0x0f35] = 0 ucd.width[0x0f37] = 0 ucd.width[0x0f39] = 0 for i = 0x0f71, 0x0f7e do ucd.width[i] = 0 end for i = 0x0f80, 0x0f84 do ucd.width[i] = 0 end for i = 0x0f86, 0x0f87 do ucd.width[i] = 0 end for i = 0x0f8d, 0x0f97 do ucd.width[i] = 0 end for i = 0x0f99, 0x0fbc do ucd.width[i] = 0 end ucd.width[0x0fc6] = 0 for i = 0x102d, 0x1030 do ucd.width[i] = 0 end for i = 0x1032, 0x1037 do ucd.width[i] = 0 end for i = 0x1039, 0x103a do ucd.width[i] = 0 end for i = 0x103d, 0x103e do ucd.width[i] = 0 end for i = 0x1058, 0x1059 do ucd.width[i] = 0 end for i = 0x105e, 0x1060 do ucd.width[i] = 0 end for i = 0x1071, 0x1074 do ucd.width[i] = 0 end ucd.width[0x1082] = 0 for i = 0x1085, 0x1086 do ucd.width[i] = 0 end ucd.width[0x108d] = 0 ucd.width[0x109d] = 0 for i = 0x1100, 0x115f do ucd.width[i] = 2 end for i = 0x135d, 0x135f do ucd.width[i] = 0 end for i = 0x1712, 0x1714 do ucd.width[i] = 0 end for i = 0x1732, 0x1733 do ucd.width[i] = 0 end for i = 0x1752, 0x1753 do ucd.width[i] = 0 end for i = 0x1772, 0x1773 do ucd.width[i] = 0 end for i = 0x17b4, 0x17b5 do ucd.width[i] = 0 end for i = 0x17b7, 0x17bd do ucd.width[i] = 0 end ucd.width[0x17c6] = 0 for i = 0x17c9, 0x17d3 do ucd.width[i] = 0 end ucd.width[0x17dd] = 0 for i = 0x180b, 0x180d do ucd.width[i] = 0 end ucd.width[0x180f] = 0 for i = 0x1885, 0x1886 do ucd.width[i] = 0 end ucd.width[0x18a9] = 0 for i = 0x1920, 0x1922 do ucd.width[i] = 0 end for i = 0x1927, 0x1928 do ucd.width[i] = 0 end ucd.width[0x1932] = 0 for i = 0x1939, 0x193b do ucd.width[i] = 0 end for i = 0x1a17, 0x1a18 do ucd.width[i] = 0 end ucd.width[0x1a1b] = 0 ucd.width[0x1a56] = 0 for i = 0x1a58, 0x1a5e do ucd.width[i] = 0 end ucd.width[0x1a60] = 0 ucd.width[0x1a62] = 0 for i = 0x1a65, 0x1a6c do ucd.width[i] = 0 end for i = 0x1a73, 0x1a7c do ucd.width[i] = 0 end ucd.width[0x1a7f] = 0 for i = 0x1ab0, 0x1add do ucd.width[i] = 0 end for i = 0x1ae0, 0x1aeb do ucd.width[i] = 0 end for i = 0x1b00, 0x1b03 do ucd.width[i] = 0 end ucd.width[0x1b34] = 0 for i = 0x1b36, 0x1b3a do ucd.width[i] = 0 end ucd.width[0x1b3c] = 0 ucd.width[0x1b42] = 0 for i = 0x1b6b, 0x1b73 do ucd.width[i] = 0 end for i = 0x1b80, 0x1b81 do ucd.width[i] = 0 end for i = 0x1ba2, 0x1ba5 do ucd.width[i] = 0 end for i = 0x1ba8, 0x1ba9 do ucd.width[i] = 0 end for i = 0x1bab, 0x1bad do ucd.width[i] = 0 end ucd.width[0x1be6] = 0 for i = 0x1be8, 0x1be9 do ucd.width[i] = 0 end ucd.width[0x1bed] = 0 for i = 0x1bef, 0x1bf1 do ucd.width[i] = 0 end for i = 0x1c2c, 0x1c33 do ucd.width[i] = 0 end for i = 0x1c36, 0x1c37 do ucd.width[i] = 0 end for i = 0x1cd0, 0x1cd2 do ucd.width[i] = 0 end for i = 0x1cd4, 0x1ce0 do ucd.width[i] = 0 end for i = 0x1ce2, 0x1ce8 do ucd.width[i] = 0 end ucd.width[0x1ced] = 0 ucd.width[0x1cf4] = 0 for i = 0x1cf8, 0x1cf9 do ucd.width[i] = 0 end for i = 0x1dc0, 0x1dff do ucd.width[i] = 0 end for i = 0x200b, 0x200f do ucd.width[i] = 0 end for i = 0x2028, 0x202e do ucd.width[i] = 0 end for i = 0x2060, 0x2062 do ucd.width[i] = 0 end ucd.width[0x2066] = 0 for i = 0x20d0, 0x20f0 do ucd.width[i] = 0 end for i = 0x231a, 0x231b do ucd.width[i] = 2 end for i = 0x2329, 0x232a do ucd.width[i] = 2 end for i = 0x23e9, 0x23ec do ucd.width[i] = 2 end ucd.width[0x23f0] = 2 ucd.width[0x23f3] = 2 for i = 0x25fd, 0x25fe do ucd.width[i] = 2 end for i = 0x2614, 0x2615 do ucd.width[i] = 2 end for i = 0x2630, 0x2637 do ucd.width[i] = 2 end for i = 0x2648, 0x2653 do ucd.width[i] = 2 end ucd.width[0x267f] = 2 for i = 0x268a, 0x268f do ucd.width[i] = 2 end ucd.width[0x2693] = 2 ucd.width[0x26a1] = 2 for i = 0x26aa, 0x26ab do ucd.width[i] = 2 end for i = 0x26bd, 0x26be do ucd.width[i] = 2 end for i = 0x26c4, 0x26c5 do ucd.width[i] = 2 end ucd.width[0x26ce] = 2 ucd.width[0x26d4] = 2 ucd.width[0x26ea] = 2 for i = 0x26f2, 0x26f3 do ucd.width[i] = 2 end ucd.width[0x26f5] = 2 ucd.width[0x26fa] = 2 ucd.width[0x26fd] = 2 ucd.width[0x2705] = 2 for i = 0x270a, 0x270b do ucd.width[i] = 2 end ucd.width[0x2728] = 2 ucd.width[0x274c] = 2 ucd.width[0x274e] = 2 for i = 0x2753, 0x2755 do ucd.width[i] = 2 end ucd.width[0x2757] = 2 for i = 0x2795, 0x2797 do ucd.width[i] = 2 end ucd.width[0x27b0] = 2 ucd.width[0x27bf] = 2 for i = 0x2b1b, 0x2b1c do ucd.width[i] = 2 end ucd.width[0x2b50] = 2 ucd.width[0x2b55] = 2 for i = 0x2cef, 0x2cf1 do ucd.width[i] = 0 end ucd.width[0x2d7f] = 0 for i = 0x2de0, 0x2dff do ucd.width[i] = 0 end for i = 0x2e80, 0x2e99 do ucd.width[i] = 2 end for i = 0x2e9b, 0x2ef3 do ucd.width[i] = 2 end for i = 0x2f00, 0x2fd5 do ucd.width[i] = 2 end for i = 0x2ff0, 0x303e do ucd.width[i] = 2 end for i = 0x3041, 0x3096 do ucd.width[i] = 2 end for i = 0x3099, 0x30ff do ucd.width[i] = 2 end for i = 0x3105, 0x312f do ucd.width[i] = 2 end for i = 0x3131, 0x318e do ucd.width[i] = 2 end for i = 0x3190, 0x31e5 do ucd.width[i] = 2 end for i = 0x31ef, 0x321e do ucd.width[i] = 2 end for i = 0x3220, 0x3247 do ucd.width[i] = 2 end for i = 0x3250, 0xa48c do ucd.width[i] = 2 end for i = 0xa490, 0xa4c6 do ucd.width[i] = 2 end for i = 0xa66f, 0xa672 do ucd.width[i] = 0 end for i = 0xa674, 0xa67d do ucd.width[i] = 0 end for i = 0xa69e, 0xa69f do ucd.width[i] = 0 end for i = 0xa6f0, 0xa6f1 do ucd.width[i] = 0 end ucd.width[0xa802] = 0 ucd.width[0xa806] = 0 ucd.width[0xa80b] = 0 for i = 0xa825, 0xa826 do ucd.width[i] = 0 end ucd.width[0xa82c] = 0 for i = 0xa8c4, 0xa8c5 do ucd.width[i] = 0 end for i = 0xa8e0, 0xa8f1 do ucd.width[i] = 0 end ucd.width[0xa8ff] = 0 for i = 0xa926, 0xa92d do ucd.width[i] = 0 end for i = 0xa947, 0xa951 do ucd.width[i] = 0 end for i = 0xa960, 0xa97c do ucd.width[i] = 2 end for i = 0xa980, 0xa982 do ucd.width[i] = 0 end ucd.width[0xa9b3] = 0 for i = 0xa9b6, 0xa9b9 do ucd.width[i] = 0 end for i = 0xa9bc, 0xa9bd do ucd.width[i] = 0 end ucd.width[0xa9e5] = 0 for i = 0xaa29, 0xaa2e do ucd.width[i] = 0 end for i = 0xaa31, 0xaa32 do ucd.width[i] = 0 end for i = 0xaa35, 0xaa36 do ucd.width[i] = 0 end ucd.width[0xaa43] = 0 ucd.width[0xaa4c] = 0 ucd.width[0xaa7c] = 0 ucd.width[0xaab0] = 0 for i = 0xaab2, 0xaab4 do ucd.width[i] = 0 end for i = 0xaab7, 0xaab8 do ucd.width[i] = 0 end for i = 0xaabe, 0xaabf do ucd.width[i] = 0 end ucd.width[0xaac1] = 0 for i = 0xaaec, 0xaaed do ucd.width[i] = 0 end ucd.width[0xaaf6] = 0 ucd.width[0xabe5] = 0 ucd.width[0xabe8] = 0 ucd.width[0xabed] = 0 for i = 0xac00, 0xd7a3 do ucd.width[i] = 2 end for i = 0xf900, 0xfaff do ucd.width[i] = 2 end ucd.width[0xfb1e] = 0 for i = 0xfe00, 0xfe0f do ucd.width[i] = 0 end for i = 0xfe10, 0xfe19 do ucd.width[i] = 2 end for i = 0xfe20, 0xfe2f do ucd.width[i] = 0 end for i = 0xfe30, 0xfe52 do ucd.width[i] = 2 end for i = 0xfe54, 0xfe66 do ucd.width[i] = 2 end for i = 0xfe68, 0xfe6b do ucd.width[i] = 2 end for i = 0xff01, 0xff60 do ucd.width[i] = 2 end for i = 0xffe0, 0xffe6 do ucd.width[i] = 2 end ucd.width[0x101fd] = 0 ucd.width[0x102e0] = 0 for i = 0x10376, 0x1037a do ucd.width[i] = 0 end for i = 0x10a01, 0x10a03 do ucd.width[i] = 0 end for i = 0x10a05, 0x10a06 do ucd.width[i] = 0 end for i = 0x10a0c, 0x10a0f do ucd.width[i] = 0 end for i = 0x10a38, 0x10a3a do ucd.width[i] = 0 end ucd.width[0x10a3f] = 0 for i = 0x10ae5, 0x10ae6 do ucd.width[i] = 0 end for i = 0x10d24, 0x10d27 do ucd.width[i] = 0 end for i = 0x10d69, 0x10d6d do ucd.width[i] = 0 end for i = 0x10eab, 0x10eac do ucd.width[i] = 0 end for i = 0x10efa, 0x10eff do ucd.width[i] = 0 end for i = 0x10f46, 0x10f50 do ucd.width[i] = 0 end for i = 0x10f82, 0x10f85 do ucd.width[i] = 0 end ucd.width[0x11001] = 0 for i = 0x11038, 0x11046 do ucd.width[i] = 0 end ucd.width[0x11070] = 0 for i = 0x11073, 0x11074 do ucd.width[i] = 0 end for i = 0x1107f, 0x11081 do ucd.width[i] = 0 end for i = 0x110b3, 0x110b6 do ucd.width[i] = 0 end for i = 0x110b9, 0x110ba do ucd.width[i] = 0 end ucd.width[0x110c2] = 0 for i = 0x11100, 0x11102 do ucd.width[i] = 0 end for i = 0x11127, 0x1112b do ucd.width[i] = 0 end for i = 0x1112d, 0x11134 do ucd.width[i] = 0 end ucd.width[0x11173] = 0 for i = 0x11180, 0x11181 do ucd.width[i] = 0 end for i = 0x111b6, 0x111be do ucd.width[i] = 0 end for i = 0x111c9, 0x111cc do ucd.width[i] = 0 end ucd.width[0x111cf] = 0 for i = 0x1122f, 0x11231 do ucd.width[i] = 0 end ucd.width[0x11234] = 0 for i = 0x11236, 0x11237 do ucd.width[i] = 0 end ucd.width[0x1123e] = 0 ucd.width[0x11241] = 0 ucd.width[0x112df] = 0 for i = 0x112e3, 0x112ea do ucd.width[i] = 0 end for i = 0x11300, 0x11301 do ucd.width[i] = 0 end for i = 0x1133b, 0x1133c do ucd.width[i] = 0 end ucd.width[0x11340] = 0 for i = 0x11366, 0x1136c do ucd.width[i] = 0 end for i = 0x11370, 0x11374 do ucd.width[i] = 0 end for i = 0x113bb, 0x113c0 do ucd.width[i] = 0 end ucd.width[0x113ce] = 0 ucd.width[0x113d0] = 0 ucd.width[0x113d2] = 0 for i = 0x113e1, 0x113e2 do ucd.width[i] = 0 end for i = 0x11438, 0x1143f do ucd.width[i] = 0 end for i = 0x11442, 0x11444 do ucd.width[i] = 0 end ucd.width[0x11446] = 0 ucd.width[0x1145e] = 0 for i = 0x114b3, 0x114b8 do ucd.width[i] = 0 end ucd.width[0x114ba] = 0 for i = 0x114bf, 0x114c0 do ucd.width[i] = 0 end for i = 0x114c2, 0x114c3 do ucd.width[i] = 0 end for i = 0x115b2, 0x115b5 do ucd.width[i] = 0 end for i = 0x115bc, 0x115bd do ucd.width[i] = 0 end for i = 0x115bf, 0x115c0 do ucd.width[i] = 0 end for i = 0x115dc, 0x115dd do ucd.width[i] = 0 end for i = 0x11633, 0x1163a do ucd.width[i] = 0 end ucd.width[0x1163d] = 0 for i = 0x1163f, 0x11640 do ucd.width[i] = 0 end ucd.width[0x116ab] = 0 ucd.width[0x116ad] = 0 for i = 0x116b0, 0x116b5 do ucd.width[i] = 0 end ucd.width[0x116b7] = 0 ucd.width[0x1171d] = 0 ucd.width[0x1171f] = 0 for i = 0x11722, 0x11725 do ucd.width[i] = 0 end for i = 0x11727, 0x1172b do ucd.width[i] = 0 end for i = 0x1182f, 0x11837 do ucd.width[i] = 0 end for i = 0x11839, 0x1183a do ucd.width[i] = 0 end for i = 0x1193b, 0x1193c do ucd.width[i] = 0 end ucd.width[0x1193e] = 0 ucd.width[0x11943] = 0 for i = 0x119d4, 0x119d7 do ucd.width[i] = 0 end for i = 0x119da, 0x119db do ucd.width[i] = 0 end ucd.width[0x119e0] = 0 for i = 0x11a01, 0x11a0a do ucd.width[i] = 0 end for i = 0x11a33, 0x11a38 do ucd.width[i] = 0 end for i = 0x11a3b, 0x11a3e do ucd.width[i] = 0 end ucd.width[0x11a47] = 0 for i = 0x11a51, 0x11a56 do ucd.width[i] = 0 end for i = 0x11a59, 0x11a5b do ucd.width[i] = 0 end for i = 0x11a8a, 0x11a96 do ucd.width[i] = 0 end for i = 0x11a98, 0x11a99 do ucd.width[i] = 0 end ucd.width[0x11b60] = 0 for i = 0x11b62, 0x11b64 do ucd.width[i] = 0 end ucd.width[0x11b66] = 0 for i = 0x11c30, 0x11c36 do ucd.width[i] = 0 end for i = 0x11c38, 0x11c3d do ucd.width[i] = 0 end ucd.width[0x11c3f] = 0 for i = 0x11c92, 0x11ca7 do ucd.width[i] = 0 end for i = 0x11caa, 0x11cb0 do ucd.width[i] = 0 end for i = 0x11cb2, 0x11cb3 do ucd.width[i] = 0 end for i = 0x11cb5, 0x11cb6 do ucd.width[i] = 0 end for i = 0x11d31, 0x11d36 do ucd.width[i] = 0 end ucd.width[0x11d3a] = 0 for i = 0x11d3c, 0x11d3d do ucd.width[i] = 0 end for i = 0x11d3f, 0x11d45 do ucd.width[i] = 0 end ucd.width[0x11d47] = 0 for i = 0x11d90, 0x11d91 do ucd.width[i] = 0 end ucd.width[0x11d95] = 0 ucd.width[0x11d97] = 0 for i = 0x11ef3, 0x11ef4 do ucd.width[i] = 0 end for i = 0x11f00, 0x11f01 do ucd.width[i] = 0 end for i = 0x11f36, 0x11f3a do ucd.width[i] = 0 end ucd.width[0x11f40] = 0 ucd.width[0x11f42] = 0 ucd.width[0x11f5a] = 0 ucd.width[0x13440] = 0 for i = 0x13447, 0x13455 do ucd.width[i] = 0 end for i = 0x1611e, 0x16129 do ucd.width[i] = 0 end for i = 0x1612d, 0x1612f do ucd.width[i] = 0 end for i = 0x16af0, 0x16af4 do ucd.width[i] = 0 end for i = 0x16b30, 0x16b36 do ucd.width[i] = 0 end ucd.width[0x16f4f] = 0 for i = 0x16f8f, 0x16f92 do ucd.width[i] = 0 end for i = 0x16fe0, 0x16fe4 do ucd.width[i] = 2 end for i = 0x16ff0, 0x16ff6 do ucd.width[i] = 2 end for i = 0x17000, 0x18cd5 do ucd.width[i] = 2 end for i = 0x18cff, 0x18d1e do ucd.width[i] = 2 end for i = 0x18d80, 0x18df2 do ucd.width[i] = 2 end for i = 0x1aff0, 0x1aff3 do ucd.width[i] = 2 end for i = 0x1aff5, 0x1affb do ucd.width[i] = 2 end for i = 0x1affd, 0x1affe do ucd.width[i] = 2 end for i = 0x1b000, 0x1b122 do ucd.width[i] = 2 end ucd.width[0x1b132] = 2 for i = 0x1b150, 0x1b152 do ucd.width[i] = 2 end ucd.width[0x1b155] = 2 for i = 0x1b164, 0x1b167 do ucd.width[i] = 2 end for i = 0x1b170, 0x1b2fb do ucd.width[i] = 2 end for i = 0x1bc9d, 0x1bc9e do ucd.width[i] = 0 end for i = 0x1cf00, 0x1cf2d do ucd.width[i] = 0 end for i = 0x1cf30, 0x1cf46 do ucd.width[i] = 0 end for i = 0x1d167, 0x1d169 do ucd.width[i] = 0 end for i = 0x1d17b, 0x1d182 do ucd.width[i] = 0 end for i = 0x1d185, 0x1d18b do ucd.width[i] = 0 end for i = 0x1d1aa, 0x1d1ad do ucd.width[i] = 0 end for i = 0x1d242, 0x1d244 do ucd.width[i] = 0 end for i = 0x1d300, 0x1d356 do ucd.width[i] = 2 end for i = 0x1d360, 0x1d376 do ucd.width[i] = 2 end for i = 0x1da00, 0x1da36 do ucd.width[i] = 0 end for i = 0x1da3b, 0x1da6c do ucd.width[i] = 0 end ucd.width[0x1da75] = 0 ucd.width[0x1da84] = 0 for i = 0x1da9b, 0x1da9f do ucd.width[i] = 0 end for i = 0x1daa1, 0x1daaf do ucd.width[i] = 0 end for i = 0x1e000, 0x1e006 do ucd.width[i] = 0 end for i = 0x1e008, 0x1e018 do ucd.width[i] = 0 end for i = 0x1e01b, 0x1e021 do ucd.width[i] = 0 end for i = 0x1e023, 0x1e024 do ucd.width[i] = 0 end for i = 0x1e026, 0x1e02a do ucd.width[i] = 0 end ucd.width[0x1e08f] = 0 for i = 0x1e130, 0x1e136 do ucd.width[i] = 0 end ucd.width[0x1e2ae] = 0 for i = 0x1e2ec, 0x1e2ef do ucd.width[i] = 0 end for i = 0x1e4ec, 0x1e4ef do ucd.width[i] = 0 end for i = 0x1e5ee, 0x1e5ef do ucd.width[i] = 0 end ucd.width[0x1e6e3] = 0 ucd.width[0x1e6e6] = 0 for i = 0x1e6ee, 0x1e6ef do ucd.width[i] = 0 end ucd.width[0x1e6f5] = 0 for i = 0x1e8d0, 0x1e8d6 do ucd.width[i] = 0 end for i = 0x1e944, 0x1e94a do ucd.width[i] = 0 end ucd.width[0x1f004] = 2 ucd.width[0x1f0cf] = 2 ucd.width[0x1f18e] = 2 for i = 0x1f191, 0x1f19a do ucd.width[i] = 2 end for i = 0x1f200, 0x1f202 do ucd.width[i] = 2 end for i = 0x1f210, 0x1f23b do ucd.width[i] = 2 end for i = 0x1f240, 0x1f248 do ucd.width[i] = 2 end for i = 0x1f250, 0x1f251 do ucd.width[i] = 2 end for i = 0x1f260, 0x1f265 do ucd.width[i] = 2 end for i = 0x1f300, 0x1f320 do ucd.width[i] = 2 end for i = 0x1f32d, 0x1f335 do ucd.width[i] = 2 end for i = 0x1f337, 0x1f37c do ucd.width[i] = 2 end for i = 0x1f37e, 0x1f393 do ucd.width[i] = 2 end for i = 0x1f3a0, 0x1f3ca do ucd.width[i] = 2 end for i = 0x1f3cf, 0x1f3d3 do ucd.width[i] = 2 end for i = 0x1f3e0, 0x1f3f0 do ucd.width[i] = 2 end ucd.width[0x1f3f4] = 2 for i = 0x1f3f8, 0x1f43e do ucd.width[i] = 2 end ucd.width[0x1f440] = 2 for i = 0x1f442, 0x1f4fc do ucd.width[i] = 2 end for i = 0x1f4ff, 0x1f53d do ucd.width[i] = 2 end for i = 0x1f54b, 0x1f54e do ucd.width[i] = 2 end for i = 0x1f550, 0x1f567 do ucd.width[i] = 2 end ucd.width[0x1f57a] = 2 for i = 0x1f595, 0x1f596 do ucd.width[i] = 2 end ucd.width[0x1f5a4] = 2 for i = 0x1f5fb, 0x1f64f do ucd.width[i] = 2 end for i = 0x1f680, 0x1f6c5 do ucd.width[i] = 2 end ucd.width[0x1f6cc] = 2 for i = 0x1f6d0, 0x1f6d2 do ucd.width[i] = 2 end for i = 0x1f6d5, 0x1f6d8 do ucd.width[i] = 2 end for i = 0x1f6dc, 0x1f6df do ucd.width[i] = 2 end for i = 0x1f6eb, 0x1f6ec do ucd.width[i] = 2 end for i = 0x1f6f4, 0x1f6fc do ucd.width[i] = 2 end for i = 0x1f7e0, 0x1f7eb do ucd.width[i] = 2 end ucd.width[0x1f7f0] = 2 for i = 0x1f90c, 0x1f93a do ucd.width[i] = 2 end for i = 0x1f93c, 0x1f945 do ucd.width[i] = 2 end for i = 0x1f947, 0x1f9ff do ucd.width[i] = 2 end for i = 0x1fa70, 0x1fa7c do ucd.width[i] = 2 end for i = 0x1fa80, 0x1fa8a do ucd.width[i] = 2 end for i = 0x1fa8e, 0x1fac6 do ucd.width[i] = 2 end ucd.width[0x1fac8] = 2 for i = 0x1facd, 0x1fadc do ucd.width[i] = 2 end for i = 0x1fadf, 0x1faea do ucd.width[i] = 2 end for i = 0x1faef, 0x1faf8 do ucd.width[i] = 2 end for i = 0x20000, 0x2fffd do ucd.width[i] = 2 end for i = 0x30000, 0x3fffd do ucd.width[i] = 2 end for i = 0xe0100, 0xe01ef do ucd.width[i] = 0 end function ucd.codepoint_width(c) return ucd.width[c] or 1 end return ucd ]================================================================================] , "neo-ed.lib.ucd")) package.preload["neo-ed.main"] = assert(load( [================================================================================[ local posix = require "posix" local lib = require "neo-ed.lib" local term = require "neo-ed.term" local regex = require "neo-ed.regex" local state = require "neo-ed.state" ver = ver or "live (" .. os.date("%Y-%m-%d") .. ")" local cmds = {} local files = {} local lvl = nil local re_set = false local function default_main() local prof = lib.prof and lib.profiler("startup") or nil if prof then prof:start("state initialization") end local st = state(files, lvl) if prof then prof:stop() end if not re_set and regex.curr ~= regex.bre then st:warn("missing lrexlib-posix, falling back to Lua patterns") end if prof then prof:print() end return st:main() end local main = default_main local help = [=[ Usage: ned [ ..] [ [+ ..] [] ..] Open each location for editing, optionally run commands on it. Options: --doc= Export all help pages into the specified directory. --pandoc=: Export all help pages into the specified directory, using pandoc to convert them to the specified format. --help Show this help. --prof Enable profiling. --regex= Select regex implementation. Currently supports bre (default), lua. --safe[=] Start in safe mode, skipping init.lua. --show[=] Do not open the editor, just print the files once and the quit. --trace Enable stack traces. --version Show version and licensing info. Safe Mode levels: 4 (POSIX ): only modules that contain POSIX functionality 3 (Testing): POSIX plus all other commands that affect the output file 2 (Reduced): all bundled plugins minus highlighters 1 (Default): all bundled plugins - : try to use init.lua, fall back to 1 ]=] local function showf(addr, nup, ndn) return function(files) for _, f in ipairs(files) do f.cmds = { [[:lua state.curr:status_line() .. "\n"]], ":seek " .. addr, ("Lu%dd%d"):format(nup or 1, ndn or 1), } end state(files, lvl):print_msgs(true) end end local function write_doc_to(ofn) local st = state({{loc = "", cmds = {}}}, lvl) for i, name, _, n in lib.opairs(st.help) do if name:find("^/") then term:progress(i, n) st.curr:cmd("E about:" .. name) st.curr:cmd("%w " .. ofn(name, st)) end end term:progress() end local argdefs = { {pat = "^%-%-help$", fn = function() print(help) os.exit(0, true) end}, {pat = "^%-%-prof$", fn = function() lib.prof = true end}, {pat = "^%-%-regex=(.*)$", fn = function(m) re_set = true regex.curr = lib.assert(regex[m[1]], "unsupported regex implementation: " .. m[1]) end}, {pat = "^%-%-safe=?(%d*)$", fn = function(m) lvl = tonumber(m[1]) or 1 end}, {pat = "^%-%-show$", fn = function() main = showf("%") end}, {pat = "^%-%-show=(.+)$", fn = function(m) main = showf(m[1]) end}, {pat = "^%-%-show@(%d+)/(%d+)=(.+)$", fn = function(m) main = showf(m[3], tonumber(m[1]), tonumber(m[2])) end}, {pat = "^%-%-doc=(.*)$", fn = function(m) main = function() lib.cmd "mkdir" "-p" "--" (m[1]) :err() write_doc_to(function(name, state) return state:realpath(m[1] .. "/" .. (name == "/" and "index" or name):gsub("/", "-") .. ".md") end) end end}, {pat = "^%-%-pandoc=(.-):(.*)$", fn = function(m) main = function() lib.cmd "mkdir" "-p" "--" (m[2]) :err() local pandoc = lib.cmd "pandoc" "--wrap=none" "--from=markdown" if m[1] == "md" then pandoc "--to=commonmark-raw_html" end local filter_path = m[2] .. "/fix-links.lua" do local h = io.open(filter_path, "w") h:write [[return {]] h:write [[ Link = function(elem)]] h:write [[ local p = elem.target:match("^about:(/.*)$")]] h:write [[ if p then]] h:write([[ elem.target = p:gsub("/", "-") .. ".]] .. m[1] .. [["]]) h:write [[ return elem]] h:write [[ end]] h:write [[ end]] h:write [[}]] pandoc ("--lua-filter=" .. filter_path) end write_doc_to(function(name, state) local ofile = state:realpath(m[2] .. "/" .. (name == "/" and "index" or name):gsub("/", "-") .. "." .. m[1]) return ("!%s -o %s"):format(pandoc, lib.shellesc(ofile)) end) lib.cmd "rm" "--" (filter_path) :ok() end end}, {pat = "^%-%-trace$", fn = function() lib.trace = true end}, {pat = "^%-%-version$", fn = function() print("neo-ed version " .. ver) print("Copyright (C) " .. os.date("%Y") .. " Sophie Hirn") print() print [[ This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero 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 Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . ]] os.exit(0, true) end}, {pat = "^%-$", fn = function() local contents = io.stdin:read("a") local fd = lib.assert(posix.fcntl.open(posix.stdio.ctermid(), posix.fcntl.O_RDONLY)) lib.assert(posix.unistd.dup2(fd, 0)) posix.unistd.close(fd) io.input(posix.stdio.fdopen(0, "r")) term:refresh() table.insert(files, {loc = "", vpath = "/tmp/stdin.ansi.txt", contents = contents, cmds = cmds}) cmds = {} end}, {pat = "^%-.*", fn = function(m) lib.error("could not parse option: " .. m[1]) end}, {pat = "^%+(.+)$", fn = function(m) table.insert(cmds, m[1]) end}, {pat = "^.*$", fn = function(m) table.insert(files, {loc = m[1], cmds = cmds}) cmds = {} end}, } for _, v in ipairs(arg) do lib.match{s = v, choose = argdefs} end if not files[1] or cmds[1] then table.insert(files, {loc = "", cmds = cmds}) end main(files) ]================================================================================] , "neo-ed.main")) package.preload["neo-ed.parser"] = assert(load( [================================================================================[ local as = require "neo-ed.lib.as" local lib = require "neo-ed.lib" local m = {} -- interface: returns -- - function that receives buffer and current address, and returns a new address (pair) -- - unparsed rest string local bundle_spec = "table?" function m.addrs(state, s) as ("state", "string") local first = true local ret = function(buf) as ("buffer") return { base = buf.body:pos(), now = buf.body:pos(), a = nil, b = nil, } end local function do_addr_single() local f, s_ = lib.match{s = s, choose = state.impl.cmd.addr.single, def = lib.const(nil), args = {first}} if not f then return false end local ret_ = ret ret = function(buf, bundle) as ("buffer", bundle_spec) bundle = ret_(buf, bundle) bundle.b = f(buf, bundle.now) bundle.now = bundle.b return bundle end first = false s = s_ return true end local function do_addr_range() local f, s_ = lib.match{s = s, choose = state.impl.cmd.addr.range , def = lib.const(nil), args = {first}} if not f then return false end local ret_ = ret ret = function(buf, bundle) as ("buffer", bundle_spec) bundle = ret_(buf, bundle) bundle.a, bundle.b = f(buf, bundle.now) bundle.now = bundle.b return bundle end first = false s = s_ return true end local function do_comma() local s_ = s:match("^,(.*)$") if not s_ then return false end local ret_ = ret ret = function(buf, bundle) as ("buffer", bundle_spec) bundle = ret_(buf, bundle) if not bundle.b then bundle.a, bundle.b = 1, #buf.body else bundle.a = bundle.b end bundle.now = bundle.base return bundle end first = true s = s_ return true end local function do_semi() local s_ = s:match("^;(.*)$") if not s_ then return false end local ret_ = ret ret = function(buf, bundle) as ("buffer", bundle_spec) bundle = ret_(buf, bundle) if not bundle.b then bundle.a, bundle.b = bundle.now, #buf.body else bundle.a = bundle.b end bundle.base = bundle.now return bundle end first = true s = s_ return true end s = s:match("^%s*(.*)$") if not (do_addr_range() or do_addr_single() or do_comma() or do_semi()) then return nil end while true do s = s:match("^%s*(.*)$") if not (do_addr_range() or do_addr_single() or do_comma() or do_semi()) then return function(buf, bundle) as ("buffer", bundle_spec) bundle = ret(buf, bundle) return bundle.a, bundle.b end, s end end end function m.target(state, s) as ("state", "string") local f, s_ = m.addrs(state, s) if f then return function(buf, bundle) as ("buffer", bundle_spec) local _, ret = f(buf, bundle) return ret or buf.body:pos() end, s_ end return function(buf) return buf.body:pos() end, s end -- interface: returns -- - function that receives buffer and addresses, and runs the command function m.suffix(state, s) as ("state", "string") return lib.match{s = s:match("^(.*)$"), choose = state.impl.cmd.suf, def = lib.const(nil), args = {state}} end -- interface: returns -- - function that receives buffer and addresses, runs the command, and returns the affected range -- - unparsed rest string function m.cmd(state, s) as ("state", "string") local full = s local function parse_error() local pre = full:sub(1, full:len() - s:len()) lib.error(("could not parse command:\n%s\n%s%s"):format(full, (" "):rep(utf8.len(pre)), ("^"):rep(utf8.len(s)))) end local cf = nil local function do_cmd_buffer() local f, s_ = lib.match{s = s, choose = state.impl.cmd.buffer, def = lib.const(nil)} if not f then return false end lib.assert(s_, "command did not provide a parsing suffix: " .. s) cf = f s = s_ return true end local function do_cmd_line() local f, s_ = lib.match{s = s, choose = state.impl.cmd.line, def = lib.const(nil)} if not f then return false end lib.assert(s_, "command did not provide a parsing suffix: " .. s) cf = f s = s_ return true end local as = s local af, s_ = m.addrs(state, s) s = s_ or s s = s:match("^%s*(.*)$") if af then if not do_cmd_line() then parse_error() end else if not do_cmd_buffer() and not do_cmd_line() then parse_error() end end local sf = m.suffix(state, s) if not sf then parse_error() end return function(buf) local a, b = nil, nil if af then if buf.cmd_prof then buf.cmd_prof:start("address") end a, b = af(buf) if buf.cmd_prof then buf.cmd_prof:stop() end end lib.assert(a == nil or type(a) == "number", "address returned invalid first parameter (" .. type(a) .. "): " .. as) lib.assert(b == nil or type(b) == "number", "address returned invalid second parameter (" .. type(b) .. "): " .. as) if not (a == 1 and b == 0) then if a ~= nil then buf.body:check_pos(a, true) end if b ~= nil then buf.body:check_pos(b, true) end if a ~= nil and b ~= nil and a > b then lib.error(("invalid range: %d,%d"):format(a, b)) end end if buf.cmd_prof then buf.cmd_prof:start("command") end a, b = cf(buf, a, b) if buf.cmd_prof then buf.cmd_prof:stop() end if a == false then return end if not a or not b then lib.error("command " .. lib.fninfo(cf) .. " did not return affected address range") end if buf.cmd_prof then buf.cmd_prof:start("suffix") end sf(buf, a, b) if buf.cmd_prof then buf.cmd_prof:stop() end end end return m ]================================================================================] , "neo-ed.parser")) package.preload["neo-ed.plugins"] = assert(load( [================================================================================[ local m = {} local as = require "neo-ed.lib.as" local lib = require "neo-ed.lib" local term = require "neo-ed.term" local plugin_base_path = lib.xdg.data_home .. "/neo-ed/plugins" m.def_config = [=[ local state = ... local plugins = require "neo-ed.plugins" --[[ enable default plugins ]] plugins.def(state) --[[ enable syntax highlighting ]] --require "neo-ed.plugins.highlight.pygments" (state) --require "neo-ed.plugins.highlight.bat" (state) -- if multiple highlighter plugins are loaded, the last one is used by default -- any loaded highlighter can be explicitly selected using the `highlighter` setting --[[ example: path specific settings ]] --table.insert(state.impl.filter.conf, 1, function(conf, vpath, buf, set) -- if vpath:find("%.[ct]sv$") then -- set(conf, "elastic_tabstops", true) -- end --end) --[[ example: add a clock widget above the prompt ]] --local term = require "neo-ed.term" --table.insert(state.widgets, function() -- return term:sgr"weak" .. "[" .. term:sgr"accent" .. os.date("%a %F %H:%M") .. term:sgr"weak" .. "]" .. term:sgr"reset" --end) --[[ example: external plugin ]] --plugins.ext(state, "https://cgit.sowophie.io/ned-plugin-helloworld", "main") --[[ miscellaneous utilities ]] --plugins.ext(state, "https://cgit.sowophie.io/ned-plugin-merge" , "main") -- inline multi-diff viewer --plugins.ext(state, "https://cgit.sowophie.io/ned-plugin-misc-utils", "main") --plugins.ext(state, "https://cgit.sowophie.io/ned-plugin-git" , "main") -- some git commands -- more plugins can be found at https://cgit.sowophie.io/ ]=] local function check_lock(state, path, name, when) as ("state", "string", "string", "string") if lib.path_type(path .. "/.git/index.lock") then state:warn(name .. ": removing stray .git/index.lock " .. when .. " at " .. lib.fninfo(2)) require("posix").unistd.unlink(path .. "/.git/index.lock") end end function m.mgr(state) as ("state") table.insert(state.impl.cmd.buffer, { name = "plugins/update", syntax = ":plugins update", descr = "pull latest version of each plugin", see = {"/cmd/buffer/plugins/*", "/plugins"}, pat = "^:plugins update$", fn = function(m) return function(buf) lib.cmd "mkdir" "-p" "--" (plugin_base_path) :err() local sched = require "neo-ed.lib.sched" () for i, name, p, n in lib.opairs(buf.state.plugins or {}) do sched:add(function() local p = buf.state.plugins[name] local path = plugin_base_path .. "/" .. name local changes = false check_lock(state, path, name, "before update check " .. i .. "/" .. n) if not lib.cmd "test" "-e" (path) :ok() then lib.cmd "git" "clone" "--quiet" "--recurse-submodules" "--branch" (p.branch) (p.url) (path) :err() check_lock(state, path, name, "after git clone") changes = "downloaded" else lib.cmd "git" "-C" (path) "fetch" "--quiet" "--recurse-submodules" "--tags" "--force" "origin" (p.branch) :err() check_lock(state, path, name, "after git fetch") local curr = lib.cmd "git" "--no-optional-locks" "-C" (path) "show" "--no-patch" "--pretty=format:%H%n" :read("l") check_lock(state, path, name, "after git show") local origin = lib.cmd "git" "--no-optional-locks" "-C" (path) "show" "--no-patch" "--pretty=format:%H%n" ("origin/" .. p.branch) :read("l") check_lock(state, path, name, "after git show") lib.cmd "git" "-C" (path) "checkout" "--quiet" (p.branch) :err() check_lock(state, path, name, "after git checkout") lib.cmd "git" "-C" (path) "reset" "--quiet" "--hard" ("origin/" .. p.branch) :err() check_lock(state, path, name, "after git reset") if curr ~= origin then changes = "updated" end end if changes then state:info(name .. ": repository " .. changes .. ", run :restart to apply changes") end end) end sched:run(-1) return false end, "" end, }) table.insert(state.impl.cmd.buffer, { name = "plugins/list", syntax = ":plugins list", descr = "show list of current plugins", see = {"/cmd/buffer/plugins/*", "/plugins"}, pat = "^:plugins list$", fn = function(m) return function(buf) print(("%s%-30s %-40s %s%s"):format(term:sgr"accent", "Name", "Version", "URL", term:sgr"reset")) for _, name, p in lib.opairs(buf.state.plugins or {}) do print(("%-30s %-40s %s"):format(name, p.ver or "-", p.url)) end return false end, "" end, }) end function m.ext(state, url, branch) as ("state", "string", "string") local p = state.plugins_prof and state.plugins_prof:start(url) local name = url:match("([^/]+)$") local path = plugin_base_path .. "/" .. name check_lock(state, path, name, "before plugin.ext") state.plugins = state.plugins or {} state.plugins[name] = {url = url, branch = branch} local describe = lib.cmd "git" "--no-optional-locks" "-C" (path) "describe" "--tags" "--always" "--dirty" :w(2, "/dev/null") local ver = describe:read("l?") check_lock(state, path, name, "after git describe") if ver then local date = lib.cmd "git" "--no-optional-locks" "-C" (plugin_base_path .. "/" .. name) "show" "--no-patch" "--format=%cd%n" "--date=short" :read("l") check_lock(state, path, name, "after git show") state.plugins[name].ver = ver .. " (" .. date .. ")" end local m, msg = lib.require(name, plugin_base_path) if m then local ok, ret = lib.pcall(m, state) if ok then return ret else state:warn(name .. ": could not load: " .. tostring(ret)) return function() end end end state:warn(name .. ": could not load: " .. tostring(msg)) state:info("`:plugins update` might help") return function() end end -- only modules that contain POSIX functionality function m.posix(state) as ("state") require "neo-ed.plugins.addr.line" (state) require "neo-ed.plugins.addr.mark" (state) require "neo-ed.plugins.addr.regex"(state) require "neo-ed.plugins.addr.show" (state) require "neo-ed.plugins.cmd.shell" (state) require "neo-ed.plugins.misc.default" (state) require "neo-ed.plugins.misc.stubs" (state) require "neo-ed.plugins.loc.file" (state) require "neo-ed.plugins.loc.shell" (state) require "neo-ed.plugins.state.io" (state) require "neo-ed.plugins.state.undo" (state) require "neo-ed.plugins.text.acid" (state) require "neo-ed.plugins.text.global" (state) require "neo-ed.plugins.text.print" (state) require "neo-ed.plugins.text.reorder" (state) require "neo-ed.plugins.text.split_join" (state) require "neo-ed.plugins.text.subst" (state) end -- POSIX plus all other commands that affect the output file, and can thus be tested function m.testing(state) as ("state") m.posix(state) require "neo-ed.plugins.addr.indent" (state) require "neo-ed.plugins.addr.seek" (state) require "neo-ed.plugins.io.charset" (state) require "neo-ed.plugins.io.crlf" (state) require "neo-ed.plugins.io.end_nl" (state) require "neo-ed.plugins.io.trim" (state) require "neo-ed.plugins.loc.about" (state) require "neo-ed.plugins.loc.buffer" (state) require "neo-ed.plugins.loc.clipboard" (state) require "neo-ed.plugins.loc.man" (state) require "neo-ed.plugins.loc.only" (state) require "neo-ed.plugins.text.aj_ij" (state) require "neo-ed.plugins.text.clipboard" (state) require "neo-ed.plugins.text.indent" (state) end -- all bundled plugins minus highlighters function m.def(state) as ("state") m.testing(state) require "neo-ed.plugins.addr.screen" (state) require "neo-ed.plugins.cmd.lua" (state) require "neo-ed.plugins.diff.gnu" (state) require "neo-ed.plugins.misc.apidoc" (state) require "neo-ed.plugins.misc.autocmd" (state) require "neo-ed.plugins.misc.help" (state) require "neo-ed.plugins.misc.term_title" (state) require "neo-ed.plugins.picker.fzf" (state) require "neo-ed.plugins.print.eol" (state) require "neo-ed.plugins.print.printable" (state) require "neo-ed.plugins.print.tabs" (state) require "neo-ed.plugins.settings.ansi" (state) require "neo-ed.plugins.settings.autodetect" (state) require "neo-ed.plugins.settings.editorconfig" (state) require "neo-ed.plugins.settings.config" (state) require "neo-ed.plugins.settings.set" (state) require "neo-ed.plugins.state.misc" (state) require "neo-ed.plugins.text.autocomp" (state) m.mgr (state) end function m.full(state) as ("state") m.def(state) require "neo-ed.plugins.highlight.pygments" (state) require "neo-ed.plugins.highlight.bat" (state) end return m ]================================================================================] , "neo-ed.plugins")) package.preload["neo-ed.plugins.addr.indent"] = assert(load( [================================================================================[ local lib = require "neo-ed.lib" local function ind(s) if s == "" then return "/" end return s:match("^\t+") or s:match("^ *") end return function(state) table.insert(state.impl.cmd.addr.single, { name = "indent/rev", syntax = "(", descr = "first line of current indentation block, or previous line with the same indentation", see = {"/cmd/addr/single/indent/*"}, pat = "^%((.*)$", fn = function(m) return function(buf, a) local pre = buf.body:scan(function(_, l) return ind(l.text) end, a, a) local last = a buf.body:scan_r(function(n, l) if l.text:find("^%s+$") then return true end if ind(l.text) ~= pre then return true end last = n end, a - 1) if last ~= a then return last end return lib.assert(buf.body:scan_r(function(n, l) if l.text:find("^%s+$") then return end return ind(l.text) == pre and n or nil end, a - 1), "pattern not found: (") end, m[1] end, }) table.insert(state.impl.cmd.addr.single, { name = "indent/fwd", syntax = ")", descr = "last line of current indentation block, or next line with the same indentation", see = {"/cmd/addr/single/indent/*"}, pat = "^%)(.*)$", fn = function(m) return function(buf, a) local pre = buf.body:scan(function(_, l) return ind(l.text) end, a, a) local last = a buf.body:scan(function(n, l) if l.text:find("^%s+$") then return true end if ind(l.text) ~= pre then return true end last = n end, a + 1) if last ~= a then return last end return lib.assert(buf.body:scan(function(n, l) if l.text:find("^%s+$") then return end return ind(l.text) == pre and n or nil end, a + 1), "pattern not found: )") end, m[1] end, }) table.insert(state.impl.cmd.addr.single, { name = "indent/up-rev", syntax = "{", descr = "previous line with less indentation", see = {"/cmd/addr/single/indent/*"}, pat = "^{(.*)$", fn = function(m) return function(buf, a) local pre = buf.body:scan(function(_, l) return ind(l.text) end, a, a) lib.assert(pre ~= "", "already at top indentation level: {") return lib.assert(buf.body:scan_r(function(n, l) if l.text:find("^%s+$") then return end local pre_ = ind(l.text) if #pre_ < #pre and pre:find("^" .. pre_) then return n end end, a - 1), "pattern not found: {") end, m[1] end, }) table.insert(state.impl.cmd.addr.single, { name = "indent/fwd", syntax = "}", descr = "next line with less indentation", see = {"/cmd/addr/single/indent/*"}, pat = "^}(.*)$", fn = function(m) return function(buf, a) local pre = buf.body:scan(function(_, l) return ind(l.text) end, a, a) lib.assert(pre ~= "", "already at top indentation level: }") return lib.assert(buf.body:scan(function(n, l) if l.text:find("^%s+$") then return end local pre_ = ind(l.text) if #pre_ < #pre and pre:find("^" .. pre_) then return n end end, a + 1), "pattern not found: }") end, m[1] end, }) end ]================================================================================] , "neo-ed.plugins.addr.indent")) package.preload["neo-ed.plugins.addr.line"] = assert(load( [================================================================================[ return function(state) table.insert(state.impl.cmd.addr.single, { name = "line/first", syntax = "^", descr = "first line of buffer", see = {"/cmd/addr/single/line/*", "/cmd/addr/range/all"}, pat = "^%^(.*)$", fn = function(m, first) if not first then return false end return function() return 1 end, m[1] end, }) table.insert(state.impl.cmd.addr.single, { name = "sel/first", syntax = "[", descr = "first line of selection", see = {"/cmd/addr/single/sel/*", "/cmd/addr/single/curr", "/cmd/addr/range/sel"}, pat = "^%[(.*)$", fn = function(m, first) if not first then return false end return function(buf) return buf.body:sel_first() end, m[1] end, }) table.insert(state.impl.cmd.addr.single, { name = "curr", syntax = ".", descr = "current line", posix = "full", see = {"/cmd/addr/single/sel/*", "/cmd/addr/range/sel"}, pat = "^%.(.*)$", fn = function(m, first) if not first then return false end return function(buf) return buf.body:pos() end, m[1] end, }) table.insert(state.impl.cmd.addr.single, { name = "sel/last", syntax = "]", descr = "last line of selection", see = {"/cmd/addr/single/sel/*", "/cmd/addr/single/curr", "/cmd/addr/range/sel"}, pat = "^%](.*)$", fn = function(m, first) if not first then return false end return function(buf) return buf.body:sel_last() end, m[1] end, }) table.insert(state.impl.cmd.addr.single, { name = "line/last", syntax = "$", descr = "last line of buffer", posix = "full", see = {"/cmd/addr/single/line/*", "/cmd/addr/range/all"}, pat = "^%$(.*)$", fn = function(m, first) if not first then return false end return function(buf) return #buf.body end, m[1] end, }) table.insert(state.impl.cmd.addr.single, { name = "line/number", syntax = "", descr = "line number", posix = "full", see = {"/cmd/addr/single/line/*", "/cmd/addr/range/all"}, pat = "^(%d+)(.*)$", fn = function(m, first) return function(buf, a) return (first and 0 or a) + tonumber(m[1]) end, m[2] end, }) table.insert(state.impl.cmd.addr.single, { name = "line/add", syntax = "+[]", descr = "add offset to current line", details = "Add `` lines, or 1 if left out.", posix = "full", see = {"/cmd/addr/single/line/*", "/cmd/addr/range/all"}, pat = "^%+(%d*)(.*)$", fn = function(m) return function(buf, a) return a + (tonumber(m[1]) or 1) end, m[2] end, }) table.insert(state.impl.cmd.addr.single, { name = "line/sub", syntax = "-[]", descr = "subtract offset from current line", details = "Subtract `` lines, or 1 if left out.", posix = "full", see = {"/cmd/addr/single/line/*", "/cmd/addr/range/all"}, pat = "^%-(%d*)(.*)$", fn = function(m) return function(buf, a) return a - (tonumber(m[1]) or 1) end, m[2] end, }) table.insert(state.impl.cmd.addr.range, { name = "sel", syntax = "@", descr = "entire selection, equal to `[,]`", pat = "^@(.*)$", see = {"/cmd/addr/single/sel/*", "/cmd/addr/single/curr"}, fn = function(m, first) if not first then return false end return function(buf) return buf.body:sel_first(), buf.body:sel_last() end, m[1] end, }) table.insert(state.impl.cmd.addr.range, { name = "all", syntax = "%", descr = "entire buffer, equal to `^,$`", posix = "full", see = {"/cmd/addr/single/line/*"}, pat = "^%%(.*)$", fn = function(m, first) if not first then return false end return function(buf) return 1, #buf.body end, m[1] end, }) end ]================================================================================] , "neo-ed.plugins.addr.line")) package.preload["neo-ed.plugins.addr.mark"] = assert(load( [================================================================================[ local lib = require "neo-ed.lib" local parser = require "neo-ed.parser" local term = require "neo-ed.term" return function(state) table.insert(state.impl.cmd.line, { name = "mark", syntax = "k", descr = "mark line with lowercase letter ``", addr = ".", details = [[ Each line can be marked with one of the characters `a-z`, and can later be addressed with that letter. Note that multiple lines can be marked with the same letter at the same time. ]], posix = "partial", see = {"/cmd/line/unmark", "/cmd/suf/mark-range", "/cmd/addr/single/mark/*"}, pat = "^k(%l)(.*)$", fn = function(m) return function(buf, _, a) a = a or buf.body:pos() buf:change(buf.body.copy_map, function(_, l) l.mark = m[1] end, a, a, true) buf:drop_cache() return a, a end, m[2] end, }) table.insert(state.impl.cmd.line, { name = "unmark", syntax = "k", descr = "unmark line", addr = ".", see = {"/cmd/line/mark", "/cmd/suf/mark-range", "/cmd/addr/single/mark/*"}, pat = "^k(.*)$", fn = function(m) return function(buf, _, a) a = a or buf.body:pos() buf:change(buf.body.copy_map, function(_, l) l.mark = nil end, a, a, true) buf:drop_cache() return a, a end, m[1] end, }) table.insert(state.impl.cmd.suf, { name = "mark-range", syntax = "k", descr = "mark first line with `` and last line with ``", see = {"/cmd/line/mark", "/cmd/line/unmark", "/cmd/addr/single/mark/*"}, pat = "^k(%l)(%l)(.*)$", fn = function(m, state) local inner = parser.suffix(state, m[3]) if not inner then return false end return function(buf, a, b) buf:change(buf.body.copy_map, function(n, l) if n == a then l.mark = m[1] end if n == b then l.mark = m[2] end end, a, b, true) buf:drop_cache() inner(buf, a, b) end end, }) local function scanner(mark) return function(n, l) return l.mark == mark and n or nil end end table.insert(state.impl.cmd.addr.single, { name = "mark/fwd", syntax = "'", descr = "next line with mark ``, wraps around at end of buffer", posix = "full", see = {"/cmd/line/mark", "/cmd/line/unmark", "/cmd/addr/single/mark/*"}, pat = "^'(%l)(.*)$", fn = function(m) return function(buf, a) local ret = lib.assert( buf.body:scan(scanner(m[1]), a + 1, a, true), "mark not found: '" .. m[1] ) if ret <= a then buf.state:info("search wrapped: '" .. m[1]) end return ret end, m[2] end, }) table.insert(state.impl.cmd.addr.single, { name = "mark/rev", syntax = "", descr = "previous line with mark ``, wraps around at start of buffer", see = {"/cmd/line/mark", "/cmd/line/unmark", "/cmd/addr/single/mark/*"}, pat = "^`(%l)(.*)$", fn = function(m) return function(buf, a) local ret = lib.assert( buf.body:scan_r(scanner(m[1]), a - 1, a, true), "mark not found: `" .. m[1] ) if ret >= a then buf.state:info("search wrapped around: `" .. m[1]) end return ret end, m[2] end, }) table.insert(state.print.post, function(lines) for _, l in ipairs(lines) do if l.mark then l.text = ("%s %s '%s %s"):format(l.text, term:sgr"note rev", l.mark, term:sgr"reset") end end end) end ]================================================================================] , "neo-ed.plugins.addr.mark")) package.preload["neo-ed.plugins.addr.regex"] = assert(load( [================================================================================[ local lib = require "neo-ed.lib" local regex = require "neo-ed.regex" local function scanner(pat, inv) return function(n, l) return not regex.curr.find(l.text, pat) == not not inv and n or nil end end return function(state) table.insert(state.impl.cmd.addr.single, { name = "regex/fwd", syntax = "//[^]", descr = "first line after current [not] matching ``", details = [[ The first line after the current one that matches the regex ``. Search wraps around at the end of the buffer. Fails if no match is found. The suffix `^` selects the first non-matching line instead. The terminating `/` can be left out for the empty command. The format of `` is described in . ]], posix = "full", see = {"/cmd/addr/single/regex/*", "/regex/curr"}, pat = "^/(.*)$", fn = function(m) local pat, _, rest = regex.curr.parse("/", m[1], true) if not pat then return false end local inv, rest_ = rest:match("^(%^)(.*)$") if inv then rest = rest_ end return function(buf, a) local ret = lib.assert( buf.body:scan(scanner(pat, inv), a + 1, a, true), "not found: /" .. pat .. "/" .. (inv or "") ) if ret <= a then buf.state:info("search wrapped: /" .. pat .. "/" .. (inv or "")) end return ret end, rest end, }) table.insert(state.impl.cmd.addr.single, { name = "regex/rev", syntax = "??[^]", descr = "last line before current matching ``", details = [[ The last line before the current one that matches the regex ``. Search wraps around at the start of the buffer. Fails if no match is found. The suffix `^` selects the last non-matching line instead. The terminating `?` can be left out for the empty command. The format of `` is described in . ]], posix = "full", see = {"/cmd/addr/single/regex/*", "/regex/curr"}, pat = "^%?(.*)$", fn = function(m) local pat, _, rest = regex.curr.parse("?", m[1], true) if not pat then return false end local inv, rest_ = rest:match("^(%^)(.*)$") if inv then rest = rest_ end return function(buf, a) local ret = lib.assert( buf.body:scan_r(scanner(pat, inv), a - 1, a, true), "not found: ?" .. pat .. "?" .. (inv or "") ) if ret >= a then buf.state:info("search wrapped: ?" .. pat .. "?" .. (inv or "")) end return ret end, rest end, }) end ]================================================================================] , "neo-ed.plugins.addr.regex")) package.preload["neo-ed.plugins.addr.screen"] = assert(load( [================================================================================[ local lib = require "neo-ed.lib" local regex = require "neo-ed.regex" return function(state) table.insert(state.impl.cmd.addr.single, { name = "screen/rev", syntax = "Z", descr = "half a screen up", pat = "^Z(.*)$", see = {"/cmd/addr/single/screen/*", "/cmd/*/print/list/screen"}, fn = function(m) return function(buf, a) local a_, b_ = buf:screen_range(a, nil, { size = 0.5, ndn = 0 }) if a_ == a and a_ > 1 then a_ = a_ - 1 end return a_ end, m[1] end, }) table.insert(state.impl.cmd.addr.single, { name = "screen/fwd", syntax = "z", descr = "half a screen down", pat = "^z(.*)$", see = {"/cmd/addr/single/screen/*", "/cmd/*/print/list/screen"}, fn = function(m) return function(buf, a) local a_, b_ = buf:screen_range(a, nil, { size = 0.5, nup = 0 }) if b_ == a and b_ < #buf.body then b_ = b_ + 1 end return b_ end, m[1] end, }) end ]================================================================================] , "neo-ed.plugins.addr.screen")) package.preload["neo-ed.plugins.addr.seek"] = assert(load( [================================================================================[ local parser = require "neo-ed.parser" return function(state) table.insert(state.impl.cmd.buffer, { name = "seek", syntax = ":seek ", descr = "safely select the given address, avoiding command injection", pat = "^:seek%s*(.*)$", fn = function(m) local dstf, suf = parser.target(state, m[1]) if not suf:find("^%s*$") then return false end return function(buf) local a = dstf(buf) buf:change(buf.body.copy_select, a, a) return false end, "" end, }) end ]================================================================================] , "neo-ed.plugins.addr.seek")) package.preload["neo-ed.plugins.addr.show"] = assert(load( [================================================================================[ return function(state) table.insert(state.impl.cmd.line, { name = "addr", syntax = "=", addr = ".", descr = "print line number of addressed line", posix = {spec = "2017"}, pat = "^=(.*)$", fn = function(m) return function(buf, _, a) a = a or buf.body:pos() print(a) return a, a end, m[1] end, }) end ]================================================================================] , "neo-ed.plugins.addr.show")) package.preload["neo-ed.plugins.cmd.lua"] = assert(load( [================================================================================[ local lib = require "neo-ed.lib" local term = require "neo-ed.term" return function(state) table.insert(state.impl.cmd.buffer, { name = "lua", syntax = ":lua ", descr = "execute lua command", details = [[ The command runs inside the ordinary Lua environment. The variable `state` is set to the current state object and can be used to query or manipulate the editor state. ]], see = {"/api/state", "/cmd/line/map"}, pat = "^:lua +(.+)$", fn = function(m) return function(buf) local env = setmetatable({state = buf.state, term = term}, {__index = _ENV}) local f = lib.assert(load("print(" .. m[1] .. ")", ":lua command", "t", env)) f() return false end, "" end, }) table.insert(state.impl.cmd.line, { name = "map", syntax = ":map ", descr = "modify lines using Lua function", details = [[ If `` does not start with the word "function", it is wrapped as ```lua function(_, n) return end ``` , otherwise it is taken as-is. The function is called once for each addressed line. It receives the line's contents and the line number as arguments, and runs inside the same environment as . If the function returns a value, it is used as the new text of the corresponding line, otherwise the old text is kept. ]], addr = ".", see = {"/api/state", "/cmd/buffer/lua"}, pat = "^:map +(.+)$", fn = function(m) return function(buf, a, b) a = a or b or buf.body:pos() b = b or buf.body:pos() buf:change(function(body) local env = setmetatable({state = buf.state, term = term}, {__index = _ENV}) local t = m[1] if not t:find("^%s*function%(") then t = "function(_, n) return " .. t .. " end" end local f = lib.assert(load("return " .. t, ":map command", "t", env))() return body :copy_map(function(n, l) l.text = f(l.text, n) or l.text end, a, b) :copy_select(a, b) end) return a, b end, "D" end, }) end ]================================================================================] , "neo-ed.plugins.cmd.lua")) package.preload["neo-ed.plugins.cmd.shell"] = assert(load( [================================================================================[ local lib = require "neo-ed.lib" return function(state) table.insert(state.impl.cmd.buffer, { name = "shell/open", syntax = "!", descr = "open shell", see = { "/cmd/line/shell", "/cmd/line/read/shell", "/cmd/line/write/shell", }, pat = "^!$", fn = function(m) return function(buf) lib.cmd.shell():err() return false end, "" end, }) table.insert(state.impl.cmd.line, { name = "shell", syntax = "!", descr = "execute shell command", addr = "", details = [[ The command string is processed as described below, then passed with `-c` to the user's shell. The completion of the child process is awaited. If the child process exits with a non-zero exit code or is terminated by a signal, the command fails. If an address range is given, the addressed lines are fed to the standard input of the command, its standard output is captured. If the command completes successfully, the addressed lines are replaced with the captured output. Otherwise, the command fails. The following sequences have a special meaning inside a shell command string: - `%` is replaced with the buffer's path. If the current buffer does not have a path, the command fails. - `'%'` behaves like `%`, but the path is quoted for shell use. - `\%` is replaced by a literal `%`. - `\\` is replaced by a literal `\`. If a `%` or `'%'` is replaced, the final command string is also printed as an info message. ]], posix = "partial", see = { "/cmd/buffer/shell/open", "/cmd/line/read/shell", "/cmd/line/write/shell", }, pat = "^!(.+)$", fn = function(m) return function(buf, a, b) if not b then lib.cmd.shell(buf.state:loc("shell:" .. m[1]):render(buf:get_path())):err() return false end a = a or b or buf.body:pos() local cmd = buf.state:loc("shell:" .. m[1]):render(buf:get_path()) local before = lib.lines_join(buf.body:get(a, b)) local after = lib.cmd.shell(cmd):load(before):read() if before ~= after then buf:change(function(body) body = body:copy_replace(lib.lines_split(after), a, b) return body:copy_select(a, body:pos()) end) return a, buf.body:pos() else buf:change(buf.body.copy_select, a, b) return false end end, "D" end, }) table.insert(state.impl.autocomp.cmd, {pat = "^!.-(%S+)$", fn = lib.autocomp_path}) end ]================================================================================] , "neo-ed.plugins.cmd.shell")) package.preload["neo-ed.plugins.diff.gnu"] = assert(load( [================================================================================[ local lib = require "neo-ed.lib" return function(state) local v = lib.cmd "diff" "-v" :w(2, "/dev/null") :read("l?") if not v or not v:find("GNU") then return end table.insert(state.impl.diff, function(a, b, la, lb) return lib.cmd "diff" "-u" "--color=always" "-L" (la or a) "-L" (lb or b) "--" (a) (b) end) end ]================================================================================] , "neo-ed.plugins.diff.gnu")) package.preload["neo-ed.plugins.highlight.bat"] = assert(load( [================================================================================[ local lib = require "neo-ed.lib" local term = require "neo-ed.term" return function(state) state:add_conf("bat_mode", {type = "string", def = "", descr = "bat highlighting mode", drop_cache = true}) local theme = os.getenv("BAT_THEME") if not theme or theme == "" then theme = "base16" end state:add_conf("bat_theme" , {type = "string", def = theme, descr = "bat highlighting theme" , drop_cache = true}) state:add_conf("bat_theme_dark" , {type = "string", def = "" , descr = "bat highlighting theme (dark mode)" , drop_cache = true}) state:add_conf("bat_theme_light", {type = "string", def = "" , descr = "bat highlighting theme (light mode)", drop_cache = true}) if not state:check_executable("bat", "disabling syntax highlighter") then return end table.insert(state.print.highlight, {name = "bat", fn = function(lines, curr) if not term.info.sgr.reset then return end local theme = curr:conf_get("bat_theme" ) local dark_theme = curr:conf_get("bat_theme_dark" ) local light_theme = curr:conf_get("bat_theme_light") local bat = lib.cmd "bat" "--tabs" "0" "--wrap" "never" "--color" "always" "--decorations" "never" "--binary" "as-text" "--file-name" (curr:get_vpath()) local mode = curr:conf_get("bat_mode") if mode ~= "" then bat "--language" (mode) end if theme ~= "" then bat "--theme" (theme ) end if dark_theme ~= "" then bat "--theme-dark" (dark_theme ) end if light_theme ~= "" then bat "--theme-light" (light_theme) end bat:load(lines) local i = 1 for l in bat:read("L") do if not lines[i] then break end lines[i].text = l:gsub("\x1b%[37m", "\x1b[0m") i = i + 1 end end}) end ]================================================================================] , "neo-ed.plugins.highlight.bat")) package.preload["neo-ed.plugins.highlight.pygments"] = assert(load( [================================================================================[ local lib = require "neo-ed.lib" local term = require "neo-ed.term" return function(state) state:add_conf("pygments_mode", {type = "string", def = "", descr = "pygments highlighting mode", drop_cache = true}) if not state:check_executable("pygmentize", "disabling syntax highlighter") then return end local function guess(buf) local path = buf:get_vpath() local mode = lib.cmd "pygmentize" "-N" (path) :read("l") if mode ~= "text" then buf:conf_set("pygments_mode", mode, "pygments guess (file name)") return end local mode = lib.cmd "pygmentize" "-C" :load(lib.lines_join(buf.body:get())) :read("l") buf:conf_set("pygments_mode", mode, "pygments guess (file contents)") end table.insert(state.print.highlight, {name = "pygments", fn = function(lines, curr) if not term.info.sgr.reset then return end if curr:conf_get("pygments_mode") == "" then guess(curr) end local a = 1 local b = #lines while lines[a] and lines[a].text == "" do a = a + 1 end while lines[b] and lines[b].text == "" do b = b - 1 end if a <= b then local pyg = lib.cmd "pygmentize" "-f" "terminal" "-l" (curr:conf_get("pygments_mode")) "-O" "stripnl=False,encoding=utf-8" :load(lib.lines_join(lines, a, b)) local i = a for l in pyg:read("L") do if i > b then break end lines[i].text = l i = i + 1 end end end}) table.insert(state.impl.cmd.buffer, { name = "pygments/guess", syntax = ":pygments guess", descr = "guess buffer mode", see = {"/config/pygments_mode"}, pat = "^:pygments guess$", fn = function() return function(buf) guess(buf) return false end, "" end, }) end ]================================================================================] , "neo-ed.plugins.highlight.pygments")) package.preload["neo-ed.plugins.io.charset"] = assert(load( [================================================================================[ local lib = require "neo-ed.lib" return function(state) local encodings = { ["latin1" ] = "ISO8859_1", ["utf-8" ] = "UTF-8", ["utf-16be"] = "UTF-16BE", ["utf-16le"] = "UTF-16LE", } local function to_iconv(c) return lib.assert(encodings[c], "unknown charset: " .. c) end state:add_conf("charset", { type = "string", def = "utf-8", descr = "encoding for reading and writing", on_set = function(_, v) to_iconv(v); return v end, }) table.insert(state.impl.filter.read, function(s, buf) local enc = buf:conf_get("charset") if enc == "utf-8" then return s end return lib.cmd "iconv" "-t" "UTF-8" "-f" (to_iconv(enc)) :load(s) :read() end) table.insert(state.impl.filter.write, function(s, buf) local enc = buf:conf_get("charset") if enc == "utf-8" then return s end return lib.cmd "iconv" "-f" "UTF-8" "-t" (to_iconv(enc)) :load(s) :read() end) end ]================================================================================] , "neo-ed.plugins.io.charset")) package.preload["neo-ed.plugins.io.crlf"] = assert(load( [================================================================================[ return function(state) state:add_conf("crlf", {type = "boolean", def = false, descr = "CRLF line break mode"}) table.insert(state.impl.filter.read, function(s, buf) if s:find("\r") then buf:conf_set("crlf", true, "autodetected") end return s:gsub("\r", "") end) table.insert(state.impl.filter.write, function(s, buf) if not buf:conf_get("crlf") then return s end return s:gsub("\n", "\r\n") end) end ]================================================================================] , "neo-ed.plugins.io.crlf")) package.preload["neo-ed.plugins.io.end_nl"] = assert(load( [================================================================================[ return function(state) state:add_conf("end_nl", {type = "boolean", def = true, descr = "add terminating newline after last line"}) table.insert(state.impl.filter.read, function(s, buf) if buf:conf_get("end_nl") then return s end return s .. "\n" end) table.insert(state.impl.filter.write, function(s, buf) if buf:conf_get("end_nl") then return s end return s:gsub("\n$", "") end) end ]================================================================================] , "neo-ed.plugins.io.end_nl")) package.preload["neo-ed.plugins.io.trim"] = assert(load( [================================================================================[ return function(state) state:add_conf("trim", {type = "boolean", def = false, descr = "trim trailing whitespace before saving"}) table.insert(state.hooks.buffer.save_pre, function(buf) if buf:conf_get("trim") then buf:change(buf.body.copy_map, function(_, l) l.text = l.text:match("^(.-)%s*$") end) end end) end ]================================================================================] , "neo-ed.plugins.io.trim")) package.preload["neo-ed.plugins.loc.about"] = assert(load( [================================================================================[ local lib = require "neo-ed.lib" local term = require "neo-ed.term" local posix = require "posix" local mt = {__index = {}} function mt.__tostring(self) return ("about:%s"):format(self._topic) end function mt.__index:read(buf) local function ref(s) return ": " .. (type(buf.state.help[s]) == "table" and buf.state.help[s].descr or "") end local ret = {} table.insert(ret, ("# `%s`: %s\n"):format(self._topic, self._data.descr or "")) local text = self._data.text if type(text) == "function" then text = text(buf, ref) end table.insert(ret, lib.heredoc(text)) local subtopics = lib.subset_key_prefix(buf.state.help, self._topic, true) if next(subtopics) then table.insert(ret, "\n## Available Subtopics\n") for _, name, subtopic in lib.opairs(subtopics) do table.insert(ret, "- " .. ref(name)) end end if self._data.see then local globs = {} for _, s in ipairs(self._data.see) do globs[s] = false end local see = {} for t in pairs(buf.state.help) do if t ~= self._topic and not subtopics[t] then for g in pairs(globs) do if posix.fnmatch.fnmatch(g, t, 0) == 0 then see [t] = true globs[g] = true end end end end for _, g, ok in lib.opairs(globs) do if not ok and g ~= self._topic and not subtopics[g] then see[g] = true end end if next(see) then table.insert(ret, "\n## See Also\n") for _, name in lib.opairs(see) do table.insert(ret, "- " .. ref(name)) end end end ret = table.concat(ret, "\n") if lib.have_executable "pandoc" then ret = lib.cmd "pandoc" "-f" "markdown-smart" "-t" "commonmark-raw_html" "--preserve-tabs" "--reference-links=true" "--wrap=auto" (("--columns=%d"):format(math.min(120, term.cols - 5, buf:print_width()))) :load(ret) :read("a") :gsub("\x1b%[47m", "\x1b[40m") end return ret end local autocomp_key = {} return function(state) local function ac(comps, pre, buf) local cache = lib.cache[buf.state.world_key][autocomp_key] if not cache.dict then cache.dict = lib.autocomp_dict(buf.state.help, function(k) return k, buf.state.help[k].descr or "???" end) end cache.dict(comps, pre) return comps end state.impl.loc.about = { fn = function(s) return setmetatable({ _topic = s, _data = lib.assert(state.help[s], s .. ": nothing appropriate"), }, mt) end, autocomp = ac, } table.insert(state.impl.cmd.buffer, { name = "help", syntax = ":help []", descr = "show help topic", pat = "^:help *(.*)$", fn = function(m) local topic = m[1] if topic == "" then topic = "/" end return function(buf) local b = buf.state:open("about:" .. topic):focus() b:set_vpath("/tmp/about/about" .. topic:gsub("/", ":"):gsub("^:$", "") .. ".md") return false end, "" end, }) table.insert(state.impl.autocomp.cmd, {pat = "^:help +(.*)$", fn = ac}) end -- TODO: sanitize data fields on registration somehow -- TODO: check for dead links ]================================================================================] , "neo-ed.plugins.loc.about")) package.preload["neo-ed.plugins.loc.buffer"] = assert(load( [================================================================================[ local lib = require "neo-ed.lib" local mt = {__index = {}} function mt.__tostring(self) return ("#%d"):format(self._buffer.id) end function mt.__index:read() return lib.lines_join(self._buffer.body:get()) end function mt.__index:write(s) self._buffer:load_str(s) return self end function mt.__index:append(s) self._buffer:change(self._buffer.body.copy_put, lib.lines_split(s)) return self end local buffer_key = {} return function(state) local function new(s, state) return setmetatable({_buffer = state:get_buffer(s)}, mt) end state.impl.loc.buffer = {fn = new, autocomp = state:autocomp_buffer()} table.insert(state.impl.loc, 1, { pat = "^#(.*)$", fn = function(m, state) return new(m[1], state) end, autocomp = state:autocomp_buffer(), }) end ]================================================================================] , "neo-ed.plugins.loc.buffer")) package.preload["neo-ed.plugins.loc.clipboard"] = assert(load( [================================================================================[ local lib = require "neo-ed.lib" local mt = {__index = {}} function mt.__tostring(self) return "clip:" end function mt.__index:read() return self:_paste() end function mt.__index:write(s) self:_copy(s) return self end function mt.__index:append(s) self:_copy(self:_paste() .. s) return self end function mt.__index:_copy(s) self.state.clipboard = s end function mt.__index:_paste() return self.state.clipboard or "" end return function(state) local copy, paste = nil, nil if os.getenv("WAYLAND_DISPLAY") ~= "" and lib.have_executable("wl-copy") and lib.have_executable("wl-paste") then copy = function(_, s) lib.cmd "wl-copy" "--trim-newline" :w(2, "/dev/null") :load(s) :err() end paste = function( ) return lib.cmd "wl-paste" :w(2, "/dev/null") :read("a?") or "" end end if not copy and os.getenv("DISPLAY") ~= "" and lib.have_executable("xclip") then copy = function(_, s) lib.cmd "xclip" "-i" "-selection" "clipboard" "-rmlastnl" :w(2, "/dev/null") :load(s) :err() end paste = function( ) return lib.cmd "xclip" "-o" "-selection" "clipboard" :w(2, "/dev/null") :read("a?") .. "\n" or "" end end if not copy and lib.xdg.runtime_dir then state:info("using $XDG_RUNTIME_DIR/clipboard") copy = function(_, s) local h = io.open(lib.xdg.runtime_dir .. "/clipboard", "w") h:write(s) end paste = function() local h = io.open(lib.xdg.runtime_dir .. "/clipboard", "r") return h and h:read("a") or "" end end if not copy then state:info("using internal in-memory clipboard") end state.impl.loc.clip = function(_, state) return setmetatable({ _copy = copy, _paste = paste, state = state, }, mt) end end ]================================================================================] , "neo-ed.plugins.loc.clipboard")) package.preload["neo-ed.plugins.loc.file"] = assert(load( [================================================================================[ local lib = require "neo-ed.lib" local term = require "neo-ed.term" local mt = {__index = {}} function mt.__tostring(self) return self._path end function mt.__index:get_path() return self._path end function mt.__index:label(rich) if not rich then return (self._path:gsub("^" .. lib.patesc(os.getenv("HOME"):gsub("/$", "")), "~")) end local abs = self._path:find("^/") local full, have, have_not = {abs and "" or "."}, {abs and "" or nil}, {} for d in self._path:gmatch("[^/]+") do table.insert(full, d) table.insert(lib.path_type(table.concat(full, "/")) and have or have_not, d) end return ("%s%s%s%s%s%s%s"):format( term:sgr"good", have[1] and "" or ".", table.concat(have, "/"):gsub("^" .. lib.patesc(os.getenv("HOME"):gsub("/$", "")), "~"), have_not[1] and "/" or "", term:sgr"bad", table.concat(have_not, "/"), term:sgr"reset" ) end function mt.__index:read() local h = io.open(self._path, "r") if not h then return "" end return h:read("a") end local rooters = { {name = "doas", cmd = function(path, a) return lib.cmd "doas" "tee" (a) (path) .. " >/dev/null" end}, {name = "sudo", cmd = function(path, a) return lib.cmd "sudo" "tee" (a) (path) .. " >/dev/null" end}, {name = "su" , cmd = function(path, a) return lib.cmd "su" "-c" (lib.cmd "tee" (a) (path) .. " >/dev/null") end}, } local function open(state, path, append) if not lib.path_type(lib.dirname(path)) and state:pick_yes_no("Create missing directories?") then lib.mkdir(lib.dirname(path)) end local h, err = io.open(path, append and "a" or "w") for _, r in ipairs(rooters) do if h then break end if lib.have_executable(r.name) and state:pick_yes_no("Use " .. r.name .. " for privileged write?") then local cmd = tostring(r.cmd(path, append and "-a" or nil)) state:info(cmd) h, err = io.popen(cmd, "w") end end return lib.assert(h, err) end function mt.__index:write(s) local h = open(self.state, self._path, false) h:write(s) lib.assert(h:close(), "command failed") return self end function mt.__index:append(s) local h = open(self.state, self._path, true) h:write(s) lib.assert(h:close(), "command failed") return self end -- TODO: some kind of user documentation return function(state) local function new(p, state) p = state:realpath(p) lib.assert(lib.path_type(p) ~= "dir", "is a directory: " .. p) return setmetatable({_path = p, state = state}, mt) end state.impl.loc.file = {fn = new, autocomp = lib.autocomp_path} table.insert(state.impl.loc, {pat = ".*", fn = function(m, state) return new(m[1], state) end, autocomp = lib.autocomp_path}) end ]================================================================================] , "neo-ed.plugins.loc.file")) package.preload["neo-ed.plugins.loc.man"] = assert(load( [================================================================================[ local lib = require "neo-ed.lib" local term = require "neo-ed.term" local mt = {__index = {}} function mt.__tostring(self) return ("man:%s(%s)"):format(self._title, self._section) end function mt.__index:read(buf) return lib.cmd "man" ("-Tutf8") (self._section) (self._title) :setenv("MANROFFOPT", "") :setenv("MANWIDTH", ("%d"):format(math.min(120, term.cols - 5, buf:print_width()))) :w(2, "/dev/null") :read("a") end local mandb_key = {} return function(state) local function ac(comps, pre, buf) local pk = pre:match("^..") if not pk then return end local cache = lib.cache[buf.state.world_key][mandb_key] if not cache[pk] then local dict = {} for l in lib.cmd "man" "-f" "--wildcard" (pk .. "*") :w(2, "/dev/null") :read("L!") do local title, section, desc = l:match("^(%S+) %(([^()]+)%) +%- (.*)$") if title then dict[("%s(%s)"):format(title, section)] = desc end end cache[pk] = lib.autocomp_dict(dict, function(k) return k, dict[k] end) end cache[pk](comps, pre) end state.impl.loc.man = { fn = function(s) local ret = setmetatable({}, mt) ret._title, ret._section = s:match("^(.+)%(([^()]+)%)$") if not ret._title then local section = lib.cmd "man" "-w" (s) :w(2, "/dev/null") :read("l") :match("/man([^/]+)/[^/]+$") if section then ret._title, ret._section = s, section end end lib.assert(ret._title, s .. ": nothing appropriate") return ret end, autocomp = ac, } table.insert(state.impl.cmd.buffer, { name = "man", syntax = ":man [(
)]", descr = "open man page", pat = "^:man *(.*)$", fn = function(m) local topic = m[1] return function(buf) local b = buf.state:open("man:" .. topic):focus() local t, s = b:get_loc()._title, b:get_loc()._section b:set_vpath(("/tmp/man/man%s/%s.%s.ansi.txt"):format(s, t, s)) return false end, "" end, }) table.insert(state.impl.autocomp.cmd, {pat = "^:man +(.*)$", fn = ac}) end ]================================================================================] , "neo-ed.plugins.loc.man")) package.preload["neo-ed.plugins.loc.only"] = assert(load( [================================================================================[ local lib = require "neo-ed.lib" local term = require "neo-ed.term" local ro_mt = {__index = {}} local wo_mt = {__index = {}} function ro_mt.__tostring(self) return "ro:" .. tostring(self._inner) end function wo_mt.__tostring(self) return "wo:" .. tostring(self._inner) end function ro_mt.__index:get_path() local r = self._inner:get_path(); return r and "ro:" .. r or nil end function wo_mt.__index:get_path() local r = self._inner:get_path(); return r and "wo:" .. r or nil end local function lbl(s) return function(self, rich) return ("%s%s%s:%s"):format( rich and term:sgr"bold bad rev" or "", s, rich and term:sgr"reset" or "", self._inner:label(rich) ) end end ro_mt.__index.label = lbl("ro") wo_mt.__index.label = lbl("wo") function ro_mt.__index:read (...) return self._inner:read (...) end function wo_mt.__index:write (...) return self._inner:write(...) end function wo_mt.__index:append(...) return self.inner:append(...) end function wo_mt.__index:read () lib.error("write-only location") end function ro_mt.__index:write () lib.error( "read-only location") end function ro_mt.__index:append() lib.error( "read-only location") end return function(state) state.impl.loc.ro = {} state.impl.loc.wo = {} state.impl.loc.ro.fn = function(s, state) return setmetatable({_inner = state:loc(s)}, ro_mt) end state.impl.loc.wo.fn = function(s, state) return setmetatable({_inner = state:loc(s)}, wo_mt) end state.impl.loc.ro.autocomp = state:autocomp_loc() state.impl.loc.wo.autocomp = state:autocomp_loc() end ]================================================================================] , "neo-ed.plugins.loc.only")) package.preload["neo-ed.plugins.loc.shell"] = assert(load( [================================================================================[ local lib = require "neo-ed.lib" local mt = {__index = {}} function mt.__tostring(self) return "!" .. self._cmd end function mt.__index:render(p) local subst = false local ret = self._cmd :gsub("(\\*)('?)%%%2", function(s, d) if #s % 2 == 1 then return ("\\"):rep(#s - 1) .. d .. "%" .. d end local ret = lib.assert(p, "no path for '%'") subst = true if #d > 0 then ret = lib.shellesc(ret) end return s .. ret end) :gsub("\\\\", "\\") return ret, subst and ("=> " .. ret) or nil end function mt.__index:read(buf, quiet) local cmd, info = self:render(buf and buf:get_path()) if info and not quiet then buf.state:info(info) end return lib.cmd.shell(cmd):read("a") end function mt.__index:write(s, buf) local cmd, info = self:render(buf and buf:get_path()) if info and not quiet then buf.state:info(info) end lib.cmd.shell(cmd):load(s):err() return self end function mt.__index:append(s, buf) return self:write(s, buf) end return function(state) local ac = function(comps, pre) local s = pre:match("%S+$") if s then lib.autocomp_path(comps, s) end end state.impl.loc.shell = { fn = function(cmd) return setmetatable({_cmd = cmd}, mt) end, autocomp = ac, } table.insert(state.impl.loc, 1, { pat = "^!(.*)$", fn = function(m) return setmetatable({_cmd = m[1]}, mt) end, autocomp = ac, }) end ]================================================================================] , "neo-ed.plugins.loc.shell")) package.preload["neo-ed.plugins.misc.apidoc"] = assert(load( [================================================================================[ local apidoc = require "neo-ed.apidoc" local lib = require "neo-ed.lib" return function(state) require "neo-ed.apidoc" require "neo-ed.lib" for k, v in pairs(apidoc) do local text = {("Type: `%s`"):format(v.type or "???")} if v.def then table.insert(text, ("Default value: %s"):format(v.def)) end if v.details then table.insert(text, lib.heredoc(v.details)) end local descr = (v.name and ("`%s`: "):format(v.name) or "") .. v.descr if not v.details then descr = descr .. " (stub page)" end state.help[k] = {descr = descr, text = table.concat(text, "\n\n"), see = v.see} end end ]================================================================================] , "neo-ed.plugins.misc.apidoc")) package.preload["neo-ed.plugins.misc.autocmd"] = assert(load( [================================================================================[ local lib = require "neo-ed.lib" local posix = require "posix" local function go(buf, hook, cmd, origin) if not cmd or cmd == "" then return end local home = os.getenv("HOME"):gsub("/*$", "") local path = buf:get_path() path = path and buf.state:realpath(path, true):gsub("^" .. lib.patesc(home), "~") local loc = buf:get_loc() loc = loc and tostring(loc) local dir = buf.state:realpath(".", true):gsub("^" .. lib.patesc(home), "~") local env = setmetatable({ cmd = cmd, dir = dir, hook = hook, origin = origin, path = path, loc = loc, }, {__index = _ENV}) local acl_path = buf.state.config_dir .. "/autocmd_acl.lua" local acl_stat = posix.sys.stat.stat(acl_path) local ok = nil if acl_stat then ok = lib.assert(loadfile(acl_path, "t", env))() end if ok == true then buf.state:info("autocmd_" .. hook .. ": " .. cmd) buf:cmd(cmd) return end if ok == false then buf.state:warn("denied execution of autocmd_" .. hook .. "=" .. cmd) return end if not posix.unistd.isatty(0) then buf.state:warn("cannot ask for permission to run autocmd_" .. hook ": " .. cmd) return end print("Tried to run autocmd_" .. hook .. "=" .. cmd) if buf.state:pick_yes_no("Add to allowlist?") then local b = buf.state:open(acl_path) b:change(function(body) local tests = {} table.insert(tests, ("cmd == %q" ):format(cmd )) table.insert(tests, ("dir == %q" ):format(dir )) table.insert(tests, ("hook == %q" ):format(hook )) table.insert(tests, ("origin == %q"):format(origin)) if path then table.insert(tests, ("path:find(%q)"):format("^" .. lib.patesc(dir))) end if loc then table.insert(tests, ("loc == %q"):format(loc)) end body = body:copy_put(lib.lines_split(("if %s\nthen return true end"):format(table.concat(tests, "\nand ")))) return body:copy_seek(#body) end) b:focus() end end return function(state) local function add_autocmd(hook) state:add_conf("autocmd_" .. hook, {type = "string", def = "", descr = "command to run in the " .. hook .. " hook"}) table.insert(state.hooks.buffer[hook], function(buf) go(buf, hook, buf:conf_get("autocmd_" .. hook)) end) end add_autocmd "close" add_autocmd "load_pre" add_autocmd "load_post" add_autocmd "print_pre" add_autocmd "print_post" add_autocmd "prompt_pre" add_autocmd "prompt_post" add_autocmd "save_pre" add_autocmd "save_post" end ]================================================================================] , "neo-ed.plugins.misc.autocmd")) package.preload["neo-ed.plugins.misc.default"] = assert(load( [================================================================================[ return function(state) state:add_conf("default_command" , {type = "string", def = "L", descr = "Default command to run when an empty command is given"}) state:add_conf("default_command_addr", {type = "string", def = "@", descr = "Default address for the default command" }) table.insert(state.impl.cmd.line, { name = "empty", syntax = "", addr = "", descr = "run the command specified in the default_command setting", see = {"/config/default_command*"}, pat = "^$", fn = function(m) return function(buf, a, b) local addr = buf:conf_get("default_command_addr") if b then addr = (a and a .. "," or "") .. b end buf:cmd(addr .. buf:conf_get("default_command")) return false end, "" end, }) end ]================================================================================] , "neo-ed.plugins.misc.default")) package.preload["neo-ed.plugins.misc.help"] = assert(load( [================================================================================[ local lib = require "neo-ed.lib" local regex = require "neo-ed.regex" return function(state) table.insert(state.impl.cmd.buffer, { name = "help", syntax = ":help []", descr = "show help topic", pat = "^:help *(.*)$", fn = function(m) local topic = m[1] if topic == "" then topic = "/" end return function(buf) local b = buf.state:open("about:" .. topic):focus() b:set_vpath("/tmp/about" .. topic:gsub("[^/]$", "%0/") .. "about.md") return false end, "" end, }) table.insert(state.impl.autocomp.cmd, { pat = "^:help +(.*)$", fn = function(comps, pre, buf) if buf.state.impl.loc.about and buf.state.impl.loc.about.autocomp then buf.state.impl.loc.about.autocomp(comps, pre, buf) end return comps end, }) state.help["/"] = {descr = "Help Topic Overview", text = [[ **TODO**: introduction for new users goes here ## Documentation Conventions Within regular text: - `` refers to another help page, accessible via `:help /path/to/topic` Within syntax examples: - `` denotes a placeholder - `[]` denotes an optional part `` ]]} state.help["/regex/bre" ] = {descr = "POSIX BRE Overview" , text = regex._bre_doc, see = {"/regex/*"}} state.help["/regex/curr"] = {descr = "Current Regex Overview", text = regex.curr.doc, see = {"/regex/*"}} state.help["/regex/lua" ] = {descr = "Lua Patterns Overview" , text = regex.lua.doc , see = {"/regex/*"}} state.help["/hooks"] = {descr = "Hooks and Filters", text = function(buf) local ret = {"## Active Filters"} local function go(tbl, name) table.insert(ret, "\n- `" .. (name or tbl.name) .. "`:") for i, v in ipairs(tbl) do table.insert(ret, " " .. i .. ". `" .. lib.fninfo(v) .. "`") end end for _, _, v in lib.opairs(state.impl.filter) do go(v) end table.insert(ret, "\n### Print Pipeline") go(state.print.pre, "state.print.pre") table.insert(ret, "\n- `state.print.highlight` (last is used by default):") for _, v in ipairs(state.print.highlight) do table.insert(ret, " - `" .. v.name .. "` (`" .. lib.fninfo(v.fn) .. "`)") end go(state.print.post, "state.print.post") table.insert(ret, "\n## Active Hooks") for _, _, v in lib.opairs(state.hooks.buffer) do go(v) end for _, _, v in lib.opairs(state.hooks.state ) do go(v) end return table.concat(ret, "\n") end} state.help["/posix"] = {descr = "POSIX.1 Compliance Overview", text = function(buf, ref) local full = {} local part = {} local function do_cmds(prefix, t) for _, c in ipairs(t) do if c.name and c.posix then (c.posix.notes and part or full)[prefix .. c.name] = c.posix.spec or "2017" end end end do_cmds("/cmd/addr/range/" , state.impl.cmd.addr.range ) do_cmds("/cmd/addr/single/", state.impl.cmd.addr.single) do_cmds("/cmd/buffer/" , state.impl.cmd.buffer ) do_cmds("/cmd/line/" , state.impl.cmd.line ) do_cmds("/cmd/suf/" , state.impl.cmd.suf ) local ret = {} table.insert(ret, "## Fully Compliant\n") for _, k, v in lib.opairs(full) do table.insert(ret, ("- %s (POSIX.1-%s)"):format(ref(k), v)) end table.insert(ret, "\n## Partially Compliant\n") for _, k, v in lib.opairs(part) do table.insert(ret, ("- %s (POSIX.1-%s)"):format(ref(k), v)) end return table.concat(ret, "\n") end} table.insert(state.hooks.state.init_post, function(state) local function do_cmds(prefix, t) for _, c in ipairs(t) do if not c.name then state:warn(lib.fninfo(c.fn) .. ": missing command name") elseif not c.syntax then state:warn(lib.fninfo(c.fn) .. ": missing syntax") elseif not c.descr then state:warn(lib.fninfo(c.fn) .. ": missing description") else if prefix == "/cmd/line/" and not c.addr then state:warn(lib.fninfo(c.fn) .. ": missing default address") end local syntax = c.syntax if syntax:find("`") then syntax = "``" .. syntax .. "``" else syntax = "`" .. syntax .. "`" end local text = {syntax .. "\n",} if c.addr then table.insert(text, "Default address: `" .. c.addr .. "`\n") end if c.details then table.insert(text, lib.heredoc(c.details) .. "\n") end if c.posix then table.insert(text, "## POSIX.1-" .. (c.posix.spec or "2017") .. " Compliance\n") table.insert(text, c.posix.notes and lib.heredoc(c.posix.notes) or "Fully compliant.") end table.insert(text, "## Implementation Details\n") table.insert(text, "Lua pattern: `" .. c.pat .. "`\n") table.insert(text, "Defined at `" .. lib.fninfo(c.fn) .. "`\n") local descr = syntax .. ": " .. c.descr if not c.details then descr = descr .. " (stub page)" end state.help[prefix .. c.name] = {descr = descr, text = table.concat(text, "\n"), see = c.see} end end end do_cmds("/cmd/addr/range/" , state.impl.cmd.addr.range ) do_cmds("/cmd/addr/single/", state.impl.cmd.addr.single) do_cmds("/cmd/buffer/" , state.impl.cmd.buffer ) do_cmds("/cmd/line/" , state.impl.cmd.line ) do_cmds("/cmd/suf/" , state.impl.cmd.suf ) for _, name, def in lib.opairs(state.conf_defs) do local descr = ("`%s`: %s"):format(name, def.descr or "") local def_val = type(def.def) == "boolean" and (def.def and "y" or "n") or tostring(def.def) local text = {def.type .. " `" .. name .. "` = `" .. def_val .. "`"} if def.details then table.insert(text, "") table.insert(text, lib.heredoc(def.details)) else descr = descr .. " (stub page)" end state.help["/config/" .. name] = {descr = descr, text = table.concat(text, "\n"), see = def.see} end end) end ]================================================================================] , "neo-ed.plugins.misc.help")) package.preload["neo-ed.plugins.misc.stubs"] = assert(load( [================================================================================[ return function(state) table.insert(state.impl.cmd.buffer, { name = "stub/help", syntax = "h", descr = "do nothing (POSIX compatibility stub)", posix = {spec = "2017", notes = "Does nothing."}, see = {"/cmd/buffer/help"}, pat = "^h[lnp]?$", fn = function() return function(buf) buf.state:info("use :help for help on various topics") return false end, "" end, }) table.insert(state.impl.cmd.buffer, { name = "stub/help-mode", syntax = "H", descr = "do nothing (POSIX compatibility stub)", posix = {spec = "2017", notes = "Does nothing."}, see = {"/cmd/buffer/help"}, pat = "^H[lnp]?$", fn = function() return function(buf) buf.state:info("neo-ed is always in verbose help mode") return false end, "" end, }) table.insert(state.impl.cmd.buffer, { name = "stub/prompt", syntax = "P", descr = "do nothing (POSIX compatibility stub)", posix = {spec = "2017", notes = "Does nothing."}, pat = "^P[lnp]?$", fn = function() return function(buf) buf.state:info("neo-ed always shows a prompt") return false end, "" end, }) table.insert(state.impl.cmd.line, { name = "comment", syntax = "#[<...>]", addr = ".", descr = "comment, discard command and do nothing", details = "If a range is given explicitly, it is selected.", pat = "^#.*$", fn = function() return function(buf, a, b) if b then buf:change(buf.body.copy_select, a or b, b) end return false end, "" end, }) table.insert(state.impl.cmd.suf, { name = "null", syntax = "", descr = "do nothing", posix = {spec = "2017"}, pat = "^$", fn = function() return function() end end, }) end ]================================================================================] , "neo-ed.plugins.misc.stubs")) package.preload["neo-ed.plugins.misc.term_title"] = assert(load( [================================================================================[ local term = require "neo-ed.term" return function(state) if term.title then table.insert(state.hooks.buffer.prompt_pre, function(buf) io.stdout:write(term:title(buf:label())) end) end end ]================================================================================] , "neo-ed.plugins.misc.term_title")) package.preload["neo-ed.plugins.picker.fzf"] = assert(load( [================================================================================[ local lib = require "neo-ed.lib" local term = require "neo-ed.term" return function(state) if not state:check_executable("fzf", "falling back to builtin picker") then return end state._picker = function(state, choices, opts) if not choices[1] then lib.error("nothing to pick from") end local fzf = lib.cmd "fzf" "--ansi" "--layout=reverse-list" "--no-sort" "--exact" "--read0" "--delimiter=\n" "--accept-nth={n}" if opts.start then fzf ("--bind=load:pos:" .. opts.start) "--sync" end local inp = {} if opts.show_preview then fzf "--with-nth={2..}" "--preview={r1}" "--preview-window=up,80%,border-line,noinfo" local overrides = { 'export NEO_ED_TERM="${NEO_ED_TERM}-cursor' .. (term.feat.fmt and "+" or "-") .. 'fmt' .. (term.feat.color and "+" or "-") .. 'color' .. '"', 'export NEO_ED_TERM_LINES="$FZF_PREVIEW_LINES"', 'export NEO_ED_TERM_COLUMNS=9999', -- fzf does not auto-wrap lines "", } for _, c in ipairs(choices) do table.insert(inp, ("%s\n%s\0"):format(table.concat(overrides, "; ") .. c.preview, c.text)) end else for _, c in ipairs(choices) do table.insert(inp, ("%s\0"):format(c.text)) end end local ret = fzf:load(inp):read("l?") if not ret or ret == "" then return nil end return tonumber(ret) + 1 end end ]================================================================================] , "neo-ed.plugins.picker.fzf")) package.preload["neo-ed.plugins.print.eol"] = assert(load( [================================================================================[ local term = require "neo-ed.term" return function(state) state:add_conf("nl_marker", { type = "string", def = "$", descr = "Marker to place after each line", drop_cache = true, }) table.insert(state.print.post, 1, function(lines, buf) local s = buf:conf_get("nl_marker") for i, l in ipairs(lines) do lines[i].text = ("%s%s%s%s"):format(l.text, term:sgr"faint", s, term:sgr"reset") end end) end ]================================================================================] , "neo-ed.plugins.print.eol")) package.preload["neo-ed.plugins.print.printable"] = assert(load( [================================================================================[ local lib = require "neo-ed.lib" local term = require "neo-ed.term" local ucd = require "neo-ed.lib.ucd" -- Unfortunately Lua assumes valid utf-8 input for most functions, so we have to handle potentially unclean input ourselves. -- `https://encoding.spec.whatwg.org/#utf-8-decoder` local patterns = { "^([\x00-\x7F])", "^([\xC2-\xDF][\x80-\xBF])", "^(\xE0[\xA0-\xBF][\x80-\xBF])", -- avoid overlong encodings "^([\xE1-\xEC][\x80-\xBF][\x80-\xBF])", "^(\xED[\x80-\x9F][\x80-\xBF])", -- avoid utf-16 surrogates "^([\xEE-\xEF][\x80-\xBF][\x80-\xBF])", "^(\xF0[\x90-\xBF][\x80-\xBF][\x80-\xBF])", -- avoid overlong encodings "^([\xF1-\xF3][\x80-\xBF][\x80-\xBF][\x80-\xBF])", "^(\xF4[\x80-\x8F][\x80-\xBF][\x80-\xBF])", -- avoid upper bound } local function blob(s) local i = 1 return function() for _, pat in ipairs(patterns) do local _, j, cp = s:find(pat, i) if cp then i = j + 1; return utf8.codepoint(cp), true end end i = i + 1 return s:byte(i), false end end local function render_blob(s) local ret = {} local function rv(s) return ("%s%s%s"):format(term:sgr"rev", s, term:sgr"nrev") end local last = nil local ctr = 0 local function push(cs, special) special = special or 0 if special >= 2 and cs == last then ctr = ctr + 1 return end if last and ctr > 1 then table.insert(ret, rv(("[%d x %s]"):format(ctr, last))) last, ctr = nil, 0 elseif last then table.insert(ret, rv(("[%s]"):format(last))) last, ctr = nil, 0 end if special >= 2 then last = cs ctr = 1 elseif special >= 1 then table.insert(ret, rv(cs)) elseif cs then table.insert(ret, cs) end end for cp, valid in blob(s) do if not valid then push(("\\x%02X"):format(cp), 2) else local r = ucd.replace[cp] local cs = r or utf8.char(cp) if cs == true then cs = (cp < 0x7f and "\\x%02X" or "U+%04X"):format(cp) end push(cs, r and 2 or r == false and 1 or 0) end end push() return table.concat(ret) end return function(state) state:add_conf("verbatim", {type = "boolean", def = false, descr = "Render all characters as-is", drop_cache = true}) -- handle escape character before print pipeline table.insert(state.print.pre, function(lines, buf) if not buf:conf_get("verbatim") then for _, l in ipairs(lines) do l.text = l.text:gsub("\x1b", term:sgr"rev" .. "[1B]" .. term:sgr"nrev") end end end) table.insert(state.print.post, function(lines, buf) if not buf:conf_get("verbatim") then for _, l in ipairs(lines) do l.text = l.text:gsub("[\x00-\x08\x0B-\x1A\x1C-\x1F\x7F-\xFF]+", render_blob) end end end) end ]================================================================================] , "neo-ed.plugins.print.printable")) package.preload["neo-ed.plugins.print.tabs"] = assert(load( [================================================================================[ local lib = require "neo-ed.lib" local term = require "neo-ed.term" local superscript = { ["0"] = "⁰", ["1"] = "¹", ["2"] = "²", ["3"] = "³", ["4"] = "⁴", ["5"] = "⁵", ["6"] = "⁶", ["7"] = "⁷", ["8"] = "⁸", ["9"] = "⁹", } local function indentlvl(lvl, s, num, colors) local cf = colors[(lvl - 1) % #colors + 1] if not num then return cf(s) end local pre, spc, suf = s:match("^([^ ]*)( *)([^ ]*)$") local lvl_s = ("%d"):format(lvl) lib.assert(pre, ("indentlvl match failed for string %q"):format(s)) if #lvl_s > #spc then return cf(s) end return ("%s%s%s%s%s%s"):format( cf(pre), (" "):rep(#spc - #lvl_s), term:sgr"weak", num == "small" and lvl_s:gsub(".", superscript) or lvl_s, term:sgr"reset", cf(suf) ) end local function make_marker(base, width) local pre, suf = base:match("^([^ ]*) *(.*)$") pre, suf = term:str(pre), term:str(suf) local ret = term:str("") :append(pre) :append((" "):rep(width - pre.len - suf.len)) :append(suf) return (ret:show_last(width)) end return function(state) state:add_conf("elastic_tabstops", { type = "boolean", def = false, descr = "Enable elastic tabstops", drop_cache = true, }) state:add_conf("tab_marker", { type = "string", def = " " .. term.box.vline, descr = "Indentation marker for tabs", drop_cache = true, }) state:add_conf("space_marker", { type = "string", def = " " .. term.box.vtick, descr = "Indentation marker for spaces", drop_cache = true, }) state:add_conf("indent_levels", { type = "string", def = "full", descr = "Add numbers to indentation levels (full|small|none)", drop_cache = true, }) state:add_conf("indent_colors", { type = "string", def = "red yellow green cyan blue magenta", descr = "space-separated colors for leading indentation", drop_cache = true, }) table.insert(state.print.post, function(lines, b) local tab2spc = b:conf_get("tab2spc" ) local indent, origin = b:conf_get("indent" ) local tabs = b:conf_get("tabs" ) local lvls = b:conf_get("indent_levels") local colors = {} for fmt in b:conf_get("indent_colors"):gmatch("%S+") do table.insert(colors, function(s) return term:sgr(fmt) .. s .. term:sgr"reset" end) end if lvls == "none" then lvls = false elseif lvls ~= "small" and lvls ~= "full" then b.state:warn(("indent_levels: expecting full|small|none, got %q"):format(lvls)) lvls = "full" end if b:conf_get("elastic_tabstops") then local tbl = {} local ptrs = {} local tab = b:conf_get("tab_marker") -- assemble table with linked cell width references for _, l in ipairs(lines) do local line = {} local cno = 1 for col_text in l.text:gmatch("([^\t]*)") do -- table with single member as substitute for a pointer ptrs[cno] = ptrs[cno] or {0} table.insert(line, {text = col_text, width = ptrs[cno]}) cno = cno + 1 end table.insert(tbl, line) for i = #ptrs, #line, -1 do ptrs[i] = nil end end -- find indent depth and maximum width for each column section for _, l in ipairs(tbl) do for cno = 1, #l - 1 do l[cno].text_width = term:display_width(l[cno].text) l[cno].width[1] = math.max(l[cno].width[1], l[cno].text_width + tabs) end l.indent = 0 for cno, c in ipairs(l) do if c.text_width == 0 then l.indent = l.indent + 1 else break end end end -- add a suitable delimiter after each cell for lno, l in ipairs(tbl) do local text = {} for cno, c in ipairs(l) do table.insert(text, c.text) if cno < #l then table.insert(text, indentlvl( cno, make_marker(tab, c.width[1] - c.text_width), cno == l.indent and l.indent > (tbl[lno-1] and tbl[lno-1].indent or 0) and lvls, colors )) end end lines[lno].text = table.concat(text) end else local indent_spc = make_marker(b:conf_get("space_marker"), indent) local indent_tab = make_marker(b:conf_get("tab_marker" ), tabs ) local show_spaces = tab2spc and origin ~= "default value" local tab_inline = indent_tab local last = 0 local function highlight(wsp) if wsp:find("^\t") then local ind, rest = wsp:match("^(\t*)(.-)$") local ret = {} local lvl = 0 while ind ~= "" do lvl = lvl + 1 table.insert(ret, indentlvl( lvl, indent_tab, #ind == 1 and lvl > last and lvls, colors )) ind = ind:sub(2) end last = lvl table.insert(ret, rest) return table.concat(ret) end if show_spaces then local ind, rest = wsp:match("^( *)(.-)$") local ret = {} local lvl = 0 while #ind >= indent do lvl = lvl + 1 table.insert(ret, indentlvl( lvl, indent_spc, #ind < 2*indent and lvl > -last and lvls, colors )) ind = ind:sub(indent + 1) end last = -lvl table.insert(ret, ind ) table.insert(ret, rest) return table.concat(ret) end last = 0 return wsp end for i, l in ipairs(lines) do local s = l.text local pre, wsp, rest = s:match("^(\x1b[^m]-m)([\t ]+)(.*)$") if not wsp then pre = "" wsp, rest = s:match("^([\t ]*)(.*)$") end if wsp then s = highlight(wsp) .. pre .. rest end l.text = s:gsub("\t", tab_inline) end end end) end ]================================================================================] , "neo-ed.plugins.print.tabs")) package.preload["neo-ed.plugins.settings.ansi"] = assert(load( [================================================================================[ local lib = require "neo-ed.lib" return function(state) table.insert(state.impl.filter.conf, 1, function(conf, vpath) if vpath:match("%.ansi%.txt$") then conf.highlighter = {value = "none", origin = ".ansi.txt file"} conf.verbatim = {value = true , origin = ".ansi.txt file"} end end) end ]================================================================================] , "neo-ed.plugins.settings.ansi")) package.preload["neo-ed.plugins.settings.autodetect"] = assert(load( [================================================================================[ local lib = require "neo-ed.lib" --[[ Spaces Heuristic Algorithm Explanation: For a file with `i` spaces indentation steps, we would expect the indentation of most lines be `n*i` with a natural number `n`. Moreover, we expect the `n`s to be distributed somewhat evenly. Factors of `n` will have evenly spaced holes in their distribution. So we score based on the product of two numbers: - The root mean square deviation from the expected uniform space distribution, - The proportion of lines for which `n` is an integer. ]] return function(state) table.insert(state.hooks.buffer.load_post, function(buf) local tabs = 0 local spaces = 0 local indent = {} local max_indent = 0 local _, origin = buf:conf_get("tab2spc") if origin ~= "default value" then return end local _, origin = buf:conf_get("indent") if origin ~= "default value" then return end buf.body:inspect(function(n, l) if l.text:find("^\t+") then tabs = tabs + 1 return end local _, spc = l.text:find("^ +") if spc then spaces = spaces + 1 indent[spc] = (indent[spc] or 0) + 1 max_indent = math.max(max_indent, spc) end end) if tabs == 0 and spaces == 0 then return end if tabs >= 2*spaces then buf:conf_set("tab2spc", false, "autodetected") return end if spaces < 2*tabs then return end for i = 1, max_indent do indent[i] = indent[i] or 0 end local best_score = math.huge local best_indent = 4 local next_score = math.huge for i = 1, max_indent do local score = 0 local found = 0 local steps = max_indent // i local expect = spaces / steps for n = 1, steps do score = score + (indent[n*i] - expect)^2 found = found + indent[n*i] end -- generous extra +1 space to avoid `NaN`s score = score * ((spaces + 1) / (found + 1)) if score < best_score then best_score, best_indent, next_score = score, i, best_score elseif score < next_score then next_score = score end end if best_score < math.huge and next_score / best_score > 1.02 and best_indent > 1 then buf:conf_set("indent", best_indent, "autodetected") buf:conf_set("tab2spc", true, "autodetected") end end) end ]================================================================================] , "neo-ed.plugins.settings.autodetect")) package.preload["neo-ed.plugins.settings.config"] = assert(load( [================================================================================[ local lib = require "neo-ed.lib" return function(state) table.insert(state.impl.filter.conf, function(conf, vpath, buf, set) local home = os.getenv("HOME"):gsub("/*$", "") local path = buf.state:realpath(vpath, true):gsub("^" .. lib.patesc(home), "~") local loc = buf:get_loc() loc = loc and tostring(loc) local dir = buf.state:realpath(".", true):gsub("^" .. lib.patesc(home), "~") local env = setmetatable({ buf = buf, dir = dir, loc = loc, vpath = path, conf = setmetatable({}, { __index = function(_, k) return conf[k] and conf[k].value or nil end, __newindex = function(_, k, v) set(conf, k, v) conf[k].origin = lib.fninfo(2) end, }), }, {__index = _ENV}) local function try_file(path) path = buf.state:realpath(path) if lib.path_type(path) then -- TODO: errors could probably be better lib.assert(loadfile(path, "t", env))() end end try_file(buf.state.config_dir .. "/conf.lua") try_file(buf.state.config_dir .. "/conf-" .. lib.cmd "hostname" :read("l?") .. ".lua") end) end ]================================================================================] , "neo-ed.plugins.settings.config")) package.preload["neo-ed.plugins.settings.editorconfig"] = assert(load( [================================================================================[ local lib = require "neo-ed.lib" local autocomp_key = {} return function(state) state.help["/editorconfig"] = {descr = "editorconfig quick reference", text = [=[ For more details, see: - [Introduction](https://editorconfig.org) - [Specification](https://spec.editorconfig.org) ## Patterns - `*`: any string of characters, except `/` - `**`: any string of characters - `?`: any single character, except `/` - `[]`: any single character in `` - `[!]`: any single character not in `seq` - `{,,}`: any of the strings given - `{..}`: any integer numbers between `` and `` (can be negative) - `\`: escape other character ## Keys - `indent_style`: set to `tab` or `space` to use hard or soft tabs (i.e. tabs are replaced by spaces when entered) - `indent_size`: set to a whole number defining the number of columns per indentation level and the width of soft tabs, or `tab` to default to the setting of `tab_width` - `tab_width`: set to a whole number defining the width of a tab character, defaults to `indent_size` - `end_of_line`: set to `lf` or `crlf` to control how line breaks are saved - `charset`: set to `latin1`, `utf-8`, `utf-16be` or `utf-16le` to control input and output character set - `trim_trailing_whitespace`: set to `true` to remove trailing whitespace characters - `insert_final_newline`: set to `true` to save the file with a final newline Any other setting in `neo-ed` can be set from the editorconfig file by prefixing it with `ned_`, e.g. `ned_highlighter=pygments` sets `highlighter`. ## `neo-ed's` implementation For every file `` with extension ``, `neo-ed` loads the editorconfig for the following files in order. Directives encountered later override previous ones. - `$XDG_CONFIG_HOME/neo-ed/` - `` Global configuration (the first case) is usually added to `$XDG_CONFIG_HOME/neo-ed/.editorconfig`, which can be opened quickly using the command `:config .editorconfig`. An example of this file might look like this: ```editorconfig [*] insert_final_newline = true [*.hs] indent_style = space [Makefile] indent_style = tab [/home/user/.bashrc] trim_trailing_whitespace = true ``` ]=], see = {"/config", "/cmd/file/config/*"}} if not state:check_executable("editorconfig", "disabling editorconfig integration") then return end local function from_editorconfig(from) local ret = {} local t = { charset = function(v) return "charset", v end, end_of_line = function(v) return "crlf", v:lower() == "crlf" and "y" or "n" end, indent_size = function( ) return "" end, indent_style = function(v) return "tab2spc", v:lower() == "space" and "y" or "n" end, tab_width = function(v) return "tabs", tostring(tonumber(v) or 4) end, trim_trailing_whitespace = function(v) return "trim", v:lower() == "true" and "y" or "n" end, } for k, v in pairs(from) do local k_, v_ k_ = k:match("^ned_(.*)$") if k_ then v_ = v end if not k_ and t[k] then if k == "indent_size" or state.conf_defs[t[k](v)] then k_, v_ = t[k](v) end end if k_ then ret[k_] = v_ end end if from.indent_size then ret.indent = (from.indent_size:lower() == "tab" and (from.tab_width or ret.tabs or "4")) or from.indent_size or "4" end return ret end local ignore = {} local function load_editorconfig_for(buf, conf, eff_path, disp_path) local data = {} for l in lib.cmd "editorconfig" (eff_path) :read("L") do local k, v = l:match("^([^=]+)=(.*)$") if k then data[k] = v end end for k, v in pairs(from_editorconfig(data)) do if ignore[k] then ; elseif buf.state.conf_defs[k] then local ok, v = lib.pcall(buf.conf_read, buf, k, v) if ok then conf[k] = {value = v, origin = "editorconfig for " .. disp_path} else buf.state:warn(("ignoring %q from editorconfig: %s"):format(k, v)) end else buf.state:info(("ignoring unknown editorconfig setting: ned_%s=%s"):format(k, v)) ignore[k] = true end end end table.insert(state.impl.filter.conf, 1, function(conf, vpath, buf) local abs_path = buf.state:realpath(vpath, true) local full = buf.state.config_dir .. abs_path load_editorconfig_for(buf, conf, full , vpath) load_editorconfig_for(buf, conf, abs_path, vpath) end) local settings = { charset = {"latin1", "utf-8", "utf-8-bom", "utf-16be", "utf-16le"}, end_of_line = {"cr", "crlf", "lf"}, indent_size = {"tab"}, indent_style = {"tab", "space"}, insert_final_newline = {"true", "false"}, tab_width = {}, trim_trailing_whitespace = {"true", "false"}, } local key_dict = lib.autocomp_dict(settings, function(s) return s .. "=" end) local function conf_dict(state) local cache = lib.cache[state.frame_key][autocomp_key] cache.conf_dict = cache.conf_dict or lib.autocomp_dict(state.conf_defs, function(s) return "ned_" .. s .. "=" end) return cache.conf_dict end table.insert(state.impl.autocomp.src, { pat = "^%s*([^[%s]%S*)$", fn = function(comps, pre, buf) if not buf:get_vpath():find("%.editorconfig$") then return end key_dict(comps, pre) conf_dict(buf.state)(comps, pre) end, }) table.insert(state.impl.autocomp.src, { pat = "^%s*(%S+%s*=.*)$", fn = function(comps, pre, buf) if not buf:get_vpath():find("%.editorconfig$") then return end local k, v = pre:match("^(%S+)%s*=%s*(%S*)$") if not k then return end if settings[k] then lib.autocomp_dict(settings[k])(comps, v) end end, }) end ]================================================================================] , "neo-ed.plugins.settings.editorconfig")) package.preload["neo-ed.plugins.settings.set"] = assert(load( [================================================================================[ local lib = require "neo-ed.lib" local term = require "neo-ed.term" return function(state) table.insert(state.impl.cmd.buffer, { name = "config/set", syntax = ":set [=]", descr = "print / change settings for the current buffer", see = {"/config", "/cmd/buffer/config/*"}, pat = "^:set +([^ =]+)(=?)(.*)$", fn = function(m) return function(buf) if m[2] ~= "" then buf:conf_set(m[1], buf:conf_read(m[1], m[3]), ":set command") end print(m[1] .. "=" .. buf:conf_show(buf:conf_get(m[1]))) return false end, "" end, }) table.insert(state.impl.cmd.buffer, { name = "config/dump", syntax = ":set", descr = "print all configuration values", see = {"/config", "/cmd/buffer/config/*"}, pat = "^:set$", fn = function() return function(buf) local out = {} for _, k in lib.opairs(buf.state.conf_defs) do local v, origin = buf:conf_get(k) table.insert(out, ("%s# %s (%s)%s\n"):format( term:sgr"accent", buf.state.conf_defs[k].descr, origin, term:sgr"reset" )) table.insert(out, ("%s=%s%s%s\n\n"):format( k, origin == ":set command" and term:sgr"note rev" or origin ~= "default value" and term:sgr"good rev" or "", buf:conf_show(v), term:sgr"reset" )) end lib.pager(table.concat(out)) return false end, "" end, }) table.insert(state.impl.cmd.buffer, { name = "config/reset", syntax = ":reset ", descr = "reset configuration value to configured / default value", see = {"/config", "/cmd/buffer/config/*"}, pat = "^:reset +([^ =]+)$", fn = function(m) return function(buf) buf:conf_reset(m[1]) return false end, "" end, }) table.insert(state.hooks.state.init_post, function(state) table.insert(state.impl.autocomp.cmd, { pat = "^:reset +([^=]*)$", fn = lib.autocomp_dict(state.conf_defs, function(k) return k, state.conf_defs[k].descr end), }) table.insert(state.impl.autocomp.cmd, { pat = "^:set +([^=]*=?)$" , fn = lib.autocomp_dict(state.conf_defs, function(k) return k .. "=", state.conf_defs[k].descr end), }) end) end ]================================================================================] , "neo-ed.plugins.settings.set")) package.preload["neo-ed.plugins.state.io"] = assert(load( [================================================================================[ local posix = require "posix" local lib = require "neo-ed.lib" return function(state) table.insert(state.impl.cmd.buffer, { name = "io/edit", syntax = "e []", descr = "replace buffer contents", details = [=[ The contents of the current buffer are discarded, and replaced with the contents read from ``. If the buffer contents are modified, this command fails. This protection can be bypassed by writing the `e` in uppercase, i.e. `E [...]`. If `` is not given, it defaults to the current location. If the buffer has no location, this fails. If `` is given, it is also used as the new location for the buffer. ]=], posix = { spec = "2017", notes = [[ Buffers can be set to non-file locations. Using `e` twice instead of `E` is not supported. ]], }, see = {"/cmd/buffer/io/*", "/cmd/line/io/*", "/cmd/line/shell", "/loc"}, pat = "^([eE])( *)(.*)$", fn = function(m) local force = m[1] == "E" local loc = m[3] if m[3] ~= "" and m[2] == "" then return false end return function(buf) if buf:is_modified() and not force then lib.error("buffer modified") end buf:change(function() buf:load(loc ~= "" and buf.state:loc(loc) or nil, force) end) buf:set_modified(false) return false end, "" end, }) table.insert(state.impl.cmd.line, { name = "io/read", syntax = "r []", descr = "read into buffer", details = [=[ The contents read from `` are appended after the addressed line. If `` is not given, it defaults to the buffer's location. If the buffer has no location, this fails. If the buffer has no location, `` is given, and refers to a path, it is also used as the new location for the buffer. ]=], addr = "$", posix = { spec = "2017", notes = "Buffers can be set to non-file locations.", }, pat = "^r( *)(.*)$", see = {"/cmd/buffer/io/*", "/cmd/line/io/*", "/cmd/line/shell", "/loc"}, fn = function(m) local loc = m[2] if m[2] ~= "" and m[1] == "" then return false end return function(buf, _, a) a = a or #buf.body local clear_modified = false buf:change(function(body) local loc = loc ~= "" and buf.state:loc(loc) or lib.assert(buf:get_loc(), "buffer has no location") body = body:copy_put(lib.lines_split(loc:read()), a) if loc and loc:get_path() and not buf:get_loc() then buf:set_loc(loc) clear_modified = true end return body end) if #buf.body >= 1 then buf:change(buf.body.copy_select, a + 1, buf.body:pos()) end if clear_modified then buf:set_modified(false) end if #buf.body < 1 then return false end return buf.body:sel_first(), buf.body:sel_last() end, "" end, }) table.insert(state.impl.cmd.line, { name = "io/write", syntax = "w[q[q]] []", addr = "1,$", descr = "write buffer contents", details = [=[ The addressed lines are written to ``. If the command is a lowercase `w`, the contents are overwritten, if it is an uppercase `W`, the lines are appended. If `` is not given, it defaults to the buffer's location. If the buffer has no location, this fails. Appending to the current location is explicitly not allowed, use instead. If the buffer has no location, append mode is not selected, `` is given, and refers to a path, it is also used as the new location for the buffer. If the buffer has a location, `` is not given, the entire buffer is written, and not appended, this resets the "modified since last write" condition. The command variants `wq` and `wqq` behave like a `w` command followed by a `q` or `qq` command respectively. ]=], posix = { spec = "2017", notes = [[ - If the entire buffer has been written to a named location using the `w` command, and the remembered location is not changed to that file by this command, `neo-ed` does not reset the "last w command that wrote the entire buffer" flag. This avoids a situation where the buffer contents differ from the contents of the location, but the modified flag is not set, which the authors believe is a situation that should never occur. The `wq` command only closes the current buffer. If no buffers remain after this, the editor is closed as well. Note that the concept of "multiple open buffers" does not exist in POSIX `ed`. The `wqq` closes all buffers and the editor in a single command. ]], }, see = {"/cmd/buffer/io/*", "/cmd/line/io/*", "/cmd/buffer/quit", "/loc"}, pat = "^([wW])(q?q?)( *)(.*)$", fn = function(m) local app = m[1] == "W" local quit = m[2]:len() local loc = m[4] if m[4] ~= "" and m[3] == "" then return false end if app and loc == "" then return false end return function(buf, a, b) a = a or b or 1 b = b or #buf.body buf:save(loc ~= "" and buf.state:loc(loc) or nil, a, b, app) if quit == 1 then buf:close() elseif quit == 2 then buf.state:quit() elseif a > 1 or b < #buf.body then buf:change(buf.body.copy_select, a, b) end return false end, "" end, }) table.insert(state.impl.cmd.buffer, { name = "quit", syntax = "q[q]", descr = "close buffer / editor", details = [=[ The command `q` closes the current buffer. If that was the last open buffer, the editor is closed as well. This command fails if the current buffer was modified since the last save. This protection can be bypassed by writing the `q` in uppercase, i.e.`Q`. The command `qq` closes all buffers and the editor. It fails at the first buffer it encounters that was modified since the last save. This protection can be bypassed by writing the `qq` in uppercase, i.e.`QQ`. Mixed-case combinations like `Qq` and `qQ` are not allowed. ]=], posix = { spec = "2017", notes = [=[ Using `q` twice instead of `Q` is not supported. The `q` command only closes the current buffer. If no buffers remain after this, the editor is closed as well. Note that the concept of "multiple open buffers" does not exist in POSIX `ed`. The `qq` closes all buffers and the editor in a single command. ]=], }, see = {"/cmd/buffer/restart", "/cmd/line/io/write", "/loc"}, pat = "^[qQ][qQ]?$", fn = function(m) if not m[1]:find("^q+$") and not m[1]:find("^Q+$") then return false end local force = m[1]:find("Q") local full = m[1]:find("..") return function(buf) if full then buf.state:quit(force) else buf:close(force) end return false end, "" end, }) table.insert(state.impl.cmd.buffer, { name = "io/loc/print", syntax = "f", descr = "print location", posix = {spec = "2017"}, see = {"/cmd/buffer/io/loc", "/cmd/buffer/name", "/loc"}, pat = "^f$", fn = function() return function(buf) print(buf:get_loc()) return false end, "" end, }) table.insert(state.impl.cmd.buffer, { name = "io/loc", syntax = "f ", descr = "set location", posix = {spec = "2017"}, see = {"/cmd/buffer/name", "/loc"}, pat = "^f +(.*)$", fn = function(m) return function(buf) buf:set_loc(buf.state:loc(m[1])) print(buf:get_loc()) return false end, "" end, }) table.insert(state.impl.cmd.buffer, { name = "io/open/pick", syntax = "o", descr = "pick file to open in new buffer", see = {"/cmd/buffer/io/edit", "/cmd/buffer/io/open"}, pat = "^o$", fn = function() return function(buf) buf.state:open(buf.state:pick_file()):focus() return false end, "" end, }) table.insert(state.impl.cmd.buffer, { name = "io/open", syntax = "o ", descr = "open location in new buffer", see = {"/cmd/buffer/io/edit", "/cmd/buffer/new", "/loc"}, pat = "^o +(.+)$", fn = function(m) local loc = m[1] return function(buf) buf.state:open(loc):focus() return false end, "" end, }) table.insert(state.impl.cmd.buffer, { name = "new", syntax = ":new", descr = "open unnamed buffer", see = {"/cmd/buffer/io/open"}, pat = "^:new$", fn = function(m) return function(buf) buf.state:new():focus() return false end, "" end, }) table.insert(state.impl.cmd.buffer, { name = "name", syntax = ":name []", descr = "set cosmetic buffer name", pat = "^:name *(.*)$", see = {"/cmd/buffer/io/filename"}, fn = function(m) return function(buf) buf.name = m[1] ~= "" and m[1] or nil return false end, "" end, }) table.insert(state.impl.cmd.buffer, { name = "vpath", syntax = ":vpath []", descr = "set buffer vpath", pat = "^:vpath *(.*)$", see = {"/cmd/buffer/io/filename"}, fn = function(m) return function(buf) buf:set_vpath(m[1] ~= "" and m[1] or nil) return false end, "" end, }) table.insert(state.impl.autocomp.cmd, {pat = "^[eErwWfo] +(.*)$", fn = state:autocomp_loc()}) table.insert(state.impl.cmd.buffer, { name = "switch", syntax = "O[]", descr = "pick buffer to switch to", see = {"/cmd/buffer/new", "/cmd/buffer/open"}, pat = "^O(.*)$", fn = function(m) return function(buf) local buf = m[1] ~= "" and buf.state:get_buffer(m[1]) or state:pick_buffer(nil, {start = next(buf.state.buffers, next(buf.state.buffers)) and 2 or 1}) buf:focus() return false end, "" end, }) table.insert(state.impl.autocomp.cmd, {pat = "^O(.*)$", fn = state:autocomp_buffer()}) table.insert(state.impl.cmd.buffer, { name = "io/config", syntax = ":config []", descr = "open named config file, defaults to init.lua", see = {"/cmd/buffer/io/open"}, pat = "^:config( *)(.*)$", fn = function(m) local path = "init.lua" if m[2] ~= "" then if m[1] == "" then return false end path = m[2] end return function(buf) lib.mkdir(buf.state.config_dir) if path == "init.lua" then local ok, _, errno = posix.unistd.access(buf.state.init_file, "r") local b = buf.state:open(buf.state.init_file):focus() if not ok then b:load_str(require("neo-ed.plugins").def_config) end else local b = buf.state:open(buf.state.config_dir .. "/" .. path):focus() end return false end, "" end, }) table.insert(state.impl.autocomp.cmd, {pat = "^:config%s+(.*)$", fn = function(comps, pre, buf) lib.autocomp_path(comps, buf.state.config_dir .. "/" .. pre) end}) end ]================================================================================] , "neo-ed.plugins.state.io")) package.preload["neo-ed.plugins.state.misc"] = assert(load( [================================================================================[ local lib = require "neo-ed.lib" local term = require "neo-ed.term" return function(state) table.insert(state.impl.cmd.buffer, { name = "restart", syntax = ":restart", descr = "restart the editor", details = [=[ The editor is replaced by a new instance of itself, and all open files are re-opened. All open buffers must have an associated location, and must not be modified. While passing unsaved buffer contents to the new instance would not be a problem, doing so without the possibility of data loss if the initialization fails is a bit more tricky. ]=], see = {"/cmd/buffer/io/quit"}, pat = "^:restart$", fn = function(m) return function(buf) buf.state:restart() end, "" end, }) table.insert(state.impl.cmd.buffer, { name = "refresh", syntax = ":refresh", descr = "clear world cache", pat = "^:refresh$", fn = function(m) return function(buf) buf.state:drop_cache() return false end, "" end, }) table.insert(state.impl.cmd.buffer, { name = "debug/trace", syntax = ":trace ", descr = "control stack tracebacks for errors", see = {"/cmd/buffer/debug/*"}, pat = "^:trace ([yn])$", fn = function(m) return function(buf) lib.trace = m[1] == "y" return false end, "" end, }) table.insert(state.impl.cmd.buffer, { name = "debug/prof", syntax = ":prof ", descr = "control profiling", see = {"/cmd/buffer/debug/*"}, pat = "^:prof ([yn])", fn = function(m) return function(buf) lib.prof = m[1] == "y" return false end, "" end, }) table.insert(state.impl.cmd.buffer, { name = "term", syntax = ":term []", descr = "print and optionally change terminal settings", pat = "^:term *(.-)$", fn = function(m) return function(buf) if m[1] ~= "" then term:set(m[1]) end print(term:get()) return false end, "" end, }) end ]================================================================================] , "neo-ed.plugins.state.misc")) package.preload["neo-ed.plugins.state.undo"] = assert(load( [================================================================================[ local lib = require "neo-ed.lib" local term = require "neo-ed.term" return function(state) table.insert(state.impl.cmd.buffer, { name = "undo", syntax = "u", descr = "undo last change", posix = {spec = "2017"}, pat = "^u(.*)$", fn = function(m) return function(buf) buf:undo() return false end, "" end, }) table.insert(state.impl.cmd.buffer, { name = "undo/history", syntax = "U", descr = "pick a previous buffer state to return to", pat = "^U$", fn = function(m) return function(buf) local prof = lib.prof and lib.profiler("undo-history") or nil buf.state:shadow_root(function(root, paths) local choices = {} local branches = {} local dag_width = 0 for id = #buf.history, 1, -1 do if prof then prof:start("preprocessing") end local pt = buf.history[id] local first_br = nil local last_br = nil for br, pt_last in ipairs(branches) do if pt_last and pt_last.pred == id then first_br = first_br or br last_br = br end end if not first_br then for br, pt_last in ipairs(branches) do if not pt_last then first_br = br last_br = br break end end end if not first_br then table.insert(branches, false) first_br = #branches last_br = first_br end if prof then prof:stop(); prof:start("branch search") end local pre = {} for br, pt_last in ipairs(branches) do if first_br and first_br <= br and br <= last_br then if br == first_br then table.insert(pre, not pt_last and term.dag.node_end or first_br == last_br and term.dag.node or term.dag.node_branch ) base = br branches[br] = pt elseif pt_last and pt_last.pred == id then table.insert(pre, term.dag.branch_corner) branches[br] = false elseif pt_last then table.insert(pre, term.dag.crossover) else table.insert(pre, term.dag.hext) end elseif pt_last then table.insert(pre, term.dag.vext) else table.insert(pre, term.dag.void) end end if prof then prof:stop(); prof:start("branch pruning") end for i = #branches, 1, -1 do if not branches[i] then branches[i] = nil else break end end if prof then prof:stop(); prof:start("entry generation") end dag_width = math.max(dag_width, #pre) table.insert(choices, { id = id, pt = pt, pre = pre, preview = tostring( id == buf.history_id and lib.cmd (arg[0]) (("--show=%d,%d"):format(pt.body:sel_first(), pt.body:sel_last())) (paths[buf]) or buf.state.impl.diff[#buf.state.impl.diff]( paths[buf], paths[id], "current", pt.cmd or "" ) ), }) if prof then prof:stop() end end if prof then prof:start("entry rendering") end for i, c in ipairs(choices) do c.text = ("%s%s %" .. #("%d"):format(#buf.history) .. "d %s %s%s%s"):format( table.concat(c.pre), (" "):rep(dag_width - #c.pre), c.id, c.pt.body.text_key == buf._modified_key and term:sgr"good bold" .. "*" .. term:sgr"reset" or " ", c.pt.body.text_key == buf.body.text_key and term:sgr"accent bold" .. ">" .. term:sgr"reset bold" or " ", c.pt.cmd or "", c.pt.body.text_key == buf.body.text_key and term:sgr"accent bold" .. "<" .. term:sgr"reset" or " " ) c.pre = nil end if prof then prof:stop(); prof:print() end local r = buf.state:pick(choices, { start = #buf.history - buf.history_id + 1, show_preview = true, }) buf:history_checkout(r.id) end, buf) return false end, "" end, }) end ]================================================================================] , "neo-ed.plugins.state.undo")) package.preload["neo-ed.plugins.text.acid"] = assert(load( [================================================================================[ local function scan_indent(_, l) return not l.text:find("^%s*$") and l.text:match("^(%s*)") or nil end return function(state) state:add_conf("insert_mode_history", { type = "string", def = "empty indent previous", descr = "pre-filled history choices for commands a, c, and i", }) table.insert(state.impl.cmd.line, { name = "append", syntax = "a", addr = ".", descr = "append lines after", details = [=[ Start appending lines after the addressed line. Lines can be inserted at the beginning of the buffer by using address `0`. The editor switches into , the indentation of the last non-empty line is already pre-filled. Pressing `Up` once switches to an empty line instead. ]=], posix = {spec = "2017"}, see = { "/cmd/line/append/blank", "/cmd/line/change", "/cmd/line/insert", "/cmd/line/delete", "/mode/insert", }, pat = "^a(.*)$", fn = function(m) return function(buf, _, a) a = a or buf.body:pos() buf:change(function(body) body = body:copy_seek(a) while true do local hist = {} for w in buf:conf_get("insert_mode_history"):gmatch("%S+") do if w == "empty" then table.insert(hist, "") elseif w == "indent" then table.insert(hist, body:scan_r(scan_indent, body:pos()) or "") elseif w == "previous" then ; else buf.state:warn("invalid insert_mode_history component: " .. w) end end local s = buf:get_input(hist, body:pos() + 1) if not s then break end for l in s:gmatch("[^\n]*") do if l == "." then goto after end body = body:copy_append({text = l}) end end ::after:: return body:copy_select(body:pos() > a and a + 1 or body:pos(), body:pos()) end) return buf.body:sel_first(), buf.body:sel_last() end, m[1] end, }) table.insert(state.impl.cmd.line, { name = "append/blank", syntax = "A[]", addr = ".", descr = "append or 1 blank line(s) after", see = {"/cmd/line/append", "/cmd/line/insert", "/cmd/line/insert/blank"}, pat = "^A(%d*)(.*)$", fn = function(m) local n = m[1] == "" and 1 or tonumber(m[1]) if not n then return false end return function(buf, _, a) a = a or buf.body:pos() buf:change(function(body) body = body:copy_seek(a) for i = 1, n do body = body:copy_append({text = ""}) end return body:copy_select(body:pos() > a and a + 1 or body:pos(), body:pos()) end) return buf.body:sel_first(), buf.body:sel_last() end, m[2] end, }) table.insert(state.impl.cmd.line, { name = "change", syntax = "c[.]", addr = ".", descr = "change lines", details = [=[ Read replacement lines for the addressed lines. Conceptually similar to followed by . The editor switches into , the previous line contents are already pre-filled. Pressing `Up` once, switches to just the indentation, pressing `Up` again switches to an empty line. Automatically exits insert mode after changing the addressed number of lines if a `.` suffix is given, otherwise continues as with . ]=], posix = {spec = "2017"}, see = { "/cmd/line/append", "/cmd/line/insert", "/cmd/line/delete", "/mode/insert", }, pat = "^c(%.?)(.*)$", fn = function(m) return function(buf, a, b) a = a or b or buf.body:pos() b = b or buf.body:pos() if a == 0 then a = 1 end if b == 0 then b = 1 end buf:change(function(body) local prev = body:get(a, b) body = body :copy_drop(a, b) :copy_seek(a - 1) while true do local hist = {} if m[1] == "." then break end for w in buf:conf_get("insert_mode_history"):gmatch("%S+") do if w == "empty" then table.insert(hist, "") elseif w == "indent" then table.insert(hist, prev[1] and (prev[1].text:match("^(%s*)") or "") or body:scan_r(scan_indent, body:pos()) or "" ) elseif w == "previous" then table.insert(hist, prev[1] and prev[1].text or nil) else buf.state:warn("invalid insert_mode_history component: " .. w) end end table.remove(prev, 1) local s = buf:get_input(hist, body:pos() + 1) if not s then break end for l in s:gmatch("[^\n]*") do if l == "." then goto after end body = body:copy_append({text = l}) end end ::after:: if body:pos() >= a then return body:copy_select(a , body:pos()) elseif body:pos() < #body then return body:copy_select(a , a ) else return body:copy_select(body:pos(), body:pos()) end end) return buf.body:sel_first(), buf.body:sel_last() end, m[2] end, }) table.insert(state.impl.cmd.line, { name = "delete", syntax = "d", addr = ".", descr = "delete lines", posix = {spec = "2017"}, see = { "/cmd/line/append", "/cmd/line/change", "/cmd/line/insert", }, pat = "^d(.*)$", fn = function(m) return function(buf, a, b) a = a or b or buf.body:pos() b = b or buf.body:pos() buf:change(function(body) body = body:copy_drop(a, b) if #body > 0 then body = body:copy_select(body:pos(), body:pos()) end return body end) if #buf.body > 0 then return buf.body:pos(), buf.body:pos() else return false end end, m[1] end, }) table.insert(state.impl.cmd.line, { name = "insert", syntax = "i", addr = ".", descr = "insert lines before", details = [=[ Start inserting lines before the addressed line. Conceptually similar to on the preceding line. The editor switches into . The indentation of the addressed line is pre-filled, pressing `Up` once switches to an empty line instead. ]=], posix = {spec = "2017"}, see = { "/cmd/line/append", "/cmd/line/change", "/cmd/line/insert/line", "/cmd/line/delete", "/mode/insert", }, pat = "^i(.*)$", fn = function(m) return function(buf, _, a) a = a or buf.body:pos() if a == 0 then a = 1 end buf:change(function(body) local first = true body = body:copy_seek(a) while true do local hist = {} for w in buf:conf_get("insert_mode_history"):gmatch("%S+") do if w == "empty" then table.insert(hist, "") elseif w == "indent" then table.insert(hist, body:scan_r(scan_indent, first and a or body:pos()) or "" ) elseif w == "previous" then ; else buf.state:warn("invalid insert_mode_history component: " .. w) end end local s = buf:get_input(hist, first and a or body:pos() + 1) if not s then break end for l in s:gmatch("[^\n]*") do if l == "." then goto after end if first then body = body:copy_seek(a - 1) end first = false body = body:copy_append({text = l}) end end ::after:: return body:copy_select(first and body:pos() or a, body:pos()) end) return buf.body:sel_first(), buf.body:sel_last() end, m[1] end, }) table.insert(state.impl.cmd.line, { name = "insert/blank", syntax = "I[]", addr = ".", descr = "insert or 1 blank line(s) before", see = {"/cmd/line/insert", "/cmd/line/append", "/cmd/line/append/blank"}, pat = "^I(%d*)(.*)$", fn = function(m) local n = m[1] == "" and 1 or tonumber(m[1]) if not n then return false end return function(buf, _, a) a = a or buf.body:pos() if a == 0 then a = 1 end buf:change(function(body) local first = true body = body:copy_seek(a) for i = 1, n do if first then body = body:copy_seek(a - 1) end first = false body = body:copy_append({text = ""}) end return body:copy_select(first and body:pos() or a, body:pos()) end) return buf.body:sel_first(), buf.body:sel_last() end, m[2] end, }) state.help["/mode/insert"] = { descr = "insert mode", text = [=[ In insert mode, the editor reads lines to add to the buffer. Entered lines are read and appended without any further processing. Insert mode can be exited by entering a line containing only a single `.`, or by sending an EOF using `Ctrl-D`. The `.` is not added to the buffer. Note that a `.` with leading indentation does not exit insert mode. ]=], see = {"/cmd/line/append", "/cmd/line/change", "/cmd/line/insert"}, } end ]================================================================================] , "neo-ed.plugins.text.acid")) package.preload["neo-ed.plugins.text.aj_ij"] = assert(load( [================================================================================[ return function(state) table.insert(state.impl.cmd.line, 1, { name = "append-join", syntax = "aj[]", addr = ".", descr = "`a` command, then `j` appended lines to the addressed line", see = {"/cmd/line/append", "/cmd/line/join", "/cmd/line/insert-join", "/mode/insert"}, pat = "^aj(.*)$", fn = function(m) return function(buf, _, a) a = a or buf.body:pos() buf:change_group(function() buf:cmd(("%da"):format(a)) buf:cmd(("%d,.j%s"):format(a, m[1])) end) return false end, "" end, }) table.insert(state.impl.cmd.line, 1, { name = "insert-join", syntax = "ij[]", addr = ".", descr = "`i` command, then `j` inserted lines to the addressed line", see = {"/cmd/line/insert", "/cmd/line/join", "/cmd/line/append-join", "/mode/insert"}, pat = "^ij(.*)$", fn = function(m) return function(buf, _, a) a = a or buf.body:pos() buf:change_group(function() local l = #buf.body buf:cmd(("%di"):format(a)) if #buf.body > l then buf:cmd(("%d,+j%s"):format(a, m[1])) else buf:cmd(("%dj%s"):format(a, m[1])) end end) return false end, "" end, }) end ]================================================================================] , "neo-ed.plugins.text.aj_ij")) package.preload["neo-ed.plugins.text.autocomp"] = assert(load( [================================================================================[ local lib = require "neo-ed.lib" local regex = require "neo-ed.regex" local posix = require "posix" local autocomp_key = {} local function get_words_dict(buf) local cache = lib.cache[buf.body.text_key][buf.conf_key][autocomp_key] if not cache.words then local words = {} buf.body:inspect(function(_, l) buf:get_words(l.text, {into = words}) end) cache.words = lib.autocomp_dict(words) end return cache.words end local function get_file_dict(buf, path) local cache = buf.state:file_cache(path) if not cache[autocomp_key].words then local words = {} for l in cache.text:gmatch("[^\n]+") do buf:get_words(l, {into = words}) end cache[autocomp_key].words = lib.autocomp_dict(words) end return cache[autocomp_key].words end local function file_dicts(buf) local ret = {} local globs = buf:conf_get("autocomp_srcfiles") if globs ~= "" then for path in buf.state:editor_files() do local found = false for glob in globs:gmatch("%S+") do found = found or posix.fnmatch.fnmatch(glob, path) == 0 end if found then table.insert(ret, get_file_dict(buf, path)) end end end return ipairs(ret) end return function(state) state:add_conf("autocomp_srcfiles", { type = "string", def = "", descr = "space separated glob patterns for files to also read autocompletion words from", }) table.insert(state.hooks.buffer.prompt_pre, function(buf) get_words_dict(buf); file_dicts(buf) end) table.insert(state.impl.autocomp.dict, function(comps, pre, buf) get_words_dict(buf)(comps, pre) for _, d in file_dicts(buf) do d(comps, pre) end end) end ]================================================================================] , "neo-ed.plugins.text.autocomp")) package.preload["neo-ed.plugins.text.clipboard"] = assert(load( [================================================================================[ local lib = require "neo-ed.lib" return function(state) table.insert(state.impl.cmd.line, { name = "clip/copy", syntax = "C", addr = ".", descr = "copy lines to clipboard", details = [=[ The lines are copied to the clipboard. ]=], see = {"/cmd/line/clip", "/cmd/line/clip/*"}, pat = "^C(.*)$", fn = function(m) return function(buf, a, b) a = a or b or buf.body:pos() b = b or buf.body:pos() local lines = buf.body:get(a, b) buf.state:loc("clip:"):write(lib.lines_join(lines)) buf:change(buf.body.copy_select, a, b) return a, b end, m[1] end, }) table.insert(state.impl.cmd.line, { name = "clip/cut", syntax = "X", addr = ".", descr = "copy lines to clipboard, then delete them", details = [=[ The lines are copied to the clipboard, and then deleted. ]=], see = {"/cmd/line/clip", "/cmd/line/clip/*"}, pat = "^X(.*)$", fn = function(m) return function(buf, a, b) a = a or b or buf.body:pos() b = b or buf.body:pos() buf:change(function(body) local lines = body:get(a, b) buf.state:loc("clip:"):write(lib.lines_join(lines)) body = body:copy_drop(a, b) return body:copy_select(body:pos(), body:pos()) end) return buf.body:pos(), buf.body:pos() end, m[1] end, }) table.insert(state.impl.cmd.line, { name = "clip/paste", syntax = "V", addr = ".", descr = "paste lines from clipboard after", details = [=[ The clipboard contents are split into lines, and then appended after the addressed line. ]=], see = {"/cmd/line/clip", "/cmd/line/clip/*"}, pat = "^V(.*)$", fn = function(m) return function(buf, _, a) a = a or buf.body:pos() buf:change(function(body) local tmp = lib.lines_split(buf.state:loc("clip:"):read()) body = body:copy_put(tmp, a) return body:copy_select(a + 1, body:pos()) end) return a + 1, buf.body:pos() end, m[1] end, }) state.help["/cmd/line/clip"] = { descr = "clipboard management", text = [=[ The clipboard provides space for a single piece of text. It is shared between buffers. Writing to the clipboard and reading back from it using , , and is guaranteed to be a round trip. - If the environment variable `WAYLAND_DISPLAY` is set and the commands `wl-copy` and `wl-paste` are available, the Wayland clipboard is used. - Otherwise, if the environment variable `DISPLAY` is set and the command `xclip` is available, the X11 clipboard is used. - Otherwise, if `$XDG_RUNTIME_DIR` is set, the file `$XDG_RUNTIME_DIR/clipboard` is used. - Otherwise, an internal in-memory fallback is provided that cannot be accessed from outside the editor. To improve interoperability in the first two cases, the terminating newline is removed when writing to the clipboard, and added when reading from the clipboard. ]=], } end ]================================================================================] , "neo-ed.plugins.text.clipboard")) package.preload["neo-ed.plugins.text.global"] = assert(load( [================================================================================[ local regex = require "neo-ed.regex" return function(state) table.insert(state.impl.cmd.line, { name = "global", syntax = "[]", addr = "1,$", descr = "perform on every line (not) matching ", details = [=[ The `g` command goes through the selected lines from top to bottom. If a line matches `` according to , `` is executed with the current line set to the address of the matched line. `` may be any single character. It can occur within ``, but has to be escaped according to the rules of . If `` is empty, the last regex used in any command or address is used. `` may consist of multiple commands to be executed in sequence. This can be achieved by putting a `\` at the end of every line of the command string except the last one. The insert mode of commands like , , and can be used like this as well, in which case the last line containing only a `.` but no trailing `\` can be left out. The `v` command behaves like the `g` command, but runs `` on every line _not_ matching `` instead. ]=], posix = { spec = "2017", notes = "The commands `g` and `v` do not default to `p` if `` is empty, they run the empty command instead.", }, pat = "^([gv])(.)(.*)$", fn = function(m) local neg = m[1] == "v" local pat, sep, rest = regex.curr.parse(m[2], m[3], true) if not pat then return false end if rest == "" then rest = "l" end return function(buf, a, b) a = a or b or 1 b = b or #buf.body buf:change_group(function() local mark = {} local old_len = #buf.body local match = false local targets = {} buf:change(buf.body.copy_map, function(_, l) if not neg and regex.curr.find(l.text, pat) or neg and not regex.curr.find(l.text, pat) then l[mark] = true match = true end end, a, b, true) if not match then return end buf:change(buf.body.copy_seek, a) while true do local pos = buf.body:scan(function(n, l) return l[mark] and n or nil end) if not pos then break end buf:change(buf.body.copy_seek, pos) buf:change(buf.body.copy_map, function(_, l) l[mark] = nil end, pos, pos, true) buf:cmd_list(rest) end end) return 1, #buf.body end, "" end, }) end ]================================================================================] , "neo-ed.plugins.text.global")) package.preload["neo-ed.plugins.text.indent"] = assert(load( [================================================================================[ return function(state) table.insert(state.impl.cmd.line, { name = "indent/add", syntax = ">[]", addr = ".", descr = "indent lines by `` or 1 step(s)", see = {"/cmd/line/indent/*", "/config/indent", "/config/tab2spc"}, pat = "^(>+)(%d*)(.*)$", fn = function(m) return function(buf, a, b) a = a or b or buf.body:pos() b = b or buf.body:pos() local spc = (buf:get_indent_text()):rep(#m[1] - 1 + (tonumber(m[2]) or 1)) buf:change(function(body) return body :copy_map(function(_, l) l.text = spc .. l.text end, a, b) :copy_select(a, b) end) return a, b end, m[3] end, }) table.insert(state.impl.cmd.line, { name = "indent/sub", syntax = "<[]", addr = ".", descr = "unindent lines by `number` or 1 step(s)", see = {"/cmd/line/indent/*", "/config/indent", "/config/tab2spc"}, pat = "^(<+)(%d*)(.*)$", fn = function(m) return function(buf, a, b) a = a or b or buf.body:pos() b = b or buf.body:pos() local n = #m[1] - 1 + (tonumber(m[2]) or 1) local spc = buf:get_indent_text() buf:change(function(body) return body :copy_map(function(_, l) for i = 1, n do l.text = l.text:gsub("^" .. spc, "") or l.text end end, a, b) :copy_select(a, b) end) return buf.body:sel_first(), buf.body:sel_last() end, m[3] end, }) end ]================================================================================] , "neo-ed.plugins.text.indent")) package.preload["neo-ed.plugins.text.print"] = assert(load( [================================================================================[ local lib = require "neo-ed.lib" return function(state) table.insert(state.impl.cmd.buffer, { name = "diff/last", syntax = "D", descr = "show diff to last undo point", see = {"/cmd/buffer/diff/*", "/cmd/suf/diff"}, pat = "^D.*$", fn = function(m) return function(buf) return 1, #buf.body end, "D" end, }) table.insert(state.impl.cmd.buffer, { name = "diff/disk", syntax = ":diff", descr = "show diff to saved state", see = {"/cmd/buffer/diff/*", "/cmd/suf/diff"}, pat = "^:diff$", fn = function(m) return function(buf) local s = lib.assert(buf:get_loc(), "no location to compare against"):read(buf) buf:diff_lines(lib.lines_split(s), nil, tostring(buf:get_loc()), "buffer") -- TODO: buffer uri return false end, "" end, }) table.insert(state.impl.cmd.line, { name = "print/list", syntax = "l", descr = "print code listing (use the print pipeline)", addr = ".", posix = { spec = "2017", notes = [=[ The `l` command employs the print pipeline to format the lines. With default settings, this format is unambiguous, but different to the format described by POSIX. ]=], }, see = {"/cmd/line/print/*", "/cmd/suf/print/*", "/hooks/print"}, pat = "^l[lnp]?$", fn = function(m) return function(buf, a, b) a = a or b or buf.body:pos() b = b or buf.body:pos() buf:change(buf.body.copy_select, a, b) return a, b end, "l" end, }) table.insert(state.impl.cmd.line, { name = "print/list/screen", syntax = "L[u[]][d[]]", descr = "print at least one screen of code listing, including the selected lines (use the print pipeline)", details = [[ The numbers `` and `` define multipliers for the upwards and downwards expansion respectively. The given address range is repeatedly expanded downwards by `` lines, then upwards by `` lines, until the next expansion would overflow the screen range. - If no parameters are given, both `` and `` default to 1, effectively placing the selected range in the vertical center of the screen. - If either of the flags `u` or `d` is given without a number, it defaults to 1. - If one of both parameters is given, the other defaults to `0`. The command `Lu` places the selected range at the bottom of the screen, `Ld` at the top. - If both parameters are given, their ratio defines roughly where the selected range will be positioned. The command `Lu2d1` places the selected range at 1/3 from the top of the screen. ]], addr = "@", see = {"/cmd/line/print/*", "/cmd/suf/print/*", "/hooks/print"}, pat = "^L(.*)$", fn = function(m) return function(buf, a, b) a = a or b or buf.body:sel_first() b = b or buf.body:sel_last () buf:change(buf.body.copy_select, a, b) return a, b end, "L" .. m[1] end, }) table.insert(state.impl.cmd.line, { name = "print/numbered", syntax = "n", descr = "print lines with line numbers", addr = ".", posix = {spec = "2017"}, see = {"/cmd/line/print/*", "/cmd/suf/print/*"}, pat = "^n[lnp]?$", fn = function(m) return function(buf, a, b) a = a or b or buf.body:pos() b = b or buf.body:pos() buf:change(buf.body.copy_select, a, b) return a, b end, "n" end, }) table.insert(state.impl.cmd.line, { name = "print/plain", syntax = "p", descr = "print lines as-is", addr = ".", posix = {spec = "2017"}, see = {"/cmd/line/print/*", "/cmd/suf/print/*"}, pat = "^p[lnp]?$", fn = function(m) return function(buf, a, b) a = a or b or buf.body:pos() b = b or buf.body:pos() buf:change(buf.body.copy_select, a, b) return a, b end, "p" end, }) table.insert(state.impl.cmd.suf, { name = "diff", syntax = "D", descr = "print diff to last step", see = {"/cmd/buffer/diff/*"}, pat = "^D$", fn = function(m) return function(buf) buf:diff() end end, }) table.insert(state.impl.cmd.suf, { name = "print/list", syntax = "l", descr = "print code listing (use the print pipeline)", posix = { spec = "2017", notes = [=[ The `l` command employs the print pipeline to format the lines. With default settings, this format is unambiguous, but different to the format described by POSIX. ]=], }, see = {"/cmd/line/print/*", "/cmd/suf/print/*", "/hooks/print"}, pat = "^l$", fn = function(m) return function(buf, a, b) buf:print(a, b) end end, }) table.insert(state.impl.cmd.suf, { name = "print/list/screen", syntax = "L[u[]][d[]]", descr = "print at least one screen of code listing, including the affected lines (use the print pipeline)", see = {"/cmd/line/print/*", "/cmd/suf/print/*", "/hooks/print"}, pat = "^L([ud0-9]*)$", fn = function(m) local rest = m[1] local u, rest_ = rest:match("^u(%d*)(.*)") if u then u, rest = u == "" and 1 or tonumber(u), rest_ end local d, rest_ = rest:match("^d(%d*)(.*)$") if d then d, rest = d == "" and 1 or tonumber(d), rest_ end if rest ~= "" then return false end return function(buf, a, b) if not u and not d then u, d = 1, 1 end u = u or 0 d = d or 0 local a_, b_ = buf:screen_range(a, b, {nup = u, ndn = d}) buf:print(a_, b_) end end, }) table.insert(state.impl.cmd.suf, { name = "print/numbered", syntax = "n", descr = "print lines with line numbers", posix = {spec = "2017"}, see = {"/cmd/line/print/*", "/cmd/suf/print/*"}, pat = "^n$", fn = function(m) return function(buf, a, b) buf.body:inspect(function(n, l) print(n, l.text) end, a, b) end end, }) table.insert(state.impl.cmd.suf, { name = "print/plain", syntax = "p", descr = "print lines as-is", posix = {spec = "2017"}, see = {"/cmd/line/print/*", "/cmd/suf/print/*"}, pat = "^p$", fn = function(m) return function(buf, a, b) buf.body:inspect(function(n, l) print(l.text) end, a, b) end end, }) end ]================================================================================] , "neo-ed.plugins.text.print")) package.preload["neo-ed.plugins.text.reorder"] = assert(load( [================================================================================[ local lib = require "neo-ed.lib" local parser = require "neo-ed.parser" return function(state) table.insert(state.impl.cmd.line, { name = "move", syntax = "m[]", addr = ".", descr = "move lines after target address", posix = {spec = "2017"}, see = {"/cmd/line/transfer"}, pat = "^m(.*)$", fn = function(m) local dstf, suf = parser.target(state, m[1]) return function(buf, a, b) a = a or b or buf.body:pos() b = b or buf.body:pos() buf:change(function(body) local dst = dstf(buf) if dst > b then dst = dst - (b - a + 1) elseif dst > a then lib.error("destination inside source range") end local tmp = body:get(a, b) body = body :copy_drop(a, b) :copy_put(tmp, dst) return body:copy_select(dst + 1, body:pos()) end) return buf.body:sel_first(), buf.body:sel_last() end, suf end, }) table.insert(state.impl.cmd.line, { name = "transfer", syntax = "t[]", addr = ".", descr = "copy lines after target address", posix = {spec = "2017"}, see = {"/cmd/line/move"}, pat = "^t(.*)$", fn = function(m) local dstf, suf = parser.target(state, m[1]) return function(buf, a, b) a = a or b or buf.body:pos() b = b or buf.body:pos() buf:change(function(body) local dst = dstf(buf) body = body:copy_put(body:get(a, b), dst) return body:copy_select(dst + 1, body:pos()) end) return buf.body:sel_first(), buf.body:sel_last() end, suf end, }) end ]================================================================================] , "neo-ed.plugins.text.reorder")) package.preload["neo-ed.plugins.text.split_join"] = assert(load( [================================================================================[ local lib = require "neo-ed.lib" local regex = require "neo-ed.regex" return function(state) table.insert(state.impl.cmd.line, { name = "join", syntax = "j[[]]", descr = "join lines", addr = ".,+", details = [=[ The addressed lines are joined together into one line. No changes if only a single line is addressed (but that line is selected). `` is an optional punctuation character, which may not occur within ``. The second `` is only necessary to separate `` from a command suffix. If `` is given, the leading whitespace of every line except the first is stripped. Whitespace at the end of each line is kept as-is. Then the lines are joined with `` between. ]=], posix = {spec = "2017"}, see = {"/cmd/line/split"}, pat = "^j(.*)$", fn = function(m) local suf = m[1] local inter = nil local sep, s = suf:match("^(%p)(.*)$") if sep then inter = s local inter_, s = s:match("^(.-)" .. lib.patesc(sep) .. "(.*)$") if inter_ then inter, suf = inter_, s else suf = "" end end return function(buf, a, b) if not a and b then return b, b end a = a or buf.body:pos() b = b or buf.body:pos() + 1 buf:change(function(body) local tmp = {} body:inspect(function(_, l) table.insert(tmp, (#tmp > 0 and inter) and l.text:match("^%s*(.*)$") or l.text) end, a, b) return body :copy_replace({{text = table.concat(tmp, inter)}}, a, b) :copy_select(a, a) end) return buf.body:sel_first(), buf.body:sel_last() end, suf end, }) table.insert(state.impl.cmd.line, { name = "split", syntax = "J[]", descr = "split lines on a regex", addr = ".", details = [=[ Each line is split on the first match of `` using the current regex implementation (). If `` contains a number ``, the split occurs on n-th match instead. If `` contains `g`, all matches trigger a split instead. Any command suffixes must come after any `` meaningful to this command. If nothing is matched in a line, the line is kept as-is. If nothing is matched in any line, the command fails. The current line is set to the last line in which a match occured. By default, the string matched by `` is not kept. If `` contains `a`, the split occurs _after_ the match, which is kept at the end of the line above the split. If `` contains `b`, the split occurs _before_ the match, which is kept at the beginning of the line below the split. If `` contains `s`, the leading whitespace of the first line is also used as the leading whitespace before every line that is split from it. This whitespace is inserted before the separator kept with the `b` flag. `` may be any single character. It can occur within ``, but has to be escaped according to the rules of . If `` and the `` before it are omitted, `` defaults to `l`. If `` is empty, the last regex used in any command or address is used. ]=], see = {"/cmd/line/join", "/cmd/line/subst"}, pat = "^J(.)(.*)$", fn = function(m) local pat, _, suf = regex.curr.parse(m[1], m[2], true) if not pat then return end local spacing = false local repl_pat = "\n" if suf:find("s") then spacing = true ; suf = suf:gsub("s", "", 1) end if suf:find("a") then repl_pat = regex.curr.capture_all .. "\n"; suf = suf:gsub("a", "", 1) end if suf:find("b") then repl_pat = "\n" .. regex.curr.capture_all; suf = suf:gsub("b", "", 1) end local flags flags, suf = suf:match("^([0-9g]*)(.*)$") return function(buf, a, b) a = a or b or buf.body:pos() b = b or buf.body:pos() buf:change(function(body) local last = nil local new = {} body:inspect(function(_, l) local t, n = regex.s(l.text, pat, repl_pat, flags) if n > 0 then local prefix = "" for l_ in t:gmatch("[^\n]*") do table.insert(new, {text = prefix .. l_}) prefix = spacing and l.text:match("^%s*") or "" end last = a + #new - 1 else table.insert(new, l) end end, a, b) if not last then lib.error("substitution failed") end body = body:copy_replace(new, a, b) return body :copy_select(a, body:pos()) :copy_seek(last) end) return buf.body:sel_first(), buf.body:sel_last() end, suf end, }) end ]================================================================================] , "neo-ed.plugins.text.split_join")) package.preload["neo-ed.plugins.text.subst"] = assert(load( [================================================================================[ local lib = require "neo-ed.lib" local regex = require "neo-ed.regex" return function(state) table.insert(state.impl.cmd.line, { name = "subst", syntax = "s[[]]", descr = "substitute `` for ``", addr = ".", details = [==[ The first match of `` in each line is replaced with `` using the current regex implementation (). If `` contains a number ``, the n-th match is replaced instead. If `` contains `g`, all matches are replaced instead. Any command suffixes must come after any `` meaningful to this command. If nothing is matched in a line, the line is kept as-is. If nothing is matched in any line, the command fails. The current line is set to the last line in which a match occured. Alternatively, if the `` contains `?`, the command succeeds even if no match was found. In that case, the last line of the addressed range becomes the current line. `` may be any single character. It can occur within `` and ``, but has to be escaped according to the rules of . If `` and the `` before it are omitted, `` defaults to `l`. If `` is empty, the last regex used in any command or address is used. If `` is a single `%`, the last replacement used in a `s` command is used. A literal `%` can be used as a replacement by escaping it according to . If `` contains `!`, sequences in `` of the form `%[=[` ... `]=]` with any equal number of `=` characters will be replaced with their result when evaluated as Lua expressions. The local variables `_1` .. `_9` are initialized with the respective captures from ``. This feature is HIGHLY EXPERIMENTAL and subject to change. Lines can be split by including a newline preceded by a `\` in ``. ]==], posix = {spec = "2017", notes = [=[ Print suffixes must come after any pattern matching flags to be recognized correctly. `neo-ed` supports additional command suffixes beyond those listed in the POSIX spec, so it will allow the use of those. ]=]}, see = {"/cmd/line/split"}, pat = "^s(.)(.*)$", fn = function(m) local pat, _, suf = regex.curr.parse(m[1], m[2]) if not pat then return false end local repl, flags, suf_ = suf:match("(.-)" .. lib.patesc(m[1]) .. "([0-9g?!]*)(.*)$") if not repl then repl, flags, suf_ = suf, "", "l" end local try = false if flags:find("%?") then try, flags = true, flags:gsub("%?", "") end return function(buf, a, b) a = a or b or buf.body:pos() b = b or buf.body:pos() buf:change(function(body) local last = nil local new = {} body:inspect(function(_, l) local t, n = regex.s(l.text, pat, repl, flags) if n > 0 then for l_ in t:gmatch("[^\n]*") do table.insert(new, {text = l_}) end last = a + #new - 1 else table.insert(new, l) end end, a, b) if not last and not try then lib.error("substitution failed") end body = body:copy_replace(new, a, b) body = body:copy_select(a, body:pos()) if last then body = body:copy_seek(last) end return body end) return buf.body:sel_first(), buf.body:sel_last() end, suf_ end, }) end ]================================================================================] , "neo-ed.plugins.text.subst")) package.preload["neo-ed.regex"] = assert(load( [================================================================================[ local as = require "neo-ed.lib.as" local lib = require "neo-ed.lib" local m = {} local function find_nth(r) as ("table") return function(str, pat, n) as ("string", "string", "number") local ret = 0 for i = 1, n do ret = r.find(str, pat, ret + 1) if not ret then return nil end end return ret end end local last_regex local last_repl local function parse(r) as ("table") return function(delim, str, allow_unterminated, internal) as ("string", "string", "*", "*") local n = 1 local pat, close, rest while true do local a, b = str:find(lib.patesc(r.esc_chr) .. "*" .. lib.patesc(delim), n) if not a then if allow_unterminated then pat, close, rest = str, "", "" break end lib.error("could not parse pattern: expected '" .. delim .. "', got end of input") end if (b - a) % 2 == 0 then pat, close, rest = str:sub(1, b - 1), str:sub(b, b), str:sub(b + 1) break end n = b + 1 end if pat ~= "" then if not internal then last_regex = pat end return pat, close, rest end return lib.assert(last_regex, "no last regex to reuse found"), close, rest end end local function parse_list(r) as ("table") return function(s) as ("string") local delim, rest = s:match("^(.)(.*)$") if not delim then return {} end local ret = {} while rest ~= "" do local p, _, rest_ = r.parse(delim, rest, false, true) table.insert(ret, p) rest = rest_ end return ret end end local function with_last_repl(s) as ("function|string") if type(s) == "function" then return s end if s == "%" then return lib.assert(last_repl, "no last replacement to reuse found") end last_repl = s return s end m.lua = {esc_chr = "%", capture_all = "%0", capture_group = function(n) return "%" .. n end} function m.lua.esc(pat) as ("string") return (pat:gsub("[%^$()%%.[%]*+%-?]", "%%%1")) end m.lua.find = string.find m.lua.match = string.match m.lua.gmatch = string.gmatch function m.lua.gsub(str, pat, repl, n) as ("string", "string", "function|string", "number?") return str:gsub(pat, with_last_repl(repl), n) end m.lua.find_nth = find_nth (m.lua) m.lua.parse = parse (m.lua) m.lua.parse_list = parse_list(m.lua) m.lua.doc = [=[ This page is only a quick reference. For details, see [the manual][manual]. ## Character classes (Uppercase character = inverted set) - `` where `` not in `^$()%.[]*+-?`: `` itself - `%` where `` not alphanumeric: character `` (escaped) - `.`: all characters - `%a`: all letters - `%c`: control characters - `%d`: digits - `%g`: printable characters except space - `%l`: lowercase letters - `%p`: punctuation characters - `%s`: space characters - `%u`: uppercase letters - `%w`: alphanumeric characters - `%x`: hexadecimal digits - `[]`: all characters in `` - `[^]`: all characters not in `` ## Patterns - `*`: zero or more times - `+`: one or more times - `-`: zero or more times, shortest possible match - `?`: zero or one time - `%`: capture `` (1-9) - `%b`: string that starts with ``, ends with ``, and contains an equal amount of `` and `` - `%f[]`: empty string between a character not in `` and a character in ``, beginning and end of string are treated as `\0` ## Special sequences in replacement strings - `%0`: the entire matched string - `%`: capture `` (1-9) - `%%`: a literal `%` [manual]: https://www.lua.org/manual/5.4/manual.html#6.4.1 ]=] function m.s(text, pat, repl, flags, impl) as ("string", "string", "string", "string", "table?") impl = impl or m.curr if flags:find("!") then flags = flags:gsub("!", "") repl = with_last_repl(repl) local delim = ("="):rep(20) local d1 = "[" .. delim .. "[" local d2 = "]" .. delim .. "]" repl = "local _1, _2, _3, _4, _5, _6, _7, _8, _9 = ...; return " .. d1 .. repl:gsub("%%%[(=*)%[(.-)%]%1%]", d2 .. " .. %2 .. " .. d1) .. d2 repl = lib.assert(load(repl)) end if flags == "g" then return impl.gsub(text, pat, repl ) end if flags == "" then return impl.gsub(text, pat, repl, 1) end local n = lib.assert(tonumber(flags), "could not parse flags: " .. flags) local pos = impl.find_nth(text, pat, n) if not pos then return text, 0 end local tmp, ctr = impl.gsub(text:sub(pos), pat, repl, 1) return text:sub(1, pos - 1) .. tmp, ctr end m._bre_doc = [=[ This page is only a quick reference. For details, see [POSIX.1-2017][posix]. ## Character classes Named classes must be used inside `[]`, e.g. `[[:alnum:]]`. - `` where `` not in `^$()%.[]*+-?`: `` itself - `\` where `` in `\*^$`: character `` (escaped) - `.`: all characters - `[:alnum:]`: alphanumeric characters - `[:alpha:]`: all letters - `[:cntrl:]`: control characters - `[:digit:]`: digits - `[:lower:]`: lowercase letters - `[:print:]`: printable characters - `[:punct:]`: punctuation characters - `[:space:]`: space characters - `[:upper:]`: uppercase letters - `[:xdigit:]`: hexadecimal digits - `[]`: all characters in `` - `[^]`: all characters not in `` ## Regular Expressions - `\(\)`: match `` as subexpression - `*`: zero or more times - `{}`: `` times - `{,}`: `` to `` times, leave `` out for 0 and `` for infinity - `\`: matched string for subexpression `` (1-9) ## Special sequences in replacement strings - `&`: the entire matched string - `\`: matched string for subexpression `` (1-9) - `\\`: a literal `\` ## `neo-ed` Extensions - `\a` = `[[:alpha:]]`, `\A` = `[^[:alpha:]]` - `\c` = `[[:cntrl:]]`, `\C` = `[^[:cntrl:]]` - `\d` = `[[:digit:]]`, `\D` = `[^[:digit:]]` - `\g` = `[[:graph:]]`, `\G` = `[^[:graph:]]` - `\l` = `[[:lower:]]`, `\L` = `[^[:lower:]]` - `\p` = `[[:punct:]]`, `\P` = `[^[:punct:]]` - `\s` = `[[:space:]]`, `\S` = `[^[:space:]]` - `\u` = `[[:upper:]]`, `\U` = `[^[:upper:]]` - `\w` = `[[:alnum:]]`, `\W` = `[^[:alnum:]]` - `\x` = `[[:xdigit:]]`, `\X` = `[^[:xdigit:]]` [posix]: https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap09.html ]=] local rp = lib.require "rex_posix" if rp then local function extend(s) as ("string") return s:gsub("(\\*)\\(.)", function(pre, c) if #pre % 2 == 0 then return pre .. (({ ["a"] = "[[:alpha:]]", ["A"] = "[^[:alpha:]]", ["c"] = "[[:cntrl:]]", ["C"] = "[^[:cntrl:]]", ["d"] = "[[:digit:]]", ["D"] = "[^[:digit:]]", ["g"] = "[[:graph:]]", ["G"] = "[^[:graph:]]", ["l"] = "[[:lower:]]", ["L"] = "[^[:lower:]]", ["p"] = "[[:punct:]]", ["P"] = "[^[:punct:]]", ["s"] = "[[:space:]]", ["S"] = "[^[:space:]]", ["u"] = "[[:upper:]]", ["U"] = "[^[:upper:]]", ["w"] = "[[:alnum:]]", ["W"] = "[^[:alnum:]]", ["x"] = "[[:xdigit:]]", ["X"] = "[^[:xdigit:]]", })[c] or "\\" .. c) end end) end m.bre = {esc_chr = "\\", capture_all = "&", capture_group = function(n) return "\\" .. n end} function m.bre.esc(pat) as ("string") return (pat:gsub("[.[\\*^$]", "\\%1")) end function m.bre.find (str, pat, off) as ("string", "string", "number?") return rp.find (str, extend(pat), off or 1, 0) end function m.bre.match (str, pat, off) as ("string", "string", "number?") return rp.match (str, extend(pat), off or 1, 0) end function m.bre.gmatch(str, pat ) as ("string", "string" ) return rp.gmatch(str, extend(pat), 0) end local function gsub_repl(r) as ("function|string") if type(r) == "function" then return r end return with_last_repl(r) :gsub("(\\*)%%", function(pre) if #pre % 2 == 1 then return ("\\"):rep(#pre - 1) .. "%%" end return pre .. "%%" end) :gsub("(\\*)\\([1-9])", function(pre, n) if #pre % 2 == 1 then return pre .. "\\" .. n end return pre .. "%" .. n end) :gsub("(\\*)&", function(pre) if #pre % 2 == 1 then return ("\\"):rep(#pre - 1) .. "&" end return pre .. "%0" end) :gsub("\\\\", "\\") end function m.bre.gsub(str, pat, repl, n) as ("string", "string", "function|string", "number?") return rp.gsub( str, extend(pat), gsub_repl(repl), n, 0 ) end m.bre.find_nth = find_nth (m.bre) m.bre.parse = parse (m.bre) m.bre.parse_list = parse_list(m.bre) m.bre.doc = m._bre_doc end m.curr = m.bre or m.lua return m ]================================================================================] , "neo-ed.regex")) package.preload["neo-ed.state"] = assert(load( [================================================================================[ local as = require "neo-ed.lib.as" local lib = require "neo-ed.lib" local parser = require "neo-ed.parser" local regex = require "neo-ed.regex" local term = require "neo-ed.term" local widgets = require "neo-ed.widgets" local posix = require "posix" local mt = {__index = {}, __name = "state"} function mt.__index:add_conf(name, data) as ( "state", "string", {type = "string", descr = "string"} ) lib.assert(data.type == "boolean" or data.type == "number" or data.type == "string", "invalid config data type for config key " .. name .. ": " .. data.type) lib.assert(type(data.def) == data.type, "data default does not match declared type") self.conf_defs[name] = data return self end local autocomp_buffer_key = {} function mt.__index:autocomp_buffer() as ("state") return function(comps, pre, buf) local cache = lib.cache[buf.state.frame_key][autocomp_buffer_key] if not cache.dict then local dict = {} for i, b in pairs(buf.state.buffers) do dict[("%d" ):format(i)] = b:label() end for i, b in buf.state:recent_buffers() do dict[("+%d"):format(i)] = b:label() end for _, b in pairs(buf.state.buffers) do if b.name then dict[b.name] = b:label() end end cache.dict = lib.autocomp_dict(dict, function(k) return k, dict[k] end) end cache.dict(comps, pre) end end function mt.__index:autocomp_loc() as ("state") return function(comps, pre, buf) local scheme, rest = pre:match("^([a-z0-9_]+):(.*)$") if scheme then local hdl = buf.state.impl.loc[scheme] if type(hdl) == "table" and hdl.autocomp then hdl.autocomp(comps, rest, buf) end else for _, t in ipairs(buf.state.impl.loc) do local s = pre:match(t.pat) if s and t.autocomp then t.autocomp(comps, s, buf) end end end end end function mt.__index:check_executable(name, consequence) as ("state", "string", "string") local ok = lib.have_executable(name) if not ok then self:warn("missing executable " .. name .. ", " .. consequence) end return ok end function mt.__index:drop_cache() self.world_key = {} return self end -- TODO: integrate with `file_cache`? local editor_files_key = {} function mt.__index:editor_files(base) as ("state", "string?") base = base or "." local cache = lib.cache[self.world_key][editor_files_key] if not cache[base] then local cmd = lib.cmd "sed" "-e" "s:^./::" local s = self.curr:conf_get("prune_paths") if regex.curr == regex.bre then for _, r in ipairs(regex.curr.parse_list(s)) do cmd "-e" (("/%s/d"):format(r:gsub("/", "\\/"))) end elseif s ~= "" then self:warn("not implemented: prune_paths with non-standard regex implementation") end cmd = lib.cmd "find" (base) "-type" "f" "-size" "-1024k" | cmd cmd = self:filter("files", cmd, self) cache[base] = {} for l in lib.cmd.shell(cmd):read("L!") do table.insert(cache[base], self:realpath(l)) end end local started = lib.timestamp() local progress = false local t = cache[base] local i = 0 return function() i = i + 1 progress = progress or (not term.in_getline and lib.timestamp() - started > 1) if progress then term:progress(i, #t) end return t[i] end end -- TODO: explicit interactive / script mode? function mt.__index:err(s) as ("state", "string") table.insert(self.msgs, {type = "err", text = s}) if not posix.unistd.isatty(0) then self:print_msgs() os.exit(1, true) end return self end local file_key = {} function mt.__index:file_cache(loc) as ("state", "string") -- path = self:realpath(path) loc = self:loc(loc) local path = loc:get_path() local b = path and self:get_buffer_for(path) local cache = lib.cache[self.world_key][file_key] local locs = tostring(loc) if b then cache = cache[b.body.text_key] cache.text = cache.text or lib.lines_join(b.body:get()) return cache elseif path then cache[path] = cache[path] or lib.cache[{}] cache = cache[path] if not cache.text then cache.text = loc:read() end return cache else cache[self.curr.conf_key][locs] = cache[self.curr.conf_key][locs] or lib.cache[{}] cache = cache[self.curr.conf_key][locs] if not cache.text then cache.text = loc:read(self.curr) end return cache end end function mt.__index:filter(name, val, ...) as ("state", "string") local tbl = self.impl.filter[name] if not tbl then self:err("unknown filter: " .. name) return val end local prof = lib.prof and lib.profiler("filter " .. name) or nil for i, f in ipairs(tbl) do local p = prof and prof:start(lib.fninfo(f)) or nil local ok, msg = lib.pcall(f, val, ...) if ok then val = msg or val else self:err(("filter %s: [%d] = %s failed: %s"):format(name, i, lib.fninfo(f), msg)) end end if prof then prof:print() end return val end function mt.__index:fix_lru() as ("state") for i, b in self:recent_buffers() do b.lru = i if i == 1 then self.curr = b end end return self end function mt.__index:focus(buf) as ("state", "buffer") buf.lru = 0 self:fix_lru() return buf end function mt.__index:get_buffer(id) as ("state", "number|string") if id == "*" then return self:new() end local ret = nil if type(id) == "number" then ret = self.buffers[id] else local rel, n = id:match("^(%+?)(%d*)$") if rel then n = tonumber(n) or rel ~= "" and 2 if rel == "" and n then ret = self.buffers[n] else for i, b in self:recent_buffers() do if i == n then ret = b; break end end end else for _, b in pairs(self.buffers) do if b.name == id then ret = b; break end end end end return lib.assert(ret, "no such buffer: #" .. tostring(id)) end -- TODO: should non-path locations also be deduplicated? function mt.__index:get_buffer_for(path) as ("state", "string") path = self:realpath(path) for _, v in pairs(self.buffers) do if v:get_path() == path then return v end end end function mt.__index:hook(tbl, ...) as ("state", "table") local prof = lib.prof and lib.profiler("hook " .. (tbl.name or "")) or nil for i, f in ipairs(tbl) do local p = prof and prof:start(lib.fninfo(f)) or nil local ok, msg = lib.pcall(f, ...) if not ok then self:err(("hook %s: [%d] = %s failed: %s"):format((tbl.name or ""), i, lib.fninfo(f), msg)) end end if prof then prof:print() end end function mt.__index:info(s) as ("state", "string") table.insert(self.msgs, {type = "info", text = s}) return self end function mt.__index:loc(s) as ("state", "string") local ret = nil local scheme, rest = s:match("^([a-z0-9_]+):(.*)$") if scheme then local hdl = lib.assert(self.impl.loc[scheme], "unknown uri scheme: " .. s) if type(hdl) == "table" then hdl = hdl.fn end ret = hdl(rest, self) else ret = lib.match{ s = s, choose = self.impl.loc, args = {self}, def = function(s) for _, l in ipairs(self.impl.loc) do self:info(("tried to match %q for %s"):format(l.pat, lib.fninfo(l.fn))) end lib.error(("could not open location: %q"):format(s)) end, } end lib.assert(ret, ("could not open location: %q"):format(s)) local mt = getmetatable(ret) if mt and not mt.__name then mt.__name = "loc" end if mt and not mt.__index.get_path then mt.__index.get_path = function() end end if mt and not mt.__index.label then mt.__index.label = mt.__tostring end if mt and not mt.__index.read then mt.__index.read = lib.not_impl("loc.read" ) end if mt and not mt.__index.write then mt.__index.write = lib.not_impl("loc.write" ) end if mt and not mt.__index.append then mt.__index.append = lib.not_impl("loc.append") end return ret end function mt.__index:main() as ("state") local last_error = nil while true do self:hook(self.hooks.buffer.prompt_pre, self.curr) print() self:print_msgs() print(self.curr:status_line()) local ok, cmd = lib.pcall(self.curr.get_cmd, self.curr) self.frame_key = {} self:hook(self.hooks.buffer.prompt_post, self.curr) if not ok then self:err("failed to read input: " .. cmd) if last_error == cmd and lib.trace then os.exit(1, true) end last_error = cmd elseif not cmd then local ok, status = lib.pcall(self.quit, self) if not ok then self:err(status) end else local ok, status = lib.pcall(self.curr.cmd, self.curr, cmd) if not ok then self:err(status) end end end end function mt.__index:new(contents) as ("state", "string?") local ret = require("neo-ed.buffer")(self) if contents then ret:load_str(contents) end return ret end function mt.__index:open(loc) as ("state", "string") local l = self:loc(loc) local b = l:get_path() and self:get_buffer_for(l:get_path()) if b then return b, false end return require("neo-ed.buffer")(self, l), true end function mt.__index:pick(choices, opts) as ({"string|table"}, "table?") opts = opts or {} local tmp = {} for _, c in ipairs(choices) do table.insert(tmp, type(c) == "string" and {text = c} or c) end local ret = lib.assert(self:_picker(tmp, opts), "nothing selected") return lib.assert(choices[ret], ("picker returned invalid result: %q"):format(ret)) end function mt.__index:pick_buffer(buffers, opts) as ("state", "table?", "table?") if buffers then as ("*", {{buffer = "buffer", pos = "number?"}}) end opts = opts or {} if not buffers then buffers = {} for _, b in self:recent_buffers() do table.insert(buffers, {buffer = b}) end end return self:pick_buffer_or_file(buffers, opts).buffer end function mt.__index:pick_buffer_or_file(choices, opts) as ( "state", {{ buffer = "buffer?", path = "string?", pos = "number?", prefix = "string?", preview = "string?" }}, "table?" ) opts = opts or {} return self:shadow_root(function(root, paths) for _, c in ipairs(choices) do local path = nil local pos = nil if c.buffer then path, pos = paths[c.buffer], c.pos or c.buffer.body:pos() elseif c.path then path, pos = c.path , c.pos or 1 end pos = c.seek or pos and ("%d"):format(pos) or nil c.text = ("%s%s%s%s%s"):format( c.prefix or "", c.buffer and c.buffer:label(true) or term:sgr"weak" .. self:loc(c.path):label() .. term:sgr"reset", c.pos and "+" .. term:sgr"cyan" .. c.pos .. term:sgr"reset" or "", c.text and " " or "", c.text or "" ) local u = c.nup or c.ndn and 0 or 1 local d = c.ndn or c.nup and 0 or 2 c.preview = c.preview or path and tostring(lib.cmd (arg[0]) (("--show@%d/%d=%s"):format(u, d, pos)) (path)) or "true" c._sort_name = self:realpath(c.buffer and (c.buffer:get_path() or c.buffer:get_vpath()) or c.path, true) end if opts.sort then table.sort(choices, function(a, b) if a.sort_pre and b.sort_pre and a.sort_pre < b.sort_pre then return true end if a.sort_pre and b.sort_pre and a.sort_pre > b.sort_pre then return false end if a._sort_name < b._sort_name then return true end if a._sort_name > b._sort_name then return false end if (a.pos or 1) < (b.pos or 1) then return true end if (a.pos or 1) > (b.pos or 1) then return false end if (a.seek or "") < (b.seek or "") then return true end if (a.seek or "") > (b.seek or "") then return false end if a.sort_post and b.sort_post and a.sort_post < b.sort_post then return true end if a.sort_post and b.sort_post and a.sort_post > b.sort_post then return false end return false end) if not opts.start then local p = self:realpath(self.curr:get_path() or self.curr:get_vpath(), true) local n = self.curr.body:pos() opts.start = 1 for i, m in ipairs(choices) do if m._sort_name < p or m._sort_name == p and (not m.pos or m.pos <= n) then opts.start = i else break end end end end opts.show_preview = true return self:pick(choices, opts) end) end function mt.__index:pick_file(basedir, opts) as ("state", "string?", "table?") opts = opts or {} basedir = basedir or "." local tmp = {} for f in self:editor_files(basedir) do table.insert(tmp, {buffer = self:get_buffer_for(f), path = f}) end table.sort(tmp, function(a, b) return a.path < b.path end) return self:pick_buffer_or_file(tmp, opts).path end function mt.__index:pick_yes_no(question) as ("state", "string") while true do local s = term:getline{ prompt = question .. " ", state = self, autocomp = function(comp, pre) if pre == "" then comp[""] = "yes | no" comp["y"] = true comp["n"] = true elseif pre == "y" then comp[""] = "yes" elseif pre == "n" then comp[""] = "no" end end, } if s == "y" then return true end if s == "n" then return false end end end function mt.__index:_picker(choices, opts) as ( "state", {{text = "string"}}, "table?" ) if opts then as ("*", "*", {start = "number?"}) end if not choices[1] then lib.error("nothing to pick from") end opts = opts or {} local len = #("%d"):format(#choices) for i, c in ipairs(choices) do print(("%s%" .. len .. "%s\t%s"):format(i == opts.start and term:sgr"note" or "", i, term:sgr"reset", c.text)) end local ret = term:getline{prompt = "Enter a number:", state = self, history = {start and tostring(start) or ""}} if not ret or ret == "" then return nil end ret = tonumber(ret) if not ret or not choices[ret] then return self:_picker(mode, choices, start) end return ret end local msg_key = {} function mt.__index:print_msgs(skip_info) as ("state", "*") local cache = lib.cache[self.world_key][msg_key] cache.warn = cache.warn or {} cache.err = cache.err or {} for _, v in ipairs(self.msgs ) do local pre = v.type == "err" and term:sgr"bad rev" .. " ERR " .. term:sgr"nrev" or v.type == "warn" and term:sgr"note rev" .. " WARN " .. term:sgr"nrev" or not skip_info and term:sgr"weak rev" .. " INFO " .. term:sgr"reset" or nil local fmt = "" local text = v.text if (v.type == "err" or v.type == "warn") and term.info.ctl then fmt = term.info.ctl.bell or "" end if cache[v.type] and cache[v.type][v.text] then if not lib.trace then text = v.text:gsub("%s+", " ") local cut = utf8.offset(text, 60) text = text:sub(1, utf8.offset(text, 60)) .. (cut and "..." or "") end fmt = term:sgr"reset weak" end if cache[v.type] then cache[v.type][v.text] = true end if pre then io.stdout:write(pre, " ", fmt, text, term:sgr"reset", "\n") end end self.msgs = {} return self end function mt.__index:quit(force) as ("state", "*") while self.curr do self.curr:close(force) end end local realpath_key = {} function mt.__index:realpath(path, abs) as ("state", "string", "*") local cache = lib.cache[self.world_key][realpath_key] cache.__HOME = cache.__HOME or os.getenv("HOME"):gsub("/$", "") cache.__PWD = cache.__PWD or os.getenv("PWD" ):gsub("/$", "") cache.abs = cache.abs or {} cache.rel = cache.rel or {} path = path:gsub("^~", cache.__HOME) if not cache.abs[path] then local r = posix.stdlib.realpath(path) if not r then local base, rest = path:match("^(.+)/([^/]+)/?$") if not base then base, rest = ".", path end r = self:realpath(base, true) .. "/" .. rest r = r:gsub("%f[^\0/]([^/]+)/%.%.%f[\0/]", ""):gsub("%f[^\0/]%./", ""):gsub("/%.%f[\0/]", "") end cache.abs[path] = r end if abs then return cache.abs[path] end if not cache.rel[path] then local a = cache.abs[path] if a == cache.__PWD then cache.rel[path] = "." elseif a:find(cache.__PWD, 1, true) == 1 then cache.rel[path] = a:sub(#cache.__PWD + 2) else cache.rel[path] = a end end return cache.rel[path] end function mt.__index:recent_buffers() as ("state") local ret = {} for _, b in pairs(self.buffers) do table.insert(ret, b) end table.sort(ret, function(a, b) return a.lru < b.lru end) return ipairs(ret) end function mt.__index:register(buf) as ("state", "buffer") buf.id = self.next_buffer_id self.buffers[buf.id] = buf self.next_buffer_id = self.next_buffer_id + 1 buf.lru = math.maxinteger self:fix_lru() return self end function mt.__index:restart() as ("state") -- note: no shell escaping needed with execp local args = {[0] = arg[0]} for _, b in self:recent_buffers() do lib.assert(not b:is_modified(), "buffer modified: " .. b:label()) lib.assert(b:get_loc(), "buffer has no location: " .. b:label()) if b.name then table.insert(args, ":name " .. b.name) end table.insert(args, ("+%d,%d#"):format(b.body:sel_first(), b.body:sel_last())) table.insert(args, tostring(b:get_loc())) end lib.assert(posix.execp(arg[0], args)) lib.error("restart failed") end function mt.__index:shadow_root(f, buffer) as ("state", "function", "buffer?") if self._in_shadow_root then return f(self._in_shadow_root.root, self._in_shadow_root.paths) end local root = nil local paths = {} for _, r in ipairs{"/dev/shm", "/tmp"} do if lib.cmd "findmnt" "--types" "tmpfs" "--target" (r) :hush() :ok() then root = r break end end if root then local ok, msg = lib.pcall(function() root = ("%s/ned-%d"):format(root, posix.unistd.getpid()) lib.cmd "rm" "-r" (root) :hush() :ok() lib.assert(posix.sys.stat.mkdir(root, posix.sys.stat.S_IRWXU)) for _, b in pairs(self.buffers) do if not buffer or buffer == b then if b:get_path() and not b:is_modified() then paths[b] = b:get_path() else paths[b] = ("%s/root%s"):format(root, self:realpath(b:get_vpath(), true)) lib.mkdir(lib.dirname(paths[b])) local h = io.open(paths[b], "w") h:write(lib.lines_join(b.body:get())) end if not paths[b] then self:warn("shadow_root: missing " .. b:label()) end end end if buffer then local p = ("%s/root%s"):format(root, self:realpath(buffer:get_vpath(), true)) local d, f = p:match("^(.*)/([^/]+)$") lib.assert(d, ("could not process supposedly absolute path: %q"):format(p)) lib.mkdir(d) for i, pt in ipairs(buffer.history) do paths[i] = ("%s/%06d-%s"):format(d, i, f) local h = io.open(paths[i], "w") h:write(lib.lines_join(pt.body:get())) end end end) if not ok then self:warn("failed to create shadow root: " .. msg) lib.cmd "rm" "-r" (root) :hush() :ok() root = nil end else self:warn("could not find tmpfs for shadow root") end self._in_shadow_root = {root = root, paths = paths} local _ = lib.defer(function() self._in_shadow_root = nil if root then lib.cmd "rm" "-r" (root) :hush() :ok() end end) return self:shadow_root(f) end function mt.__index:unregister(buf) as ("state", "buffer") self.buffers[buf.id] = nil self.curr = nil self:fix_lru() if not self.curr then os.exit(0, true) end return self end function mt.__index:warn(s) as ("state", "string") table.insert(self.msgs, {type = "warn", text = s}) return self end local function new(files, safety_lvl) as ({{loc = "string", cmds = {"string"}, contents = "string?"}}, "number?") safety_lvl = safety_lvl or 0 local ret = setmetatable({ buffers = {}, config_dir = lib.xdg.config_home .. "/neo-ed", frame_key = {}, help = {}, hooks = { buffer = { close = {name = "buffer.close" }, -- triggered before closing a buffer load_pre = {name = "buffer.load_pre" }, -- triggered before loading buffer contents load_post = {name = "buffer.load_post" }, -- triggered after loading buffer contents loc_post = {name = "buffer.loc_post" }, -- triggered after setting or changing the location of a buffer, additionally receives the old location print_pre = {name = "buffer.print_pre" }, -- triggered before printing code print_post = {name = "buffer.print_post" }, -- triggered after printing code prompt_pre = {name = "buffer.prompt_pre" }, -- triggered before writing the prompt prompt_post = {name = "buffer.prompt_post"}, -- triggered after receiving input from the user save_pre = {name = "buffer.save_pre" }, -- triggered before saving a buffer to a file save_post = {name = "buffer.save_post" }, -- triggered after saving a buffer to a file }, state = { init_post = {name = "state.init_post"}, }, }, next_buffer_id = 1, print = { pre = {}, highlight = {{name = "none", fn = function(lines) return lines end}}, post = {}, }, history = {}, impl = { autocomp = { cmd = {}, dict = {}, src = {}, }, cmd = { addr = { range = {}, single = {}, }, buffer = {}, line = {}, suf = {}, }, diff = {}, filter = { conf = {name = "conf" }, files = {name = "files"}, read = {name = "read" }, write = {name = "write"}, }, loc = {}, }, conf_defs = {}, msgs = {}, widgets = { widgets.buffer_lines, widgets.buffer_label, widgets.buffer_vpath, widgets.other_buffers, widgets.buffer_stat_mode, widgets.buffer_stat_owner, widgets.buffer_stat_write, widgets.buffer_conf_tabs, widgets.buffer_conf_crlf, }, world_key = {}, }, mt) ret:add_conf("indent" , {type = "number" , def = 4 , descr = "indentation depth step (spaces)" , drop_cache = true}) ret:add_conf("tab2spc" , {type = "boolean", def = false, descr = "indent using spaces, convert tabs on entry", drop_cache = true}) ret:add_conf("tabs" , {type = "number" , def = 4 , descr = "tab width" , drop_cache = true}) ret:add_conf("highlighter", {type = "string" , def = "" , descr = "highlighter to use" , drop_cache = true}) ret:add_conf("prune_paths", {type = "string" , def = "" , descr = "regex list matching paths to ignore" , drop_cache = true}) ret:add_conf("words", { type = "string" , def = [[/[A-Za-z_][A-Za-z0-9_]\{2,\}/[A-Za-z_][A-Za-z0-9_.]\{2,\}/]], -- TODO: lua? descr = "list of regexes that match words", drop_cache = true }) table.insert(ret.hooks.buffer.prompt_pre, function(buf) buf:check_for_writes() end) if lib.prof then ret.plugins_prof = lib.profiler("plugin initialization") end ret.init_file = ret.config_dir .. "/init.lua" if safety_lvl >= 4 then ret:info("using posix initialization") require("neo-ed.plugins").posix(ret) elseif safety_lvl == 3 then ret:info("using testing initialization") require("neo-ed.plugins").testing(ret) elseif safety_lvl == 2 then ret:info("using reduced initialization") elseif safety_lvl == 1 then ret:info("using default initialization") require("neo-ed.plugins").full(ret) else local ok, _, errno = posix.unistd.access(ret.init_file, "r") if not ok and errno == posix.errno.ENOENT then return new(files, 1) :warn("no init file found") :info("Use command `:config` to open init file with defaults pre-filled.") end local f, err = loadfile(ret.init_file) if not f then return new(files, 1) :err("error loading init file: " .. err) :warn("Use command `:config` to open init file, then fix errors and try again.") end local ok, err = lib.pcall(f, ret) if not ok then return new(files, 1) :err("error running init file: " .. err) :warn("Use command `:config` to open init file, then fix errors and try again.") end end if ret.plugins_prof then ret.plugins_prof:print() ret.plugins_prof = nil end local tmp = ret.impl.filter.write ret.impl.filter.write = {name = "write"} for i = #tmp, 1, -1 do table.insert(ret.impl.filter.write, tmp[i]) end ret:hook(ret.hooks.state.init_post, ret) local show_progress = #files >= 3 if show_progress then for _, v in ipairs(files) do if v.cmds[1] then show_progress = false break end end end for i, v in ipairs(files) do if show_progress then term:progress(i - 1, #files) end ret._cmd = "o " .. v.loc local ok, msg = lib.pcall(function() if v.loc and v.loc ~= "" then return ret:open(v.loc) end return ret:new(v.contents) end) if ok then if v.vpath then msg:set_vpath(v.vpath) end for _, c in ipairs(v.cmds or {}) do local ok, msg = lib.pcall(msg.cmd, msg, c) if not ok then ret:err(msg) end end else ret:err(msg) end ret._cmd = nil end if show_progress then term:progress() end if not ret.curr then ret:warn("no open buffers after initialization, exiting") ret:info(("tried to open %d buffers"):format(#files)) ret:print_msgs() os.exit(1, true) end return ret end return new ]================================================================================] , "neo-ed.state")) package.preload["neo-ed.term"] = assert(load( [================================================================================[ local as = require "neo-ed.lib.as" local lib = require "neo-ed.lib" local posix = require "posix" local term = os.getenv("TERM") local have_no_color = os.getenv("NO_COLOR") local have_clicolor_force = os.getenv("CLICOLOR_FORCE") local have_force_color = os.getenv("FORCE_COLOR") local have_stdin_tty = not not posix.unistd.isatty(0) local have_stdout_tty = not not posix.unistd.isatty(1) if (term or "") == "" then term = "dumb" end local lc_all = os.getenv("LC_ALL") local lc_ctype = os.getenv("LC_CTYPE") local lang = os.getenv("LANG") local have_tput = lib.have_executable("tput") local mt = {__index = {fmt={}}, __name = "term"} mt.__index.str = require "neo-ed.term.str" function mt.__index:display_width(s) return self:str(s).len end function mt.__index:detect() as ("term") -- defaults: enable formatting self.feat.color = true self.feat.cursor = true self.feat.fmt = true -- defaults: interactivity based on ttys self.feat.interactive = have_stdin_tty and have_stdout_tty -- defaults: desktop if DISPLAY or WAYLAND_DISPLAY is set self.feat.desktop = (os.getenv("DISPLAY") or "") ~= "" or (os.getenv("WAYLAND_DISPLAY") or "") ~= "" -- defaults: utf-8 according to locale if (lc_all or "") ~= "" then self.feat.utf8 = lc_all :lower():find("utf.*8") elseif (lc_ctype or "") ~= "" then self.feat.utf8 = lc_ctype:lower():find("utf.*8") elseif (lang or "") ~= "" then self.feat.utf8 = lang :lower():find("utf.*8") else self.feat.utf8 = false end -- except: disable terminal formatting and cursor control if no terminal if term == "dumb" or not have_stdout_tty then self.feat.color = false self.feat.cursor = false self.feat.fmt = false end -- except: disable features if tput says so if have_tput then if (lib.cmd "tput" "bold" :read("a?") or "") == "" then self.feat.fmt = false end if tonumber(lib.cmd "tput" "colors" :read("a?") or "-1") < 8 then self.feat.color = false end if (lib.cmd "tput" "cuf1" :read("a?") or "") == "" then self.feat.cursor = false end end -- except: respect environment overrides if (have_clicolor_force or "") ~= "" or (have_force_color or "") ~= "" then self.feat.color = true self.feat.fmt = true end if (have_no_color or "") ~= "" then self.feat.color = false end -- except: apply environment overrides self:set(os.getenv("NEO_ED_TERM") or "") return self end function mt.__index:get() as ("term") local ret = {} for _, k, v in lib.opairs(self.feat) do table.insert(ret, v and "+" or "-") table.insert(ret, k) end return table.concat(ret) end function mt.__index:set(attrs) as ("term", "string") for op, ft in attrs:gmatch("([+-])([^+-]*)") do if self.feat[ft] ~= nil then self.feat[ft] = op == "+" end end self:configure() end function mt.__index:configure() as ("term") self.box.hline = self.feat.utf8 and "─" or "-" self.box.vline = self.feat.utf8 and "│" or "|" self.box.vtick = self.feat.utf8 and "╵" or "'" self.box.ulcorner = self.feat.utf8 and "┌" or "+" self.box.urcorner = self.feat.utf8 and "┐" or "+" self.box.llcorner = self.feat.utf8 and "└" or "+" self.box.lrcorner = self.feat.utf8 and "┘" or "+" self.box.ttee = self.feat.utf8 and "┬" or "+" self.box.ltee = self.feat.utf8 and "├" or "+" self.box.btee = self.feat.utf8 and "┴" or "+" self.box.rtee = self.feat.utf8 and "┤" or "+" self.box.plus = self.feat.utf8 and "┼" or "+" self.dag.node = self.feat.utf8 and "╽" or "*" self.dag.node_end = self.feat.utf8 and "╻" or "*" self.dag.node_branch = self.feat.utf8 and "┟" or "*" self.dag.node_merge = self.feat.utf8 and "┟" or "*" self.dag.branch_corner = self.feat.utf8 and "╯" or "+" self.dag.merge_corner = self.feat.utf8 and "╮" or "+" self.dag.hext = self.feat.utf8 and "─" or "-" self.dag.vext = self.feat.utf8 and "│" or "|" self.dag.crossover = self.feat.utf8 and "┤" or "|" self.dag.void = " " self.info.sgr.reset = self.feat.fmt and "0" or nil self.info.sgr.bold = self.feat.fmt and "1" or nil self.info.sgr.nbold = self.feat.fmt and "22" or nil -- <- not a typo i swear self.info.sgr.ul = self.feat.fmt and "4" or nil self.info.sgr.nul = self.feat.fmt and "24" or nil self.info.sgr.rev = self.feat.fmt and "7" or nil self.info.sgr.nrev = self.feat.fmt and "27" or nil self.info.sgr.red = self.feat.color and "31" or nil self.info.sgr.green = self.feat.color and "32" or nil self.info.sgr.yellow = self.feat.color and "33" or nil self.info.sgr.blue = self.feat.color and "34" or nil self.info.sgr.magenta = self.feat.color and "35" or nil self.info.sgr.cyan = self.feat.color and "36" or nil self.info.sgr.accent = self.feat.color and "34" or nil self.info.sgr.bad = self.feat.color and "31" or nil self.info.sgr.faint = self.feat.color and "30" or nil self.info.sgr.good = self.feat.color and "32" or nil self.info.sgr.note = self.feat.color and "33" or nil self.info.sgr.weak = self.feat.color and "37" or nil self.info.ctl = self.feat.cursor and { u = function(n ) return ("\x1b[%dA" ):format(n or 1 ) end, d = function(n ) return ("\x1b[%dB" ):format(n or 1 ) end, r = function(n ) return ("\x1b[%dC" ):format(n or 1 ) end, l = function(n ) return ("\x1b[%dD" ):format(n or 1 ) end, nl = function(n ) return ("\x1b[%dE" ):format(n or 1 ) end, pl = function(n ) return ("\x1b[%dF" ):format(n or 1 ) end, to = function(r, c) return ("\x1b[%d;%dH"):format(r or 1, c or 1) end, cr = "\r", clear = "\x1b[2J", erase_r = "\x1b[K", l_erase = "\x1b[1K", erase = "\x1b[2K", bell = self.feat.interactive and "\a" or "", vis = self.feat.interactive and (have_tput and lib.cmd "tput" "cnorm" :read("l!") or "\x1b[?25h") or "", nvis = self.feat.interactive and (have_tput and lib.cmd "tput" "civis" :read("l!") or "\x1b[?25l") or "", } or nil self.title = self.feat.desktop and function(self, s) as ("term", "string") return ("\x1b]0;%s\x1b\\"):format(s) end or nil self.key = {} if self.feat.interactive then posix.signal.signal(posix.signal.SIGINT, function() end) end return self end function mt.__index:refresh() as ("term") self.rows, self.cols = nil, nil local r = os.getenv("NEO_ED_TERM_LINES") if (r or "") ~= "" then self.rows = tonumber(r) end local c = os.getenv("NEO_ED_TERM_COLUMNS") if (c or "") ~= "" then self.cols = tonumber(c) end if (not self.rows or not self.cols) and lib.have_executable("tput") then self.rows = self.rows or tonumber(lib.cmd "tput" "lines" :read("l!")) self.cols = self.cols or tonumber(lib.cmd "tput" "cols" :read("l!")) end if (not self.rows or not self.cols) and have_stdout_tty then local r, c = lib.cmd "stty" "size" :read("l!") :match("^(%d+) (%d+)$") if r then self.rows, self.cols = self.rows or tonumber(r), self.cols or tonumber(c) end end self.rows = self.rows or 24 self.cols = self.cols or 80 return self end function mt.__index:_raw() as ("term") if not self.feat.cursor or not self.feat.interactive then return end local t = posix.termio local attrs = t.tcgetattr(1) t.tcsetattr(0, t.TCSANOW, { iflag = attrs.iflag & ~(t.IGNBRK | t.BRKINT | t.PARMRK | t.ISTRIP | t.INLCR | t.IGNCR | t.ICRNL | t.IXON), oflag = attrs.oflag & ~t.OPOST, lflag = attrs.lflag & ~(t.ECHO | t.ECHONL | t.ICANON | t.ISIG | t.IEXTEN), cflag = attrs.cflag & ~(t.CSIZE | t.PARENB) | t.CS8, cc = { [t.VMIN] = 1, [t.VTIME] = 0 }, }) local winch = nil if posix.signal.SIGWINCH then winch = posix.signal.signal(posix.signal.SIGWINCH, function() self:refresh() end) end return lib.defer(function() if posix.signal.SIGWINCH then posix.signal.signal(posix.signal.SIGWINCH, winch) end t.tcsetattr(1, t.TCSANOW, attrs) end) end function mt.__index:getline(tbl) as ("term", "table") if self.feat.cursor and self.feat.interactive then local p = require "neo-ed.term.prompt" (self, tbl) return p:run() end io.stdout:write(tbl.prompt or ""):flush() local r = io.stdin:read("l") if not posix.unistd.isatty(0) then print(r or "") end return r end function mt.__index:sgr(s) as ("term", "string") local ret = {} for s_ in s:gmatch("%S+") do table.insert(ret, self.info.sgr[s_]) end if ret[1] then return "\x1b[" .. table.concat(ret, ";") .. "m" end return "" end function mt.__index:progress(done, total, active) as ("term", "number?", "number?", "number?") if not self.feat.cursor or not self.feat.interactive or self.in_getline then return self end if not done then io.stdout:write(self.info.ctl.cr, self.info.ctl.erase_r):flush() return self end local l_no = #tostring(total) local l_total = self.cols - (2*l_no + 5) local l_done = math.floor(done / total * l_total + 0.5) local l_active = math.min(active and math.floor(active / total * l_total + 0.5) or 0, l_total - l_done) local l_empty = l_total - l_done - l_active io.stdout:write( self.info.ctl.cr, ("%" .. l_no .. "d/%d ["):format(done, total), self:sgr"good", (self.feat.utf8 and "█" or "#"):rep(l_done ), self:sgr"note", (self.feat.utf8 and "━" or "="):rep(l_active), self:sgr"bad" , (self.feat.utf8 and "─" or "-"):rep(l_empty ), self:sgr"reset", "]" ):flush() return self end function mt.__index:spinner(text) as ("term", "string?") if not self.feat.cursor or not self.feat.interactive or self.in_getline then return self end if not text then io.stdout:write(self.info.ctl.cr, self.info.ctl.erase_r):flush() self._spinner_ctr = nil return self end local spinner = self.feat.utf8 and {"⠙", "⠸", "⠴", "⠦", "⠇", "⠋"} or {"|", "/", "-", "\\"} self._spinner_ctr = self._spinner_ctr and (self._spinner_ctr + 1) % #spinner or 0 local s = self:str(("[%s] %s"):format(spinner[self._spinner_ctr + 1], text:gsub("\n", " "))) io.stdout:write( self.info.ctl.cr, self:sgr"weak", s:show_first(self.cols), self:sgr"reset", self.info.ctl.erase_r ):flush() return self end local ret = setmetatable({ box = {}, dag = {}, feat = {}, info = {sgr = {}}, key = {}, tab = " ", }, mt) return ret:detect():refresh() ]================================================================================] , "neo-ed.term")) package.preload["neo-ed.term.prompt"] = assert(load( [================================================================================[ -- Inspiration and reference: `https://github.com/antirez/linenoise/blob/master/linenoise.c` local as = require "neo-ed.lib.as" local lib = require "neo-ed.lib" local posix = require "posix" local mt = {__index = {}, __name = "term.prompt"} function mt:__close() self.term.in_getline = false end function mt:__gc () self.term.in_getline = false end function mt.__index.autocomp() end function mt.__index:get_some() as ("term.prompt") local r, _, errno = posix.unistd.read(0, 1) if errno == posix.errno.EINTR then return self:get_some() end return r ~= "" and r end function mt.__index:get_more() as ("term.prompt") local n = posix.poll.rpoll(0, 100) if not n or n == 0 then return false end return self:get_some() end function mt.__index:msg(fmt, ...) as ("term.prompt", "string") io.stdout:write( self.term.info.ctl.cr, self.term.info.ctl.erase_r, (self.term:str(fmt:format(...)):show_first(self.term.cols - 1)) ):flush() return self:handle(self:get_some() or "\x04") end function mt.__index:from_history(pos) as ("term.prompt", "number?") self.pre = self.term:str(self.history[pos or self.history_pos]) self.suf = self.term:str("" ) end function mt.__index:to_history(pos) as ("term.prompt", "number?") local ret = tostring(self.pre) .. tostring(self.suf) self.history[pos or self.history_pos] = ret return ret end function mt.__index:commit() as ("term.prompt") local ret = self:to_history(#self.history) for i = #self.history - 1, 1, -1 do if self.history[i] == ret or self.history[i] == "" then table.remove(self.history, i) end end return ret end function mt.__index:handle(input) as ("term.prompt", "string") -- cache and reset, to avoid having this line after every choice below local offer_completion = self.offer_completion self.offer_completion = false if input == "\x0e" --[[ Ctrl-N ]] or input == "\x1b[B" --[[ Down ]] then if self.history[self.history_pos + 1] then self:to_history() self.history_pos = self.history_pos + 1 self:from_history() end elseif input == "\x10" --[[ Ctrl-P ]] or input == "\x1b[A" --[[ Up ]] then if self.history[self.history_pos - 1] then self:to_history() self.history_pos = self.history_pos - 1 self:from_history() end elseif input == "\x12" --[[ Ctrl-R ]] and self.state then self:to_history() local picks = {} for i, s in ipairs(self.history) do picks[i] = {idx = i, text = s} end local attrs = posix.termio.tcgetattr(1) local p = self.state:pick(picks, {start = self.history_pos}) posix.termio.tcsetattr(0, posix.termio.TCSAFLUSH, attrs) self.history_pos = p.idx self:from_history() elseif input == "\x00" --[[ Ctrl-Space for some reason o.0 ]] then if not offer_completion then self.offer_completion = true return elseif self.state then local pre = self.pre:show_last(self.term.cols // 3) local picks = {} local comp_set = {} self.autocomp(comp_set, tostring(self.pre)) for k, v in pairs(comp_set) do table.insert(picks, { text = table.concat{ self.term:sgr"weak", pre, self.term:sgr"rev good", k, self.term:sgr"reset", "\t", type(v) == "string" and v or "", }, comp = k, }) end table.sort(picks, function(a, b) return a.comp < b.comp end) local ok, p = pcall(self.state.pick, self.state, picks) if ok then self.pre:append(p.comp) end self.offer_completion = true else io.stdout:write(self.term.info.ctl.bell):flush() end elseif input == "\n" or input == "\r" or input == "\r\n" or input == "\x1b[27;2;13~" --[[ Shift-NL ]] or input == "\x1b[27;5;13~" --[[ Ctrl-NL ]] then return self:commit() elseif input == "\x02" --[[ Ctrl-B ]] or input == "\x1b[D" --[[ Left ]] then if self.pre[1] then self.suf:prepend(self.pre:remove()) end elseif input == "\x06" --[[ Ctrl-F ]] or input == "\x1b[C" --[[ Right ]] then if self.suf[1] then self.pre:append(self.suf:remove(1)) end elseif input == "\x1b[1;5D" --[[ Ctrl-Left ]] then if self.pre[1] then self.suf:prepend(self.pre:remove()) end while self.pre[1] and (self.pre[#self.pre].text:find("^%w+$") or not self.suf[1].text:find("^%w+$")) do self.suf:prepend(self.pre:remove()) end elseif input == "\x1b[1;5C" --[[ Ctrl-Right ]] then if self.suf[1] then self.pre:append(self.suf:remove(1)) end while self.suf[1] and (self.suf[1].text:find("^%w+$") or not self.pre[#self.pre].text:find("^%w+$")) do self.pre:append(self.suf:remove(1)) end elseif input == "\x01" --[[ Ctrl-A ]] or input == "\x1b[H" --[[ Home ]] or input == "\x1b[1~" then if #self.pre == 0 then while self.suf[1] and self.suf[1].text:find("^%s+$") do self.pre:append(self.suf:remove(1)) end else self.suf = self.pre:append(self.suf) self.pre = self.term:str("") end elseif input == "\x05" --[[ Ctrl-E ]] or input == "\x1b[F" --[[ End ]] or input == "\x1b[4~" then self.pre:append(self.suf) self.suf = self.term:str("") elseif input == "\x08" --[[ Ctrl-H ]] or input == "\x1b[3~" --[[ Delete ]] then self.suf:remove(1) elseif input == "\x7f" --[[ Backspace ]] then self.pre:remove() self.offer_completion = true elseif input == "\x03" --[[ Ctrl-C ]] then lib.error("keyboard interrupt") elseif input == "\x04" --[[ Ctrl-D ]] then if not self.had_input or (not self.pre[1] and not self.suf[1]) then return false end io.stdout:write(self.term.info.ctl.bell):flush() elseif input == "\x0b" --[[ Ctrl-K ]] then self.suf = self.term:str("") elseif input == "\x15" --[[ Ctrl-U ]] then self.pre, self.suf = self.term:str(""), self.term:str("") elseif input == "\x0c" and false --[[ Ctrl-L ]] then --[[ TODO: clear screen? ]] ; elseif input == "\x14" and false --[[ Ctrl-T ]] then --[[ TODO: swap current and previous characters? ]] ; elseif input == "\x17" and false --[[ Ctrl-W ]] then --[[ TODO: delete word? ]] ; elseif input:find("^\x1b.*") or not utf8.len(input, 1) then local c = self:get_more() if c then return self:handle(input .. c) end if input == "\x1b" then if not self.had_input or (not self.pre[1] and not self.suf[1]) then return false end io.stdout:write(self.term.info.ctl.bell):flush() else return self:msg( "%scould not process input sequence: %q", self.term.info.ctl.bell, input ) end elseif input == "\t" then if offer_completion then if self.completion then self.pre:append(self.completion) self.offer_completion = true else io.stdout:write(self.term.info.ctl.bell):flush() end else self.pre:append(self.on_tab) end else self.pre:append(input) self.offer_completion = input ~= " " end end function mt.__index:build_comp() as ("term.prompt") local fmt_info = self.term:str(self.term:sgr"rev" ):visual() local fmt_hint = self.term:str(self.term:sgr"good" ):visual() local fmt_hint_info = self.term:str(self.term:sgr"good ul" ):visual() local fmt_hint_part = self.term:str(self.term:sgr"note" ):visual() local fmt_comp = self.term:str(self.term:sgr"good rev"):visual() local fmt_comp_part = self.term:str(self.term:sgr"info rev"):visual() local fmt_reset = self.term:str(self.term:sgr"reset" ):visual() local comp_set = {} self.autocomp(comp_set, tostring(self.pre)) local trie = {leaves = 0, len = 0, next = {}, sort_by = "", prefix = self.term:str("")} for k, v in pairs(comp_set) do local bin = trie for _, t in ipairs(self.term:str(k)) do bin.leaves = bin.leaves + 1 if not bin.next[t.text] then bin.next[t.text] = { leaves = 0, len = bin.len + t.len, next = {}, sort_by = bin.sort_by .. t.text, prefix = bin.prefix:dup(), } table.insert(bin.next[t.text].prefix, t) end bin = bin.next[t.text] end bin.leaves = bin.leaves + 1 bin.comp = v end -- list of completion prefixes to show, gets iteratively expanded from -- the trie root until the line is full local comps = {trie} while true do -- sort by expansion priority ascending to reduce `table.remove` shuffling table.sort(comps, function(a, b) if a.expanded and not b.expanded then return true end if not a.expanded and b.expanded then return false end if a.leaves < b.leaves then return true end if a.leaves > b.leaves then return false end if a.len > b.len then return true end if a.len < b.len then return false end return a.sort_by > b.sort_by end) -- find next node to expand, note that already expanded nodes -- can stay in the list if they are a completion local expand = nil for i = #comps, 1, -1 do if not comps[i].expanded and next(comps[i].next) then expand = i; break end end if not expand then break end -- calculate hypothetical length if this node was expanded local len = -1 for _, b in ipairs(comps) do len = len + b.len + 1 end for _, b in pairs(comps[expand].next) do len = len + b.len + 1 end if not comps[expand].comp then len = len - comps[expand].len - 1 end -- ^ subtract the length of the expanded node again if it will be removed if len >= self.term.cols then break end local expanded = comps[expand] expanded.expanded = true if not expanded.comp then table.remove(comps, expand) end for _, b in pairs(expanded.next) do table.insert(comps, b) end end table.sort(comps, function(a, b) return a.sort_by < b.sort_by end) -- hints in second line local hints = nil if comps[2] then hints = self.term:str("") for i, c in ipairs(comps) do if i > 1 then hints:append(" ") end hints:append(type(c.comp) == "string" and fmt_hint_info or c.comp and fmt_hint or fmt_hint_part) hints:append(c.prefix) hints:append(fmt_reset) end end -- longest common prefix of all completions local common = trie while true do if common.comp then break end local c = next(common.next) if not c or next(common.next, c) then break end common = common.next[c] end local info = common and type(common.comp) == "string" and self.term:str(common.comp) if info then info = self.term:str(" "):append(fmt_info):append(info):append(fmt_reset) end -- trim completion using existing text after cursor local comp = nil if common then local last = nil for offset = 0, #common.prefix - 1 do local match = true for i = 1, #common.prefix - offset do if not self.suf[i] or common.prefix[offset + i].text ~= self.suf[i].text then match = false break end end if match then last = offset; break end end if last ~= 0 then comp = self.term:str(""):append(common.comp and fmt_comp or fmt_comp_part) comp:append(common.prefix:bake(1, last)) comp:append(fmt_reset) end if last then info = nil end end return comp, info, hints end function mt.__index:run() as ("term.prompt") local prof = lib.prof and not self.had_input and lib.profiler("prompt") or nil local _ = prof and lib.defer(function() prof:print() end) local __ = self.term:_raw() while true do local comp, info, hints if self.offer_completion then local _ = prof and prof:start("completion") comp, info, hints = self:build_comp() end if prof then prof:start("render line 1") end self.completion = comp and tostring(comp) or nil if self.completion == "" then self.completion = nil end if hints then self.line2 = true end -- the prompt is always shown in full, if at all possible local prompt_s, prompt_len = self.prompt:show_first(self.term.cols) local len_left = self.term.cols - prompt_len -- next: the completion text local comp_s, comp_len = "", 0 if comp then comp_s, comp_len = comp:show_first(len_left) end len_left = len_left - comp_len -- where in the line is the cursor? local want_pre_len = prompt_len + self.pre.len local want_line_len = want_pre_len + self.suf.len local cursor_ratio = math.min(1, want_pre_len / math.max(1, want_line_len)) -- the completion info gets truncated down to 1/2 screen if the line overflows local info_s, info_len = "", 0 if info then info_s, info_len = info:show_first( math.min(len_left, math.max(self.term.cols // 2, self.term.cols - want_pre_len - comp_len )) ) end len_left = len_left - info_len -- truncate the prefix so that the ratio cursor/screen is equal to cursor/line on overflow local pre_maxlen = math.min(len_left, self.term.cols - prompt_len - 1) if want_line_len + comp_len + info_len >= self.term.cols then pre_maxlen = math.min(pre_maxlen, math.ceil((self.term.cols - prompt_len - 1) * cursor_ratio)) end local pre_s, pre_len = self.pre:show_last(math.min(pre_maxlen, self.term.cols - prompt_len - 1), true) -- the suffix is drawn after the comp and below the hint local suf_s, suf_len = self.suf:show_first(self.term.cols - prompt_len - pre_len - comp_len, true) if prof then prof:stop(); prof:start("render line 2") end local hints_s, hints_len = "", 0 if hints then hints_s, hints_len = hints:show_first(self.term.cols) end if prof then prof:stop(); prof:start("print") end io.stdout:write( self.term.info.ctl.cr, self.term.info.ctl.erase_r, prompt_s, self.term:sgr"reset", pre_s , self.term:sgr"reset", comp_s , self.term:sgr"reset", suf_s , self.term:sgr"reset" ) io.stdout:write( self.term.info.ctl.cr, prompt_len + pre_len + comp_len > 0 and self.term.info.ctl.r(prompt_len + pre_len + comp_len) or "", info_s, self.term:sgr"reset" ) if self.line2 then -- use `\n` to force scrolling if on last line io.stdout:write("\n", self.term.info.ctl.cr, self.term.info.ctl.erase_r) if hints then -- start close to the cursor's column, if there is enough space if hints_len < self.term.cols then io.stdout:write(self.term.info.ctl.r( math.min(prompt_len + pre_len, self.term.cols - hints_len) )) end io.stdout:write(hints_s, self.term:sgr"reset") end io.stdout:write(self.term.info.ctl.pl()) else io.stdout:write(self.term.info.ctl.cr) end if prompt_len + pre_len > 0 then io.stdout:write(self.term.info.ctl.r(prompt_len + pre_len)) end io.stdout:write(self.term.info.ctl.vis):flush() if prof then prof:stop(); prof:start("read") end local input = self:get_some() if prof then prof:stop(); prof:start("handle") end local r = input and self:handle(input) if prof then prof:stop() end if not input or input and r ~= nil then local _ = prof and prof:start("exit prompt") -- remove the line entirely if not r then io.stdout:write( self.term.info.ctl.cr, self.term.info.ctl.erase_r, "\n", self.term.info.ctl.cr, self.term.info.ctl.erase_r ):flush() return r -- activate special exit prompt if given elseif self.prompt_done then prompt_s, prompt_len = self.term:str(self.prompt_done):show() end -- simplified form of the line, with no completion etc. local len_left = self.term.cols - prompt_len local pre_s, pre_len = self.pre:show_last (len_left - 1); len_left = len_left - pre_len local suf_s, suf_len = self.suf:show_first(len_left ); len_left = len_left - suf_len io.stdout:write( self.term.info.ctl.cr, self.term.info.ctl.erase_r, prompt_s, self.term:sgr"reset", pre_s, self.term:sgr"reset", suf_s, self.term:sgr"reset", "\n", self.term.info.ctl.cr, self.term.info.ctl.erase_r ):flush() return r end self.had_input = true end end return function(term, ret) as ("term", { autocomp = "function?", history = "table?", prompt = "string?", prompt_done = "string?", state = "state?", on_tab = "string?", }) setmetatable(ret, mt) ret.had_input = false ret.history = ret.history or {} ret.line2 = false ret.offer_completion = false ret.on_tab = ret.on_tab or "\t" ret.prompt = term:str(ret.prompt or "") ret.term = term ret.history[1] = ret.history[1] or "" ret.history_pos = #ret.history ret.term:refresh() ret.term.in_getline = true ret:from_history() return ret end ]================================================================================] , "neo-ed.term.prompt")) package.preload["neo-ed.term.str"] = assert(load( [================================================================================[ local as = require "neo-ed.lib.as" local lib = require "neo-ed.lib" local ucd = require "neo-ed.lib.ucd" local mt = {__index = {}, __name = "term.str"} local tabs = {} local function new(term) as ("term") local self = setmetatable({term = term, len = 0}, mt) if not tabs[term.tab] then if term.tab:find("\t") then tabs[term.tab] = {text = term.tab, show = " ", len = 8} else local tmp = setmetatable({term = term, len = 0}, mt) local s, l = tmp:append(term.tab):show() tabs[term.tab] = {text = "\t", show = s, len = l} end end self.tab = tabs[term.tab] return self end function mt:__call(s) as ("term.str", "string|term.str") if type(s) == "table" then table.move(s, 1, #s, #self + 1, self) self.len = self.len + s.len return self end local function match(pat, fn) local n s, n = s:gsub("^" .. pat, function(...) local t = fn(...) table.insert(self, t) self.len = self.len + t.len return "" end) return n ~= 0 end local function trivial(pat) return match("(" .. pat .. ")", function(s) return {text = s, show = s, len = ucd.codepoint_width(utf8.codes(s))} end) end while s ~= "" do local _ = match("(\a)", function(s) return {text = s, show = s, len = 0} end) or match("(\t)", function(_) return self.tab end) or -- SGR match("(\x1B%[[0-9;]*m)" , function(s) return {text = s, show = s, len = 0} end) or -- private modes match("(\x1B%[%??[0-9;]*[hlsr])", function(s) return {text = s, show = s, len = 0} end) or -- OSCs match("(\x1B%][^\a\x1B]-\x1B\\)", function(s) return {text = s, show = s, len = 0} end) or match("(\x1B%][^\a\x1B]-\a)" , function(s) return {text = s, show = s, len = 0} end) or -- explicitly marked control sequence match("(\x01(.-)\x02)", function(s, r) return {text = s, show = r, len = 0} end) or -- Unfortunately Lua assumes valid utf-8 input for most functions, -- so we have to handle potentially unclean input ourselves. -- `https://encoding.spec.whatwg.org/#utf-8-decoder` trivial "[\x20-\x7E]" or trivial "[\xC2-\xDF][\x80-\xBF]" or trivial "\xE0[\xA0-\xBF][\x80-\xBF]" or -- avoid overlong encodings trivial "[\xE1-\xEC][\x80-\xBF][\x80-\xBF]" or trivial "\xED[\x80-\x9F][\x80-\xBF]" or -- avoid utf-16 surrogates trivial "[\xEE-\xEF][\x80-\xBF][\x80-\xBF]" or trivial "\xF0[\x90-\xBF][\x80-\xBF][\x80-\xBF]" or -- avoid overlong encodings trivial "[\xF1-\xF3][\x80-\xBF][\x80-\xBF][\x80-\xBF]" or trivial "\xF4[\x80-\x8F][\x80-\xBF][\x80-\xBF]" or -- avoid upper bound match("(.)", function(s) return { text = s, show = ("%s\\x%02x%s"):format(self.term:sgr"rev", s:byte(), self.term:sgr"reset"), len = 4, } end) or lib.error(("failed to match character sequence: %q"):format(s)) end return self end function mt.__index:append(s) as ("term.str", "term.str|string") return self(s) end function mt.__index:fmt(fmt, ...) as ("term.str", "string") return self(fmt:format(...)) end function mt.__index:sgr(s) as ("term.str", "string") return self(self.term:sgr(s)) end function mt.__index:bake(a, b) as ("term.str", "number?", "number?") a = a or 1 b = b or #self local ret = {} for i = a, b do table.insert(ret, self[i].text) end return table.concat(ret) end function mt:__tostring() as ("term.str") return self:bake() end function mt.__index:show() as ("term.str") return self:show_first(math.maxinteger) end local mirror = { ["("] = ")", [")"] = "(", ["["] = "]", ["]"] = "[", ["{"] = "}", ["}"] = "{", ["<"] = ">", [">"] = "<", } function mt.__index:show_first(n, highlight) as ("term.str", "number", "*") local ret = {} local len = 0 local fmt = {} local ctr = {[")"] = 0, ["]"] = 0, ["}"] = 0, [">"] = 0} if highlight then fmt[")"] = self.term:sgr("rev yellow" ) .. "%s" .. self.term:sgr("reset") fmt["]"] = self.term:sgr("rev blue" ) .. "%s" .. self.term:sgr("reset") fmt["}"] = self.term:sgr("rev magenta") .. "%s" .. self.term:sgr("reset") fmt[">"] = self.term:sgr("rev cyan" ) .. "%s" .. self.term:sgr("reset") end for _, t in ipairs(self) do if len + t.len > n then break end if fmt[t.text] and ctr[t.text] == 0 then table.insert(ret, (fmt[t.text]):format(t.show)) fmt[t.text] = nil else if fmt[ t.text ] then ctr[ t.text ] = ctr[ t.text ] - 1 elseif fmt[mirror[t.text]] then ctr[mirror[t.text]] = ctr[mirror[t.text]] + 1 end table.insert(ret, t.show) end len = len + t.len end return table.concat(ret), len end function mt.__index:show_last(n, highlight) as ("term.str", "number", "*") local ret = {} local len = 0 local fmt = {} local ctr = {["("] = 0, ["["] = 0, ["{"] = 0, ["<"] = 0} if highlight then fmt["("] = self.term:sgr("rev yellow" ) .. "%s" .. self.term:sgr("reset") fmt["["] = self.term:sgr("rev blue" ) .. "%s" .. self.term:sgr("reset") fmt["{"] = self.term:sgr("rev magenta") .. "%s" .. self.term:sgr("reset") fmt["<"] = self.term:sgr("rev cyan" ) .. "%s" .. self.term:sgr("reset") end for i = #self, 1, -1 do local t = self[i] if len + t.len > n then break end if fmt[t.text] and ctr[t.text] == 0 then table.insert(ret, (fmt[t.text]):format(t.show)) fmt[t.text] = nil else if fmt[ t.text ] then ctr[ t.text ] = ctr[ t.text ] - 1 elseif fmt[mirror[t.text]] then ctr[mirror[t.text]] = ctr[mirror[t.text]] + 1 end table.insert(ret, t.show) end len = len + t.len end for i = 1, #ret // 2 do ret[i], ret[#ret - i + 1] = ret[#ret - i + 1], ret[i] end return table.concat(ret), len end function mt.__index:print() as ("term.str") return self.term:write(self:show()) end function mt.__index:print_first(...) as ("term.str", "number", "*") return self.term:write(self:show_first(...)) end function mt.__index:print_last (...) as ("term.str", "number", "*") return self.term:write(self:show_last (...)) end function mt.__index:visual() as ("term.str") for _, t in ipairs(self) do t.text = "" end return self end function mt.__index:remove(n) as ("term.str", "number?") local tmp = table.remove(self, n) if tmp then self.len = self.len - tmp.len end return tmp and tmp.text or nil end function mt.__index:dup() as ("term.str") return new(self.term)(self) end function mt.__index:prepend(s) as ("string|term.str") if type(s) == "string" then s = new(self.term)(s) end table.move(self, 1, #self, #s + 1, self) table.move(s, 1, #s, 1, self) self.len = self.len + s.len return self end return function(term, s) as ("term", "string?") local ret = new(term) if s then ret(s) end return ret end ]================================================================================] , "neo-ed.term.str")) package.preload["neo-ed.test"] = assert(load( [================================================================================[ local m = {} local ok = true local function writefile(path, contents) local h = assert(io.open(path, "w")) h:write(contents) end local curr_step local function begin_step(name) print(" " .. name); curr_step = name end local function end_step_ok () print("\x1b[1F \x1b[32m✓\x1b[0m " .. curr_step); curr_step = nil end local function end_step_err () print("\x1b[1F \x1b[31m❗\x1b[0m " .. curr_step); curr_step = nil end local function end_step_wrong() print("\x1b[1F \x1b[31m✗\x1b[0m " .. curr_step); curr_step = nil end local function skip_step () print("\x1b[1F \x1b[33m❓\x1b[0m " .. curr_step) curr_step = nil end local function failed(t) if t.input then print("\n\x1b[31mInput:\x1b[0m") os.execute("nl -ba -w3 -s'│' testenv/input") end print("\n\x1b[31mScript:\x1b[0m") os.execute("nl -ba -w3 -s'│' testenv/script") print("\n\x1b[31mCommand output (first 100 lines):\x1b[0m") os.execute("head -n 100 testenv/log") print() ok = false return false end local function run(t, cmd) os.execute("rm -rf testenv >/dev/null 2>&1") assert(os.execute("mkdir testenv")) assert(os.execute("cd testenv && ln -s ../neo-ed ./")) if t.input then writefile("testenv/input" , t.input :gsub("\t", "")) end if t.expect then writefile("testenv/expect", t.expect:gsub("\t", "")) end do local h = assert(io.open("testenv/script", "w")) if t.expect then h:write("f output\n") end if t.input then h:write("r input\n") end h:write("H\n") h:write((t.script:gsub("\t", ""))) h:write(t.expect and "wq\n" or "Q\n") end local r, what, n = os.execute("cd testenv && timeout 3 " .. cmd .. "