├── LICENSE ├── README.md ├── lua ├── luadev.lua └── luadev │ ├── complete.lua │ └── inspect.lua └── plugin └── luadev.vim /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018: Björn Linse 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nvim-luadev 2 | 3 | This plugins set up a REPL-like environment for developing lua plugins in Nvim. 4 | The `:Luadev` command will open an scratch window which will show output from executing lua code. 5 | 6 | Use the folllowing mappings to execute lua code: 7 | 8 | Binding | Action 9 | ------------------------- | ------ 10 | `(Luadev-RunLine)` | Execute the current line 11 | `(Luadev-Run)` | Operator to execute lua code over a movement or text object. 12 | `(Luadev-RunWord)` | Eval identifier under cursor, including `table.attr` 13 | `(Luadev-Complete)` | in insert mode: complete (nested) global table fields 14 | 15 | If the code is a expression, it will be evaluated, and the result shown with 16 | `inspect.lua`. Otherwise it will be executed as a block of code. A top-level 17 | `return` will cause the returned value to be inspected. A bare `nil` will not 18 | be shown. 19 | 20 | Global `print()` is also redirected to the output buffer, but only when executing 21 | code via this plugin. `require'luadev'.print(...)` can be used to print to the 22 | buffer from some other context. 23 | 24 | Planned features: 25 | 26 | - [x] autodetect expression vs statements 27 | - [x] Fix `inspect.lua` to use `tostring()` on userdata (done on a local copy) 28 | - [x] completion of global names and table attributes (WIP: basic implementation done) 29 | - [x] make `(Luadev-Run)` a proper operator 30 | - [ ] solution for step-wise execution of code with `local` assignments (such 31 | as a flag to copy local values to an env) 32 | - [x] tracebacks 33 | - [ ] interactive debugging 34 | - [x] debug helpers for async callbacks (WIP) 35 | -------------------------------------------------------------------------------- /lua/luadev.lua: -------------------------------------------------------------------------------- 1 | local luadev_inspect = require'luadev.inspect' 2 | 3 | local a = vim.api 4 | if _G._luadev_mod == nil then 5 | _G._luadev_mod = {execount = 0} 6 | end 7 | 8 | local s = _G._luadev_mod 9 | 10 | local function create_buf() 11 | if s.buf ~= nil then 12 | return 13 | end 14 | local buf = a.nvim_create_buf(true,true) 15 | a.nvim_buf_set_name(buf, "[nvim-lua]") 16 | s.buf = buf 17 | end 18 | 19 | local function open_win() 20 | if s.win and a.nvim_win_is_valid(s.win) and a.nvim_win_get_buf(s.win) == s.buf then 21 | return 22 | end 23 | create_buf() 24 | local w0 = a.nvim_get_current_win() 25 | a.nvim_command("split") 26 | local w = a.nvim_get_current_win() 27 | a.nvim_win_set_buf(w,s.buf) 28 | a.nvim_set_current_win(w0) 29 | s.win = w 30 | end 31 | 32 | local function dosplit(str, delimiter) 33 | local result = { } 34 | local from = 1 35 | local delim_from, delim_to = string.find( str, delimiter, from ) 36 | while delim_from do 37 | table.insert( result, string.sub( str, from , delim_from-1 ) ) 38 | from = delim_to + 1 39 | delim_from, delim_to = string.find( str, delimiter, from ) 40 | end 41 | table.insert( result, string.sub( str, from ) ) 42 | return result 43 | end 44 | 45 | local function splitlines(str) 46 | return vim.split(str, "\n", true) 47 | end 48 | 49 | local function append_buf(lines, hl, penda) 50 | if s.buf == nil then 51 | create_buf() 52 | end 53 | local l0 = a.nvim_buf_line_count(s.buf) 54 | if type(lines) == type("") then 55 | lines = splitlines(lines) 56 | end 57 | 58 | if penda then 59 | local linelen = #(a.nvim_buf_get_lines(s.buf, l0-1, l0, false)[1]) 60 | a.nvim_buf_set_text(s.buf, l0-1, linelen, l0-1, linelen, lines) 61 | else 62 | a.nvim_buf_set_lines(s.buf, l0, l0, true, lines) 63 | end 64 | local l1 = a.nvim_buf_line_count(s.buf) 65 | if hl ~= nil then 66 | for i = l0, l1-1 do 67 | a.nvim_buf_add_highlight(s.buf, -1, hl, i, 0, -1) 68 | end 69 | end 70 | local curwin = a.nvim_get_current_win() 71 | for _,win in ipairs(a.nvim_list_wins()) do 72 | if a.nvim_win_get_buf(win) == s.buf and win ~= curwin then 73 | a.nvim_win_set_cursor(win, {l1, 1e9}) 74 | end 75 | end 76 | return l0 77 | end 78 | 79 | local function luadev_print(...) 80 | local strs = {} 81 | local args = {...} 82 | for i = 1,select('#', ...) do 83 | strs[i] = tostring(args[i]) 84 | end 85 | append_buf(table.concat(strs, ' ')) 86 | end 87 | 88 | local function dedent(str, leave_indent) 89 | -- find minimum common indent across lines 90 | local indent = nil 91 | for line in str:gmatch('[^\n]+') do 92 | local line_indent = line:match('^%s+') or '' 93 | if indent == nil or #line_indent < #indent then 94 | indent = line_indent 95 | end 96 | end 97 | if indent == nil or #indent == 0 then 98 | -- no minimum common indent 99 | return str 100 | end 101 | local left_indent = (' '):rep(leave_indent or 0) 102 | -- create a pattern for the indent 103 | indent = indent:gsub('%s', '[ \t]') 104 | -- strip it from the first line 105 | str = str:gsub('^'..indent, left_indent) 106 | -- strip it from the remaining lines 107 | str = str:gsub('[\n]'..indent, '\n' .. left_indent) 108 | return str 109 | end 110 | 111 | local function coro_run(chunk, doeval) 112 | local coro = coroutine.create(chunk) 113 | local thunk 114 | thunk = function(...) 115 | local oldprint = _G.print 116 | _G.print = luadev_print 117 | local res = vim.F.pack_len(coroutine.resume(coro, ...)) 118 | _G.print = oldprint 119 | if not res[1] then 120 | _G._errstack = coro 121 | -- if the only frame on the traceback is the chunk itself, skip the traceback 122 | if debug.getinfo(coro, 0,"f").func ~= chunk then 123 | res[2] = debug.traceback(coro, res[2], 0) 124 | end 125 | append_buf(res[2],"WarningMsg") 126 | end 127 | 128 | if coroutine.status(coro) == 'dead' then 129 | if doeval or res[2] ~= nil or res.n > 2 then 130 | append_buf(luadev_inspect(res[2])) 131 | if res.n > 2 then 132 | append_buf("MERE", "WarningMsg") -- TODO: implement me 133 | end 134 | end 135 | elseif coroutine.status(coro) == 'suspended' then 136 | if res[2] == nil then 137 | vim.schedule(thunk) 138 | elseif type(res[2]) == "string" then 139 | append_buf(res[2],nil,true) 140 | vim.cmd'redraw' -- uugh, should not be needed 141 | vim.schedule(thunk) 142 | elseif type(res[1]) == "function" then 143 | res[2](thunk) 144 | else 145 | error 'WHATTTAAF' 146 | end 147 | end 148 | 149 | return res 150 | end 151 | return thunk() 152 | end 153 | 154 | local function ld_pcall(chunk, ...) 155 | local coro = coroutine.create(chunk) 156 | local oldprint = _G.print 157 | _G.print = luadev_print 158 | local res = {coroutine.resume(coro, ...)} 159 | _G.print = oldprint 160 | if not res[1] then 161 | _G._errstack = coro 162 | -- if the only frame on the traceback is the chunk itself, skip the traceback 163 | if debug.getinfo(coro, 0,"f").func ~= chunk then 164 | res[2] = debug.traceback(coro, res[2], 0) 165 | end 166 | end 167 | return unpack(res) 168 | end 169 | 170 | local function default_reader(str, count) 171 | local name = "@[luadev "..count.."]" 172 | local doeval = true 173 | local chunk, err = loadstring("return \n"..str, name) 174 | if chunk == nil then 175 | chunk, err = loadstring(str, name) 176 | doeval = false 177 | end 178 | return chunk, err, doeval 179 | end 180 | 181 | local function exec(str) 182 | local count = s.execount + 1 183 | s.execount = count 184 | local reader = s.reader or default_reader 185 | local chunk, err, doeval = reader(str, count) 186 | local inlines = splitlines(dedent(str)) 187 | if inlines[#inlines] == "" then 188 | inlines[#inlines] = nil 189 | end 190 | firstmark = tostring(count)..">" 191 | contmark = string.rep(".", string.len(firstmark)) 192 | for i,l in ipairs(inlines) do 193 | local marker = ((i == 1) and firstmark) or contmark 194 | inlines[i] = marker.." "..l 195 | end 196 | local start = append_buf(inlines) 197 | for i,_ in ipairs(inlines) do 198 | a.nvim_buf_add_highlight(s.buf, -1, "Question", start+i-1, 0, 2) 199 | end 200 | if chunk == nil then 201 | append_buf(err,"WarningMsg") 202 | else 203 | coro_run(chunk, doeval) 204 | end 205 | append_buf({""}) 206 | end 207 | 208 | local function start() 209 | open_win() 210 | end 211 | 212 | local function err_wrap(cb) 213 | return (function (...) 214 | local res = {ld_pcall(cb, ...)} 215 | if not res[1] then 216 | open_win() 217 | append_buf(res[2],"WarningMsg") 218 | return nil 219 | else 220 | table.remove(res, 1) 221 | return unpack(res) 222 | end 223 | end) 224 | end 225 | 226 | local function schedule_wrap(cb) 227 | return vim.schedule_wrap(err_wrap(cb)) 228 | end 229 | 230 | local funcs = { 231 | create_buf=create_buf, 232 | start=start, 233 | exec=exec, 234 | print=luadev_print, 235 | append_buf=append_buf, 236 | err_wrap = err_wrap, 237 | schedule_wrap = schedule_wrap, 238 | coro_run = coro_run, 239 | } 240 | 241 | -- TODO: export abstraction for autoreload 242 | for k,v in pairs(funcs) do 243 | s[k] = v 244 | end 245 | return s 246 | -------------------------------------------------------------------------------- /lua/luadev/complete.lua: -------------------------------------------------------------------------------- 1 | local function complete() 2 | local line = vim.api.nvim_get_current_line() 3 | local endcol = vim.api.nvim_win_get_cursor(0)[2] 4 | local text = string.sub(line,1,endcol) 5 | local x,y,z = string.match(text,"([%.%w%[%]_]-)([:%.%[])([%w_]-)$") 6 | --require'luadev'.print(x,y,z) 7 | local status, obj 8 | if x ~= nil then 9 | status, obj = pcall(loadstring("return "..x)) 10 | else 11 | status, obj = true, _G 12 | y, z = ".", string.match(text,"([%w_]-)$") 13 | end 14 | --require'luadev'.print(status,obj,y,z) 15 | local entries = {} 16 | 17 | if (not status) then 18 | return 19 | end 20 | 21 | local function insertify(o) 22 | for k,_ in pairs(o) do 23 | if type(k) == "string" and k:sub(1,string.len(z)) == z then 24 | table.insert(entries,k) 25 | end 26 | end 27 | end 28 | 29 | if type(obj) == 'table' then 30 | insertify(obj) 31 | end 32 | 33 | local index = (debug.getmetatable(obj) or {}).__index 34 | if type(index) == 'table' then 35 | insertify(index) 36 | end 37 | 38 | local start = endcol - string.len(z) + 1 39 | table.sort(entries) 40 | --require'luadev'.print(vim.inspect(entries)) 41 | -- BUG/REGRESSION?? complete() doesn't respect 'completeopt' 42 | vim.api.nvim_call_function("complete", {start, entries}) 43 | end 44 | -- TODO: luadev should add a command to source a file like this: 45 | --package.loaded['luadev.complete']=complete 46 | return complete 47 | -------------------------------------------------------------------------------- /lua/luadev/inspect.lua: -------------------------------------------------------------------------------- 1 | local inspect ={ 2 | _VERSION = 'inspect.lua 3.1.0', 3 | _URL = 'http://github.com/kikito/inspect.lua', 4 | _DESCRIPTION = 'human-readable representations of tables', 5 | _LICENSE = [[ 6 | MIT LICENSE 7 | 8 | Copyright (c) 2013 Enrique García Cota 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a 11 | copy of this software and associated documentation files (the 12 | "Software"), to deal in the Software without restriction, including 13 | without limitation the rights to use, copy, modify, merge, publish, 14 | distribute, sublicense, and/or sell copies of the Software, and to 15 | permit persons to whom the Software is furnished to do so, subject to 16 | the following conditions: 17 | 18 | The above copyright notice and this permission notice shall be included 19 | in all copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 22 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 23 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 24 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 25 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 26 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 27 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 28 | ]] 29 | } 30 | 31 | local tostring = tostring 32 | 33 | inspect.KEY = setmetatable({}, {__tostring = function() return 'inspect.KEY' end}) 34 | inspect.METATABLE = setmetatable({}, {__tostring = function() return 'inspect.METATABLE' end}) 35 | 36 | local function rawpairs(t) 37 | return next, t, nil 38 | end 39 | 40 | -- Apostrophizes the string if it has quotes, but not aphostrophes 41 | -- Otherwise, it returns a regular quoted string 42 | local function smartQuote(str) 43 | if str:match('"') and not str:match("'") then 44 | return "'" .. str .. "'" 45 | end 46 | return '"' .. str:gsub('"', '\\"') .. '"' 47 | end 48 | 49 | -- \a => '\\a', \0 => '\\0', 31 => '\31' 50 | local shortControlCharEscapes = { 51 | ["\a"] = "\\a", ["\b"] = "\\b", ["\f"] = "\\f", ["\n"] = "\\n", 52 | ["\r"] = "\\r", ["\t"] = "\\t", ["\v"] = "\\v" 53 | } 54 | local longControlCharEscapes = {} -- \a => nil, \0 => \000, 31 => \031 55 | for i=0, 31 do 56 | local ch = string.char(i) 57 | if not shortControlCharEscapes[ch] then 58 | shortControlCharEscapes[ch] = "\\"..i 59 | longControlCharEscapes[ch] = string.format("\\%03d", i) 60 | end 61 | end 62 | 63 | local function escape(str) 64 | return (str:gsub("\\", "\\\\") 65 | :gsub("(%c)%f[0-9]", longControlCharEscapes) 66 | :gsub("%c", shortControlCharEscapes)) 67 | end 68 | 69 | local function isIdentifier(str) 70 | return type(str) == 'string' and str:match( "^[_%a][_%a%d]*$" ) 71 | end 72 | 73 | local function isSequenceKey(k, sequenceLength) 74 | return type(k) == 'number' 75 | and 1 <= k 76 | and k <= sequenceLength 77 | and math.floor(k) == k 78 | end 79 | 80 | local defaultTypeOrders = { 81 | ['number'] = 1, ['boolean'] = 2, ['string'] = 3, ['table'] = 4, 82 | ['function'] = 5, ['userdata'] = 6, ['thread'] = 7 83 | } 84 | 85 | local function sortKeys(a, b) 86 | local ta, tb = type(a), type(b) 87 | 88 | -- strings and numbers are sorted numerically/alphabetically 89 | if ta == tb and (ta == 'string' or ta == 'number') then return a < b end 90 | 91 | local dta, dtb = defaultTypeOrders[ta], defaultTypeOrders[tb] 92 | -- Two default types are compared according to the defaultTypeOrders table 93 | if dta and dtb then return defaultTypeOrders[ta] < defaultTypeOrders[tb] 94 | elseif dta then return true -- default types before custom ones 95 | elseif dtb then return false -- custom types after default ones 96 | end 97 | 98 | -- custom types are sorted out alphabetically 99 | return ta < tb 100 | end 101 | 102 | -- For implementation reasons, the behavior of rawlen & # is "undefined" when 103 | -- tables aren't pure sequences. So we implement our own # operator. 104 | local function getSequenceLength(t) 105 | local len = 1 106 | local v = rawget(t,len) 107 | while v ~= nil do 108 | len = len + 1 109 | v = rawget(t,len) 110 | end 111 | return len - 1 112 | end 113 | 114 | local function getNonSequentialKeys(t) 115 | local keys, keysLength = {}, 0 116 | local sequenceLength = getSequenceLength(t) 117 | for k,_ in rawpairs(t) do 118 | if not isSequenceKey(k, sequenceLength) then 119 | keysLength = keysLength + 1 120 | keys[keysLength] = k 121 | end 122 | end 123 | table.sort(keys, sortKeys) 124 | return keys, keysLength, sequenceLength 125 | end 126 | 127 | local function countTableAppearances(t, tableAppearances) 128 | tableAppearances = tableAppearances or {} 129 | 130 | if type(t) == 'table' then 131 | if not tableAppearances[t] then 132 | tableAppearances[t] = 1 133 | for k,v in rawpairs(t) do 134 | countTableAppearances(k, tableAppearances) 135 | countTableAppearances(v, tableAppearances) 136 | end 137 | countTableAppearances(getmetatable(t), tableAppearances) 138 | else 139 | tableAppearances[t] = tableAppearances[t] + 1 140 | end 141 | end 142 | 143 | return tableAppearances 144 | end 145 | 146 | local copySequence = function(s) 147 | local copy, len = {}, #s 148 | for i=1, len do copy[i] = s[i] end 149 | return copy, len 150 | end 151 | 152 | local function makePath(path, ...) 153 | local keys = {...} 154 | local newPath, len = copySequence(path) 155 | for i=1, #keys do 156 | newPath[len + i] = keys[i] 157 | end 158 | return newPath 159 | end 160 | 161 | local function processRecursive(process, item, path, visited) 162 | if item == nil then return nil end 163 | if visited[item] then return visited[item] end 164 | 165 | local processed = process(item, path) 166 | if type(processed) == 'table' then 167 | local processedCopy = {} 168 | visited[item] = processedCopy 169 | local processedKey 170 | 171 | for k,v in rawpairs(processed) do 172 | processedKey = processRecursive(process, k, makePath(path, k, inspect.KEY), visited) 173 | if processedKey ~= nil then 174 | processedCopy[processedKey] = processRecursive(process, v, makePath(path, processedKey), visited) 175 | end 176 | end 177 | 178 | local mt = processRecursive(process, getmetatable(processed), makePath(path, inspect.METATABLE), visited) 179 | if type(mt) ~= 'table' then mt = nil end -- ignore not nil/table __metatable field 180 | setmetatable(processedCopy, mt) 181 | processed = processedCopy 182 | end 183 | return processed 184 | end 185 | 186 | 187 | 188 | ------------------------------------------------------------------- 189 | 190 | local Inspector = {} 191 | local Inspector_mt = {__index = Inspector} 192 | 193 | function Inspector:puts(...) 194 | local args = {...} 195 | local buffer = self.buffer 196 | local len = #buffer 197 | for i=1, #args do 198 | len = len + 1 199 | buffer[len] = args[i] 200 | end 201 | end 202 | 203 | function Inspector:down(f) 204 | self.level = self.level + 1 205 | f() 206 | self.level = self.level - 1 207 | end 208 | 209 | function Inspector:tabify() 210 | self:puts(self.newline, string.rep(self.indent, self.level)) 211 | end 212 | 213 | function Inspector:alreadyVisited(v) 214 | return self.ids[v] ~= nil 215 | end 216 | 217 | function Inspector:getId(v) 218 | local id = self.ids[v] 219 | if not id then 220 | local tv = type(v) 221 | id = (self.maxIds[tv] or 0) + 1 222 | self.maxIds[tv] = id 223 | self.ids[v] = id 224 | end 225 | return tostring(id) 226 | end 227 | 228 | function Inspector:putKey(k) 229 | if isIdentifier(k) then return self:puts(k) end 230 | self:puts("[") 231 | self:putValue(k) 232 | self:puts("]") 233 | end 234 | 235 | function Inspector:putTable(t) 236 | if t == inspect.KEY or t == inspect.METATABLE then 237 | self:puts(tostring(t)) 238 | elseif self:alreadyVisited(t) then 239 | self:puts('') 240 | elseif self.level >= self.depth then 241 | self:puts('{...}') 242 | else 243 | if self.tableAppearances[t] > 1 then self:puts('<', self:getId(t), '>') end 244 | 245 | local nonSequentialKeys, nonSequentialKeysLength, sequenceLength = getNonSequentialKeys(t) 246 | local mt = getmetatable(t) 247 | if (vim and vim._empty_dict_mt 248 | and sequenceLength == 0 and nonSequentialKeysLength == 0 249 | and mt == vim._empty_dict_mt) then 250 | self:puts(tostring(t)) 251 | return 252 | end 253 | 254 | self:puts('{') 255 | self:down(function() 256 | local count = 0 257 | for i=1, sequenceLength do 258 | if count > 0 then self:puts(',') end 259 | self:puts(' ') 260 | self:putValue(t[i]) 261 | count = count + 1 262 | end 263 | 264 | for i=1, nonSequentialKeysLength do 265 | local k = nonSequentialKeys[i] 266 | if count > 0 then self:puts(',') end 267 | self:tabify() 268 | self:putKey(k) 269 | self:puts(' = ') 270 | self:putValue(t[k]) 271 | count = count + 1 272 | end 273 | 274 | if type(mt) == 'table' then 275 | if count > 0 then self:puts(',') end 276 | self:tabify() 277 | self:puts(' = ') 278 | self:putValue(mt) 279 | end 280 | end) 281 | 282 | if nonSequentialKeysLength > 0 or type(mt) == 'table' then -- result is multi-lined. Justify closing } 283 | self:tabify() 284 | elseif sequenceLength > 0 then -- array tables have one extra space before closing } 285 | self:puts(' ') 286 | end 287 | 288 | self:puts('}') 289 | end 290 | end 291 | 292 | function Inspector:putValue(v) 293 | local tv = type(v) 294 | 295 | if tv == 'string' then 296 | self:puts(smartQuote(escape(v))) 297 | elseif tv == 'number' or tv == 'boolean' or tv == 'nil' or 298 | tv == 'cdata' or tv == 'ctype' or tv == 'userdata' then 299 | self:puts(tostring(v)) 300 | elseif tv == 'table' then 301 | self:putTable(v) 302 | else 303 | self:puts('<', tv, ' ', self:getId(v), '>') 304 | end 305 | end 306 | 307 | ------------------------------------------------------------------- 308 | 309 | function inspect.inspect(root, options) 310 | options = options or {} 311 | 312 | local depth = options.depth or math.huge 313 | local newline = options.newline or '\n' 314 | local indent = options.indent or ' ' 315 | local process = options.process 316 | 317 | if process then 318 | root = processRecursive(process, root, {}, {}) 319 | end 320 | 321 | local inspector = setmetatable({ 322 | depth = depth, 323 | level = 0, 324 | buffer = {}, 325 | ids = {}, 326 | maxIds = {}, 327 | newline = newline, 328 | indent = indent, 329 | tableAppearances = countTableAppearances(root) 330 | }, Inspector_mt) 331 | 332 | inspector:putValue(root) 333 | 334 | return table.concat(inspector.buffer) 335 | end 336 | 337 | setmetatable(inspect, { __call = function(_, ...) return inspect.inspect(...) end }) 338 | 339 | return inspect 340 | 341 | -------------------------------------------------------------------------------- /plugin/luadev.vim: -------------------------------------------------------------------------------- 1 | command! -bar Luadev lua require'luadev'.start() 2 | 3 | noremap (Luadev-RunLine) lua require'luadev'.exec(vim.api.nvim_get_current_line()) 4 | vnoremap (Luadev-Run) :call luadev_run_operator(v:true) 5 | nnoremap (Luadev-Run) :set opfunc=luadev_run_operatorg@ 6 | noremap (Luadev-RunWord) :call luaeval("require'luadev'.exec(_A)", get_current_word()) 7 | inoremap (Luadev-Complete) lua require'luadev.complete'() 8 | 9 | " thanks to @xolox on stackoverflow 10 | function! s:luadev_run_operator(is_op) 11 | let [lnum1, col1] = getpos(a:is_op ? "'<" : "'[")[1:2] 12 | let [lnum2, col2] = getpos(a:is_op ? "'>" : "']")[1:2] 13 | 14 | if lnum1 > lnum2 15 | let [lnum1, col1, lnum2, col2] = [lnum2, col2, lnum1, col1] 16 | endif 17 | 18 | let lines = getline(lnum1, lnum2) 19 | if a:is_op == v:true || lnum1 == lnum2 20 | let lines[-1] = lines[-1][: col2 - (&selection == 'inclusive' ? 1 : 2)] 21 | let lines[0] = lines[0][col1 - 1:] 22 | end 23 | let lines = join(lines, "\n")."\n" 24 | call v:lua.require'luadev'.exec(lines) 25 | endfunction 26 | 27 | function! s:get_current_word() 28 | let isk_save = &isk 29 | let &isk = '@,48-57,_,192-255,.' 30 | let word = expand("") 31 | let &isk = isk_save 32 | return word 33 | endfunction 34 | 35 | 36 | --------------------------------------------------------------------------------