mirror of
1
0
Fork 0
ultimate-vim/sources_non_forked/vim-cmake/autoload/cmake/terminal.vim

473 lines
15 KiB
VimL

" ==============================================================================
" Location: autoload/cmake/terminal.vim
" Description: Terminal abstraction layer
" ==============================================================================
let s:terminal = {}
let s:terminal.console_buffer = -1
let s:terminal.console_cmd_info = {
\ 'generate': 'Generating buildsystem...',
\ 'build': 'Building...',
\ 'install': 'Installing...',
\ 'NONE': '',
\ }
let s:terminal.console_cmd = {
\ 'id': -1,
\ 'running': v:false,
\ 'callbacks': [],
\ 'callbacks_err': [],
\ 'autocmds': [],
\ 'autocmds_err': [],
\ }
let s:terminal.console_cmd_output = []
let s:term_tty = ''
let s:term_id = -1
let s:term_chan_id = -1
let s:exit_term_mode = 0
let s:logger = cmake#logger#Get()
let s:statusline = cmake#statusline#Get()
let s:system = cmake#system#Get()
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" ANSI sequence filters
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" https://en.wikipedia.org/wiki/ANSI_escape_code
" https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences
let s:ansi_esc = '\e'
let s:ansi_csi = s:ansi_esc . '\['
let s:ansi_st = '\(\%x07\|\\\)'
let s:pre_echo_filter = ''
let s:post_echo_filter = ''
let s:post_echo_rep_pat = ''
let s:post_echo_rep_sub = ''
" In ConPTY (MS-Windows), remove ANSI sequences that mess up the terminal before
" echoing data to the terminal:
" - 'erase screen' sequences
" - 'move cursor' sequences
if has('win32')
let s:pre_echo_filter = s:pre_echo_filter
\ . '\(' . s:ansi_csi . '\d*J' . '\)'
\ . '\|\(' . s:ansi_csi . '\(\d\+;\)*\d*H' . '\)'
endif
" Remove ANSI sequences for coloring and style after echoing to the terminal.
let s:post_echo_filter .= '\(' . s:ansi_csi . '\(\d\+;\)*\d*m' . '\)'
" Remove additional ANSI sequences returened by ConPTY (MS-Windows) after
" echoing to the terminal:
" - 'erase from cursor' sequences
" - 'erase from cursor to EOL' sequences
" - 'hide/show cursor' sequences
" - 'console title' sequences
if has('win32')
let s:post_echo_filter = s:post_echo_filter
\ . '\|\(' . s:ansi_csi . '\d*X' . '\)'
\ . '\|\(' . s:ansi_csi . 'K' . '\)'
\ . '\|\(' . s:ansi_csi . '?25[hl]' . '\)'
\ . '\|\(' . s:ansi_esc . '\]' . '0;.*' . s:ansi_st . '\)'
endif
" Replace 'move forward' sequences with spaces in ConPTY (MS-Windows) after
" echoing to the terminal
if has('win32')
let s:post_echo_rep_pat .= s:ansi_csi . '\(\d*\)C'
let s:post_echo_rep_sub .= '\=repeat('' '', submatch(1))'
endif
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" Private functions
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" Callback for the stdout of the command running in the Vim-CMake console.
"
function! s:ConsoleCmdStdoutCb(...) abort
let l:data = s:system.ExtractStdoutCallbackData(a:000)
call s:FilterStdoutPreEcho(l:data)
call s:TermEcho(l:data)
call s:FilterStdoutPostEcho(l:data)
" Save console output to list.
let s:terminal.console_cmd_output += l:data
endfunction
" Callback for the end of the command running in the Vim-CMake console.
"
function! s:ConsoleCmdExitCb(...) abort
call s:logger.LogDebug('Invoked console exit callback')
" Waiting for the job's channel to be closed ensures that all output has
" been processed. This is useful in Vim, where buffered stdout may still
" come in after entering this function.
call s:system.ChannelWait(s:terminal.console_cmd.id)
let l:error = s:system.ExtractExitCallbackData(a:000)
call s:OnCompleteCommand(l:error, v:false)
endfunction
" Enter terminal mode.
"
function! s:EnterTermMode() abort
if mode() !=# 't'
execute 'normal! i'
endif
endfunction
" Exit terminal mode.
"
function! s:ExitTermMode() abort
if mode() ==# 't'
call feedkeys("\<C-\>\<C-N>", 'n')
endif
endfunction
" Define actions to perform when completing/stopping a command.
"
function! s:OnCompleteCommand(error, stopped) abort
if a:error == 0
let l:callbacks = s:terminal.console_cmd.callbacks
let l:autocmds = s:terminal.console_cmd.autocmds
else
let l:callbacks = s:terminal.console_cmd.callbacks_err
let l:autocmds = s:terminal.console_cmd.autocmds_err
endif
" Reset state
let s:terminal.console_cmd.id = -1
let s:terminal.console_cmd.running = v:false
let s:terminal.console_cmd.callbacks = []
let s:terminal.console_cmd.callbacks_err = []
let s:terminal.console_cmd.autocmds = []
let s:terminal.console_cmd.autocmds_err = []
" Append empty line to terminal.
call s:TermEcho([''])
" Exit terminal mode if inside the Vim-CMake console window (useful for
" Vim). Otherwise the terminal mode is exited after WinEnter event.
if win_getid() == bufwinid(s:terminal.console_buffer)
call s:ExitTermMode()
else
let s:exit_term_mode = 1
endif
" Update statusline.
call s:statusline.SetCmdInfo(s:terminal.console_cmd_info['NONE'])
call s:statusline.Refresh()
" The rest of the tasks are not to be carried out if the running command was
" stopped by the user.
if a:stopped
return
endif
" Focus Vim-CMake console window, if requested.
if g:cmake_jump_on_completion
call s:terminal.Focus()
else
if a:error != 0 && g:cmake_jump_on_error
call s:terminal.Focus()
endif
endif
" Handle callbacks and autocmds.
" Note: Funcref variable names must start with a capital.
for l:Callback in l:callbacks
call s:logger.LogDebug('Callback invoked: %s()', l:Callback)
call l:Callback()
endfor
for l:autocmd in l:autocmds
call s:logger.LogDebug('Executing autocmd %s', l:autocmd)
execute 'doautocmd <nomodeline> User ' . l:autocmd
endfor
endfunction
" Define actions to perform when entering the Vim-CMake console window.
"
function! s:OnEnterConsoleWindow() abort
if winnr() == bufwinnr(s:terminal.console_buffer) && s:exit_term_mode
let s:exit_term_mode = 0
call s:ExitTermMode()
endif
endfunction
" Start arbitrary command with output to be displayed in Vim-CMake console.
"
" Params:
" command : List
" the command to be run, as a list of command and arguments
"
" Return:
" Number
" job id
"
function! s:ConsoleCmdStart(command) abort
let l:console_win_id = bufwinid(s:terminal.console_buffer)
" For Vim, must go back into Terminal-Job mode for the command's output to
" be appended to the buffer.
if !has('nvim')
call win_execute(l:console_win_id, 'call s:EnterTermMode()', '')
endif
" Run command.
let l:job_id = s:system.JobRun(
\ a:command, v:false, function('s:ConsoleCmdStdoutCb'),
\ function('s:ConsoleCmdExitCb'), v:true)
" For Neovim, scroll manually to the end of the terminal buffer while the
" command's output is being appended.
if has('nvim')
let l:buffer_length = nvim_buf_line_count(s:terminal.console_buffer)
call nvim_win_set_cursor(l:console_win_id, [l:buffer_length, 0])
endif
return l:job_id
endfunction
" Create Vim-CMake window.
"
" Returns:
" Number
" number of the created window
"
function! s:CreateConsoleWindow() abort
execute join([g:cmake_console_position, g:cmake_console_size . 'split'])
setlocal winfixheight
setlocal winfixwidth
call s:logger.LogDebug('Created console window')
endfunction
" Create Vim-CMake buffer and apply local settings.
"
" Returns:
" Number
" number of the created buffer
"
function! s:CreateConsoleBuffer() abort
execute 'enew'
call s:TermSetup()
nnoremap <buffer> <silent> cg :CMakeGenerate<CR>
nnoremap <buffer> <silent> cb :CMakeBuild<CR>
nnoremap <buffer> <silent> ci :CMakeInstall<CR>
nnoremap <buffer> <silent> cq :CMakeClose<CR>
nnoremap <buffer> <silent> <C-C> :CMakeStop<CR>
setlocal nonumber
setlocal norelativenumber
setlocal signcolumn=auto
setlocal nobuflisted
setlocal filetype=vimcmake
setlocal statusline=[CMake]
setlocal statusline+=\ %{cmake#statusline#GetBuildInfo(0)}
setlocal statusline+=\ %{cmake#statusline#GetCmdInfo()}
" Avoid error E37 on :CMakeClose in some Vim instances.
setlocal bufhidden=hide
augroup cmake
autocmd WinEnter <buffer> call s:OnEnterConsoleWindow()
augroup END
return bufnr()
call s:logger.LogDebug('Created console buffer')
endfunction
" Setup Vim-CMake console terminal.
"
function! s:TermSetup() abort
" Open job-less terminal to echo command outputs to.
let l:options = {}
if has('nvim')
let s:term_chan_id = nvim_open_term(bufnr(''), l:options)
else
let l:options['curwin'] = 1
let l:term = term_start('NONE', l:options)
let s:term_id = term_getjob(l:term)
let s:term_tty = job_info(s:term_id)['tty_in']
call term_setkill(l:term, 'term')
endif
endfunction
" Echo strings to terminal.
"
" Params:
" data : List
" list of strings to echo
"
function! s:TermEcho(data) abort
if len(a:data) == 0
return
endif
if has('nvim')
call chansend(s:term_chan_id, join(a:data, "\r\n") . "\r\n")
else
call writefile(a:data, s:term_tty)
endif
endfunction
" Filter stdout data to remove ANSI sequences that should not be sent to the
" console terminal.
"
" Params:
" data : List
" list of stdout strings to filter (filtering is done in-place)
"
function! s:FilterStdoutPreEcho(data) abort
if s:pre_echo_filter !=# ''
call map(a:data, {_, val -> substitute(
\ val, s:pre_echo_filter, '', 'g')})
endif
endfunction
" Filter stdout data to remove remaining ANSI sequences after sending the data
" to the console terminal.
"
" Params:
" data : List
" list of stdout strings to filter (filtering is done in-place)
"
function! s:FilterStdoutPostEcho(data) abort
if s:post_echo_filter !=# ''
call map(a:data, {_, val -> substitute(
\ val, s:post_echo_filter, '', 'g')})
endif
if s:post_echo_rep_pat !=# ''
call map(a:data, {_, val -> substitute(
\ val, s:post_echo_rep_pat, s:post_echo_rep_sub, 'g')})
endif
endfunction
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" Public functions
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" Open Vim-CMake console window.
"
" Params:
" clear : Boolean
" if set, a new buffer is created and the old one is deleted
"
function! s:terminal.Open(clear) abort
call s:logger.LogDebug('Invoked: terminal.Open(%s)', a:clear)
let l:original_win_id = win_getid()
let l:cmake_win_id = bufwinid(l:self.console_buffer)
if l:cmake_win_id == -1
" If a Vim-CMake window does not exist, create it.
call s:CreateConsoleWindow()
if bufexists(l:self.console_buffer)
" If a Vim-CMake buffer exists, open it in the Vim-CMake window, or
" delete it if a:clear is set.
if !a:clear
execute 'b ' . l:self.console_buffer
call win_gotoid(l:original_win_id)
return
else
execute 'bd! ' . l:self.console_buffer
endif
endif
" Create Vim-CMake buffer if none exist, or if the old one was deleted.
let l:self.console_buffer = s:CreateConsoleBuffer()
else
" If a Vim-CMake window exists, and a:clear is set, create a new
" Vim-CMake buffer and delete the old one.
if a:clear
let l:old_buffer = l:self.console_buffer
call l:self.Focus()
let l:self.console_buffer = s:CreateConsoleBuffer()
if bufexists(l:old_buffer) && l:old_buffer != l:self.console_buffer
execute 'bd! ' . l:old_buffer
endif
endif
endif
if l:original_win_id != win_getid()
call win_gotoid(l:original_win_id)
endif
endfunction
" Focus Vim-CMake console window.
"
function! s:terminal.Focus() abort
call s:logger.LogDebug('Invoked: terminal.Focus()')
call win_gotoid(bufwinid(l:self.console_buffer))
endfunction
" Close Vim-CMake console window.
"
function! s:terminal.Close() abort
call s:logger.LogDebug('Invoked: terminal.Close()')
if bufexists(l:self.console_buffer)
let l:cmake_win_id = bufwinid(l:self.console_buffer)
if l:cmake_win_id != -1
execute win_id2win(l:cmake_win_id) . 'wincmd q'
endif
endif
endfunction
" Run arbitrary command in the Vim-CMake console.
"
" Params:
" command : List
" the command to be run, as a list of command and arguments
" tag : String
" command tag, must be an item of keys(l:self.console_cmd_info)
" cbs : List
" list of callbacks (Funcref) to be invoked upon successful completion
" of the command
" cbs_err : List
" list of callbacks (Funcref) to be invoked upon unsuccessful completion
" of the command
" aus : List
" list of autocmds (String) to be invoked upon successful completion of
" the command
" aus_err : List
" list of autocmds (String) to be invoked upon unsuccessful completion
" of the command
"
function! s:terminal.Run(command, tag, cbs, cbs_err, aus, aus_err) abort
call s:logger.LogDebug('Invoked: terminal.Run(%s, %s, %s, %s, %s, %s)',
\ a:command, string(a:tag), a:cbs, a:cbs_err, a:aus, a:aus_err)
call assert_notequal(index(keys(l:self.console_cmd_info), a:tag), -1)
" Prevent executing this function when a command is already running
if l:self.console_cmd.running
call s:logger.EchoError('Another CMake command is already running')
call s:logger.LogError('Another CMake command is already running')
return
endif
let l:self.console_cmd.running = v:true
let l:self.console_cmd.callbacks = a:cbs
let l:self.console_cmd.callbacks_err = a:cbs_err
let l:self.console_cmd.autocmds = a:aus
let l:self.console_cmd.autocmds_err = a:aus_err
let l:self.console_cmd_output = []
" Open Vim-CMake console window.
call l:self.Open(v:false)
" Echo start message to terminal.
if g:cmake_console_echo_cmd
call s:TermEcho([printf(
\ '%sRunning command: %s%s',
\ "\e[1;35m",
\ join(a:command),
\ "\e[0m")
\ ])
endif
" Run command.
call s:statusline.SetCmdInfo(l:self.console_cmd_info[a:tag])
let l:self.console_cmd.id = s:ConsoleCmdStart(a:command)
" Jump to Vim-CMake console window if requested.
if g:cmake_jump
call l:self.Focus()
endif
endfunction
" Stop command currently running in the Vim-CMake console.
"
function! s:terminal.Stop() abort
call s:logger.LogDebug('Invoked: terminal.Stop()')
call s:system.JobStop(l:self.console_cmd.id)
call s:OnCompleteCommand(0, v:true)
endfunction
" Get output from the last command run.
"
" Returns
" List:
" output from the last command, as a list of strings
"
function! s:terminal.GetOutput() abort
return l:self.console_cmd_output
endfunction
" Get terminal 'object'.
"
function! cmake#terminal#Get() abort
return s:terminal
endfunction