diff --git a/.luacheckrc b/.luacheckrc index 2304834..60343aa 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -26,4 +26,6 @@ read_globals = { "assert" } -exclude_files = { } +exclude_files = { + "spec/utfTerminal.lua" +} diff --git a/Makefile b/Makefile index 7a4039f..95b7fa5 100644 --- a/Makefile +++ b/Makefile @@ -14,10 +14,10 @@ stylua: test: eval $(luarocks path --lua-version 5.1 --bin) - busted --run unit + busted -o spec/utfTerminal.lua --run unit watch: - while sleep 0.1; do ls -d spec/**/*.lua | entr -d -c make test; done + while sleep 0.1; do ls -d spec/**/*.lua | entr -d -c busted -o spec/utfTerminal.lua --run unit; done watch_current: - while sleep 0.1; do ls -d spec/**/*.lua | entr -d -c busted --run unit -t=current; done + while sleep 0.1; do ls -d spec/**/*.lua | entr -d -c busted -o spec/utfTerminal.lua --run unit -t=current; done diff --git a/README.md b/README.md index aeb098d..e478166 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,8 @@ otherwise, install with your favourite package manager and add ## ⚙️ Configuration +All settings are very straight forward, but please read below about `preserve_context` option below. +
Default Settings @@ -49,7 +51,8 @@ opts = { buffer = { prepend_result_with = '=> ', save_path = vim.fn.stdpath('state') .. '/lua-console.lua', - load_on_start = true -- load saved session on first entry + load_on_start = true, -- load saved session on first entry + preserve_context = true -- preserve context between executions }, window = { anchor = 'SW', @@ -83,8 +86,31 @@ opts = { - Hit `Enter` in normal mode to evaluate a variable, statement or an expression in the current line. - Visually select a range of lines and press `Enter` to evaluate the code in the range. - The evaluation of the last line is returned and printed, so no `return` is needed in most cases. -- Use `print()` in your code to output the result into the console. Objects and functions are pretty printed. +- Use `print()` in your code to output the results into the console. Objects and functions are pretty printed. - Press `M` to load Neovim messages into the console. -- Press `gf` to follow the paths in stack traces and to function sources. +- Press `gf` to follow the paths in stack traces and to function sources. Truncated paths work too. - Use `S` and `L` to save / load the console session to preserve history of your hacking. - You can resize the console with `` and ``. + +## 📓 Notes on globals, locals and preserving context +> [!NOTE] +> Highlights information that users should take into account, even when skimming. + +> [!TIP] +> Optional information to help a user be more successful. + +> [!IMPORTANT] +> Crucial information necessary for users to succeed. + +> [!WARNING] +> Critical content demanding immediate user attention due to potential risks. + +> [!CAUTION] +> Negative potential consequences of an action. +- +- + +## Alternatives and comparison + +- +- diff --git a/doc/lua-console.nvim_api.txt b/doc/lua-console.nvim_api.txt index 33527a5..c677296 100644 --- a/doc/lua-console.nvim_api.txt +++ b/doc/lua-console.nvim_api.txt @@ -2,9 +2,12 @@ ------------------------------------------------------------------------------ *load_console()* -`load_console`() +`load_console`({on_start}) + saved console +Parameters ~ + {on_start} `(boolean)` ------------------------------------------------------------------------------ *append_current_buffer()* diff --git a/lua/lua-console/utils.lua b/lua/lua-console/utils.lua index b2e3dc5..e0df06f 100644 --- a/lua/lua-console/utils.lua +++ b/lua/lua-console/utils.lua @@ -8,6 +8,16 @@ local to_table = function(str) return vim.split(str or '', '\n', { trimempty = true }) end +local pack = function(...) + local ret = {} + for i = 1, select("#", ...) do + local el = select(i, ...) + table.insert(ret, el) + end + + return ret +end + local toggle_help = function() local buf = Lua_console.buf local cm = config.mappings @@ -28,8 +38,6 @@ local toggle_help = function() vim.api.nvim_buf_set_extmark(buf, ns, 0, 0, { id=2, virt_text = { { message, 'Comment' } }, virt_text_pos = 'overlay', undo_restore = false, invalidate = true }) end - - ids = vim.api.nvim_buf_get_extmarks(buf, ns, 0, -1, {}) end ---Loads saved console @@ -48,8 +56,8 @@ end local infer_truncated_path = function(truncated_path) local pos, _ = truncated_path:find('/lua/') local path = truncated_path:sub(pos + 1, #truncated_path) - local found = vim.api.nvim_get_runtime_file(path, true) + return not vim.tbl_isempty(found) and found[1] or false end @@ -97,7 +105,6 @@ end local pretty_print = function(...) local result, var_no = '', '' local nargs = select('#', ...) - for i=1, nargs do local o = select(i, ...) @@ -189,11 +196,15 @@ local eval_lua = function(lines) print_buffer = {} ---@cast code function - local status, result = xpcall(code, debug.traceback) - if status then - pretty_print(result) + local result = pack(xpcall(code, debug.traceback)) + if result[1] then + table.remove(result, 1) + if #result > 0 then pretty_print(unpack(result)) + else + pretty_print(nil) + end else - vim.list_extend(print_buffer, clean_stacktrace(result)) + vim.list_extend(print_buffer, clean_stacktrace(result[2])) end return print_buffer diff --git a/spec/spec_helper.lua b/spec/spec_helper.lua index 6f26fcb..af91d4b 100644 --- a/spec/spec_helper.lua +++ b/spec/spec_helper.lua @@ -59,7 +59,7 @@ M.get_virtual_text = function(buf, line) local ids = vim.api.nvim_buf_get_extmarks(buf, ns, line, -1, {}) if vim.tbl_isempty(ids) then - LOG('No extmarks found') + _G.LOG('No extmarks found') return end diff --git a/spec/unit/lua-console_spec.lua b/spec/unit/lua-console_spec.lua index 83dd0d4..a046954 100644 --- a/spec/unit/lua-console_spec.lua +++ b/spec/unit/lua-console_spec.lua @@ -1,6 +1,7 @@ local h = require('spec_helper') describe('lua-console.nvim', function() + local buf, win local console, config local expected, result @@ -47,8 +48,6 @@ describe('lua-console.nvim', function() end) describe('lua-console - open/close window', function() - local buf, win - before_each(function() console.toggle_console() buf = vim.fn.bufnr('lua-console') diff --git a/spec/unit/mappings_spec.lua b/spec/unit/mappings_spec.lua index 6c0be74..7429ae7 100644 --- a/spec/unit/mappings_spec.lua +++ b/spec/unit/mappings_spec.lua @@ -193,7 +193,8 @@ describe("lua-console.nvim - mappings", function() h.send_keys("gf") local new_buf = vim.fn.bufnr() - assert.has_string(vim.fn.bufname(new_buf), "/usr/share/nvim-linux64/share/nvim/runtime/lua/vim/lsp/diagnostic.lua") + local path = vim.fn.expand('$VIMRUNTIME') .. "/lua/vim/lsp/diagnostic.lua" + assert.has_string(vim.fn.bufname(new_buf), path) local line = vim.fn.line('.') assert.is_same(line, 189) diff --git a/spec/unit/utils_spec.lua b/spec/unit/utils_spec.lua index 40af305..7e06d35 100644 --- a/spec/unit/utils_spec.lua +++ b/spec/unit/utils_spec.lua @@ -89,6 +89,16 @@ describe("lua-console.utils", function() result = eval_lua(code) assert.has_string(result, "nil") end) + + it('handles code that returns multiple values', function() + code = h.to_table([[ + a = 'test_string' + a:find('str') + ]]) + + result = eval_lua(code) + assert.has_string(result, "[1] 6, [2] 8") + end) end) describe('preserving context', function() @@ -164,7 +174,7 @@ describe("lua-console.utils", function() local eval_lua = utils.eval_lua local code, result, expected - it("pretty prints objects", function() + it("pretty prints objects #current", function() code = h.to_table([[ local ret = {} for i=1, 5 do @@ -381,8 +391,9 @@ describe("lua-console.utils", function() end) it("infers truncated paths in the stacktrace", function() + local rtp = vim.fn.expand('$VIMRUNTIME') local truncated = '...e/nvim-linux64/share/nvim/runtime/lua/vim/diagnostic.lua' - expected = "/usr/share/nvim-linux64/share/nvim/runtime/lua/vim/diagnostic.lua" + expected = rtp .. "/lua/vim/diagnostic.lua" local path, _ = utils.get_path_lnum(truncated) assert.is_same(expected, path) @@ -395,15 +406,17 @@ describe("lua-console.utils", function() end) it("infers truncated paths and line number in the stacktrace", function() + local truncated = '...testing/start/lua-console.nvim/lua/lua-console/utils.lua' content = h.to_table([[ '...testing/start/lua-console.nvim/lua/lua-console/utils.lua:85' ]]) h.set_buffer(buf, content) - vim.api.nvim_win_set_cursor(win, { 11, 0 }) + vim.api.nvim_win_set_cursor(win, { 1, 0 }) expected = vim.fn.expand('$XDG_PLUGIN_PATH') .. '/lua/lua-console/utils.lua' - path, lnum = utils.get_path_lnum(truncated) + local path, lnum = utils.get_path_lnum(truncated) + assert.is_same(expected, path) assert.is_same(85, lnum) end) @@ -427,7 +440,7 @@ describe("lua-console.utils", function() vim.api.nvim_win_set_cursor(win, { 11, 0 }) local truncated = ".../.local/share/nvim/lazy/arrow.nvim/lua/arrow/persist.lua" - local path, lnum = utils.get_path_lnum(truncated) + local _, lnum = utils.get_path_lnum(truncated) assert.equals(125, lnum) end) diff --git a/spec/utfTerminal.lua b/spec/utfTerminal.lua new file mode 100644 index 0000000..4415a6c --- /dev/null +++ b/spec/utfTerminal.lua @@ -0,0 +1,224 @@ +local s = require 'say' +local pretty = require 'pl.pretty' +local term = require 'term' +local luassert = require 'luassert' +local io = io +local type = type +local string_format = string.format +local string_gsub = string.gsub +local io_write = io.write +local io_flush = io.flush +local pairs = pairs +local colors + +local isatty = io.type(io.stdout) == 'file' and term.isatty(io.stdout) + +return function(options) + local busted = require 'busted' + local handler = require 'busted.outputHandlers.base'() + local cli = require 'cliargs' + local args = options.arguments + + cli:set_name('utfTerminal output handler') + cli:flag('--color', 'force use of color') + cli:flag('--plain', 'force use of no color') + + local cliArgs, err = cli:parse(args) + if not cliArgs and err then + io.stderr:write(string.format('%s: %s\n\n', cli.name, err)) + io.stderr:write(cli.printer.generate_help_and_usage().. '\n') + os.exit(1) + end + + if cliArgs.plain then + colors = setmetatable({}, {__index = function() return function(s) return s end end}) + luassert:set_parameter("TableErrorHighlightColor", "none") + + elseif cliArgs.color then + colors = require 'term.colors' + luassert:set_parameter("TableErrorHighlightColor", "red") + + else + if package.config:sub(1,1) == '\\' and not os.getenv("ANSICON") or not isatty then + -- Disable colors on Windows. + colors = setmetatable({}, {__index = function() return function(s) return s end end}) + luassert:set_parameter("TableErrorHighlightColor", "none") + else + colors = require 'term.colors' + luassert:set_parameter("TableErrorHighlightColor", "red") + end + end + + local successDot = colors.green('\226\151\143') -- '\226\151\143' = '●' = utf8.char(9679) + local failureDot = colors.red('\226\151\188') -- '\226\151\188' = '◼' = utf8.char(9724) + local errorDot = colors.magenta('\226\156\177') -- '\226\156\177' = '✱' = utf8.char(10033) + local pendingDot = colors.yellow('\226\151\140') -- '\226\151\140' = '◌' = utf8.char(9676) + + local pendingDescription = function(pending) + local name = pending.name + + -- '\226\134\146' = '→' = utf8.char('8594') + local string = colors.yellow(s('output.pending')) .. ' \226\134\146 ' .. + colors.cyan(pending.trace.short_src) .. ' @ ' .. + colors.cyan(pending.trace.currentline) .. + '\n' .. colors.bright(name) + + if type(pending.message) == 'string' then + string = string .. '\n' .. pending.message + elseif pending.message ~= nil then + string = string .. '\n' .. pretty.write(pending.message) + end + + return string + end + + local failureMessage = function(failure) + local string = failure.randomseed and ('Random seed: ' .. failure.randomseed .. '\n') or '' + if type(failure.message) == 'string' then + string = string .. failure.message + elseif failure.message == nil then + string = string .. 'Nil error' + else + string = string .. pretty.write(failure.message) + end + + return string + end + + local failureDescription = function(failure, isError) + -- '\226\134\146' = '→' = utf8.char(8594) + local string = colors.red(s('output.failure')) .. ' \226\134\146 ' + if isError then + string = colors.magenta(s('output.error')) .. ' \226\134\146 ' + end + + if not failure.element.trace or not failure.element.trace.short_src then + string = string .. + colors.red(failureMessage(failure)) .. '\n' .. + colors.red(failure.name) + else + string = string .. + colors.cyan(failure.element.trace.short_src) .. ' @ ' .. + colors.cyan(failure.element.trace.currentline) .. '\n' .. + colors.bright(failure.name) .. '\n' .. + colors.red(failureMessage(failure)) + end + + if options.verbose and failure.trace and failure.trace.traceback then + string = string .. '\n' .. colors.cyan(failure.trace.traceback) + end + + return string + end + + local statusString = function() + local successString = s('output.success_plural') + local failureString = s('output.failure_plural') + local pendingString = s('output.pending_plural') + local errorString = s('output.error_plural') + + local sec = handler.getDuration() + local successes = handler.successesCount + local pendings = handler.pendingsCount + local failures = handler.failuresCount + local errors = handler.errorsCount + + if successes == 0 then + successString = s('output.success_zero') + elseif successes == 1 then + successString = s('output.success_single') + end + + if failures == 0 then + failureString = s('output.failure_zero') + elseif failures == 1 then + failureString = s('output.failure_single') + end + + if pendings == 0 then + pendingString = s('output.pending_zero') + elseif pendings == 1 then + pendingString = s('output.pending_single') + end + + if errors == 0 then + errorString = s('output.error_zero') + elseif errors == 1 then + errorString = s('output.error_single') + end + + local formattedTime = string_gsub(string_format('%.6f', sec), '([0-9])0+$', '%1') + + return colors.green(successes) .. ' ' .. successString .. ' / ' .. + colors.red(failures) .. ' ' .. failureString .. ' / ' .. + colors.magenta(errors) .. ' ' .. errorString .. ' / ' .. + colors.yellow(pendings) .. ' ' .. pendingString .. ' : ' .. + colors.bright(formattedTime) .. ' ' .. s('output.seconds') + end + + handler.testEnd = function(element, parent, status, debug) + if not options.deferPrint then + local string = successDot + + if status == 'pending' then + string = pendingDot + elseif status == 'failure' then + string = failureDot + elseif status == 'error' then + string = errorDot + end + + io_write(string) + io_flush() + end + + return nil, true + end + + handler.suiteStart = function(suite, count, total) + local runString = (total > 1 and '\nRepeating all tests (run %u of %u) . . .\n\n' or '') + io_write(string_format(runString, count, total)) + io_flush() + + return nil, true + end + + handler.suiteEnd = function() + io_write('\n') + io_write(statusString()..'\n') + + for i, pending in pairs(handler.pendings) do + io_write('\n') + io_write(pendingDescription(pending)..'\n') + end + + for i, err in pairs(handler.failures) do + io_write('\n') + io_write(failureDescription(err)..'\n') + end + + for i, err in pairs(handler.errors) do + io_write('\n') + io_write(failureDescription(err, true)..'\n') + end + + return nil, true + end + + handler.error = function(element, parent, message, debug) + io_write(errorDot) + io_flush() + + return nil, true + end + + busted.subscribe({ 'test', 'end' }, handler.testEnd, { predicate = handler.cancelOnPending }) + busted.subscribe({ 'suite', 'start' }, handler.suiteStart) + busted.subscribe({ 'suite', 'end' }, handler.suiteEnd) + busted.subscribe({ 'error', 'file' }, handler.error) + busted.subscribe({ 'failure', 'file' }, handler.error) + busted.subscribe({ 'error', 'describe' }, handler.error) + busted.subscribe({ 'failure', 'describe' }, handler.error) + + return handler +end