3

Consider the following:

function! RunWithErrorHandling(command) abort
  let view = winsaveview()
  let errorOutputFile = tempname()

  let shellredir_save = &shellredir
  let &shellredir = '>%s 2>' . errorOutputFile
  silent execute a:command
  let &shellredir = shellredir_save

  if v:shell_error != 0
    silent undo
    echohl ErrorMsg
    for error in readfile(errorOutputFile)
        echoerr error
    endfor
    echohl None
  endif

  call delete(errorOutputFile)
  call winrestview(view)
endfunction

command! JsonToDict call RunWithErrorHandling('% ! python -c "import json; import sys; as_dict = json.load(sys.stdin); print(as_dict)"')

I've got multiple of these commands calling external commands (not just python) and I have wrapped them with RunWithErrorHandling (adapted from https://github.com/hashivim/vim-terraform) so that I can get an error message instead of replacing the buffer content with the error.

I've been struggling with getting an error message displayed properly because multiline messages would have \n replaced with ^@ instead of adding a line break when using echoerr and echom.

Using echo in combination with echohl ErrorMsg displays everything the way I want, but the message is not available using :messages.

I ended up with the for loop and multiple calls to echoerr as shown above, but it feels wrong and a multiline message is broken up into several "vim messages" so :1messages shows me only the last line of a multiline message instead of the whole message.

It would be nice if I could echo the message so it's displayed the same way it is now and :1messages shows me the whole message.

It seems like it should be possible because when I get an error running the following command:

command! DictToJson % python3 function_that_raises_an_exception()

I do get a nice stacktrace and :1messages shows me the whole thing.

2
  • As an aside, you may want to consider doing the cleanup (let &shellredir = shellredir_save, delete(errorOutputFile), winrestview(view)) in a finally block so it always gets run. Example. Also see :help try-finally. Commented Jun 9, 2022 at 9:46
  • What do you think can fail? Because silent execute a:command never throws an exception, no matter what goes wrong? Commented Jun 12, 2022 at 20:58

1 Answer 1

1

You need to split on newlines and run echomsg multiple times; usually it's easiest to create a little wrapper function for this; for example:

fun! s:echo(msg) abort
   for line in a:msg->split('\n')
       echom line
   endfor
endfun

Or:

fun s:error(msg) abort
   echohl Error
   for line in a:msg->split('\n')
       echom line
   endfor
   echohl None
endfun

And then :call s:error("asd\nzxc") will output:

asd
zxc

I typically use echom for this as it doesn't include a line number like echoerr, which I rarely find useful, but you can use echoerr too if you prefer that.


A more expansive version of the above would be:

" Output an error message to the screen. The message can be either a list or a
" string; every line will be echomsg'd separately.
fun! s:error(msg, ...) abort
  call s:echo(a:msg, 'ErrorMsg', a:000)
endfun

" Output an informational message to the screen. The message can be either a
" list or a string; every line will be echomsg'd separately.
fun! s:info(msg, ...) abort
  call s:echo(a:msg, 'Debug', a:000)
endfun

" Echo a message to the screen and highlight it with the group in a:hi.
"
" The message can be a list or string; every line with be :echomsg'd separately.
fun! s:echo(msg, hi, ...) abort
  if type(a:msg) is v:t_list
    let l:msg = a:msg
  else
    let l:msg = a:msg
    if len(a:000) > 0 && len(a:000[0]) > 0
      let l:msg = call('printf', [a:msg] + a:000[0])
    endif
    let l:msg = split(l:msg, "\n")
  endif

  " Tabs display as ^I or <09>, so manually expand them.
  let l:msg = map(l:msg, { _, v -> substitute(l:v, "\t", '      ', '') })

  " Redrawing here means there will be a better chance for messages to show.
  redraw

  exe 'echohl ' . a:hi
  for l:line in l:msg
    echom 'MYPLUGINNAME: ' . l:line
  endfor
  echohl None

  " Add a delay when called from insert mode, because otherwise the user will
  " never see the message.
  " TODO: maybe there is a better way? I can't find one if there is...
  if mode() is# 'i' && a:hi is# 'ErrorMsg'
    sleep 1
  endif
endfun

This also handles lists (call s:error(['line1', 'line2'])), does printf formatting, and automatically prefixes lines with MYPLUGINNAME: , which I find nice.

Probably overkill for your case though, but I've used it in some larger plugins.

2
  • This still has the same problem of only giving the last line of the message for :1messages instead of the whole message? Commented Jun 10, 2022 at 1:44
  • Oh right, I missed that you already mentioned this in your question @SebastianBlask; sorry about that 😅 I don't think there's any other way of doing this, short of joining the message by spaces. While :{count}messages is documented as "Show the {count} most recent messages." in reality it just means "most recent lines". It behaves like this for something like throw 'err' (which outputs two lines) as well: :1mess will just show the last one. Commented Jun 10, 2022 at 15:30

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.