diff --git a/autoload/vimspector.vim b/autoload/vimspector.vim index 0250b90..f2b39d5 100644 --- a/autoload/vimspector.vim +++ b/autoload/vimspector.vim @@ -96,6 +96,11 @@ function! vimspector#AddWatchPrompt( expr ) abort call vimspector#AddWatch( a:expr ) endfunction +function! vimspector#Evaluate( expr ) abort + py3 _vimspector_session.ShowOutput( 'Console' ) + py3 _vimspector_session.EvaluateConsole( vim.eval( 'a:expr' ) ) +endfunction + function! vimspector#EvaluateConsole( expr ) abort stopinsert setlocal nomodified @@ -110,6 +115,18 @@ function! vimspector#ListBreakpoints() abort py3 _vimspector_session.ListBreakpoints() endfunction +function! vimspector#CompleteOutput( ArgLead, CmdLine, CursorPos ) abort + let buffers = py3eval( '_vimspector_session.GetOutputBuffers()' ) + return join( buffers, "\n" ) +endfunction + +function! vimspector#CompleteExpr( ArgLead, CmdLine, CursorPos ) abort + return join( py3eval( '_vimspector_session.GetCompletionsSync( ' + \.' vim.eval( "a:CmdLine" ),' + \.' int( vim.eval( "a:CursorPos" ) ) )' ), + \ "\n" ) +endfunction + " Boilerplate {{{ let &cpoptions=s:save_cpo unlet s:save_cpo diff --git a/autoload/vimspector/internal/channel.vim b/autoload/vimspector/internal/channel.vim index d0cb7d2..4c3d1d9 100644 --- a/autoload/vimspector/internal/channel.vim +++ b/autoload/vimspector/internal/channel.vim @@ -25,11 +25,6 @@ _vimspector_session.OnChannelData( vim.eval( 'a:data' ) ) EOF endfunction -function! s:_OnServerError( channel, data ) abort - echom 'Channel received error: ' . a:data - redraw -endfunction - function! s:_OnClose( channel ) abort echom 'Channel closed' redraw @@ -37,22 +32,11 @@ function! s:_OnClose( channel ) abort py3 _vimspector_session.OnServerExit( 0 ) endfunction -function! s:_Send( msg ) abort - call ch_sendraw( s:ch, a:msg ) - return 1 -endfunction - -function! vimspector#internal#channel#Timeout( id ) abort - py3 << EOF -_vimspector_session.OnRequestTimeout( vim.eval( 'a:id' ) ) -EOF -endfunction - function! vimspector#internal#channel#StartDebugSession( config ) abort if exists( 's:ch' ) echo 'Channel is already running' - return v:none + return v:false endif let l:addr = 'localhost:' . a:config[ 'port' ] @@ -70,10 +54,21 @@ function! vimspector#internal#channel#StartDebugSession( config ) abort if ch_status( s:ch ) !=# 'open' echom 'Unable to connect to debug adapter' redraw - return v:none + return v:false endif - return funcref( 's:_Send' ) + return v:true +endfunction + +function! vimspector#internal#channel#Send( msg ) abort + call ch_sendraw( s:ch, a:msg ) + return 1 +endfunction + +function! vimspector#internal#channel#Timeout( id ) abort + py3 << EOF +_vimspector_session.OnRequestTimeout( vim.eval( 'a:id' ) ) +EOF endfunction function! vimspector#internal#channel#StopDebugSession() abort @@ -97,15 +92,6 @@ function! vimspector#internal#channel#Reset() abort endif endfunction -function! vimspector#internal#channel#ForceRead() abort - if exists( 's:ch' ) - let data = ch_readraw( s:ch, { 'timeout': 1000 } ) - if data !=# '' - call s:_OnServerData( s:ch, data ) - endif - endif -endfunction - " Boilerplate {{{ let &cpoptions=s:save_cpo unlet s:save_cpo diff --git a/autoload/vimspector/internal/job.vim b/autoload/vimspector/internal/job.vim index 26ba117..4ed9ea8 100644 --- a/autoload/vimspector/internal/job.vim +++ b/autoload/vimspector/internal/job.vim @@ -30,7 +30,7 @@ endfunction function! s:_OnExit( channel, status ) abort echom 'Channel exit with status ' . a:status redraw - unlet s:job + unlet s:job py3 _vimspector_session.OnServerExit( vim.eval( 'a:status' ) ) endfunction @@ -39,35 +39,11 @@ function! s:_OnClose( channel ) abort redraw endfunction -function! s:_Send( msg ) abort - if ! exists( 's:job' ) - echom "Can't send message: Job was not initialised correctly" - redraw - return 0 - endif - - if job_status( s:job ) !=# 'run' - echom "Can't send message: Job is not running" - redraw - return 0 - endif - - let ch = job_getchannel( s:job ) - if ch ==# 'channel fail' - echom 'Channel was closed unexpectedly!' - redraw - return 0 - endif - - call ch_sendraw( ch, a:msg ) - return 1 -endfunction - function! vimspector#internal#job#StartDebugSession( config ) abort if exists( 's:job' ) echom 'Not starging: Job is already running' redraw - return v:none + return v:false endif let s:job = job_start( a:config[ 'command' ], @@ -91,10 +67,34 @@ function! vimspector#internal#job#StartDebugSession( config ) abort if job_status( s:job ) !=# 'run' echom 'Unable to start job, status is: ' . job_status( s:job ) redraw - return v:none + return v:false endif - return funcref( 's:_Send' ) + return v:true +endfunction + +function! vimspector#internal#job#Send( msg ) abort + if ! exists( 's:job' ) + echom "Can't send message: Job was not initialised correctly" + redraw + return 0 + endif + + if job_status( s:job ) !=# 'run' + echom "Can't send message: Job is not running" + redraw + return 0 + endif + + let ch = job_getchannel( s:job ) + if ch ==# 'channel fail' + echom 'Channel was closed unexpectedly!' + redraw + return 0 + endif + + call ch_sendraw( ch, a:msg ) + return 1 endfunction function! vimspector#internal#job#StopDebugSession() abort @@ -115,15 +115,6 @@ function! vimspector#internal#job#Reset() abort call vimspector#internal#job#StopDebugSession() endfunction -function! vimspector#internal#job#ForceRead() abort - if exists( 's:job' ) - let data = ch_readraw( job_getchannel( s:job ), { 'timeout': 1000 } ) - if data !=# '' - call s:_OnServerData( job_getchannel( s:job ), data ) - endif - endif -endfunction - function! vimspector#internal#job#StartCommandWithLog( cmd, category ) abort if ! exists( 's:commands' ) let s:commands = {} @@ -136,7 +127,7 @@ function! vimspector#internal#job#StartCommandWithLog( cmd, category ) abort let l:index = len( s:commands[ a:category ] ) call add( s:commands[ a:category ], job_start( - \ a:cmd, + \ a:cmd, \ { \ 'out_io': 'buffer', \ 'in_io': 'null', diff --git a/autoload/vimspector/internal/neochannel.vim b/autoload/vimspector/internal/neochannel.vim new file mode 100644 index 0000000..22bc5d2 --- /dev/null +++ b/autoload/vimspector/internal/neochannel.vim @@ -0,0 +1,85 @@ +" vimspector - A multi-language debugging system for Vim +" Copyright 2020 Ben Jackson +" +" Licensed under the Apache License, Version 2.0 (the "License"); +" you may not use this file except in compliance with the License. +" You may obtain a copy of the License at +" +" http://www.apache.org/licenses/LICENSE-2.0 +" +" Unless required by applicable law or agreed to in writing, software +" distributed under the License is distributed on an "AS IS" BASIS, +" WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +" See the License for the specific language governing permissions and +" limitations under the License. + + +" Boilerplate {{{ +let s:save_cpo = &cpoptions +set cpoptions&vim +" }}} + + + +function! s:_OnEvent( chan_id, data, event ) abort + if a:data == [''] + echom 'Channel closed' + redraw + unlet s:ch + py3 _vimspector_session.OnServerExit( 0 ) + else + py3 _vimspector_session.OnChannelData( '\n'.join( vim.eval( 'a:data' ) ) ) + endif +endfunction + +function! vimspector#internal#neochannel#StartDebugSession( config ) abort + if exists( 's:ch' ) + echom 'Not starging: Channel is already running' + redraw + return v:false + endif + + let addr = 'localhost:' . a:config[ 'port' ] + + let s:ch = sockconnect( 'tcp', addr, { 'on_data': funcref( 's:_OnEvent' ) } ) + if s:ch <= 0 + unlet s:ch + return v:false + endif + + return v:true +endfunction + +function! vimspector#internal#neochannel#Send( msg ) abort + if ! exists( 's:ch' ) + echom "Can't send message: Channel was not initialised correctly" + redraw + return 0 + endif + + call chansend( s:ch, a:msg ) + return 1 +endfunction + +function! vimspector#internal#neochannel#StopDebugSession() abort + if !exists( 's:ch' ) + echom "Not stopping session: Channel doesn't exist" + redraw + return + endif + + call chanclose( s:ch ) + " It doesn't look like we get a callback after chanclos. Who knows if we will + " subsequently receive data callbacks. + call s:_OnEvent( s:ch, [ '' ], 'data' ) +endfunction + +function! vimspector#internal#neochannel#Reset() abort + call vimspector#internal#neochannel#StopDebugSession() +endfunction + +" Boilerplate {{{ +let &cpoptions=s:save_cpo +unlet s:save_cpo +" }}} + diff --git a/autoload/vimspector/internal/neojob.vim b/autoload/vimspector/internal/neojob.vim new file mode 100644 index 0000000..f885337 --- /dev/null +++ b/autoload/vimspector/internal/neojob.vim @@ -0,0 +1,212 @@ +" vimspector - A multi-language debugging system for Vim +" Copyright 2020 Ben Jackson +" +" Licensed under the Apache License, Version 2.0 (the "License"); +" you may not use this file except in compliance with the License. +" You may obtain a copy of the License at +" +" http://www.apache.org/licenses/LICENSE-2.0 +" +" Unless required by applicable law or agreed to in writing, software +" distributed under the License is distributed on an "AS IS" BASIS, +" WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +" See the License for the specific language governing permissions and +" limitations under the License. + + +" Boilerplate {{{ +let s:save_cpo = &cpoptions +set cpoptions&vim +" }}} + + + +function! s:_OnEvent( chan_id, data, event ) abort + " In neovim, the data argument is a list. + if a:event ==# 'stdout' + py3 _vimspector_session.OnChannelData( '\n'.join( vim.eval( 'a:data' ) ) ) + elseif a:event ==# 'stderr' + py3 _vimspector_session.OnServerStderr( '\n'.join( vim.eval( 'a:data' ) ) ) + elseif a:event ==# 'exit' + echom 'Channel exit with status ' . a:data + redraw + unlet s:job + " This causes terminal spam in neovim due to + " https://github.com/neovim/neovim/issues/11725 + py3 _vimspector_session.OnServerExit( vim.eval( 'a:data' ) ) + endif +endfunction + +function! vimspector#internal#neojob#StartDebugSession( config ) abort + if exists( 's:job' ) + echom 'Not starging: Job is already running' + redraw + return v:false + endif + + let s:job = jobstart( a:config[ 'command' ], + \ { + \ 'on_stdout': funcref( 's:_OnEvent' ), + \ 'on_stderr': funcref( 's:_OnEvent' ), + \ 'on_exit': funcref( 's:_OnEvent' ), + \ 'cwd': a:config[ 'cwd' ], + \ 'env': a:config[ 'env' ], + \ } + \ ) + + " FIXME: env might not work: Missing in neovim 0.4. But in master: + return v:true +endfunction + +function! s:JobIsRunning( job ) abort + return jobwait( [ s:job ], 0 )[ 0 ] == -1 +endfunction + +function! vimspector#internal#neojob#Send( msg ) abort + if ! exists( 's:job' ) + echom "Can't send message: Job was not initialised correctly" + redraw + return 0 + endif + + if !s:JobIsRunning( s:job ) + echom "Can't send message: Job is not running" + redraw + return 0 + endif + + call chansend( s:job, a:msg ) + return 1 +endfunction + +function! vimspector#internal#neojob#StopDebugSession() abort + if !exists( 's:job' ) + return + endif + + if s:JobIsRunning( s:job ) + echom 'Terminating job' + redraw + call jobstop( s:job ) + endif +endfunction + +function! vimspector#internal#neojob#Reset() abort + call vimspector#internal#neojob#StopDebugSession() +endfunction + +function! s:_OnCommandEvent( category, id, data, event ) abort + if a:data == [''] + return + endif + + if a:event ==# 'stdout' + let buffer = s:commands[ a:category ][ a:id ].stdout + elseif a:event ==# 'stderr' + let buffer = s:commands[ a:category ][ a:id ].stderr + endif + + try + call bufload( buffer ) + catch /E325/ + " Ignore E325/ATTENTION + endtry + + + let numlines = py3eval( "len( vim.buffers[ int( vim.eval( 'buffer' ) ) ] )" ) + let last_line = getbufline( buffer, '$' )[ 0 ] + + call s:MakeBufferWritable( buffer ) + try + if numlines == 1 && last_line ==# '' + call setbufline( buffer, 1, a:data[ 0 ] ) + else + call setbufline( buffer, '$', last_line . a:data[ 0 ] ) + endif + + call appendbufline( buffer, '$', a:data[ 1: ] ) + finally + call s:MakeBufferReadOnly( buffer ) + call setbufvar( buffer, '&modified', 0 ) + endtry + + " if the buffer is visible, scroll it + let w = bufwinnr( buffer ) + if w > 0 + let cw = winnr() + try + execute w . 'wincmd w' + normal! Gz. + finally + execute cw . 'wincmd w' + endtry + endif +endfunction + +function! s:SetUpHiddenBuffer( buffer ) abort + call setbufvar( a:buffer, '&hidden', 1 ) + call setbufvar( a:buffer, '&bufhidden', 'hide' ) + call setbufvar( a:buffer, '&wrap', 0 ) + call setbufvar( a:buffer, '&swapfile', 0 ) + call s:MakeBufferReadOnly( a:buffer ) +endfunction + +function! s:MakeBufferReadOnly( buffer ) abort + call setbufvar( a:buffer, '&modifiable', 0 ) + call setbufvar( a:buffer, '&readonly', 1 ) +endfunction + +function! s:MakeBufferWritable( buffer ) abort + call setbufvar( a:buffer, '&readonly', 0 ) + call setbufvar( a:buffer, '&modifiable', 1 ) +endfunction + + +let s:commands = {} + +function! vimspector#internal#neojob#StartCommandWithLog( cmd, category ) abort + if ! has_key( s:commands, a:category ) + let s:commands[ a:category ] = {} + endif + + let stdout_buf = bufnr( '_vimspector_log_' . a:category . '_out', v:true ) + let stderr_buf = bufnr( '_vimspector_log_' . a:category . '_err', v:true ) + + " FIXME: This largely duplicates the same stuff in the python layer, but we + " don't want to potentially mess up Vim behaviour where the job output is + " attached to a buffer set up by Vim. So we sort o mimic that here. + call s:SetUpHiddenBuffer( stdout_buf ) + call s:SetUpHiddenBuffer( stderr_buf ) + + let id = jobstart(a:cmd, + \ { + \ 'on_stdout': funcref( 's:_OnCommandEvent', + \ [ a:category ] ), + \ 'on_stderr': funcref( 's:_OnCommandEvent', + \ [ a:category ] ) + \ } ) + + let s:commands[ a:category ][ id ] = { + \ 'stdout': stdout_buf, + \ 'stderr': stderr_buf + \ } + + return [ stdout_buf, stderr_buf ] +endfunction + +function! vimspector#internal#neojob#CleanUpCommand( category ) abort + if ! has_key( s:commands, a:category ) + return + endif + + for id in keys( s:commands[ a:category ] ) + call jobstop( id ) + call jobwait( id ) + endfor + unlet! s:commands[ a:category ] +endfunction + +" Boilerplate {{{ +let &cpoptions=s:save_cpo +unlet s:save_cpo +" }}} diff --git a/autoload/vimspector/internal/neoterm.vim b/autoload/vimspector/internal/neoterm.vim new file mode 100644 index 0000000..ab71b2f --- /dev/null +++ b/autoload/vimspector/internal/neoterm.vim @@ -0,0 +1,67 @@ +" vimspector - A multi-language debugging system for Vim +" Copyright 2018 Ben Jackson +" +" Licensed under the Apache License, Version 2.0 (the "License"); +" you may not use this file except in compliance with the License. +" You may obtain a copy of the License at +" +" http://www.apache.org/licenses/LICENSE-2.0 +" +" Unless required by applicable law or agreed to in writing, software +" distributed under the License is distributed on an "AS IS" BASIS, +" WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +" See the License for the specific language governing permissions and +" limitations under the License. + + +" Boilerplate {{{ +let s:save_cpo = &cpoptions +set cpoptions&vim +" }}} + +" Ids are unique throughtout the life of neovim, but obviously buffer numbers +" aren't +" +" FIXME: Tidy this map when buffers are closed ? +let s:buffer_to_id = {} + +function! vimspector#internal#neoterm#Start( cmd, opts ) abort + if ! get( a:opts, 'curwin', 0 ) + if get( a:opts, 'vertical', 0 ) + vsplit + else + split + endif + endif + + " FIXME: 'env' doesn't work + let id = termopen( a:cmd, { 'cwd': a:opts[ 'cwd' ] } ) + let bufnr = bufnr() + let s:buffer_to_id[ bufnr ] = id + return bufnr +endfunction + +function! s:JobIsRunning( job ) abort + return jobwait( [ a:job ], 0 )[ 0 ] == -1 +endfunction + +function! vimspector#internal#neoterm#IsFinished( bufno ) abort + if !has_key( s:buffer_to_id, a:bufno ) + return v:true + endif + + return !s:JobIsRunning( s:buffer_to_id[ a:bufno ] ) +endfunction + +function! vimspector#internal#neoterm#GetPID( bufno ) abort + if !has_key( s:buffer_to_id, a:bufno ) + return -1 + endif + + return jobpid( s:buffer_to_id[ a:bufno ] ) +endfunction + +" Boilerplate {{{ +let &cpoptions=s:save_cpo +unlet s:save_cpo +" }}} diff --git a/autoload/vimspector/internal/state.vim b/autoload/vimspector/internal/state.vim index 6478506..11ed003 100644 --- a/autoload/vimspector/internal/state.vim +++ b/autoload/vimspector/internal/state.vim @@ -19,10 +19,16 @@ let s:save_cpo = &cpoptions set cpoptions&vim " }}} +let s:is_neovim = has( 'nvim' ) + function! vimspector#internal#state#Reset() abort + let prefix = '' + if s:is_neovim + let prefix='neo' + endif py3 << EOF from vimspector import debug_session -_vimspector_session = debug_session.DebugSession() +_vimspector_session = debug_session.DebugSession( vim.eval( 'prefix' ) ) EOF endfunction diff --git a/autoload/vimspector/internal/term.vim b/autoload/vimspector/internal/term.vim new file mode 100644 index 0000000..5a570fc --- /dev/null +++ b/autoload/vimspector/internal/term.vim @@ -0,0 +1,37 @@ +" vimspector - A multi-language debugging system for Vim +" Copyright 2018 Ben Jackson +" +" Licensed under the Apache License, Version 2.0 (the "License"); +" you may not use this file except in compliance with the License. +" You may obtain a copy of the License at +" +" http://www.apache.org/licenses/LICENSE-2.0 +" +" Unless required by applicable law or agreed to in writing, software +" distributed under the License is distributed on an "AS IS" BASIS, +" WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +" See the License for the specific language governing permissions and +" limitations under the License. + + +" Boilerplate {{{ +let s:save_cpo = &cpoptions +set cpoptions&vim +" }}} + +function! vimspector#internal#term#Start( cmd, opts ) abort + return term_start( a:cmd, a:opts ) +endfunction + +function! vimspector#internal#term#IsFinished( bufno ) abort + return index( split( term_getstatus( a:bufno ), ',' ), 'finished' ) >= 0 +endfunction + +function! vimspector#internal#term#GetPID( bufno ) abort + return job_info( term_getjob( a:bufno ) ).process +endfunction + +" Boilerplate {{{ +let &cpoptions=s:save_cpo +unlet s:save_cpo +" }}} diff --git a/plugin/vimspector.vim b/plugin/vimspector.vim index 13ffa3b..0c71090 100644 --- a/plugin/vimspector.vim +++ b/plugin/vimspector.vim @@ -26,6 +26,7 @@ if exists( 'g:loaded_vimpector' ) call s:restore_cpo() finish endif +"}}} " TODO: " - Check Vim version (for jobs) @@ -36,28 +37,54 @@ let g:loaded_vimpector = 1 let s:mappings = get( g:, 'vimspector_enable_mappings', '' ) +nnoremap VimspectorContinue :call vimspector#Continue() +nnoremap VimspectorStop :call vimspector#Stop() +nnoremap VimspectorRestart :call vimspector#Restart() +nnoremap VimspectorPause :call vimspector#Pause() +nnoremap VimspectorToggleBreakpoint + \ :call vimspector#ToggleBreakpoint() +nnoremap VimspectorAddFunctionBreakpoint + \ :call vimspector#AddFunctionBreakpoint( expand( '' ) ) +nnoremap VimspectorStepOver :call vimspector#StepOver() +nnoremap VimspectorStepInto :call vimspector#StepInto() +nnoremap VimspectorStepOut :call vimspector#StepOut() + if s:mappings ==# 'VISUAL_STUDIO' - nnoremap :call vimspector#Continue() - nnoremap :call vimspector#Stop() - nnoremap :call vimspector#Restart() - nnoremap :call vimspector#Pause() - nnoremap :call vimspector#ToggleBreakpoint() - nnoremap :call vimspector#AddFunctionBreakpoint( expand( '' ) ) - nnoremap :call vimspector#StepOver() - nnoremap :call vimspector#StepInto() - nnoremap :call vimspector#StepOut() + nmap VimspectorContinue + nmap VimspectorStop + nmap VimspectorRestart + nmap VimspectorPause + nmap VimspectorToggleBreakpoint + nmap VimspectorAddFunctionBreakpoint + nmap VimspectorStepOver + nmap VimspectorStepInto + nmap VimspectorStepOut elseif s:mappings ==# 'HUMAN' - nnoremap :call vimspector#Continue() - nnoremap :call vimspector#Stop() - nnoremap :call vimspector#Restart() - nnoremap :call vimspector#Pause() - nnoremap :call vimspector#ToggleBreakpoint() - nnoremap :call vimspector#AddFunctionBreakpoint( expand( '' ) ) - nnoremap :call vimspector#StepOver() - nnoremap :call vimspector#StepInto() - nnoremap :call vimspector#StepOut() + nmap VimspectorContinue + nmap VimspectorStop + nmap VimspectorRestart + nmap VimspectorPause + nmap VimspectorToggleBreakpoint + nmap VimspectorAddFunctionBreakpoint + nmap VimspectorStepOver + nmap VimspectorStepInto + nmap VimspectorStepOut endif -"}}} +command! -bar -nargs=1 -complete=custom,vimspector#CompleteExpr + \ VimspectorWatch + \ call vimspector#AddWatch( ) +command! -bar -nargs=1 -complete=custom,vimspector#CompleteOutput + \ VimspectorShowOutput + \ call vimspector#ShowOutput( ) +command! -bar -nargs=1 -complete=custom,vimspector#CompleteExpr + \ VimspectorEval + \ call vimspector#Evaluate( ) +command! -bar + \ VimspectorReset + \ call vimspector#Reset() +" boilerplate {{{ call s:restore_cpo() +" }}} + diff --git a/python3/vimspector/code.py b/python3/vimspector/code.py index 509008d..b1b7698 100644 --- a/python3/vimspector/code.py +++ b/python3/vimspector/code.py @@ -22,8 +22,9 @@ from vimspector import utils class CodeView( object ): - def __init__( self, window ): + def __init__( self, window, api_prefix ): self._window = window + self._api_prefix = api_prefix self._terminal_window = None self._terminal_buffer_number = None @@ -214,8 +215,9 @@ class CodeView( object ): if self._terminal_window is not None: assert self._terminal_buffer_number if ( self._terminal_window.buffer.number == self._terminal_buffer_number - and 'finished' in vim.eval( 'term_getstatus( {} )'.format( - self._terminal_buffer_number ) ) ): + and int( utils.Call( 'vimspector#internal#{}term#IsFinished'.format( + self._api_prefix ), + self._terminal_buffer_number ) ) ): window_for_start = self._terminal_window options[ 'curwin' ] = 1 @@ -224,13 +226,11 @@ class CodeView( object ): with utils.TemporaryVimOptions( { 'splitright': True, 'equalalways': False } ): with utils.LetCurrentWindow( window_for_start ): - # TODO/FIXME: Do something about closing this when we reset ? - vim_cmd = 'term_start( {}, {} )'.format( json.dumps( args ), - json.dumps( options ) ) - - self._logger.debug( 'Start terminal: {}'.format( vim_cmd ) ) - - buffer_number = int( vim.eval( vim_cmd ) ) + buffer_number = int( + utils.Call( + 'vimspector#internal#{}term#Start'.format( self._api_prefix ), + args, + options ) ) terminal_window = vim.current.window if buffer_number is None or buffer_number <= 0: diff --git a/python3/vimspector/debug_adapter_connection.py b/python3/vimspector/debug_adapter_connection.py index 73a4549..295b83d 100644 --- a/python3/vimspector/debug_adapter_connection.py +++ b/python3/vimspector/debug_adapter_connection.py @@ -65,6 +65,33 @@ class DebugAdapterConnection( object ): if not self._SendMessage( msg ): self._AbortRequest( request, 'Unable to send message' ) + + def DoRequestSync( self, msg, timeout = 5000 ): + result = {} + + def handler( msg ): + result[ 'response' ] = msg + + def failure_handler( reason, msg ): + result[ 'response' ] = msg + result[ 'exception' ] = RuntimeError( reason ) + + self.DoRequest( handler, msg, failure_handler, timeout ) + + bug_catcher = 1000 + while not result and bug_catcher >= 0: + vim.command( 'sleep 10m' ) + bug_catcher -= 10 + + if result.get( 'exception' ) is not None: + raise result[ 'exception' ] + + if result.get( 'response' ) is None: + raise RuntimeError( "No response" ) + + return result[ 'response' ] + + def OnRequestTimeout( self, timer_id ): request_id = None for seq, request in self._outstanding_requests.items(): diff --git a/python3/vimspector/debug_session.py b/python3/vimspector/debug_session.py index 8eec9f4..474e3d0 100644 --- a/python3/vimspector/debug_session.py +++ b/python3/vimspector/debug_session.py @@ -41,11 +41,14 @@ USER_CHOICES = {} class DebugSession( object ): - def __init__( self ): + def __init__( self, api_prefix ): self._logger = logging.getLogger( __name__ ) utils.SetUpLogging( self._logger ) + self._api_prefix = api_prefix + self._logger.info( "**** INITIALISING NEW VIMSPECTOR SESSION ****" ) + self._logger.info( "API is: {}".format( api_prefix ) ) self._logger.info( 'VIMSPECTOR_HOME = %s', VIMSPECTOR_HOME ) self._logger.info( 'gadgetDir = %s', install.GetGadgetDir( VIMSPECTOR_HOME, @@ -400,12 +403,35 @@ class DebugSession( object ): def ShowOutput( self, category ): self._outputView.ShowOutput( category ) + def GetOutputBuffers( self ): + return self._outputView.GetCategories() + + def GetCompletionsSync( self, text_line, column_in_bytes ): + if not self._server_capabilities.get( 'supportsCompletionsRequest' ): + return [] + + response = self._connection.DoRequestSync( { + 'command': 'completions', + 'arguments': { + 'frameId': self._stackTraceView.GetCurrentFrame()[ 'id' ], + # TODO: encoding ? bytes/codepoints + 'text': text_line, + 'column': column_in_bytes + } + } ) + # TODO: + # - start / length + # - sortText + return [ i.get( 'text' ) or i[ 'label' ] + for i in response[ 'body' ][ 'targets' ] ] + + def _SetUpUI( self ): vim.command( 'tabnew' ) self._uiTab = vim.current.tabpage # Code window - self._codeView = code.CodeView( vim.current.window ) + self._codeView = code.CodeView( vim.current.window, self._api_prefix ) # Call stack with utils.TemporaryVimOptions( { 'splitright': False, @@ -441,7 +467,8 @@ class DebugSession( object ): vim.command( '10spl' ) vim.command( 'enew' ) self._outputView = output.OutputView( self._connection, - vim.current.window ) + vim.current.window, + self._api_prefix ) def ClearCurrentFrame( self ): self.SetCurrentFrame( None ) @@ -482,6 +509,8 @@ class DebugSession( object ): port = utils.AskForInput( 'Enter port to connect to: ' ) self._adapter[ 'port' ] = port + self._connection_type = self._api_prefix + self._connection_type + # TODO: Do we actually need to copy and update or does Vim do that? env = os.environ.copy() if 'env' in self._adapter: @@ -492,19 +521,18 @@ class DebugSession( object ): self._adapter[ 'cwd' ] = os.getcwd() vim.vars[ '_vimspector_adapter_spec' ] = self._adapter - channel_send_func = vim.bindeval( - "vimspector#internal#{}#StartDebugSession( " - " g:_vimspector_adapter_spec " - ")".format( self._connection_type ) ) - - if channel_send_func is None: + if not vim.eval( "vimspector#internal#{}#StartDebugSession( " + " g:_vimspector_adapter_spec " + ")".format( self._connection_type ) ): self._logger.error( "Unable to start debug server" ) else: self._connection = debug_adapter_connection.DebugAdapterConnection( self, - channel_send_func ) + lambda msg: utils.Call( + "vimspector#internal#{}#Send".format( self._connection_type ), + msg ) ) - self._logger.info( 'Debug Adapter Started' ) + self._logger.info( 'Debug Adapter Started' ) def _StopDebugAdapter( self, callback = None ): def handler( *args ): @@ -796,11 +824,12 @@ class DebugSession( object ): self._logger.debug( 'Defaulting working directory to %s', params[ 'cwd' ] ) - buffer_number = self._codeView.LaunchTerminal( params ) + term_id = self._codeView.LaunchTerminal( params ) response = { - 'processId': vim.eval( 'job_info( term_getjob( {} ) )' - '.process'.format( buffer_number ) ) + 'processId': int( utils.Call( + 'vimspector#internal#{}term#GetPID'.format( self._api_prefix ), + term_id ) ) } self._connection.DoResponse( message, None, response ) diff --git a/python3/vimspector/output.py b/python3/vimspector/output.py index d4cf1af..38ce6d3 100644 --- a/python3/vimspector/output.py +++ b/python3/vimspector/output.py @@ -40,10 +40,11 @@ def CategoryToBuffer( category ): class OutputView( object ): - def __init__( self, connection, window ): + def __init__( self, connection, window, api_prefix ): self._window = window self._connection = connection self._buffers = {} + self._api_prefix = api_prefix for b in set( BUFFER_MAP.values() ): self._CreateBuffer( b ) @@ -95,12 +96,12 @@ class OutputView( object ): def Clear( self ): for category, tab_buffer in self._buffers.items(): if tab_buffer.is_job: - utils.CleanUpCommand( category ) + utils.CleanUpCommand( category, self._api_prefix ) try: vim.command( 'bdelete! {0}'.format( tab_buffer.buf.number ) ) except vim.error as e: # FIXME: For now just ignore the "no buffers were deleted" error - if 'E516' not in e: + if 'E516' not in str( e ): raise self._buffers = {} @@ -116,17 +117,19 @@ class OutputView( object ): def Evaluate( self, frame, expression ): console = self._buffers[ 'Console' ].buf - utils.AppendToBuffer( console, 'Evaluating: ' + expression ) + with utils.ModifiableScratchBuffer( console ): + utils.AppendToBuffer( console, 'Evaluating: ' + expression ) def print_result( message ): - utils.AppendToBuffer( console, - 'Evaluated: ' + expression ) + with utils.ModifiableScratchBuffer( console ): + utils.AppendToBuffer( console, + 'Evaluated: ' + expression ) - result = message[ 'body' ][ 'result' ] - if result is None: - result = 'null' + result = message[ 'body' ][ 'result' ] + if result is None: + result = 'null' - utils.AppendToBuffer( console, ' Result: ' + result ) + utils.AppendToBuffer( console, ' Result: ' + result ) request = { 'command': 'evaluate', @@ -161,7 +164,7 @@ class OutputView( object ): cmd = [ 'tail', '-F', '-n', '+1', '--', file_name ] if cmd is not None: - out, err = utils.SetUpCommandBuffer( cmd, category ) + out, err = utils.SetUpCommandBuffer( cmd, category, self._api_prefix ) self._buffers[ category + '-out' ] = TabBuffer( out, len( self._buffers ) ) self._buffers[ category + '-out' ].is_job = True @@ -205,3 +208,6 @@ class OutputView( object ): tab_buffer.index, utils.Escape( category ), '*' if tab_buffer.flag else '' ) ) + + def GetCategories( self ): + return [ category for category in self._buffers.keys() ] diff --git a/python3/vimspector/utils.py b/python3/vimspector/utils.py index ba930ba..a9f55df 100644 --- a/python3/vimspector/utils.py +++ b/python3/vimspector/utils.py @@ -56,24 +56,26 @@ def OpenFileInCurrentWindow( file_name ): return vim.buffers[ buffer_number ] -def SetUpCommandBuffer( cmd, name ): - bufs = vim.bindeval( - 'vimspector#internal#job#StartCommandWithLog( {}, "{}" )'.format( +def SetUpCommandBuffer( cmd, name, api_prefix ): + bufs = vim.eval( + 'vimspector#internal#{}job#StartCommandWithLog( {}, "{}" )'.format( + api_prefix, json.dumps( cmd ), name ) ) if bufs is None: raise RuntimeError( "Unable to start job {}: {}".format( cmd, name ) ) - elif not all( b > 0 for b in bufs ): + elif not all( int( b ) > 0 for b in bufs ): raise RuntimeError( "Unable to get all streams for job {}: {}".format( name, cmd ) ) - return [ vim.buffers[ b ] for b in bufs ] + return [ vim.buffers[ int( b ) ] for b in bufs ] -def CleanUpCommand( name ): - return vim.eval( 'vimspector#internal#job#CleanUpCommand( "{}" )'.format( +def CleanUpCommand( name, api_prefix ): + return vim.eval( 'vimspector#internal#{}job#CleanUpCommand( "{}" )'.format( + api_prefix, name ) ) @@ -102,7 +104,7 @@ def SetUpHiddenBuffer( buf, name ): def SetUpPromptBuffer( buf, name, prompt, callback, hidden=False ): # This feature is _super_ new, so only enable when available if not int( vim.eval( "exists( '*prompt_setprompt' )" ) ): - return SetUpScratchBuffer( buf, name ) + return SetUpHiddenBuffer( buf, name ) buf.options[ 'buftype' ] = 'prompt' buf.options[ 'swapfile' ] = False @@ -449,3 +451,19 @@ def ToUnicode( b ): if isinstance( b, bytes ): return b.decode( 'utf-8' ) return b + + +# Call a vimscript function with suplied arguments. +def Call( vimscript_function, *args ): + call = vimscript_function + '(' + for index, arg in enumerate( args ): + if index > 0: + call += ', ' + + arg_name = 'vimspector_internal_arg_{}'.format( index ) + vim.vars[ arg_name ] = arg + call += 'g:' + arg_name + + call += ')' + _logger.debug( 'Calling: {}'.format( call ) ) + return vim.eval( call ) diff --git a/python3/vimspector/variables.py b/python3/vimspector/variables.py index f0ff99e..759abdf 100644 --- a/python3/vimspector/variables.py +++ b/python3/vimspector/variables.py @@ -105,6 +105,9 @@ class VariablesView( object ): for k, v in self._oldoptions.items(): vim.options[ k ] = v + vim.command( 'bdelete! ' + str( self._watch.win.buffer.number ) ) + vim.command( 'bdelete! ' + str( self._vars.win.buffer.number ) ) + def LoadScopes( self, frame ): def scopes_consumer( message ): old_scopes = self._scopes diff --git a/run_tests b/run_tests index f216c58..1c59f76 100755 --- a/run_tests +++ b/run_tests @@ -1,8 +1,25 @@ #!/usr/bin/env bash +if [ "$1" == "--help" ]; then + echo "$(basename $0) " + echo "e.g.: " + echo " - run all tests: $0" + echo " - run specific tests script: $0 signature_help.test.vim" + echo " - run specific tests fun: $0 signature_help.test.vim:Test_signatures_TopLine\(\)" + exit 0 +fi + RUN_VIM="vim --clean --not-a-term" RUN_TEST="${RUN_VIM} -S lib/run_test.vim" +NODE_VERSION_MAJ=$(node --version \ + | awk 'BEGIN { FS="." } { print substr( $1, 2 ) }') +if [ ${NODE_VERSION_MAJ} -gt "10" ]; then + echo "ERROR: Tests must be run against node 10 or earlier because python." >&2 + echo "You have $(node --version)" >&2 + exit 1 +fi + if [ -z "$VIMSPECTOR_MIMODE" ]; then if which lldb >/dev/null 2>&1; then export VIMSPECTOR_MIMODE=lldb @@ -24,8 +41,8 @@ set -e set +e echo "%DONE - built test programs" -pushd tests > /dev/null - +# Start +pushd $(dirname $0)/tests > /dev/null echo "Running Vimspector Vim tests" RESULT=0 @@ -39,23 +56,45 @@ fi for t in ${TESTS}; do echo "" echo "%RUN: $t" - rm -f messages debuglog # split on : into fileName and testName IFS=: read -s t T <<< "$t" - if ${RUN_TEST} --cmd 'au SwapExists * let v:swapchoice = "e"' $t $T; then + TESTLOGDIR=$(pwd)/logs/$t + + if ${RUN_TEST} --cmd 'au SwapExists * let v:swapchoice = "e"' $t $T \ + && [ -f $t.res ]; then echo "%PASS: $t PASSED" else - cat messages - echo "%FAIL: $t FAILED" + echo "%FAIL: $t FAILED - see $TESTLOGDIR" RESULT=1 fi + + rm -rf $TESTLOGDIR + mkdir -p $TESTLOGDIR + ${RUN_VIM} --version > ${TESTLOGDIR}/vimversion + for l in messages debuglog test.log *.testlog; do + # In CI we can't view the output files, so we just have to cat them + if [ -f $l ]; then + if [ "$VIMSPECTOR_TEST_STDOUT" ]; then + echo "" + echo "" + echo "*** START: $l ***" + cat $l + echo "*** END: $l ***" + fi + mv $l $TESTLOGDIR + fi + done + + rm -f $t.res done +echo "Done running tests" popd > /dev/null echo "" echo "All done." + exit $RESULT diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 0000000..333c1e9 --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1 @@ +logs/ diff --git a/tests/lib/run_test.vim b/tests/lib/run_test.vim index f8cef56..d8821ed 100644 --- a/tests/lib/run_test.vim +++ b/tests/lib/run_test.vim @@ -4,7 +4,7 @@ " " To execute only specific test functions, add a second argument. It will be " matched against the names of the Test_ funtion. E.g.: -" ../vim -u NONE -S runtest.vim test_channel.vim open_delay +" ../vim -Nu NONE vimrc -S lib/run_test.vim test_channel.vim open_delay " The output can be found in the "messages" file. " " The test script may contain anything, only functions that start with @@ -26,8 +26,16 @@ " It will be called after each Test_ function. " " When debugging a test it can be useful to add messages to v:errors: -" call add(v:errors, "this happened") +" call add(v:errors, "this happened") +" +" But for real debug logging: +" call ch_log( ",,,message..." ) +" Then view it in 'debuglog' +" Let a test take up to 1 minute +let s:single_test_timeout = 60000 + +" Restrict the runtimepath to the exact minimum needed for testing set rtp=$PWD/lib,$VIM/vimfiles,$VIMRUNTIME,$VIM/vimfiles/after if has('packages') let &packpath = &rtp @@ -37,7 +45,7 @@ call ch_logfile( 'debuglog', 'w' ) " For consistency run all tests with 'nocompatible' set. " This also enables use of line continuation. -set nocp viminfo+=nviminfo +set nocp " Use utf-8 by default, instead of whatever the system default happens to be. " Individual tests can overrule this at the top of the file. @@ -47,11 +55,23 @@ set encoding=utf-8 set nomore " Output all messages in English. -lang mess C +lang messages C " Always use forward slashes. set shellslash +func s:TestFailed() + let log = readfile( expand( '~/.vimspector.log' ) ) + let logfile = s:testid_filesafe . '_vimspector.log.testlog' + call writefile( log, logfile, 's' ) + call add( s:messages, 'Wrote log for failed test: ' . logfile ) +endfunc + +func! Abort( timer_id ) + call assert_report( 'Test timed out!!!' ) + qa! +endfunc + func RunTheTest(test) echo 'Executing ' . a:test @@ -107,40 +127,38 @@ func RunTheTest(test) call add(s:messages, 'Executing ' . a:test) let s:done += 1 + let timer = timer_start( s:single_test_timeout, funcref( 'Abort' ) ) - if a:test =~ 'Test_nocatch_' - " Function handles errors itself. This avoids skipping commands after the - " error. + try + let s:test = a:test + let s:testid = g:testpath . ':' . a:test + + let test_filesafe = substitute( a:test, '[)(,:]', '_', 'g' ) + let s:testid_filesafe = g:testpath . '_' . test_filesafe + + au VimLeavePre * call EarlyExit(s:test) exe 'call ' . a:test - else - try - let s:test = a:test - let s:testid = g:testpath . ':' . a:test - let test_filesafe = substitute( a:test, ')', '_', 'g' ) - let test_filesafe = substitute( test_filesafe, '(', '_', 'g' ) - let test_filesafe = substitute( test_filesafe, ',', '_', 'g' ) - let test_filesafe = substitute( test_filesafe, ':', '_', 'g' ) - let s:testid_filesafe = g:testpath . '_' . test_filesafe - au VimLeavePre * call EarlyExit(s:test) - exe 'call ' . a:test - au! VimLeavePre - catch /^\cskipped/ - call add(s:messages, ' Skipped') - call add(s:skipped, - \ 'SKIPPED ' . a:test - \ . ': ' - \ . substitute(v:exception, '^\S*\s\+', '', '')) - catch - call add(v:errors, - \ 'Caught exception in ' . a:test - \ . ': ' - \ . v:exception - \ . ' @ ' - \ . g:testpath - \ . ':' - \ . v:throwpoint) - endtry - endif + au! VimLeavePre + catch /^\cskipped/ + call add(s:messages, ' Skipped') + call add(s:skipped, + \ 'SKIPPED ' . a:test + \ . ': ' + \ . substitute(v:exception, '^\S*\s\+', '', '')) + catch + call add(v:errors, + \ 'Caught exception in ' . a:test + \ . ': ' + \ . v:exception + \ . ' @ ' + \ . g:testpath + \ . ':' + \ . v:throwpoint) + + call s:TestFailed() + endtry + + call timer_stop( timer ) " In case 'insertmode' was set and something went wrong, make sure it is " reset to avoid trouble with anything else. @@ -203,24 +221,16 @@ endfunc func AfterTheTest() if len(v:errors) > 0 let s:fail += 1 + call s:TestFailed() call add(s:errors, 'Found errors in ' . s:testid . ':') call extend(s:errors, v:errors) let v:errors = [] - - let log = readfile( expand( '~/.vimspector.log' ) ) - let logfile = s:testid_filesafe . '.vimspector.log' - call writefile( log, logfile, 's' ) - call add( s:messages, 'Wrote log for failed test: ' . logfile ) - call extend( s:messages, log ) endif endfunc func EarlyExit(test) " It's OK for the test we use to test the quit detection. - if a:test != 'Test_zz_quit_detected()' - call add(v:errors, 'Test caused Vim to exit: ' . a:test) - endif - + call add(v:errors, 'Test caused Vim to exit: ' . a:test) call FinishTesting() endfunc @@ -233,17 +243,19 @@ func FinishTesting() if s:fail == 0 " Success, create the .res file so that make knows it's done. - exe 'split ' . fnamemodify(g:testname, ':r') . '.res' - write + call writefile( [], g:testname . '.res', 's' ) endif if len(s:errors) > 0 " Append errors to test.log - split test.log - call append(line('$'), '') - call append(line('$'), 'From ' . g:testpath . ':') - call append(line('$'), s:errors) - write + let l = [] + if filereadable( 'test.log' ) + let l = readfile( 'test.log' ) + endif + call writefile( l->extend( [ '', 'From ' . g:testpath . ':' ] ) + \ ->extend( s:errors ), + \ 'test.log', + \ 's' ) endif if s:done == 0 @@ -264,11 +276,14 @@ func FinishTesting() call extend(s:messages, s:skipped) " Append messages to the file "messages" - split messages - call append(line('$'), '') - call append(line('$'), 'From ' . g:testpath . ':') - call append(line('$'), s:messages) - write + let l = [] + if filereadable( 'messages' ) + let l = readfile( 'messages' ) + endif + call writefile( l->extend( [ '', 'From ' . g:testpath . ':' ] ) + \ ->extend( s:messages ), + \ 'messages', + \ 's' ) if s:fail > 0 cquit! @@ -317,6 +332,7 @@ endif for s:test in sort(s:tests) " Silence, please! set belloff=all + call RunTheTest(s:test) call AfterTheTest() endfor