" ============================================================================== " 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("\\", '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 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 cg :CMakeGenerate nnoremap cb :CMakeBuild nnoremap ci :CMakeInstall nnoremap cq :CMakeClose nnoremap :CMakeStop 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 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