neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

inspect.lua (9429B)


      1 --- @diagnostic disable: no-unknown
      2 local inspect = {
      3  _VERSION = 'inspect.lua 3.1.0',
      4  _URL = 'http://github.com/kikito/inspect.lua',
      5  _DESCRIPTION = 'human-readable representations of tables',
      6  _LICENSE = [[
      7    MIT LICENSE
      8 
      9    Copyright (c) 2013 Enrique GarcĂ­a Cota
     10 
     11    Permission is hereby granted, free of charge, to any person obtaining a
     12    copy of this software and associated documentation files (the
     13    "Software"), to deal in the Software without restriction, including
     14    without limitation the rights to use, copy, modify, merge, publish,
     15    distribute, sublicense, and/or sell copies of the Software, and to
     16    permit persons to whom the Software is furnished to do so, subject to
     17    the following conditions:
     18 
     19    The above copyright notice and this permission notice shall be included
     20    in all copies or substantial portions of the Software.
     21 
     22    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
     23    OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
     24    MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
     25    IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
     26    CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
     27    TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
     28    SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
     29  ]],
     30 }
     31 
     32 inspect.KEY = setmetatable({}, {
     33  __tostring = function()
     34    return 'inspect.KEY'
     35  end,
     36 })
     37 inspect.METATABLE = setmetatable({}, {
     38  __tostring = function()
     39    return 'inspect.METATABLE'
     40  end,
     41 })
     42 
     43 local tostring = tostring
     44 local rep = string.rep
     45 local match = string.match
     46 local char = string.char
     47 local gsub = string.gsub
     48 local fmt = string.format
     49 
     50 local sbavailable, stringbuffer = pcall(require, 'string.buffer')
     51 local buffnew
     52 local puts
     53 local render
     54 
     55 if sbavailable then
     56  buffnew = stringbuffer.new
     57  puts = function(buf, str)
     58    buf:put(str)
     59  end
     60  render = function(buf)
     61    return buf:get()
     62  end
     63 else
     64  buffnew = function()
     65    return { n = 0 }
     66  end
     67  puts = function(buf, str)
     68    buf.n = buf.n + 1
     69    buf[buf.n] = str
     70  end
     71  render = function(buf)
     72    return table.concat(buf)
     73  end
     74 end
     75 
     76 local _rawget
     77 if rawget then
     78  _rawget = rawget
     79 else
     80  _rawget = function(t, k)
     81    return t[k]
     82  end
     83 end
     84 local function rawpairs(t)
     85  return next, t, nil
     86 end
     87 
     88 -- Apostrophizes the string if it has quotes, but not aphostrophes
     89 -- Otherwise, it returns a regular quoted string
     90 local function smartQuote(str)
     91  if match(str, '"') and not match(str, "'") then
     92    return "'" .. str .. "'"
     93  end
     94  return '"' .. gsub(str, '"', '\\"') .. '"'
     95 end
     96 
     97 -- \a => '\\a', \0 => '\\0', 31 => '\31'
     98 local shortControlCharEscapes = {
     99  ['\a'] = '\\a',
    100  ['\b'] = '\\b',
    101  ['\f'] = '\\f',
    102  ['\n'] = '\\n',
    103  ['\r'] = '\\r',
    104  ['\t'] = '\\t',
    105  ['\v'] = '\\v',
    106  ['\127'] = '\\127',
    107 }
    108 local longControlCharEscapes = { ['\127'] = '\127' }
    109 for i = 0, 31 do
    110  local ch = char(i)
    111  if not shortControlCharEscapes[ch] then
    112    shortControlCharEscapes[ch] = '\\' .. i
    113    longControlCharEscapes[ch] = fmt('\\%03d', i)
    114  end
    115 end
    116 
    117 local function escape(str)
    118  return (
    119    gsub(
    120      gsub(gsub(str, '\\', '\\\\'), '(%c)%f[0-9]', longControlCharEscapes),
    121      '%c',
    122      shortControlCharEscapes
    123    )
    124  )
    125 end
    126 
    127 local luaKeywords = {}
    128 for k in
    129  ([[ and break do else elseif end false for function goto if
    130             in local nil not or repeat return then true until while
    131 ]]):gmatch('%w+')
    132 do
    133  luaKeywords[k] = true
    134 end
    135 
    136 local function isIdentifier(str)
    137  return type(str) == 'string'
    138    -- identifier must start with a letter and underscore, and be followed by letters, numbers, and underscores
    139    and not not str:match('^[_%a][_%a%d]*$')
    140    -- lua keywords are not valid identifiers
    141    and not luaKeywords[str]
    142 end
    143 
    144 local flr = math.floor
    145 local function isSequenceKey(k, sequenceLength)
    146  return type(k) == 'number' and flr(k) == k and 1 <= k and k <= sequenceLength
    147 end
    148 
    149 local defaultTypeOrders = {
    150  ['number'] = 1,
    151  ['boolean'] = 2,
    152  ['string'] = 3,
    153  ['table'] = 4,
    154  ['function'] = 5,
    155  ['userdata'] = 6,
    156  ['thread'] = 7,
    157 }
    158 
    159 local function sortKeys(a, b)
    160  local ta, tb = type(a), type(b)
    161 
    162  -- strings and numbers are sorted numerically/alphabetically
    163  if ta == tb and (ta == 'string' or ta == 'number') then
    164    return a < b
    165  end
    166 
    167  local dta = defaultTypeOrders[ta] or 100
    168  local dtb = defaultTypeOrders[tb] or 100
    169  -- Two default types are compared according to the defaultTypeOrders table
    170 
    171  -- custom types are sorted out alphabetically
    172  return dta == dtb and ta < tb or dta < dtb
    173 end
    174 
    175 local function getKeys(t)
    176  local seqLen = 1
    177  while _rawget(t, seqLen) ~= nil do
    178    seqLen = seqLen + 1
    179  end
    180  seqLen = seqLen - 1
    181 
    182  local keys, keysLen = {}, 0
    183  for k in rawpairs(t) do
    184    if not isSequenceKey(k, seqLen) then
    185      keysLen = keysLen + 1
    186      keys[keysLen] = k
    187    end
    188  end
    189  table.sort(keys, sortKeys)
    190  return keys, keysLen, seqLen
    191 end
    192 
    193 local function countCycles(x, cycles, depth)
    194  if type(x) == 'table' then
    195    if cycles[x] then
    196      cycles[x] = cycles[x] + 1
    197    else
    198      cycles[x] = 1
    199      if depth > 0 then
    200        for k, v in rawpairs(x) do
    201          countCycles(k, cycles, depth - 1)
    202          countCycles(v, cycles, depth - 1)
    203        end
    204        countCycles(getmetatable(x), cycles, depth - 1)
    205      end
    206    end
    207  end
    208 end
    209 
    210 local function makePath(path, a, b)
    211  local newPath = {}
    212  local len = #path
    213  for i = 1, len do
    214    newPath[i] = path[i]
    215  end
    216 
    217  newPath[len + 1] = a
    218  newPath[len + 2] = b
    219 
    220  return newPath
    221 end
    222 
    223 local function processRecursive(process, item, path, visited)
    224  if item == nil then
    225    return nil
    226  end
    227  if visited[item] then
    228    return visited[item]
    229  end
    230 
    231  local processed = process(item, path)
    232  if type(processed) == 'table' then
    233    local processedCopy = {}
    234    visited[item] = processedCopy
    235    local processedKey
    236 
    237    for k, v in rawpairs(processed) do
    238      processedKey = processRecursive(process, k, makePath(path, k, inspect.KEY), visited)
    239      if processedKey ~= nil then
    240        processedCopy[processedKey] =
    241          processRecursive(process, v, makePath(path, processedKey), visited)
    242      end
    243    end
    244 
    245    local mt =
    246      processRecursive(process, getmetatable(processed), makePath(path, inspect.METATABLE), visited)
    247    if type(mt) ~= 'table' then
    248      mt = nil
    249    end
    250    setmetatable(processedCopy, mt)
    251    processed = processedCopy
    252  end
    253  return processed
    254 end
    255 
    256 local Inspector = {}
    257 
    258 local Inspector_mt = { __index = Inspector }
    259 
    260 local function tabify(inspector)
    261  puts(inspector.buf, inspector.newline .. rep(inspector.indent, inspector.level))
    262 end
    263 
    264 function Inspector:getId(v)
    265  local id = self.ids[v]
    266  local ids = self.ids
    267  if not id then
    268    local tv = type(v)
    269    id = (ids[tv] or 0) + 1
    270    ids[v], ids[tv] = id, id
    271  end
    272  return tostring(id)
    273 end
    274 
    275 function Inspector:putValue(v)
    276  local buf = self.buf
    277  local tv = type(v)
    278  if tv == 'string' then
    279    puts(buf, smartQuote(escape(v)))
    280  elseif
    281    tv == 'number'
    282    or tv == 'boolean'
    283    or tv == 'nil'
    284    or tv == 'cdata'
    285    or tv == 'ctype'
    286    or (vim and v == vim.NIL)
    287  then
    288    puts(buf, tostring(v))
    289  elseif tv == 'table' and not self.ids[v] then
    290    local t = v
    291 
    292    if t == inspect.KEY or t == inspect.METATABLE then
    293      puts(buf, tostring(t))
    294    elseif self.level >= self.depth then
    295      puts(buf, '{...}')
    296    else
    297      if self.cycles[t] > 1 then
    298        puts(buf, fmt('<%d>', self:getId(t)))
    299      end
    300 
    301      local keys, keysLen, seqLen = getKeys(t)
    302      local mt = getmetatable(t)
    303 
    304      if vim and seqLen == 0 and keysLen == 0 and mt == vim._empty_dict_mt then
    305        puts(buf, tostring(t))
    306        return
    307      end
    308 
    309      puts(buf, '{')
    310      self.level = self.level + 1
    311 
    312      for i = 1, seqLen + keysLen do
    313        if i > 1 then
    314          puts(buf, ',')
    315        end
    316        if i <= seqLen then
    317          puts(buf, ' ')
    318          self:putValue(t[i])
    319        else
    320          local k = keys[i - seqLen]
    321          tabify(self)
    322          if isIdentifier(k) then
    323            puts(buf, k)
    324          else
    325            puts(buf, '[')
    326            self:putValue(k)
    327            puts(buf, ']')
    328          end
    329          puts(buf, ' = ')
    330          self:putValue(t[k])
    331        end
    332      end
    333 
    334      if type(mt) == 'table' then
    335        if seqLen + keysLen > 0 then
    336          puts(buf, ',')
    337        end
    338        tabify(self)
    339        puts(buf, '<metatable> = ')
    340        self:putValue(mt)
    341      end
    342 
    343      self.level = self.level - 1
    344 
    345      if keysLen > 0 or type(mt) == 'table' then
    346        tabify(self)
    347      elseif seqLen > 0 then
    348        puts(buf, ' ')
    349      end
    350 
    351      puts(buf, '}')
    352    end
    353  else
    354    puts(buf, fmt('<%s %d>', tv, self:getId(v)))
    355  end
    356 end
    357 
    358 function inspect.inspect(root, options)
    359  options = options or {}
    360 
    361  local depth = options.depth or math.huge
    362  local newline = options.newline or '\n'
    363  local indent = options.indent or '  '
    364  local process = options.process
    365 
    366  if process then
    367    root = processRecursive(process, root, {}, {})
    368  end
    369 
    370  local cycles = {}
    371  countCycles(root, cycles, depth)
    372 
    373  local inspector = setmetatable({
    374    buf = buffnew(),
    375    ids = {},
    376    cycles = cycles,
    377    depth = depth,
    378    level = 0,
    379    newline = newline,
    380    indent = indent,
    381  }, Inspector_mt)
    382 
    383  inspector:putValue(root)
    384 
    385  return render(inspector.buf)
    386 end
    387 
    388 setmetatable(inspect, {
    389  __call = function(_, root, options)
    390    return inspect.inspect(root, options)
    391  end,
    392 })
    393 
    394 return inspect