Merge pull request #77 from puremourning/neovim

Support neovim
This commit is contained in:
mergify[bot] 2020-01-19 20:21:56 +00:00 • committed by GitHub
commit 4318e567f2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 760 additions and 193 deletions

View file

@ -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

View file

@ -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

View file

@ -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',

View file

@ -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
" }}}

View file

@ -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
" }}}

View file

@ -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
" }}}

View file

@ -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

View file

@ -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
" }}}

View file

@ -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 <Plug>VimspectorContinue :<c-u>call vimspector#Continue()<CR>
nnoremap <Plug>VimspectorStop :<c-u>call vimspector#Stop()<CR>
nnoremap <Plug>VimspectorRestart :<c-u>call vimspector#Restart()<CR>
nnoremap <Plug>VimspectorPause :<c-u>call vimspector#Pause()<CR>
nnoremap <Plug>VimspectorToggleBreakpoint
\ :<c-u>call vimspector#ToggleBreakpoint()<CR>
nnoremap <Plug>VimspectorAddFunctionBreakpoint
\ :<c-u>call vimspector#AddFunctionBreakpoint( expand( '<cexpr>' ) )<CR>
nnoremap <Plug>VimspectorStepOver :<c-u>call vimspector#StepOver()<CR>
nnoremap <Plug>VimspectorStepInto :<c-u>call vimspector#StepInto()<CR>
nnoremap <Plug>VimspectorStepOut :<c-u>call vimspector#StepOut()<CR>
if s:mappings ==# 'VISUAL_STUDIO'
nnoremap <F5> :call vimspector#Continue()<CR>
nnoremap <S-F5> :call vimspector#Stop()<CR>
nnoremap <C-S-F5> :call vimspector#Restart()<CR>
nnoremap <F6> :call vimspector#Pause()<CR>
nnoremap <F9> :call vimspector#ToggleBreakpoint()<CR>
nnoremap <S-F9> :call vimspector#AddFunctionBreakpoint( expand( '<cexpr>' ) )<CR>
nnoremap <F10> :call vimspector#StepOver()<CR>
nnoremap <F11> :call vimspector#StepInto()<CR>
nnoremap <S-F11> :call vimspector#StepOut()<CR>
nmap <F5> <Plug>VimspectorContinue
nmap <S-F5> <Plug>VimspectorStop
nmap <C-S-F5> <Plug>VimspectorRestart
nmap <F6> <Plug>VimspectorPause
nmap <F9> <Plug>VimspectorToggleBreakpoint
nmap <S-F9> <Plug>VimspectorAddFunctionBreakpoint
nmap <F10> <Plug>VimspectorStepOver
nmap <F11> <Plug>VimspectorStepInto
nmap <S-F11> <Plug>VimspectorStepOut
elseif s:mappings ==# 'HUMAN'
nnoremap <F5> :call vimspector#Continue()<CR>
nnoremap <F3> :call vimspector#Stop()<CR>
nnoremap <F4> :call vimspector#Restart()<CR>
nnoremap <F6> :call vimspector#Pause()<CR>
nnoremap <F9> :call vimspector#ToggleBreakpoint()<CR>
nnoremap <F8> :call vimspector#AddFunctionBreakpoint( expand( '<cexpr>' ) )<CR>
nnoremap <F10> :call vimspector#StepOver()<CR>
nnoremap <F11> :call vimspector#StepInto()<CR>
nnoremap <F12> :call vimspector#StepOut()<CR>
nmap <F5> <Plug>VimspectorContinue
nmap <F3> <Plug>VimspectorStop
nmap <F4> <Plug>VimspectorRestart
nmap <F6> <Plug>VimspectorPause
nmap <F9> <Plug>VimspectorToggleBreakpoint
nmap <F8> <Plug>VimspectorAddFunctionBreakpoint
nmap <F10> <Plug>VimspectorStepOver
nmap <F11> <Plug>VimspectorStepInto
nmap <F12> <Plug>VimspectorStepOut
endif
"}}}
command! -bar -nargs=1 -complete=custom,vimspector#CompleteExpr
\ VimspectorWatch
\ call vimspector#AddWatch( <f-args> )
command! -bar -nargs=1 -complete=custom,vimspector#CompleteOutput
\ VimspectorShowOutput
\ call vimspector#ShowOutput( <f-args> )
command! -bar -nargs=1 -complete=custom,vimspector#CompleteExpr
\ VimspectorEval
\ call vimspector#Evaluate( <f-args> )
command! -bar
\ VimspectorReset
\ call vimspector#Reset()
" boilerplate {{{
call s:restore_cpo()
" }}}

View file

@ -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:

View file

@ -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():

View file

@ -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 )

View file

@ -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() ]

View file

@ -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 )

View file

@ -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

View file

@ -1,8 +1,25 @@
#!/usr/bin/env bash
if [ "$1" == "--help" ]; then
echo "$(basename $0) <optional list of tests in form file:func>"
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

1
tests/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
logs/

View file

@ -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