diff options
author | KillTheMule <KillTheMule@users.noreply.github.com> | 2019-01-07 15:42:20 +0100 |
---|---|---|
committer | Justin M. Keyes <justinkz@gmail.com> | 2019-01-14 02:14:34 +0100 |
commit | bb3aa824b7c6aedb9019978338fa960d307aa883 (patch) | |
tree | 1356ef1521046211aedd528ebfa7e0e66eebe964 | |
parent | 989fbad5028a2f7d31fef944aac166d562665477 (diff) | |
download | rneovim-bb3aa824b7c6aedb9019978338fa960d307aa883.tar.gz rneovim-bb3aa824b7c6aedb9019978338fa960d307aa883.tar.bz2 rneovim-bb3aa824b7c6aedb9019978338fa960d307aa883.zip |
lua/stdlib: vim.inspect, string functions
ref #6580
ref #8677
-rw-r--r-- | runtime/doc/if_lua.txt | 54 | ||||
-rw-r--r-- | runtime/lua/vim/inspect.lua | 333 | ||||
-rw-r--r-- | runtime/plugin/nvim.vim | 1 | ||||
-rw-r--r-- | src/nvim/lua/vim.lua | 73 | ||||
-rw-r--r-- | test/functional/lua/utility_functions_spec.lua | 92 |
5 files changed, 552 insertions, 1 deletions
diff --git a/runtime/doc/if_lua.txt b/runtime/doc/if_lua.txt index 9dbbf87995..97bbb34078 100644 --- a/runtime/doc/if_lua.txt +++ b/runtime/doc/if_lua.txt @@ -252,13 +252,55 @@ For example, to use the "nvim_get_current_line()" API function, call print(tostring(vim.api.nvim_get_current_line())) ------------------------------------------------------------------------------ -vim.* utility functions +vim.* builtin functions + +vim.deepcopy({object}) *vim.deepcopy* + Performs a deep copy of the given object, and returns that copy. + For a non-table object, that just means a usual copy of the object, + while for a table all subtables are copied recursively. + +vim.gsplit({s}, {sep}, {plain}) *vim.gsplit* + Split a given string by a separator. Returns an iterator of the + split components. The separator can be a lua pattern, see + https://www.lua.org/pil/20.2.html + Setting {plain} to `true` turns off pattern matching, as it is passed + to `string:find`, see + http://lua-users.org/wiki/StringLibraryTutorial + + Parameters:~ + {s} String: String to split + {sep} String: Separator pattern. If empty, split by chars. + {plain} Boolean: If false, match {sep} verbatim + + Return:~ + Iterator of strings, which are the components of {s} after + splitting + +vim.split({s}, {sep}, {plain}) *vim.split* + Split a given string by a separator. Returns a table containing the + split components. The separator can be a lua pattern, see + https://www.lua.org/pil/20.2.html + Setting {plain} to `true` turns off pattern matching, as it is passed + to `string:find`, see + http://lua-users.org/wiki/StringLibraryTutorial + + Parameters:~ + {s} String: String to split + {sep} String: Separator pattern. If empty, split by chars. + {plain} Boolean: If false, match {sep} verbatim + + Return:~ + Table of strings, which are the components of {s} after + splitting vim.stricmp(a, b) *lua-vim.stricmp* Function used for case-insensitive string comparison. Takes two string arguments and returns 0, 1 or -1 if strings are equal, a is greater then b or a is lesser then b respectively. +vim.trim({string}) *vim.trim* + Returns the string with all leading and trailing whitespace removed. + vim.type_idx *lua-vim.type_idx* Type index for use in |lua-special-tbl|. Specifying one of the values from |lua-vim.types| allows typing the empty table (it is @@ -294,6 +336,16 @@ vim.types *lua-vim.types* `vim.types.dictionary` will not change or that `vim.types` table will only contain values for these three types. +------------------------------------------------------------------------------ +vim.* runtime functions + +Those functions are only available after the runtime files have been loaded. +In particular, they are not available when using `nvim -u NONE`. + +vim.inspect({object}, {options}) *vim.inspect* + Return a human-readable representation of the passed object. See + https://github.com/kikito/inspect.lua + for details and possible options. ============================================================================== The luaeval function *lua-luaeval* *lua-eval* *luaeval()* diff --git a/runtime/lua/vim/inspect.lua b/runtime/lua/vim/inspect.lua new file mode 100644 index 0000000000..7cb40ca64d --- /dev/null +++ b/runtime/lua/vim/inspect.lua @@ -0,0 +1,333 @@ +local inspect ={ + _VERSION = 'inspect.lua 3.1.0', + _URL = 'http://github.com/kikito/inspect.lua', + _DESCRIPTION = 'human-readable representations of tables', + _LICENSE = [[ + MIT LICENSE + + Copyright (c) 2013 Enrique GarcĂa Cota + + 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 tostring = tostring + +inspect.KEY = setmetatable({}, {__tostring = function() return 'inspect.KEY' end}) +inspect.METATABLE = setmetatable({}, {__tostring = function() return 'inspect.METATABLE' end}) + +local function rawpairs(t) + return next, t, nil +end + +-- Apostrophizes the string if it has quotes, but not aphostrophes +-- Otherwise, it returns a regular quoted string +local function smartQuote(str) + if str:match('"') and not str:match("'") then + return "'" .. str .. "'" + end + return '"' .. str:gsub('"', '\\"') .. '"' +end + +-- \a => '\\a', \0 => '\\0', 31 => '\31' +local shortControlCharEscapes = { + ["\a"] = "\\a", ["\b"] = "\\b", ["\f"] = "\\f", ["\n"] = "\\n", + ["\r"] = "\\r", ["\t"] = "\\t", ["\v"] = "\\v" +} +local longControlCharEscapes = {} -- \a => nil, \0 => \000, 31 => \031 +for i=0, 31 do + local ch = string.char(i) + if not shortControlCharEscapes[ch] then + shortControlCharEscapes[ch] = "\\"..i + longControlCharEscapes[ch] = string.format("\\%03d", i) + end +end + +local function escape(str) + return (str:gsub("\\", "\\\\") + :gsub("(%c)%f[0-9]", longControlCharEscapes) + :gsub("%c", shortControlCharEscapes)) +end + +local function isIdentifier(str) + return type(str) == 'string' and str:match( "^[_%a][_%a%d]*$" ) +end + +local function isSequenceKey(k, sequenceLength) + return type(k) == 'number' + and 1 <= k + and k <= sequenceLength + and math.floor(k) == k +end + +local defaultTypeOrders = { + ['number'] = 1, ['boolean'] = 2, ['string'] = 3, ['table'] = 4, + ['function'] = 5, ['userdata'] = 6, ['thread'] = 7 +} + +local function sortKeys(a, b) + local ta, tb = type(a), type(b) + + -- strings and numbers are sorted numerically/alphabetically + if ta == tb and (ta == 'string' or ta == 'number') then return a < b end + + local dta, dtb = defaultTypeOrders[ta], defaultTypeOrders[tb] + -- Two default types are compared according to the defaultTypeOrders table + if dta and dtb then return defaultTypeOrders[ta] < defaultTypeOrders[tb] + elseif dta then return true -- default types before custom ones + elseif dtb then return false -- custom types after default ones + end + + -- custom types are sorted out alphabetically + return ta < tb +end + +-- For implementation reasons, the behavior of rawlen & # is "undefined" when +-- tables aren't pure sequences. So we implement our own # operator. +local function getSequenceLength(t) + local len = 1 + local v = rawget(t,len) + while v ~= nil do + len = len + 1 + v = rawget(t,len) + end + return len - 1 +end + +local function getNonSequentialKeys(t) + local keys, keysLength = {}, 0 + local sequenceLength = getSequenceLength(t) + for k,_ in rawpairs(t) do + if not isSequenceKey(k, sequenceLength) then + keysLength = keysLength + 1 + keys[keysLength] = k + end + end + table.sort(keys, sortKeys) + return keys, keysLength, sequenceLength +end + +local function countTableAppearances(t, tableAppearances) + tableAppearances = tableAppearances or {} + + if type(t) == 'table' then + if not tableAppearances[t] then + tableAppearances[t] = 1 + for k,v in rawpairs(t) do + countTableAppearances(k, tableAppearances) + countTableAppearances(v, tableAppearances) + end + countTableAppearances(getmetatable(t), tableAppearances) + else + tableAppearances[t] = tableAppearances[t] + 1 + end + end + + return tableAppearances +end + +local copySequence = function(s) + local copy, len = {}, #s + for i=1, len do copy[i] = s[i] end + return copy, len +end + +local function makePath(path, ...) + local keys = {...} + local newPath, len = copySequence(path) + for i=1, #keys do + newPath[len + i] = keys[i] + end + return newPath +end + +local function processRecursive(process, item, path, visited) + if item == nil then return nil end + if visited[item] then return visited[item] end + + local processed = process(item, path) + if type(processed) == 'table' then + local processedCopy = {} + visited[item] = processedCopy + local processedKey + + for k,v in rawpairs(processed) do + processedKey = processRecursive(process, k, makePath(path, k, inspect.KEY), visited) + if processedKey ~= nil then + processedCopy[processedKey] = processRecursive(process, v, makePath(path, processedKey), visited) + end + end + + local mt = processRecursive(process, getmetatable(processed), makePath(path, inspect.METATABLE), visited) + if type(mt) ~= 'table' then mt = nil end -- ignore not nil/table __metatable field + setmetatable(processedCopy, mt) + processed = processedCopy + end + return processed +end + + + +------------------------------------------------------------------- + +local Inspector = {} +local Inspector_mt = {__index = Inspector} + +function Inspector:puts(...) + local args = {...} + local buffer = self.buffer + local len = #buffer + for i=1, #args do + len = len + 1 + buffer[len] = args[i] + end +end + +function Inspector:down(f) + self.level = self.level + 1 + f() + self.level = self.level - 1 +end + +function Inspector:tabify() + self:puts(self.newline, string.rep(self.indent, self.level)) +end + +function Inspector:alreadyVisited(v) + return self.ids[v] ~= nil +end + +function Inspector:getId(v) + local id = self.ids[v] + if not id then + local tv = type(v) + id = (self.maxIds[tv] or 0) + 1 + self.maxIds[tv] = id + self.ids[v] = id + end + return tostring(id) +end + +function Inspector:putKey(k) + if isIdentifier(k) then return self:puts(k) end + self:puts("[") + self:putValue(k) + self:puts("]") +end + +function Inspector:putTable(t) + if t == inspect.KEY or t == inspect.METATABLE then + self:puts(tostring(t)) + elseif self:alreadyVisited(t) then + self:puts('<table ', self:getId(t), '>') + elseif self.level >= self.depth then + self:puts('{...}') + else + if self.tableAppearances[t] > 1 then self:puts('<', self:getId(t), '>') end + + local nonSequentialKeys, nonSequentialKeysLength, sequenceLength = getNonSequentialKeys(t) + local mt = getmetatable(t) + + self:puts('{') + self:down(function() + local count = 0 + for i=1, sequenceLength do + if count > 0 then self:puts(',') end + self:puts(' ') + self:putValue(t[i]) + count = count + 1 + end + + for i=1, nonSequentialKeysLength do + local k = nonSequentialKeys[i] + if count > 0 then self:puts(',') end + self:tabify() + self:putKey(k) + self:puts(' = ') + self:putValue(t[k]) + count = count + 1 + end + + if type(mt) == 'table' then + if count > 0 then self:puts(',') end + self:tabify() + self:puts('<metatable> = ') + self:putValue(mt) + end + end) + + if nonSequentialKeysLength > 0 or type(mt) == 'table' then -- result is multi-lined. Justify closing } + self:tabify() + elseif sequenceLength > 0 then -- array tables have one extra space before closing } + self:puts(' ') + end + + self:puts('}') + end +end + +function Inspector:putValue(v) + local tv = type(v) + + if tv == 'string' then + self:puts(smartQuote(escape(v))) + elseif tv == 'number' or tv == 'boolean' or tv == 'nil' or + tv == 'cdata' or tv == 'ctype' then + self:puts(tostring(v)) + elseif tv == 'table' then + self:putTable(v) + else + self:puts('<', tv, ' ', self:getId(v), '>') + end +end + +------------------------------------------------------------------- + +function inspect.inspect(root, options) + options = options or {} + + local depth = options.depth or math.huge + local newline = options.newline or '\n' + local indent = options.indent or ' ' + local process = options.process + + if process then + root = processRecursive(process, root, {}, {}) + end + + local inspector = setmetatable({ + depth = depth, + level = 0, + buffer = {}, + ids = {}, + maxIds = {}, + newline = newline, + indent = indent, + tableAppearances = countTableAppearances(root) + }, Inspector_mt) + + inspector:putValue(root) + + return table.concat(inspector.buffer) +end + +setmetatable(inspect, { __call = function(_, ...) return inspect.inspect(...) end }) + +return inspect diff --git a/runtime/plugin/nvim.vim b/runtime/plugin/nvim.vim new file mode 100644 index 0000000000..ffe4abc71e --- /dev/null +++ b/runtime/plugin/nvim.vim @@ -0,0 +1 @@ +lua vim.inspect = require("vim.inspect") diff --git a/src/nvim/lua/vim.lua b/src/nvim/lua/vim.lua index 47f40da72e..9297af27c4 100644 --- a/src/nvim/lua/vim.lua +++ b/src/nvim/lua/vim.lua @@ -118,11 +118,84 @@ local function _update_package_paths() last_nvim_paths = cur_nvim_paths end +local function gsplit(s, sep, plain) + assert(type(s) == "string") + assert(type(sep) == "string") + assert(type(plain) == "boolean" or type(plain) == "nil") + + local start = 1 + local done = false + + local function pass(i, j, ...) + if i then + assert(j+1 > start, "Infinite loop detected") + local seg = s:sub(start, i - 1) + start = j + 1 + return seg, ... + else + done = true + return s:sub(start) + end + end + + return function() + if done then + return + end + if sep == '' then + if start == #s then + done = true + end + return pass(start+1, start) + end + return pass(s:find(sep, start, plain)) + end +end + +local function split(s,sep,plain) + local t={} for c in gsplit(s, sep, plain) do table.insert(t,c) end + return t +end + +local function trim(s) + assert(type(s) == "string", "Only strings can be trimmed") + local result = s:gsub("^%s+", ""):gsub("%s+$", "") + return result +end + +local deepcopy + +local function id(v) + return v +end + +local deepcopy_funcs = { + table = function(orig) + local copy = {} + for k, v in pairs(orig) do + copy[deepcopy(k)] = deepcopy(v) + end + return copy + end, + number = id, + string = id, + ['nil'] = id, + boolean = id, +} + +deepcopy = function(orig) + return deepcopy_funcs[type(orig)](orig) +end + local module = { _update_package_paths = _update_package_paths, _os_proc_children = _os_proc_children, _os_proc_info = _os_proc_info, _system = _system, + trim = trim, + split = split, + gsplit = gsplit, + deepcopy = deepcopy, } return module diff --git a/test/functional/lua/utility_functions_spec.lua b/test/functional/lua/utility_functions_spec.lua index d5756e134d..5f511f4a43 100644 --- a/test/functional/lua/utility_functions_spec.lua +++ b/test/functional/lua/utility_functions_spec.lua @@ -2,8 +2,10 @@ local helpers = require('test.functional.helpers')(after_each) local funcs = helpers.funcs +local meths = helpers.meths local clear = helpers.clear local eq = helpers.eq +local command = helpers.command before_each(clear) @@ -106,3 +108,93 @@ describe('vim.stricmp', function() eq(1, funcs.luaeval('vim.stricmp("\\0C\\0", "\\0B\\0")')) end) end) + +describe("vim.split", function() + it("works", function() + local split = function(str, sep) + return meths.execute_lua('return vim.split(...)', {str, sep}) + end + + local tests = { + { "a,b", ",", false, { 'a', 'b' } }, + { ":aa::bb:", ":", false, { '', 'aa', '', 'bb', '' } }, + { "::ee::ff:", ":", false, { '', '', 'ee', '', 'ff', '' } }, + { "ab", ".", false, { '', '', '' } }, + { "a1b2c", "[0-9]", false, { 'a', 'b', 'c' } }, + { "xy", "", false, { 'x', 'y' } }, + { "here be dragons", " ", false, { "here", "be", "dragons"} }, + { "axaby", "ab?", false, { '', 'x', 'y' } }, + { "f v2v v3v w2w ", "([vw])2%1", false, { 'f ', ' v3v ', ' ' } }, + { "x*yz*oo*l", "*", true, { 'x', 'yz', 'oo', 'l' } }, + } + + for _, t in ipairs(tests) do + eq(t[4], split(t[1], t[2], t[3])) + end + + local loops = { + { "abc", ".-" }, + } + + for _, t in ipairs(loops) do + local status, err = pcall(split, t[1], t[2]) + eq(false, status) + assert(string.match(err, "Infinite loop detected")) + end + end) +end) + +describe("vim.trim", function() + it('works', function() + local trim = function(s) + return meths.execute_lua('return vim.trim(...)', { s }) + end + + local trims = { + { " a", "a" }, + { " b ", "b" }, + { "\tc" , "c" }, + { "r\n", "r" }, + } + + for _, t in ipairs(trims) do + assert(t[2], trim(t[1])) + end + + local status, err = pcall(trim, 2) + eq(false, status) + assert(string.match(err, "Only strings can be trimmed")) + end) +end) + +describe("vim.inspect", function() + it('works', function() + command("source runtime/plugin/nvim.vim") + -- just make sure it basically works, it has its own test suite + local inspect = function(t, opts) + return meths.execute_lua('return vim.inspect(...)', { t, opts }) + end + + eq('2', inspect(2)) + + local i = inspect({ a = { b = 1 } }, { newline = '+', indent = '' }) + eq('{+a = {+b = 1+}+}', i) + end) +end) + +describe("vim.deepcopy", function() + it("works", function() + local is_dc = meths.execute_lua([[ + local a = { x = { 1, 2 }, y = 5} + local b = vim.deepcopy(a) + + local count = 0 + for _ in pairs(b) do count = count + 1 end + + return b.x[1] == 1 and b.x[2] == 2 and b.y == 5 and count == 2 + and tostring(a) ~= tostring(b) + ]], {}) + + assert(is_dc) + end) +end) |