From 21e83f4ae15a93cc0b8f5d29fc52ad4b6098ed9b Mon Sep 17 00:00:00 2001 From: Mike <10135646+mikesmithgh@users.noreply.github.com> Date: Sun, 1 Oct 2023 15:11:59 -0400 Subject: [PATCH] fix: error handling improvements (#29) --- lua/kitty-scrollback/autocommands.lua | 3 +- lua/kitty-scrollback/kitty_commands.lua | 237 ++++++++++++++++++------ lua/kitty-scrollback/launch.lua | 95 ++++------ lua/kitty-scrollback/util.lua | 8 + lua/kitty-scrollback/windows.lua | 4 +- 5 files changed, 232 insertions(+), 115 deletions(-) diff --git a/lua/kitty-scrollback/autocommands.lua b/lua/kitty-scrollback/autocommands.lua index 19282fc8..ca3b431f 100644 --- a/lua/kitty-scrollback/autocommands.lua +++ b/lua/kitty-scrollback/autocommands.lua @@ -8,6 +8,7 @@ local ksb_win = require('kitty-scrollback.windows') local M = {} +---@type KsbPrivate local p ---@type KsbOpts local opts ---@diagnostic disable-line: unused-local @@ -153,7 +154,7 @@ M.set_yank_post_autocmd = function() { buf = vim.api.nvim_get_current_buf() } ) vim.cmd.help('clipboard-tool') - vim.cmd.redraw() + ksb_util.restore_and_redraw() local response = vim.fn.confirm(prompt_msg, '&Quit\n&Continue') if response ~= 2 then ksb_api.quit_all() diff --git a/lua/kitty-scrollback/kitty_commands.lua b/lua/kitty-scrollback/kitty_commands.lua index a17192a1..ef6f4483 100644 --- a/lua/kitty-scrollback/kitty_commands.lua +++ b/lua/kitty-scrollback/kitty_commands.lua @@ -1,7 +1,9 @@ ---@mod kitty-scrollback.kitty_commands local ksb_health = require('kitty-scrollback.health') +local ksb_util = require('kitty-scrollback.util') local M = {} +---@type KsbPrivate local p local opts ---@diagnostic disable-line: unused-local @@ -10,73 +12,194 @@ M.setup = function(private, options) opts = options ---@diagnostic disable-line: unused-local end +local error_header = { + '', + '==============================================================================', + 'kitty-scrollback.nvim', + '', + 'ERROR: failed to execute remote Kitty command', + '', +} + +local display_error = function(cmd, r) + local msg = vim.list_extend({}, error_header) + local stdout = r.stdout or '' + local stderr = r.stderr or '' + local err = {} + if r.entrypoint then + table.insert(err, '*entrypoint:* |' .. r.entrypoint:gsub('(%s+)', '|%1|') .. '| ') + end + table.insert(err, '*command:* ' .. cmd) + if r.pid then + table.insert(err, '*pid:* ' .. r.pid) + end + if r.channel_id then + table.insert(err, '*channel_id:* ' .. r.channel_id) + end + if r.code then + table.insert(err, '*code:* ' .. r.code) + end + if r.signal then + table.insert(err, '*signal:* ' .. r.signal) + end + + if r.full_cmd then + table.insert(err, '*full_command:* ') + table.insert(err, '>sh') + table.insert(err, ' ' .. r.full_cmd) + table.insert(err, '<') + end + + table.insert(err, '*stdout:*') + + local out = {} + for line in stdout:gmatch('[^\r\n]+') do + table.insert(out, ' ' .. line) + end + if next(out) then + table.insert(err, '') + vim.list_extend(err, out) + else + table.insert(err, ' ') + end + table.insert(err, '') + table.insert(err, '*stderr:*') + if #stderr > 0 then + for line in stderr:gmatch('[^\r\n]+') do + table.insert(err, '') + table.insert(err, ' ' .. line) + end + else + table.insert(err, ' ') + end + table.insert(err, '') + local error_bufid = vim.api.nvim_create_buf(false, true) + vim.api.nvim_set_current_buf(error_bufid) + vim.o.conceallevel = 2 + vim.o.concealcursor = 'n' + vim.o.foldenable = false + vim.api.nvim_set_option_value('filetype', 'checkhealth', { + buf = error_bufid, + }) + ksb_util.restore_and_redraw() + local prompt_msg = 'kitty-scrollback.nvim: Fatal error, see logs.' + if stderr:match('.*allow_remote_control.*') then + vim.list_extend(msg, ksb_health.advice().allow_remote_control) + end + if stderr:match('.*/dev/tty.*') then + vim.list_extend(msg, ksb_health.advice().listen_on) + end + vim.api.nvim_buf_set_lines(error_bufid, 0, -1, false, vim.list_extend(msg, err)) + M.close_kitty_loading_window() -- cannot use ignore parameter or will be infinite recursion + ksb_util.restore_and_redraw() + local response = vim.fn.confirm(prompt_msg, '&Quit\n&Continue') + if response ~= 2 then + M.signal_term_to_kitty_child_process(true) + end +end + local system_handle_error = function(cmd, sys_opts, ignore_error) local proc = vim.system(cmd, sys_opts or {}) local result = proc:wait() local ok = result.code == 0 - if not ok then - local msg = { - '', - '==============================================================================', - 'kitty-scrollback.nvim', - '', - 'ERROR: failed to execute remote Kitty command', - '', - } - local stdout = result.stdout or '' - local stderr = result.stderr or '' - local err = { - '*entrypoint:* |vim.system()|', - '*command:* ' .. table.concat(cmd, ' '), - '*pid:* ' .. proc.pid, - '*code:* ' .. result.code, - '*signal:* ' .. result.signal, - '*stdout:*', - } - local out = {} - for line in stdout:gmatch('[^\r\n]+') do - table.insert(out, ' ' .. line) - end - if next(out) then - vim.list_extend(err, out) - else - table.insert(err, ' ') - end - table.insert(err, '*stderr:*') - for line in stderr:gmatch('[^\r\n]+') do - table.insert(err, ' ' .. line) - end - local error_bufid = vim.api.nvim_create_buf(false, true) - vim.o.conceallevel = 2 - vim.o.concealcursor = 'n' - vim.api.nvim_set_option_value('filetype', 'checkhealth', { - buf = error_bufid, + if not ignore_error and not ok then + display_error(table.concat(cmd, ' '), { + entrypoint = 'vim.system()', + pid = proc.pid, + code = result.code, + signal = result.signal, + stdout = result.stdout, + stderr = result.stderr, }) - local prompt_msg = 'kitty-scrollback.nvim: Fatal error, see logs.' - if stderr:match('.*allow_remote_control.*') then - vim.list_extend(msg, ksb_health.advice().allow_remote_control) - ignore_error = false -- fatal error, always report this error - end - if stderr:match('.*/dev/tty.*') then - vim.list_extend(msg, ksb_health.advice().listen_on) - ignore_error = false -- fatal error, always report this error - end - if ignore_error then - return ok, result - end - vim.api.nvim_set_current_buf(error_bufid) - vim.api.nvim_buf_set_lines(error_bufid, 0, -1, false, vim.list_extend(msg, err)) - vim.cmd.redraw() - local response = vim.fn.confirm(prompt_msg, '&Quit\n&Continue') - if response ~= 2 then - M.signal_term_to_kitty_child_process(true) - end end return ok, result end +M.get_text_term = function(kitty_data, get_text_opts, on_exit_cb) + local esc = vim.fn.eval([["\e"]]) + local kitty_get_text_cmd = + string.format([[kitty @ get-text --match="id:%s" %s]], kitty_data.window_id, get_text_opts) + local sed_cmd = string.format( + [[sed -E -e 's/$/%s[0m/g' ]] -- append all lines with reset to avoid unintended colors + .. [[-e 's/%s\[\?25.%s\[.*;.*H%s\[.*//g']], -- remove control sequence added by --add-cursor flag + esc, + esc, + esc, + esc + ) + local flush_stdout_cmd = [[kitty +runpy 'sys.stdout.flush()']] + local full_cmd = kitty_get_text_cmd .. ' | ' .. sed_cmd .. ' && ' .. flush_stdout_cmd + local stdout + local stderr + local tail_max = 10 + vim.fn.termopen(full_cmd, { + stdout_buffered = true, + stderr_buffered = true, + on_stdout = function(_, data) + stdout = data + end, + on_stderr = function(_, data) + stderr = data + end, + on_exit = function(id, exit_code, event) + if exit_code == 0 then + -- no need to check allow_remote_control or dev/tty because earlier commands would have reported the error + if #stdout >= 2 then + -- the exit code may have been lost while piping the command through sed + -- so search last lines for and error reported by Kitty + local error_index = -1 + local tail_diff = #stdout - tail_max + local tail_count = tail_diff < 1 and 1 or math.min(tail_diff, #stdout) + for i = #stdout, tail_count, -1 do + -- see for match kitty/tools/cli/markup/prettify.go ans.Err = fmt_ctx.SprintFunc("bold fg=bright-red") + if stdout[i]:match('^' .. esc .. '%[1;91mError' .. esc .. '%[221;39m: .*') then + error_index = i + break + end + end + + if error_index > 0 then + display_error(kitty_get_text_cmd, { + entrypoint = 'termopen() :: exit_code = 0 and error_index > 0', + full_cmd = full_cmd, + code = 1, -- exit code is not returned through pipe but we can assume 1 due to error message + channel_id = id, + stdout = table.concat( + vim.tbl_map(function(line) + return line + :gsub('[\27\155][][()#:;?%d]*[A-PRZcf-ntqry=><~]', '') + :gsub(esc .. '\\', '') + :gsub(';k=s', '') + end, stdout), + '\n' + ), + stderr = stderr and table.concat(stderr, '\n') or nil, + }) + end + end + on_exit_cb(id, exit_code, event) + else + local out = stdout + and table + .concat(stdout, '\n', math.max(#stdout - tail_max, 1), #stdout) + :gsub('[\27\155][][()#:;?%d]*[A-PRZcf-ntqry=><~]', '') + :gsub('' .. esc .. '\\', '') + :gsub(';k=s', '') + or nil + display_error(full_cmd, { + entrypoint = 'termopen() :: exit_code ~= 0', + code = exit_code, + channel_id = id, + stdout = out, + stderr = stderr and table.concat(stderr, '\n') or nil, + }) + end + end, + }) +end + M.send_paste_buffer_text_to_kitty_and_quit = function(bracketed_paste_mode) -- convert table to string separated by carriage returns local cmd_str = table.concat( @@ -148,7 +271,7 @@ end M.open_kitty_loading_window = function(env) if p.kitty_loading_winid then - M.close_kitty_loading_window() + M.close_kitty_loading_window(true) end local kitty_cmd = vim.list_extend({ 'kitty', diff --git a/lua/kitty-scrollback/launch.lua b/lua/kitty-scrollback/launch.lua index b9c2c096..aca92753 100644 --- a/lua/kitty-scrollback/launch.lua +++ b/lua/kitty-scrollback/launch.lua @@ -336,8 +336,8 @@ local function validate_extent(extent) local prompt_msg = 'kitty-scrollback.nvim: Fatal error, see logs.' vim.api.nvim_set_current_buf(error_bufid) vim.api.nvim_buf_set_lines(error_bufid, 0, -1, false, msg) - vim.cmd.redraw() - ksb_kitty_cmds.close_kitty_loading_window() + ksb_util.restore_and_redraw() + ksb_kitty_cmds.close_kitty_loading_window(true) local response = vim.fn.confirm(prompt_msg, '&Quit\n&Continue') if response ~= 2 then ksb_kitty_cmds.signal_term_to_kitty_child_process() @@ -382,64 +382,49 @@ M.launch = function() -- do not worry about setting vim.o.columns back to original value that is taken -- care of when we trigger kitty to send a SIGWINCH to the nvim process local min_cols = 300 + p.orig_columns = vim.o.columns if vim.o.columns < min_cols then vim.o.columns = min_cols end vim.schedule(function() - local esc = vim.fn.eval([["\e"]]) - local kitty_get_text_cmd = - string.format([[kitty @ get-text --match="id:%s" %s]], kitty_data.window_id, get_text_opts) - local sed_cmd = string.format( - [[sed -E -e 's/$/%s[0m/g' ]] -- append all lines with reset to avoid unintended colors - .. [[-e 's/%s\[\?25.%s\[.*;.*H%s\[.*//g']], -- remove control sequence added by --add-cursor flag - esc, - esc, - esc, - esc - ) - local flush_stdout_cmd = [[kitty +runpy 'sys.stdout.flush()']] - local full_cmd = kitty_get_text_cmd .. ' | ' .. sed_cmd .. ' && ' .. flush_stdout_cmd - vim.fn.termopen(full_cmd, { - stdout_buffered = true, - on_exit = function() - ksb_kitty_cmds.signal_winchanged_to_kitty_child_process() - vim.fn.timer_start(20, function(t) ---@diagnostic disable-line: redundant-parameter - local timer_info = vim.fn.timer_info(t)[1] or {} - local ready = ksb_util.remove_process_exited() - if ready or timer_info['repeat'] == 0 then - vim.fn.timer_stop(t) - - if opts.kitty_get_text.extent == 'screen' or opts.kitty_get_text.extent == 'all' then - set_cursor_position(kitty_data) - end - ksb_win.show_status_window() - - -- improve buffer name to avoid displaying complex command to user - local term_buf_name = vim.api.nvim_buf_get_name(p.bufid) - term_buf_name = term_buf_name:gsub('^(term://.-:).*', '%1kitty-scrollback.nvim') - vim.api.nvim_buf_set_name(p.bufid, term_buf_name) - vim.api.nvim_buf_delete(vim.fn.bufnr('#'), { force = true }) -- delete alt buffer after rename - - ksb_kitty_cmds.close_kitty_loading_window() - if opts.restore_options then - restore_orig_options() - end - if - opts.callbacks - and opts.callbacks.after_ready - and type(opts.callbacks.after_ready) == 'function' - then - vim.cmd.redraw() - vim.schedule(function() - opts.callbacks.after_ready(kitty_data, opts) - end) - end + ksb_kitty_cmds.get_text_term(kitty_data, get_text_opts, function() + ksb_kitty_cmds.signal_winchanged_to_kitty_child_process() + vim.fn.timer_start(20, function(t) ---@diagnostic disable-line: redundant-parameter + local timer_info = vim.fn.timer_info(t)[1] or {} + local ready = ksb_util.remove_process_exited() + if ready or timer_info['repeat'] == 0 then + vim.fn.timer_stop(t) + + if opts.kitty_get_text.extent == 'screen' or opts.kitty_get_text.extent == 'all' then + set_cursor_position(kitty_data) end - end, { - ['repeat'] = 200, - }) - end, - }) + ksb_win.show_status_window() + + -- improve buffer name to avoid displaying complex command to user + local term_buf_name = vim.api.nvim_buf_get_name(p.bufid) + term_buf_name = term_buf_name:gsub('^(term://.-:).*', '%1kitty-scrollback.nvim') + vim.api.nvim_buf_set_name(p.bufid, term_buf_name) + vim.api.nvim_buf_delete(vim.fn.bufnr('#'), { force = true }) -- delete alt buffer after rename + + if opts.restore_options then + restore_orig_options() + end + if + opts.callbacks + and opts.callbacks.after_ready + and type(opts.callbacks.after_ready) == 'function' + then + ksb_util.restore_and_redraw() + vim.schedule(function() + opts.callbacks.after_ready(kitty_data, opts) + end) + end + ksb_api.close_kitty_loading_window() + end + end, { + ['repeat'] = 200, + }) + end) end) if opts.callbacks diff --git a/lua/kitty-scrollback/util.lua b/lua/kitty-scrollback/util.lua index 57ff75c3..ba894019 100644 --- a/lua/kitty-scrollback/util.lua +++ b/lua/kitty-scrollback/util.lua @@ -1,6 +1,7 @@ ---@mod kitty-scrollback.util local M = {} +---@type KsbPrivate local p local opts ---@diagnostic disable-line: unused-local @@ -83,4 +84,11 @@ M.nvim_version_tostring = function() return ret end +M.restore_and_redraw = function() + if p.orig_columns then + vim.o.columns = p.orig_columns + end + vim.cmd.redraw() +end + return M diff --git a/lua/kitty-scrollback/windows.lua b/lua/kitty-scrollback/windows.lua index ed73cf1d..316b9cc5 100644 --- a/lua/kitty-scrollback/windows.lua +++ b/lua/kitty-scrollback/windows.lua @@ -51,7 +51,7 @@ M.paste_winopts = function(row, col, height_offset) if winopts.width < 0 then -- current line is larger than window, put window below current line vim.fn.setcursorcharpos({ vim.fn.line('.'), 0 }) - vim.cmd.redraw() + ksb_util.restore_and_redraw() winopts.width = vim.o.columns - 1 winopts.col = 0 end @@ -124,7 +124,7 @@ M.open_paste_window = function(start_insert) vim.cmd.startinsert({ bang = true }) end) end - vim.cmd.redraw() + ksb_util.restore_and_redraw() vim.schedule_wrap(vim.cmd.doautocmd)('WinResized') end