├── .gitignore ├── .luacheckrc ├── README.md ├── autoload └── luapad.vim ├── lua ├── luapad.lua └── luapad │ ├── cmds.lua │ ├── completion.lua │ ├── config.lua │ ├── evaluator.lua │ ├── helper.lua │ ├── run.lua │ ├── state.lua │ ├── statusline.lua │ ├── tools.lua │ ├── toys.lua │ └── utils.lua ├── makefile ├── plugin └── luapad.vim ├── spec ├── conf_spec.lua ├── print_spec.lua ├── restore_context_spec.lua ├── run_specs.lua ├── status_spec.lua ├── test_helper.lua └── toggle_spec.lua └── tmp └── .keep /.gitignore: -------------------------------------------------------------------------------- 1 | tmp/* 2 | !tmp/.keep 3 | vendor 4 | -------------------------------------------------------------------------------- /.luacheckrc: -------------------------------------------------------------------------------- 1 | read_globals = { 2 | 'vim' 3 | } 4 | 5 | ignore = { 6 | '212' 7 | } 8 | 9 | allow_defined = true 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Interactive neovim scratchpad for lua 2 | 3 | Luapad runs your code in context with overwritten print function and displays the captured input as virtual text right there, where it was called - **in real time!** 4 | 5 | ![Luapad print demo](https://user-images.githubusercontent.com/8767998/198146427-c7488912-16e3-49de-811e-8bbc33c1f3da.gif) 6 | 7 | ![luapad colors demo](https://user-images.githubusercontent.com/8767998/198146416-04e026ce-8966-4c57-afc0-12fb62d6cff1.gif) 8 | 9 | ------- 10 | 11 | Luapad adds Lua command (as native lua command extension) with deep function completion. 12 | 13 | ![luapad Lua demo](https://user-images.githubusercontent.com/8767998/198146413-d2026d9e-f266-478c-959a-1ac65061aec0.gif) 14 | 15 | ------- 16 | 17 | # WARNING!!! 18 | 19 | Luapad evaluates every code that you put in it, so be careful what you type in, specially if it's system calls, file operations etc. Also calling functions like `nvim_open_win` isn't good idea, because every single change in buffer is evaluated (you will get new window with every typed char :D). 20 | 21 | Luapad was designed to mess with small nvim lua code chunks. It probably will not work well with big "real" / "production" scripts. 22 | 23 | All thoughts or/and error reports are welcome. 24 | 25 | ### Installation 26 | 27 | With vim-plug: 28 | 29 | ``` 30 | Plug 'rafcamlet/nvim-luapad' 31 | ``` 32 | 33 | With packer.nvim and Neovim >= [v0.8.0](https://github.com/neovim/neovim/releases/tag/v0.8.0): 34 | 35 | ``` 36 | use 'rafcamlet/nvim-luapad' 37 | ``` 38 | 39 | In versions of Neovim prior to v0.8.0, the `CursorHold` event is [buggy](https://github.com/neovim/neovim/issues/12587). If you're using an older version, it's recomended to use [this fix](https://github.com/antoinemadec/FixCursorHold.nvim): 40 | 41 | ``` 42 | use { 'rafcamlet/nvim-luapad', requires = "antoinemadec/FixCursorHold.nvim" } 43 | ``` 44 | 45 | ### Usage 46 | 47 | Luapadd provides three different commands, that will help you with developing neovim plugins in lua: 48 | - **Luapad** - which open interactive scratch buffer with real time evaluation. 49 | - **LuaRun** - which run content of current buffer as lua script in new scope. You do not need to write file to disc or have to worry about overwriting functions in global scope. 50 | - ~~**Lua** - which is extension of native lua command with function completion.~~ *This command will be removed in the next release, because the current version of Neovim has built-in cmd completion.* 51 | 52 | From version 0.2 luapad will move towards lua api exposure. Several useful functions are already available. 53 | 54 | ```lua 55 | require('luapad').init() -- same as Luapad command 56 | 57 | -- Creates a new luapad instance and attaches it to the current buffer. 58 | -- Optionally, you can pass a context table to it, the elements of which will be 59 | -- available during the evaluation as "global" variables. 60 | require('luapad').attach({ 61 | context = { return_4 = function() return 4 end } 62 | }) 63 | 64 | -- Detaches current luapad instance from buffer, which just means turning it off. :) 65 | require('luapad').detach() 66 | 67 | -- Toggles luapad in current buffer. 68 | require('luapad').toggle({ 69 | context = { return_4 = function() return 4 end } 70 | }) 71 | 72 | -- You can also create new luapad instance by yourself, which can be helpfull if you 73 | -- want to attach it to a buffer different than the current one. 74 | local buffer_handler = 5 75 | require('luapad.evaluator'):new { 76 | buf = buffer_handler, 77 | context = { a = 'asdf' } 78 | }:start() 79 | 80 | -- luapad/run offers a run function (same as the LuaRun command) but allows you 81 | -- to specify a context tbl 82 | require 'luapad.run'.run { 83 | context = { 84 | print = function(str) print(string.upper(str)) end 85 | } 86 | } 87 | 88 | -- If you turn off evaluation on change (and move) you can trigger it manualy by: 89 | local luapad = require('luapad.evaluator'):new{buf = vim.api.nvim_get_current_buf()} 90 | luapad:start() 91 | luapad:eval() 92 | 93 | -- You can always access current luapad instance by: 94 | local luapad = require 'luapad.state'.current() 95 | luapad:eval() 96 | 97 | -- ...or iterate through all instances 98 | for _, v in ipairs(require('luapad.state').instances) do 99 | v:eval() 100 | end 101 | ``` 102 | 103 | 104 | ### Configuration 105 | 106 | You can configure luapad via `require('luapad').setup({})` function (or its alias `config`). Configuration via vim globals is disabled. If you want to use old configuration method, please checkout version 0.2. 107 | 108 | 109 | | Name | default value | Description | 110 | | --- | --- | --- | 111 | | count_limit | 200000 | Luapad uses count hook method to prevent infinite loops occurring during code execution. Setting count_limit too high will make Luapad laggy, setting it too low, may cause premature code termination. | 112 | | error_indicator | true | Show virtual text with error message (except syntax or timeout. errors) | 113 | | preview | true | Show floating output window on cursor hold. It's a good idea to set low update time. For example: `let &updatetime = 300` You can jump to it by `^w` `w`. | 114 | | eval_on_change | true | Evaluate buffer content when it changes. | 115 | | eval_on_move | false | Evaluate all luapad buffers when the cursor moves. | 116 | | print_highlight | 'Comment' | Highlight group used to coloring luapad print output. | 117 | | error_highlight | 'ErrorMsg' | Highlight group used to coloring luapad error indicator. | 118 | | on_init | function | Callback function called after creating new luapad instance. | 119 | | context | {} | The default context tbl in which luapad buffer is evaluated. Its properties will be available in buffer as "global" variables. | 120 | | split_orientation | 'vertical' | The orientation of the split created by `Luapad` command. Can be `vertical` or `horizontal`. | 121 | | wipe | true | The Luapad buffer by default is wiped out after closing/loosing a window. If you're used to switching buffers, and you want to keep Luapad instance alive in the background, set it to false. | 122 | 123 | 124 | Example configuration (note it isn't the default one!) 125 | 126 | ```lua 127 | require('luapad').setup { 128 | count_limit = 150000, 129 | error_indicator = false, 130 | eval_on_move = true, 131 | error_highlight = 'WarningMsg', 132 | split_orientation = 'horizontal', 133 | on_init = function() 134 | print 'Hello from Luapad!' 135 | end, 136 | context = { 137 | the_answer = 42, 138 | shout = function(str) return(string.upper(str) .. '!') end 139 | } 140 | } 141 | ``` 142 | 143 | ### Statusline 144 | 145 | Luapad has ready to use lightline function_components. 146 | 147 |
148 | Example lightline configuration: 149 |
150 | let g:lightline = {
151 |       \ 'active': {
152 |       \   'left': [
153 |       \     [ 'mode', 'paste' ],
154 |       \     [ 'readonly', 'filename', 'modified' ],
155 |       \     [ 'luapad_msg']
156 |       \   ],
157 |       \ 'right': [
158 |       \   ['luapad_status'],
159 |       \   ['lineinfo'],
160 |       \   ['percent'],
161 |       \ ],
162 |       \ },
163 |       \ 'component_function': {
164 |       \   'luapad_msg': 'luapad#lightline_msg',
165 |       \   'luapad_status': 'luapad#lightline_status',
166 |       \ },
167 |       \ }
168 | 
169 |
170 |
171 | 172 | 173 | But you can also create your own integration, using lua functions `require'luapad.statusline'.status()` and `require'luapad.statusline'.msg()`. 174 | 175 | 176 |
177 | Example galaxyline configuration: 178 |
179 | local function luapad_color()
180 |   if require('luapad.statusline').status() == 'ok' then
181 |     return colors.green
182 |   else
183 |     return colors.red
184 |   end
185 | end
186 | 
187 | 188 |
189 | require('galaxyline').section.right[1] = {
190 |   Luapad = {
191 |     condition = require('luapad.state').current,
192 |     highlight = { luapad_color(), colors.bg },
193 |     provider = function()
194 |       vim.cmd('hi GalaxyLuapad guifg=' .. luapad_color())
195 |       local status = require('luapad.statusline').status()
196 |       return string.upper(tostring(status))
197 |     end
198 |   }
199 | }
200 | 
201 |
202 |
203 | 204 | 205 | ### Types of errors 206 | 207 | Luapad separates errors into 3 categories: 208 | 209 | | Error | Description | 210 | | --- | --- | 211 | | SYNTAX | Content of buffer is not valid lua script (you will see it a lot during typing) | 212 | | TIMEOUT | Interpreter has done more count instructions than luapad_count_limit, so there probably was a infinite loop | 213 | | ERROR | Execution logical errors | 214 | 215 | 216 | ### Changelog 217 | #### v0.3 218 | 219 | - Drop viml configuration 220 | - Improve preview window resizing (although it still needs some work) 221 | - Fix "file no longer available" error 222 | - Galaxyline example added to readme 223 | - Upgrading specs 224 | - Other minor upgrades and refactor 225 | 226 | #### v0.2 227 | 228 | - Better nvim native lsp integration (now you should have lsp completion in luapad buffers) 229 | - Enable creation of multiple luapads instances 230 | - Allow luapad to be attached to an existing buffer 231 | - Add on_init callback 232 | - Allow providing evaluation context for luapad buffers 233 | - Allow configure luapad via lua 234 | - Add `eval_on_move` and `eval_on_change` settings 235 | - Expose luapad lua api 236 | - Replace `g:luapad_status` and `g:luapad_msg` variables by `status()` and `msg()` lua functions. 237 | - Now luapad print function print also nil values 238 | 239 | ### TODO 240 | - Allow changing orientation of the preview window 241 | 242 | ### Shameless self promotion 243 | 244 | If you want to start your adventure with writing lua plugins and are you are wondering where to begin, you can take a look at the links below. 245 | 246 | 1. [How to write neovim plugins in Lua](https://www.2n.pl/blog/how-to-write-neovim-plugins-in-lua) 247 | 2. [How to make UI for neovim plugins in Lua](https://www.2n.pl/blog/how-to-make-ui-for-neovim-plugins-in-lua) 248 | -------------------------------------------------------------------------------- /autoload/luapad.vim: -------------------------------------------------------------------------------- 1 | function! luapad#lightline_status() 2 | return luaeval("require'luapad/statusline':lightline_status()") 3 | endfunction 4 | 5 | function! luapad#lightline_msg() 6 | return luaeval("require'luapad/statusline':lightline_msg()") 7 | endfunction 8 | -------------------------------------------------------------------------------- /lua/luapad.lua: -------------------------------------------------------------------------------- 1 | local set_config = require'luapad.config'.set_config 2 | local Config = require'luapad.config'.config 3 | local vim_config_disabled_warn = require'luapad.config'.vim_config_disabled_warn 4 | 5 | local Evaluator = require'luapad.evaluator' 6 | local State = require 'luapad.state' 7 | local path = require 'luapad.tools'.path 8 | local create_file = require 'luapad.tools'.create_file 9 | local remove_file = require 'luapad.tools'.remove_file 10 | 11 | local GCounter = 0 12 | 13 | local function init() 14 | vim_config_disabled_warn() 15 | 16 | GCounter = GCounter + 1 17 | local file_path = path('tmp', 'Luapad_' .. GCounter .. '.lua') 18 | 19 | -- hacky solution to deal with native lsp 20 | remove_file(file_path) 21 | create_file(file_path) 22 | 23 | local split_orientation = 'vsplit' 24 | if Config.split_orientation == 'horizontal' then 25 | split_orientation = 'split' 26 | end 27 | vim.api.nvim_command('botright ' .. split_orientation .. ' ' .. file_path) 28 | 29 | local buf = vim.api.nvim_get_current_buf() 30 | 31 | Evaluator:new{buf = buf}:start() 32 | 33 | vim.api.nvim_buf_set_option(buf, 'swapfile', false) 34 | vim.api.nvim_buf_set_option(buf, 'filetype', 'lua') 35 | vim.api.nvim_buf_set_option(buf, 'bufhidden', Config.wipe and 'wipe' or 'hide') 36 | vim.api.nvim_command('au QuitPre set nomodified') 37 | 38 | if Config.wipe then 39 | -- Always try to keep file as modified so it can't be accidentally switched 40 | vim.api.nvim_buf_set_option(buf, 'modified', true) 41 | vim.api.nvim_command([[au BufWritePost lua vim.schedule(function() vim.api.nvim_buf_set_option(0, 'modified', true) end)]]) 42 | end 43 | end 44 | 45 | local function attach(opts) 46 | if State.current() then return end 47 | opts = opts or {} 48 | opts.buf = vim.api.nvim_get_current_buf() 49 | Evaluator:new(opts):start() 50 | end 51 | 52 | local function detach() 53 | if State.current() then State.current():finish() end 54 | end 55 | 56 | local function toggle(opts) 57 | if State.current() then detach() else attach(opts) end 58 | end 59 | 60 | return { 61 | init = init, 62 | attach = attach, 63 | detach = detach, 64 | toggle = toggle, 65 | config = set_config, 66 | setup = set_config, 67 | current = State.current, 68 | version = '0.3' 69 | } 70 | -------------------------------------------------------------------------------- /lua/luapad/cmds.lua: -------------------------------------------------------------------------------- 1 | local State = require('luapad.state') 2 | local Config = require'luapad.config'.config 3 | 4 | local function on_cursor_hold(buf) 5 | if Config.preview then State.instances[buf]:preview() end 6 | end 7 | 8 | local function on_luapad_cursor_moved(buf) 9 | State.instances[buf]:close_preview() 10 | end 11 | 12 | local function on_cursor_moved() 13 | if Config.eval_on_move then 14 | for _, v in pairs(State.instances) do v:eval() end 15 | end 16 | end 17 | 18 | return { 19 | on_cursor_hold = on_cursor_hold, 20 | on_cursor_moved = on_cursor_moved, 21 | on_luapad_cursor_moved = on_luapad_cursor_moved, 22 | } 23 | -------------------------------------------------------------------------------- /lua/luapad/completion.lua: -------------------------------------------------------------------------------- 1 | local tbl_keys = require'luapad.tools'.tbl_keys 2 | local inspect = vim.inspect -- luacheck: ignore 3 | 4 | local function completion_search(s_arr, prefix, r_arr) 5 | if #s_arr == 0 then return end 6 | if not r_arr then r_arr = _G end 7 | 8 | local head = table.remove(s_arr, 1) 9 | 10 | if type(r_arr[head]) == 'table' then 11 | prefix = prefix .. head .. '.' 12 | return completion_search(s_arr, prefix, r_arr[head]) 13 | end 14 | 15 | local result = {} 16 | local keys = tbl_keys(r_arr) 17 | table.sort(keys) 18 | 19 | for _, v in ipairs(keys) do 20 | local regex = '^' .. string.gsub(head, '%*', '.*') 21 | if v:find(regex) then table.insert(result, prefix .. v) end 22 | end 23 | 24 | return result 25 | end 26 | 27 | function completion(line) 28 | local index = line:find('[%w._*]*$') 29 | local cmd = line:sub(index) 30 | local prefix = line:sub(1, index - 1) 31 | 32 | local arr = vim.split(cmd, '.', true) 33 | 34 | return completion_search(arr, prefix) 35 | end 36 | 37 | return completion 38 | -------------------------------------------------------------------------------- /lua/luapad/config.lua: -------------------------------------------------------------------------------- 1 | local warning = [[[Luapad] Configure Luapad via vim globals is disabled. Please use \"require('luapad').config\".]] 2 | local print_warn = require('luapad.tools').print_warn 3 | 4 | local deprecated_vars = { 5 | 'luapad_count_limit', 6 | 'luapad_error_indicator', 7 | 'luapad_preview', 8 | 'luapad_eval_on_change', 9 | 'luapad_eval_on_move', 10 | 'luapad_print_highlight', 11 | 'luapad_error_highlight', 12 | } 13 | 14 | local function vim_config_disabled_warn() 15 | local warn_flag = false 16 | 17 | for _, var in ipairs(deprecated_vars) do 18 | local s, v = pcall(vim.api.nvim_get_var, var) 19 | if s and v then warn_flag = true end 20 | end 21 | 22 | if warn_flag then print_warn(warning) end 23 | end 24 | 25 | local Config = { 26 | on_init = nil, 27 | context = nil, 28 | 29 | preview = true, 30 | error_indicator = true, 31 | count_limit = 2 * 1e5, 32 | print_highlight = 'Comment', 33 | error_highlight = 'ErrorMsg', 34 | eval_on_move = false, 35 | eval_on_change = true, 36 | split_orientation = 'vertical', 37 | wipe = true 38 | } 39 | 40 | local function set_config(opts) 41 | opts = opts or {} 42 | for k, v in pairs(opts) do Config[k] = v end 43 | end 44 | 45 | return { 46 | config = Config, 47 | set_config = set_config, 48 | vim_config_disabled_warn = vim_config_disabled_warn 49 | } 50 | -------------------------------------------------------------------------------- /lua/luapad/evaluator.lua: -------------------------------------------------------------------------------- 1 | local Config = require 'luapad.config'.config 2 | local set_config = require 'luapad.config'.set_config 3 | local State = require 'luapad.state' 4 | local utils = require 'luapad.utils' 5 | 6 | local parse_error = require'luapad.tools'.parse_error 7 | 8 | local ns = vim.api.nvim_create_namespace('luapad_namespace') 9 | 10 | Evaluator = {} 11 | Evaluator.__index = Evaluator 12 | 13 | local function single_line(arr) 14 | local result = {} 15 | for _, v in ipairs(arr) do 16 | local str = v:gsub("\n", ''):gsub(' +', ' ') 17 | table.insert(result, str) 18 | end 19 | return table.concat(result, ', ') 20 | end 21 | 22 | function Evaluator:set_virtual_text(line, str, color) 23 | vim.api.nvim_buf_set_virtual_text( 24 | self.buf, 25 | ns, 26 | line, 27 | {{tostring(str), color}}, 28 | {} 29 | ) 30 | end 31 | 32 | function Evaluator:update_view() 33 | if not self.buf then return end 34 | if not vim.api.nvim_buf_is_valid(self.buf) then return end 35 | 36 | for line, arr in pairs(self.output) do 37 | local res = {} 38 | for _, v in ipairs(arr) do table.insert(res, single_line(v)) end 39 | self:set_virtual_text(line - 1, ' '..table.concat(res, ' | '), Config.print_highlight) 40 | end 41 | end 42 | 43 | function Evaluator:tcall(fun) 44 | local count_limit = Config.count_limit < 1000 and 1000 or Config.count_limit 45 | 46 | success, result = pcall(function() 47 | debug.sethook(function() error('LuapadTimeoutError') end, "", count_limit) 48 | fun() 49 | end) 50 | 51 | if not success then 52 | if result:find('LuapadTimeoutError') then 53 | self.statusline.status = 'timeout' 54 | else 55 | print(result) 56 | self.statusline.status = 'error' 57 | local line, error_msg = parse_error(result) 58 | self.statusline.msg = ('%s: %s'):format((line or ''), (error_msg or '')) 59 | 60 | if Config.error_indicator and line then 61 | self:set_virtual_text(tonumber(line) - 1, '<-- '..error_msg, Config.error_highlight) 62 | end 63 | end 64 | end 65 | 66 | debug.sethook() 67 | end 68 | 69 | function Evaluator:print(...) 70 | local size = select('#', ...) 71 | if size == 0 then return end 72 | 73 | local args = {...} 74 | local str = {} 75 | 76 | for i=1, size do 77 | table.insert(str, tostring(vim.inspect(args[i]))) 78 | end 79 | 80 | local line = debug.traceback('', 3):match('^.-]:(%d-):') 81 | if not line then return end 82 | line = tonumber(line) 83 | 84 | if not self.output[line] then self.output[line] = {} end 85 | table.insert(self.output[line], str) 86 | end 87 | 88 | function Evaluator:eval() 89 | local context = self.context or vim.deepcopy(Config.context) or {} 90 | local luapad_print = function(...) self:print(...) end 91 | 92 | context.luapad = self 93 | context.p = luapad_print 94 | context.print = luapad_print 95 | context.luapad = self.helper 96 | 97 | setmetatable(context, { __index = _G}) 98 | 99 | self.statusline = { status = 'ok' } 100 | 101 | vim.api.nvim_buf_clear_namespace(self.buf, ns, 0, -1) 102 | 103 | self.output = {} 104 | 105 | local code = vim.api.nvim_buf_get_lines(self.buf, 0, -1, {}) 106 | local f, result = loadstring(table.concat(code, '\n')) 107 | 108 | if not f then 109 | local _, msg = parse_error(result) 110 | self.statusline.status = 'syntax' 111 | self.statusline.msg = msg 112 | return 113 | end 114 | 115 | setfenv(f, context) 116 | self:tcall(f) 117 | self:update_view() 118 | end 119 | 120 | function Evaluator:close_preview() 121 | vim.schedule(function() 122 | if self.preview_win and vim.api.nvim_win_is_valid(self.preview_win) then 123 | vim.api.nvim_win_close(self.preview_win, false) 124 | end 125 | end) 126 | end 127 | 128 | function Evaluator:preview() 129 | local line = vim.api.nvim_win_get_cursor(0)[1] 130 | 131 | if not self.output[line] then return end 132 | 133 | local buf = vim.api.nvim_create_buf(false, true) 134 | vim.api.nvim_buf_set_option(buf, 'bufhidden', 'wipe') 135 | vim.api.nvim_buf_set_option(buf, 'filetype', 'lua') 136 | 137 | local content = vim.split(table.concat(utils.tbl_flatten(self.output[line]), "\n"), "\n") 138 | 139 | vim.api.nvim_buf_set_lines(buf, 0, -1, false, content) 140 | 141 | local lines = tonumber(vim.api.nvim_win_get_height(0)) - 10 142 | local cols = tonumber(vim.api.nvim_win_get_width(0)) 143 | if vim.fn.screenrow() >= lines then lines = 0 end 144 | 145 | local opts = { 146 | relative = 'win', 147 | col = 0, 148 | row = lines, 149 | height = 10, 150 | width = cols, 151 | style = 'minimal' 152 | } 153 | 154 | if self.preview_win and vim.api.nvim_win_is_valid(self.preview_win) then 155 | vim.api.nvim_win_set_buf(self.preview_win, buf) 156 | vim.api.nvim_win_set_config(self.preview_win, opts) 157 | else 158 | self.preview_win = vim.api.nvim_open_win(buf, false, opts) 159 | vim.api.nvim_win_set_option(self.preview_win, 'signcolumn', 'no') 160 | end 161 | end 162 | 163 | function Evaluator:new(attrs) 164 | attrs = attrs or {} 165 | assert(attrs.buf, 'You need to set buf for luapad') 166 | 167 | attrs.statusline = { status = 'ok' } 168 | attrs.active = true 169 | attrs.output = {} 170 | attrs.helper = { 171 | buf = attrs.buf, 172 | config = set_config 173 | } 174 | 175 | local obj = setmetatable(attrs, Evaluator) 176 | State.instances[attrs.buf] = obj 177 | return obj 178 | end 179 | 180 | function Evaluator:start() 181 | local on_change = vim.schedule_wrap(function() 182 | if not self.active then return true end 183 | if Config.eval_on_change then self:eval() end 184 | end) 185 | 186 | local on_detach = vim.schedule_wrap(function() 187 | self:close_preview() 188 | State.instances[self.buf] = nil 189 | end) 190 | 191 | vim.api.nvim_buf_attach(0, false, { 192 | on_lines = on_change, 193 | on_changedtick = on_change, 194 | on_detach = on_detach 195 | }) 196 | 197 | vim.api.nvim_command('augroup LuapadAutogroup') 198 | vim.api.nvim_command('autocmd!') 199 | vim.api.nvim_command('au CursorMoved * lua require("luapad/cmds").on_cursor_moved()') 200 | vim.api.nvim_command('augroup END') 201 | vim.api.nvim_command(('augroup LuapadAutogroupNr%s'):format(self.buf)) 202 | vim.api.nvim_command('autocmd!') 203 | vim.api.nvim_command(([[au CursorHold lua require("luapad/cmds").on_cursor_hold(%s)]]):format(self.buf)) 204 | vim.api.nvim_command(([[au CursorMoved lua require("luapad/cmds").on_luapad_cursor_moved(%s)]]):format(self.buf)) 205 | vim.api.nvim_command(([[au CursorMovedI lua require("luapad/cmds").on_luapad_cursor_moved(%s)]]):format(self.buf)) 206 | vim.api.nvim_command('augroup END') 207 | 208 | if Config.on_init then Config.on_init() end 209 | self:eval() 210 | end 211 | 212 | function Evaluator:finish() 213 | self.active = false 214 | vim.api.nvim_command(('augroup LuapadAutogroupNr%s'):format(self.buf)) 215 | vim.api.nvim_command('autocmd!') 216 | vim.api.nvim_command('augroup END') 217 | State.instances[self.buf] = nil 218 | 219 | if vim.api.nvim_buf_is_valid(self.buf) then 220 | vim.api.nvim_buf_clear_namespace(self.buf, ns, 0, -1) 221 | end 222 | self:close_preview() 223 | end 224 | 225 | return Evaluator 226 | -------------------------------------------------------------------------------- /lua/luapad/helper.lua: -------------------------------------------------------------------------------- 1 | local Config = require'luapad.config'.config 2 | local set_config = require'luapad.config'.set_config 3 | 4 | local Helper = {} 5 | 6 | function Helper.set_lines(start, finish, replacement) 7 | if not vim.api.nvim_buf_is_valid(Helper.start_buf) then return end 8 | vim.api.nvim_buf_set_lines(Helper.start_buf, start, finish, false, replacement) 9 | end 10 | 11 | function Helper.add_hl(hl, line, start, finish) 12 | if not vim.api.nvim_buf_is_valid(Helper.start_buf) then return end 13 | vim.api.nvim_buf_add_highlight(Helper.start_buf, -1, hl, line, start, finish) 14 | end 15 | 16 | function Helper.clear() 17 | Helper.set_lines(0, -1, {}) 18 | end 19 | 20 | function Helper.add(str, color) 21 | local lines = type(str) == 'string' and {str} or str 22 | 23 | if Helper._first then 24 | Helper.clear() 25 | Helper.set_lines(0, 1, lines) 26 | Helper._first = false 27 | if color and type(color) == 'string' then 28 | Helper.add_hl(color, 0, 0, -1) 29 | end 30 | return 0 31 | end 32 | 33 | Helper.set_lines(-1, -1, lines) 34 | local line_nr = vim.api.nvim_buf_line_count(Helper.start_buf) - 1 35 | 36 | if color and type(color) == 'string' then 37 | Helper.add_hl(color, line_nr, 0, -1) 38 | end 39 | 40 | return line_nr 41 | end 42 | 43 | function Helper.new(start_buf) 44 | Helper.start_buf = start_buf 45 | Helper._first = true 46 | return Helper 47 | end 48 | 49 | Helper.config = set_config 50 | 51 | return Helper 52 | 53 | -- Multiline add 54 | -- setup function 55 | -- internal error handling 56 | -------------------------------------------------------------------------------- /lua/luapad/run.lua: -------------------------------------------------------------------------------- 1 | local print_error = require'luapad.tools'.print_error 2 | local parse_error = require'luapad.tools'.parse_error 3 | 4 | local function print_run_error(err) 5 | local line_nr, msg = parse_error(err) 6 | print_error(('error on line %s: %s'):format(line_nr, msg)) 7 | end 8 | 9 | local function run(opts) 10 | local context = opts and opts.context or {} 11 | setmetatable(context, { __index = _G}) 12 | 13 | local code = vim.api.nvim_buf_get_lines(0, 0, -1, {}) 14 | local f, error_str = loadstring(table.concat(code, '\n')) 15 | if not f then return print_run_error(error_str) end 16 | 17 | setfenv(f, context) 18 | local success, result = pcall(f) 19 | if not success then return print_run_error(result) end 20 | end 21 | 22 | return { 23 | run = run 24 | } 25 | -------------------------------------------------------------------------------- /lua/luapad/state.lua: -------------------------------------------------------------------------------- 1 | State = { 2 | instances = {} 3 | } 4 | 5 | State.current = function() 6 | return State.instances[vim.api.nvim_get_current_buf()] 7 | end 8 | 9 | return State 10 | -------------------------------------------------------------------------------- /lua/luapad/statusline.lua: -------------------------------------------------------------------------------- 1 | local State = require 'luapad.state' 2 | 3 | local function status() 4 | if State.current() then return State.current().statusline.status end 5 | end 6 | 7 | local function msg() 8 | if State.current() then return State.current().statusline.msg end 9 | end 10 | 11 | 12 | local function lightline_status() 13 | if status() then return string.upper(status()) else return '' end 14 | end 15 | 16 | local function lightline_msg() 17 | return msg() or '' 18 | end 19 | 20 | return { 21 | status = status, 22 | msg = msg, 23 | lightline_msg = lightline_msg, 24 | lightline_status = lightline_status 25 | } 26 | -------------------------------------------------------------------------------- /lua/luapad/tools.lua: -------------------------------------------------------------------------------- 1 | local function parse_error(str) 2 | return str:match("%[string.*%]:(%d*): (.*)") 3 | end 4 | 5 | local function tbl_keys(t) 6 | local keys = {} 7 | for k, _ in pairs(t) do 8 | table.insert(keys, k) 9 | end 10 | return keys 11 | end 12 | 13 | local sep = vim.api.nvim_call_function('has', {'win32'}) == 0 and '/' or '\\' 14 | 15 | local function path(...) 16 | return vim.api.nvim_eval('tempname()') .. '_Luapad.lua' 17 | end 18 | 19 | local function create_file(f) 20 | local fd = vim.loop.fs_open(f, "w", 438) 21 | vim.loop.fs_close(fd) 22 | end 23 | 24 | local function remove_file(f) 25 | vim.loop.fs_unlink(f) 26 | end 27 | 28 | local function print_warn(str) 29 | vim.api.nvim_command('echohl WarningMsg') 30 | vim.api.nvim_command(('echomsg "%s"'):format(str)) 31 | vim.api.nvim_command('echohl None') 32 | end 33 | 34 | local function print_error(str) 35 | vim.api.nvim_command('echohl Error') 36 | vim.api.nvim_command(('echomsg "%s"'):format(str)) 37 | vim.api.nvim_command('echohl None') 38 | end 39 | 40 | 41 | return { 42 | parse_error = parse_error, 43 | tbl_keys = tbl_keys, 44 | path = path, 45 | create_file = create_file, 46 | remove_file = remove_file, 47 | print_warn = print_warn, 48 | print_error = print_error 49 | } 50 | -------------------------------------------------------------------------------- /lua/luapad/toys.lua: -------------------------------------------------------------------------------- 1 | local function vprint(str, color) 2 | 3 | local line = debug.traceback('', 2):match(':(%d*):') 4 | if not line then return end 5 | line = tonumber(line) - 1 6 | 7 | vim.api.nvim_buf_set_virtual_text( 8 | 0, 0, line, {{tostring(str), color or 'Comment'}}, {} 9 | ) 10 | end 11 | 12 | return { 13 | vprint = vprint 14 | } 15 | -------------------------------------------------------------------------------- /lua/luapad/utils.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | ---@param t table 4 | ---@return table 5 | function M.tbl_flatten(t) 6 | return vim.fn.has('nvim-0.11') == 1 and vim.iter(t):flatten(math.huge):totable() or vim.tbl_flatten(t) 7 | end 8 | 9 | return M 10 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | test: 2 | nvim --headless -c "luafile spec/run_specs.lua" 3 | stable: 4 | # nvim-stable -u ~/.config/nvim/clean.vim --headless -c 'luafile spec/$(FILE).lua' -c 'lua os.exit(require("luaunit").LuaUnit.run())' -c quit -i NONE 5 | -------------------------------------------------------------------------------- /plugin/luapad.vim: -------------------------------------------------------------------------------- 1 | " Maintainer: Rafał Camlet 2 | " License: GNU General Public License v3.0 3 | 4 | if exists('g:luapad_loaded') | finish | endif 5 | 6 | let s:save_cpo = &cpo 7 | set cpo&vim 8 | 9 | command! Luapad lua require'luapad'.init() 10 | command! LuaRun lua require'luapad.run'.run() 11 | 12 | function! Luapd_lua_complete (ArgLead, CmdLine, CursorPos) abort 13 | return luaeval('require"luapad/completion"(_A)', a:ArgLead) 14 | endfunction 15 | 16 | command! -complete=customlist,Luapd_lua_complete -nargs=1 Lua lua 17 | 18 | let &cpo = s:save_cpo 19 | unlet s:save_cpo 20 | 21 | let g:luapad_loaded = 1 22 | -------------------------------------------------------------------------------- /spec/conf_spec.lua: -------------------------------------------------------------------------------- 1 | local t = require 'spec/test_helper' 2 | 3 | describe("config", function() 4 | 5 | t.setup() 6 | 7 | before_each(function() 8 | t.restart() 9 | end) 10 | 11 | local set_config = function(opts) 12 | t.exec_lua([[require'luapad'.config(...)]], {opts}) 13 | end 14 | 15 | it('handel error_indicator setting', function () 16 | local vt 17 | 18 | set_config({ error_indicator = true }) 19 | t.set_lines(0, -1, 'local a = b + c') 20 | 21 | vt = t.get_virtual_text(0) 22 | 23 | assert.matches('attempt to perform arithmetic on.*a nil value', vt) 24 | 25 | 26 | set_config({ error_indicator = false }) 27 | t.set_lines(0, -1, 'local d = e + f') 28 | 29 | vt = t.get_virtual_text(0) 30 | 31 | assert.is.Nil(vt) 32 | end) 33 | 34 | t.finish() 35 | end) 36 | -------------------------------------------------------------------------------- /spec/print_spec.lua: -------------------------------------------------------------------------------- 1 | local t = require 'spec/test_helper' 2 | 3 | describe("print", function() 4 | 5 | t.setup() 6 | 7 | before_each(function() 8 | t.restart() 9 | end) 10 | 11 | it("prints virtual text", function() 12 | t.set_lines(0, 0,"print('incredible!')") 13 | local vt = t.get_virtual_text(0) 14 | 15 | assert.matches('incredible!', vt) 16 | end) 17 | 18 | 19 | it("prints tbl", function() 20 | t.set_lines(0, 0, "print(vim.split('wow|wow', '|'))") 21 | local vt = t.get_virtual_text(0) 22 | 23 | assert.matches('{ "wow", "wow" }', vt) 24 | end) 25 | 26 | 27 | it('prints function', function() 28 | t.set_lines(0, 0, [[ 29 | function wow(...) 30 | print({...}) 31 | end 32 | 33 | wow(1, 2, 3, 'aaa') 34 | ]]) 35 | 36 | local vt = t.get_virtual_text(1) 37 | assert.matches('{ 1, 2, 3, "aaa" }', vt) 38 | end) 39 | 40 | 41 | it('prints fun called multiple times', function() 42 | t.set_lines(0,0, [[function asdf(a) print(a) end 43 | asdf('foo') 44 | asdf('bar')]]) 45 | local vt = t.get_virtual_text(0) 46 | 47 | assert.matches('"foo" | "bar"', vt) 48 | end) 49 | 50 | 51 | it('prints loop', function () 52 | t.set_lines(0,0, [[for i=0, 5 do print(i) end]]) 53 | local vt = t.get_virtual_text(0) 54 | 55 | assert.matches('0 | 1 | 2 | 3 | 4 | 5', vt) 56 | end) 57 | 58 | 59 | it('print error messages', function() 60 | t.set_lines(0,0,[[ local a = '' .. nil ]]) 61 | local vt = t.get_virtual_text(0) 62 | 63 | assert.matches('attempt to concatenate a nil value', vt) 64 | end) 65 | 66 | 67 | t.finish() 68 | end) 69 | -------------------------------------------------------------------------------- /spec/restore_context_spec.lua: -------------------------------------------------------------------------------- 1 | local t = require 'spec/test_helper' 2 | 3 | describe("context", function() 4 | 5 | t.setup() 6 | before_each(function() 7 | t.restart() 8 | end) 9 | 10 | 11 | it('clears context after buffer change', function () 12 | t.set_lines(0,0,[[ 13 | a = 5 14 | 15 | function asdf() 16 | print(a) 17 | end 18 | 19 | asdf() 20 | ]]) 21 | 22 | assert.matches('5', t.get_virtual_text(3)) 23 | 24 | t.set_lines(2,5, {'', '', ''}) 25 | t.command('1') 26 | t.set_lines(0,1, 'a = 30') 27 | 28 | assert.is_nil(t.get_virtual_text(3)) 29 | 30 | local msg = t.exec_lua('return require"luapad/statusline".msg()') 31 | 32 | assert.matches('attempt to call global .asdf.', msg) 33 | end) 34 | 35 | t.finish() 36 | end) 37 | -------------------------------------------------------------------------------- /spec/run_specs.lua: -------------------------------------------------------------------------------- 1 | local Job = require("plenary.job") 2 | local harness = require'plenary.test_harness' 3 | local log = require("plenary.log") 4 | 5 | local print_output = vim.schedule_wrap(function(_, ...) 6 | for _, v in ipairs({...}) do 7 | io.stdout:write(tostring(v)) 8 | io.stdout:write("\n") 9 | end 10 | 11 | vim.cmd [[mode]] 12 | end) 13 | 14 | local function non_paraller_test_directory(directory) 15 | 16 | print("Starting...") 17 | 18 | local res = {} 19 | 20 | local outputter = print_output 21 | 22 | local paths = harness._find_files_to_run(directory) 23 | 24 | local failure = false 25 | 26 | local jobs = vim.tbl_map( 27 | function(p) 28 | local args = { 29 | '--headless', 30 | '-c', 31 | string.format('lua require("plenary.busted").run("%s")', p:absolute()) 32 | } 33 | 34 | local job = Job:new { 35 | command = vim.v.progpath, 36 | args = args, 37 | 38 | on_stdout = function(_, data) 39 | outputter(res.bufnr, data) 40 | end, 41 | 42 | on_stderr = function(_, data) 43 | outputter(res.bufnr, data) 44 | end, 45 | 46 | on_exit = vim.schedule_wrap(function(j_self, _, _) 47 | outputter(res.bufnr, unpack(j_self:stderr_result())) 48 | outputter(res.bufnr, unpack(j_self:result())) 49 | 50 | vim.cmd('mode') 51 | end) 52 | } 53 | job.nvim_busted_path = p.filename 54 | return job 55 | end, 56 | paths 57 | ) 58 | 59 | log.debug("Running...") 60 | for i, j in ipairs(jobs) do 61 | j:start() 62 | log.debug("... Sequential wait for job number", i) 63 | Job.join(j,50000) 64 | log.debug("... Completed job number", i) 65 | if j.code ~= 0 then 66 | failure = true 67 | end 68 | end 69 | 70 | vim.wait(100) 71 | 72 | if failure then os.exit(1) end 73 | 74 | os.exit(0) 75 | end 76 | 77 | non_paraller_test_directory('spec/') 78 | -------------------------------------------------------------------------------- /spec/status_spec.lua: -------------------------------------------------------------------------------- 1 | local t = require 'spec/test_helper' 2 | 3 | describe("status", function() 4 | 5 | t.setup() 6 | 7 | before_each(function() 8 | t.restart() 9 | end) 10 | 11 | local status = function() 12 | return t.exec_lua('return require"luapad/statusline".status()') 13 | end 14 | 15 | it('is eq "ok" when evryting is ok', function () 16 | t.set_lines(0,0, "print('hello!')") 17 | assert.equals('ok', status()) 18 | end) 19 | 20 | 21 | it('is eq "error" when the code is invalid', function() 22 | t.set_lines(0,0, "local a = '' .. nil") 23 | 24 | assert.equals('error', status()) 25 | end) 26 | 27 | 28 | it('is eq "timeout" if there was timeout', function() 29 | t.set_lines(0,0, "while true do print('wow') end") 30 | 31 | assert.equals('timeout', status()) 32 | end) 33 | 34 | 35 | t.finish() 36 | end) 37 | -------------------------------------------------------------------------------- /spec/test_helper.lua: -------------------------------------------------------------------------------- 1 | local TestHelper = { 2 | address = '/tmp/luapad_nvim_socket', 3 | nr = 10 4 | } 5 | 6 | function TestHelper.sleep(n) 7 | os.execute("sleep " .. tonumber(n)) 8 | end 9 | 10 | function TestHelper.split_keys(str) 11 | local flag = false 12 | local result = {} 13 | local buf 14 | 15 | for c in str:gmatch"." do 16 | if c == '<' then 17 | buf = '<' 18 | flag = true 19 | elseif c == '>' then 20 | table.insert(result, buf .. '>') 21 | flag = false 22 | elseif flag then 23 | buf = buf .. c 24 | else 25 | table.insert(result, c) 26 | end 27 | end 28 | 29 | return result 30 | end 31 | 32 | function TestHelper.setup() 33 | local arr = { 34 | 'tmux kill-pane -a -t 0', 35 | 'tmux split-window -h -d -p 30' 36 | } 37 | 38 | if os.execute 'tmux has-session -t .1 2>/dev/null' ~= 0 then 39 | for _,v in ipairs(arr) do os.execute(v) end 40 | end 41 | end 42 | 43 | 44 | function TestHelper.restart() 45 | TestHelper.nr = TestHelper.nr + 1 46 | local cmd = ('tmux respawn-pane -k -t .1 "nvim --listen %s"'):format(TestHelper.address .. TestHelper.nr) 47 | os.execute(cmd) 48 | 49 | repeat 50 | TestHelper.sleep(0.2) 51 | local ok, val = pcall(vim.fn.sockconnect, 'pipe', TestHelper.address .. TestHelper.nr, {rpc = true}) 52 | TestHelper.connection = val 53 | until(ok) 54 | 55 | TestHelper.command('Luapad') 56 | TestHelper.command('only!') 57 | end 58 | 59 | 60 | function TestHelper.nvim(str, ...) 61 | return vim.rpcrequest(TestHelper.connection, "nvim_" .. str, unpack({...})) 62 | end 63 | 64 | function TestHelper.finish() 65 | pcall(vim.rpcrequest, TestHelper.connection, 'nvim_command', 'qa!') 66 | vim.api.nvim_call_function('chanclose', {TestHelper.connection}) 67 | end 68 | 69 | function TestHelper.command(str) 70 | TestHelper.nvim('command', str) 71 | end 72 | 73 | function TestHelper.input(str) 74 | TestHelper.nvim('input', str) 75 | end 76 | 77 | function TestHelper.typein(str) 78 | -- command('set insertmode') 79 | for _, v in ipairs(TestHelper.split_keys(str)) do 80 | TestHelper.input(v) 81 | TestHelper.sleep(0.1) 82 | end 83 | end 84 | 85 | function TestHelper.exec_lua(str, args) 86 | return TestHelper.nvim('exec_lua', str, args or {}) 87 | end 88 | 89 | function TestHelper.exec(str) 90 | TestHelper.nvim('exec', str, false) 91 | end 92 | 93 | function TestHelper.set_lines(start, finish, arr) 94 | if type(arr) == 'string' then arr = vim.split(arr, "\n") end 95 | TestHelper.nvim('buf_set_lines', 0, start, finish, false, arr) 96 | end 97 | 98 | function TestHelper.get_lines(start, finish) 99 | return TestHelper.nvim('buf_get_lines', 0, start, finish, false) 100 | end 101 | 102 | function TestHelper.get_virtual_text(line) 103 | local ns = TestHelper.nvim('create_namespace', 'luapad_namespace') 104 | local result = TestHelper.nvim('buf_get_extmarks', 0, ns, {line, 0}, {line, -1}, { details = true }) 105 | 106 | if #result == 0 then return end 107 | return result[1][#result[1]]["virt_text"][1][1] 108 | end 109 | 110 | function TestHelper.print(...) 111 | if #{...} > 1 then 112 | io.stdout:write(tostring(vim.inspect({...}))) 113 | else 114 | io.stdout:write(tostring(vim.inspect(...))) 115 | end 116 | 117 | io.stdout:write("\n") 118 | end 119 | 120 | return TestHelper 121 | -------------------------------------------------------------------------------- /spec/toggle_spec.lua: -------------------------------------------------------------------------------- 1 | local t = require 'spec/test_helper' 2 | 3 | describe("toggle", function() 4 | 5 | t.setup() 6 | 7 | before_each(function() 8 | t.restart() 9 | end) 10 | 11 | 12 | it('toggles luapad', function () 13 | t.command 'tabnew' 14 | t.set_lines(0,0, [[ 15 | local a, b 16 | a = 10 17 | b = 20 18 | print(a + b) 19 | ]]) 20 | 21 | assert.is_nil(t.get_virtual_text(3)) 22 | t.exec_lua('require"luapad".toggle()') 23 | 24 | assert.matches('30', t.get_virtual_text(3)) 25 | 26 | t.exec_lua('require"luapad".toggle()') 27 | 28 | assert.is_nil(t.get_virtual_text(3)) 29 | 30 | t.exec_lua('require"luapad".toggle()') 31 | t.set_lines(1,2, {'a = 20'}) 32 | 33 | assert.matches('40', t.get_virtual_text(3)) 34 | end) 35 | 36 | t.finish() 37 | end) 38 | -------------------------------------------------------------------------------- /tmp/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rafcamlet/nvim-luapad/176686eb616a5ada5dfc748f2b5109194bbe8a71/tmp/.keep --------------------------------------------------------------------------------