diff --git a/README.md b/README.md
index 151928a..d0d9636 100644
--- a/README.md
+++ b/README.md
@@ -14,18 +14,21 @@ After installing Neovim, it took me some time to configure it, learn its setting
I got fed up of constantly hitting `:`, typing `lua= command`, then typing `:messages` to see the output, only to find out that a made a typo or a
syntax error and retyping the whole thing again, copying the paths from error stacktraces and so on. I needed something better, so there it is.
+
+
## ✨ Features
- Evaluate single line expressions and statements, visually selected lines of code or the whole buffer
-- Pretty print Lua objects, including function details and their source paths
-- Show normal and error output in the console/buffer, including output of `print()`, errors and stacktraces.
+- Pretty print Lua objects, including results of assignments, function details and their source paths
+- Show normal and error output inline in the console/buffer, including output of `print()`, errors and stacktraces.
- Syntax highlighting and autocompletion
-- Load Neovim’s messages into console for inspection and copy/paste
+- Load Neovim’s messages and output of ex commands into console for inspection and copy/paste
- Open links from stacktraces and function sources
- Save / Load / Autosave console session
- Use as a scratch pad for code gists
- Attach code evaluators to any buffer
+
## 📦 Installation
@@ -34,12 +37,18 @@ With [lazy.nvim](https://github.com/folke/lazy.nvim):
```lua
return {
"yarospace/lua-console.nvim",
- lazy = true, keys = "`", opts = {},
+ lazy = true,
+ keys = {'`', '`'},
+ opts = {},
}
```
-otherwise, install with your favorite package manager and add
-`require('lua-console').setup { custom_options }` somewhere in your config.
+otherwise, install with your favorite package manager and add somewhere in your config:
+
+```lua
+require('lua-console').setup { your_custom_options }
+```
+
## ⚙️ Configuration
@@ -87,6 +96,7 @@ opts = {
+
## 🚀 Basic usage (with default mappings)
@@ -110,6 +120,7 @@ opts = {
saved whenever it is toggled or closed.
- You can resize the console with `` and ``.
+
## 📓 Notes on globals, locals and preserving execution context
@@ -129,6 +140,7 @@ There are two functions available within the console:
- `_ctx()` - will print the contents of the context
- `_ctx_clear()` - clears the context
+
## ⭐ Extra features
@@ -257,6 +269,8 @@ There are two functions available within the console:
]]
```
+
+
## Alternatives and comparison
There are a number of alternatives available, notably:
@@ -268,5 +282,6 @@ There are a number of alternatives available, notably:
Initially, when starting with Lua and Neovim, I tried all the REPLs/code runners I could find. However, I was not satisfied with all of them in one way or another.
Lua-console is an attempt to combine the best features of all of them, like REPL / scratch pad / code runner / debug console, while leaving the UX and config simple.
+
## 🔥 All feedback and feature requests are very welcome! Enjoy!
diff --git a/lua/lazy.lua b/lua/lazy.lua
index 0e59a16..58c8884 100644
--- a/lua/lazy.lua
+++ b/lua/lazy.lua
@@ -1,6 +1,6 @@
return {
'yarospace/lua-console.nvim',
lazy = true,
- keys = '`',
+ keys = {'`', '`'},
opts = {},
}
diff --git a/lua/lua-console.lua b/lua/lua-console.lua
index 2d4c177..55dab37 100644
--- a/lua/lua-console.lua
+++ b/lua/lua-console.lua
@@ -1,4 +1,7 @@
-local config, mappings, utils, injections
+local config = require('lua-console.config')
+local mappings = require('lua-console.mappings')
+local utils = require('lua-console.utils')
+local injections = require('lua-console.injections')
local get_or_create_buffer = function()
--- @type number
@@ -77,10 +80,7 @@ end
local setup = function(opts)
_G.Lua_console = { buf = false, win = false, height = 0, ctx = {} }
- config = require('lua-console.config').setup(opts)
- mappings = require('lua-console.mappings')
- utils = require('lua-console.utils')
- injections = require('lua-console.injections')
+ config.setup(opts)
mappings.set_global_mappings()
mappings.set_console_commands()
diff --git a/lua/lua-console/mappings.lua b/lua/lua-console/mappings.lua
index 3f04cf5..cfc948e 100644
--- a/lua/lua-console/mappings.lua
+++ b/lua/lua-console/mappings.lua
@@ -1,6 +1,5 @@
local M = {}
-local console = require('lua-console')
local config = require('lua-console.config')
local utils = require('lua-console.utils')
@@ -13,6 +12,7 @@ local function set_map(buf, map, opts, mode)
end
M.set_global_mappings = function()
+ local console = require('lua-console')
local m = config.mappings
set_map(nil, m.toggle, {
diff --git a/lua/lua-console/utils.lua b/lua/lua-console/utils.lua
index 099d992..97f730d 100644
--- a/lua/lua-console/utils.lua
+++ b/lua/lua-console/utils.lua
@@ -5,6 +5,8 @@ local to_string = function(tbl, sep, trim)
tbl = tbl or {}
sep = sep or '\n'
+ if type(tbl) ~= 'table' then tbl = { tbl } end
+
local line = table.concat(tbl, sep)
local patterns = { '\\r', '\\t', '\\n' }
@@ -123,9 +125,9 @@ local get_path_lnum = function(path)
return path, lnum
end
----Determines if there is an assigment on the line and returns its value
----@param line string[]
-local get_assignment = function(line)
+----Determines if there is an assigment on the line and returns its value
+----@param line string[]
+local get_line_assignment = function(line)
if not line or #line == 0 then return end
local lhs = line[1]:match('^(.-)%s*=')
@@ -138,39 +140,29 @@ local get_assignment = function(line)
end
end
-local print_buffer = {}
-
----@param buf number
----@param lines string[] Text to append to current buffer after current selection
-local append_current_buffer = function(buf, lines)
- if not lines or #lines == 0 then return end
-
+local get_last_assignment = function()
+ local ctx = get_ctx()
local lnum = vim.fn.line('.')
- local prefix = config.buffer.result_prefix
- local empty_results = { 'nil', '', '""', "''" }
- local virtual_text
- local line = lines[#lines]
-
- if vim.tbl_contains(empty_results, line) then
- table.remove(lines)
- virtual_text = line
- end
+ local last_var = ctx._last_assignment
+ local last_val = ctx[ctx._last_assignment]
- local assignment_value = get_assignment(vim.fn.getbufline(buf, lnum, lnum))
- if assignment_value ~= nil then virtual_text = assignment_value end
+ if not last_var then return end
- if virtual_text then show_virtual_text(buf, 3, prefix .. virtual_text, lnum - 1, 'eol', 'Comment') end
+ local line
+ local offset = 0
- if #lines == 0 then return end
+ for i = lnum - 1, 0, -1 do
+ line = vim.api.nvim_buf_get_lines(0, i, i + 1, false)[1]
- lines[1] = prefix .. lines[1]
- table.insert(lines, 1, '') -- insert an empty line
+ if line:match('^%s*' .. last_var .. '%s*=') then break end
+ offset = offset + 1
+ end
- vim.api.nvim_buf_set_lines(buf, lnum, lnum, false, lines)
+ return last_var, last_val, offset
end
----Pretty prints objects and appends to print_buffer
+---Pretty prints objects
---@param ... any[]
---@return string[]
local pretty_print = function(...)
@@ -187,10 +179,47 @@ local pretty_print = function(...)
result = result .. var_no .. vim.inspect(o)
end
- result = to_table(result)
- vim.list_extend(print_buffer, result)
+ return to_table(result)
+end
+
+local print_buffer = {}
+
+local print_to_buffer = function(...)
+ local ret = pretty_print(...)
+ vim.list_extend(print_buffer, ret)
+end
+
+---@param buf number
+---@param lines string[] Text to append to current buffer after current selection
+local append_current_buffer = function(buf, lines)
+ if not lines or #lines == 0 then return end
+
+ local lnum = vim.fn.line('.')
+ local prefix = config.buffer.result_prefix
+ local empty_results = { 'nil', '', '""', "''" }
+
+ local virtual_text
+ local line = lines[#lines]
+
+ local last_assigned_var, last_assigned_val, last_assignment_offset = get_last_assignment()
+ if last_assigned_var then
+ virtual_text = to_string(pretty_print(last_assigned_val), '', true)
+ show_virtual_text(buf, 3, prefix .. virtual_text, lnum - last_assignment_offset - 1, 'eol', 'Comment')
+ end
+
+ if vim.tbl_contains(empty_results, line) then
+ table.remove(lines)
+
+ virtual_text = get_line_assignment(vim.fn.getbufline(buf, lnum, lnum)) or line
+ show_virtual_text(buf, 4, prefix .. virtual_text, lnum - 1, 'eol', 'Comment')
+ end
+
+ if #lines == 0 then return end
+
+ lines[1] = prefix .. lines[1]
+ table.insert(lines, 1, '') -- insert an empty line
- -- return result
+ vim.api.nvim_buf_set_lines(buf, lnum, lnum, false, lines)
end
local function remove_empty_lines(tbl)
@@ -227,11 +256,11 @@ local function clean_stacktrace(error)
return lines
end
-local function add_return(tbl)
- if vim.fn.trim(tbl[#tbl]) == 'end' then return tbl end
+local function add_return(tbl, lnum)
+ if to_string(tbl[lnum], '', true):match('^%s*end') then return tbl end
local ret = vim.deepcopy(tbl)
- ret[#ret] = 'return ' .. ret[#ret]
+ ret[lnum] = 'return ' .. ret[lnum]
return ret
end
@@ -245,19 +274,26 @@ function get_ctx(buf)
local ctx = lc.ctx[buf]
if config.buffer.preserve_context and ctx then return ctx end
- local env, mt = {}, {}
+ local env, mt, values = {}, {}, {}
mt = {
- print = pretty_print,
+ print = print_to_buffer,
_ctx = function()
- return vim.tbl_extend('force', {}, env)
+ return vim.deepcopy(values)
end,
_ctx_clear = function()
lc.ctx[buf] = nil
end,
__index = function(_, key)
- return mt[key] and mt[key] or _G[key]
+ return mt[key] and mt[key] or values[key] or _G[key]
end,
+ __newindex = function(_, k, v)
+ values[k] = v
+ mt._last_assignment = k
+ end,
+ _reset_last_assignment = function()
+ mt._last_assignment = nil
+ end
}
lc.ctx[buf] = env
@@ -271,10 +307,17 @@ end
function lua_evaluator(lines, ctx)
vim.validate { lines = { lines, 'table' } }
- local lines_with_return = add_return(lines)
local env = ctx or get_ctx()
+ env._reset_last_assignment()
- if not select(2, load(to_string(lines_with_return), '', 't', env)) then lines = lines_with_return end
+ local lines_with_return_first_line = add_return(lines, 1)
+ local lines_with_return_last_line = add_return(lines, #lines)
+
+ if not select(2, load(to_string(lines_with_return_first_line), '', 't', env)) then
+ lines = lines_with_return_first_line
+ elseif not select(2, load(to_string(lines_with_return_last_line), '', 't', env)) then
+ lines = lines_with_return_last_line
+ end
local code, error = load(to_string(lines), 'Lua console: ', 't', env)
if error then return to_table(error) end
@@ -289,9 +332,9 @@ function lua_evaluator(lines, ctx)
table.remove(result, 1)
if #result > 0 then
- pretty_print(unpack(result))
+ print_to_buffer(unpack(result))
else
- pretty_print(nil)
+ print_to_buffer(nil)
end
else
vim.list_extend(print_buffer, clean_stacktrace(err))
@@ -476,6 +519,8 @@ local attach_toggle = function(buf)
end
mappings.set_evaluator_mappings(buf, toggle)
+ vim.api.nvim_set_option_value('syntax', 'on', { buf = buf })
+
vim.notify(
('Evaluator %s for buffer [%s:%s]'):format(toggle and 'attached' or 'dettached', name, buf),
vim.log.levels.INFO
diff --git a/spec/unit/utils_spec.lua b/spec/unit/utils_spec.lua
index 5b7b579..6c12464 100644
--- a/spec/unit/utils_spec.lua
+++ b/spec/unit/utils_spec.lua
@@ -403,6 +403,44 @@ describe('lua-console.utils', function()
assert.has_string(result, expected)
end)
+ it('multiline expressions', function()
+ vim.api.nvim_win_set_cursor(win, { 1, 0 })
+ h.send_keys('V2j')
+
+ content = h.to_table([[
+ vim.iter({ 1, 2, 3, 4, 5 }):map(function(e)
+ return e
+ end):totable()
+ ]])
+
+ h.set_buffer(buf, content)
+ utils.eval_code_in_buffer()
+
+ expected = config.buffer.result_prefix .. '{ 1, 2, 3, 4, 5 }'
+
+ result = h.get_buffer(buf)
+ assert.has_string(result, expected)
+ end)
+
+ it('multiline statements', function()
+ vim.api.nvim_win_set_cursor(win, { 1, 0 })
+ h.send_keys('V2j')
+
+ content = h.to_table([[
+ ret = vim.iter({ 1, 2, 3, 4, 5 }):map(function(e)
+ return e
+ end):totable()
+ ]])
+
+ h.set_buffer(buf, content)
+ utils.eval_code_in_buffer()
+
+ expected = config.buffer.result_prefix .. '{ 1, 2, 3, 4, 5 }'
+
+ result = h.get_virtual_text(buf, 0, 0)
+ assert.has_string(result, expected)
+ end)
+
it('shows nil as virtual text', function()
vim.api.nvim_win_set_cursor(win, { 1, 0 })
h.send_keys('V2j')
@@ -441,6 +479,26 @@ describe('lua-console.utils', function()
assert.has_string(result, expected)
end)
+ it('shows value of the last assignment on the corresponging line as virtual text', function()
+ vim.api.nvim_win_set_cursor(win, { 1, 0 })
+ h.send_keys('V4j')
+
+ content = h.to_table([[
+ a = 5
+ for i = 1, 5 do
+ i = i + 5
+ end
+ vim.bo.filetype = tostring(a + 5) .. 'test'
+ ]])
+ h.set_buffer(buf, content)
+ utils.eval_code_in_buffer()
+
+ expected = config.buffer.result_prefix .. '5'
+
+ result = h.get_virtual_text(buf, 0, 0)
+ assert.has_string(result, expected)
+ end)
+
it('gives syntax error message', function()
vim.api.nvim_win_set_cursor(win, { 1, 0 })