7

I'm trying to modify test runner plugin to do the following:

  1. When running test show a window with them (that part is already covered by the plugin)
  2. After all tests are done if there is no errors hide window.
  3. If there were errors leave the window open.

I've written a shell script that exits if the exit code of the test runner is 0. But neovim still shows the buffer and waits for me to press any key to close it. How can I tell neovim to close buffer if the the shell session within it have ended?

6

From vim/src/eval.c:

static void eval_job_process_exit_cb(Process *proc, int status, void *d) {
    [..]
    if (data->term && !data->exited) {
      data->exited = true;
      char msg[sizeof("\r\n[Process exited ]") + NUMBUFLEN];
      snprintf(msg, sizeof msg, "\r\n[Process exited %d]", proc->status);
      terminal_close(data->term, msg);
    }
    [..]
}

There is no setting or something else to disable this message here. It is always displayed. The only other place where terminal_close() is called is from the close_buffer() function, which is the :close command (among others). I'm not sure if you can send the :close command from a script within the terminal to Neovim; I don't think so.


The first way to fix this is to modify the way you open the terminal:

fun! TermTest(cmd)
    call termopen(a:cmd, {'on_exit': 's:OnExit'})
endfun

fun! s:OnExit(job_id, code, event) dict
    if a:code == 0
        close
    endif
endfun

The on_exit callback gets run before the message is displayed, so we can use that to close this buffer. The parameters seem undocumented (:help jobstart merely mentions "exit event handler"), but found some information on a random internet page where a Neovim developer posted it.


If you can't modify the way the terminal gets opened you can still sort-of do the same by (ab)using the TermClose autocommand:

augroup terminal
        autocmd!
        autocmd TermClose * if getline('$') == 'Exit 0' | close | endif
augroup end

This requires you to modify your test script to output the text Exit 0 or Exit 1. As far as I can find there is no way to get the exit code of the process from the autocommand.

4

This is my solution for closing terminal windows automatically when the terminal process completes with 0 exit status.

Using Martin Tournoij's answer as a starting point I did some investigating on how to get the terminal process exit code in a TermClose event handler. You can examine buffer content to parse the [Process exited ?] line that is appended to the terminal buffer - the problem is that line is not appended until after the TermClose event runs. What you can do is to use timer_start to run a function after a short delay.

Here is my code:

" Get the exit status from a terminal buffer by looking for a line near the end
" of the buffer with the format, '[Process exited ?]'.
func! s:getExitStatus() abort
  let ln = line('$')
  " The terminal buffer includes several empty lines after the 'Process exited'
  " line that need to be skipped over.
  while ln >= 1
    let l = getline(ln)
    let ln -= 1
    let exitCode = substitute(l, '^\[Process exited \([0-9]\+\)\]$', '\1', '')
    if l != '' && l == exitCode
      " The pattern did not match, and the line was not empty. It looks like
      " there is no process exit message in this buffer.
      break
    elseif exitCode != ''
      return str2nr(exitCode)
    endif
  endwhile
  throw 'Could not determine exit status for buffer, ' . expand('%')
endfunc

func! s:afterTermClose() abort
  if s:getExitStatus() == 0
    bdelete!
  endif
endfunc

augroup MyNeoterm
  autocmd!
  " The line '[Process exited ?]' is appended to the terminal buffer after the
  " `TermClose` event. So we use a timer to wait a few milliseconds to read the
  " exit status. Setting the timer to 0 or 1 ms is not sufficient; 20 ms seems
  " to work for me.
  autocmd TermClose * call timer_start(20, { -> s:afterTermClose() })
augroup END

You can target the event handler using a pattern in the autocmd. For example if you only want the terminal window to close automatically when you run tests, and your test command is make test then you can change the autocmd to look like this:

autocmd TermClose *:make\ test call timer_start(20, { -> s:afterTermClose() })

Or if you want the window to close when you exit your shell you can use this pattern:

autocmd TermClose *:$SHELL call timer_start(20, { -> s:afterTermClose() })

You can use multiple patterns separated by commas.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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