Merge branch 'master' into master

This commit is contained in:
Ben Jackson 2021-03-19 23:20:41 +00:00 committed by GitHub
commit 91e0426e88
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 363 additions and 61 deletions

View file

@ -59,6 +59,7 @@ For detailed explanatin of the `.vimspector.json` format, see the
* [Console autocompletion](#console-autocompletion) * [Console autocompletion](#console-autocompletion)
* [Log View](#log-view) * [Log View](#log-view)
* [Closing debugger](#closing-debugger) * [Closing debugger](#closing-debugger)
* [Terminate debuggee](#terminate-debuggee)
* [Debug profile configuration](#debug-profile-configuration) * [Debug profile configuration](#debug-profile-configuration)
* [C, C , Rust, etc.](#c-c-rust-etc) * [C, C , Rust, etc.](#c-c-rust-etc)
* [C Remote debugging](#c-remote-debugging) * [C Remote debugging](#c-remote-debugging)
@ -76,6 +77,7 @@ For detailed explanatin of the `.vimspector.json` format, see the
* [Debug cli application](#debug-cli-application) * [Debug cli application](#debug-cli-application)
* [JavaScript, TypeScript, etc.](#javascript-typescript-etc) * [JavaScript, TypeScript, etc.](#javascript-typescript-etc)
* [Java](#java) * [Java](#java)
* [Hot code replace](#hot-code-replace)
* [Usage with YouCompleteMe](#usage-with-youcompleteme) * [Usage with YouCompleteMe](#usage-with-youcompleteme)
* [Other LSP clients](#other-lsp-clients) * [Other LSP clients](#other-lsp-clients)
* [Lua](#lua) * [Lua](#lua)
@ -91,7 +93,7 @@ For detailed explanatin of the `.vimspector.json` format, see the
* [Example](#example) * [Example](#example)
* [FAQ](#faq) * [FAQ](#faq)
<!-- Added by: ben, at: Sun 21 Feb 2021 21:15:32 GMT --> <!-- Added by: ben, at: Fri 19 Mar 2021 22:57:28 GMT -->
<!--te--> <!--te-->
@ -1591,6 +1593,20 @@ editor plugin to use Java. I recommend [YouCompleteMe][], which has full support
for jdt.ls, and most importantly a trivial way to load the debug adapter and to for jdt.ls, and most importantly a trivial way to load the debug adapter and to
use it with Vimspector. use it with Vimspector.
### Hot code replace
When using the [java debug server][java-debug-server], Vimspector supports the
hot code replace custom feature. By default, when the underlying class files
change, vimspector asks the user if they wish to reload these classes at
runtime.
This behaviour can be customised:
* `let g:ycm_java_hotcodereplace_mode = 'ask'` - the default, ask the user for
each reload.
* `let g:ycm_java_hotcodereplace_mode = 'always'` - don't ask, always reload
* `let g:ycm_java_hotcodereplace_mode = 'never'` - don't ask, never reload
### Usage with YouCompleteMe ### Usage with YouCompleteMe
* Set up [YCM for java][YcmJava]. * Set up [YCM for java][YcmJava].

View file

@ -41,11 +41,11 @@ function! s:Enabled() abort
return s:enabled return s:enabled
endfunction endfunction
function! vimspector#Launch() abort function! vimspector#Launch( ... ) abort
if !s:Enabled() if !s:Enabled()
return return
endif endif
py3 _vimspector_session.Start() py3 _vimspector_session.Start( *vim.eval( 'a:000' ) )
endfunction endfunction
function! vimspector#LaunchWithSettings( settings ) abort function! vimspector#LaunchWithSettings( settings ) abort

View file

@ -71,11 +71,7 @@ function! vimspector#internal#balloon#CreateTooltip( is_hover, ... ) abort
\ 'callback': 'vimspector#internal#balloon#CloseCallback', \ 'callback': 'vimspector#internal#balloon#CloseCallback',
\ } \ }
" When ambiwidth is single, use prettier characters for the border. This let config = vimspector#internal#popup#SetBorderChars( config )
" would look silly when ambiwidth is double.
if &ambiwidth ==# 'single' && &encoding ==? 'utf-8'
let config[ 'borderchars' ] = [ '─', '│', '─', '│', '╭', '╮', '┛', '╰' ]
endif
if a:is_hover if a:is_hover
let config[ 'filter' ] = 'vimspector#internal#balloon#MouseFilter' let config[ 'filter' ] = 'vimspector#internal#balloon#MouseFilter'

View file

@ -80,6 +80,56 @@ function! vimspector#internal#neopopup#HideSplash( id ) abort
unlet s:db[ a:id ] unlet s:db[ a:id ]
endfunction endfunction
function! vimspector#internal#neopopup#Confirm( confirm_id,
\ text,
\ options,
\ default_value,
\ keys ) abort
" Neovim doesn't have an equivalent of popup_dialog, and it's way too much
" effort to write one, so we just use confirm()...
" Annoyingly we can't use confirm() here because for some reason it doesn't
" render properly in a channel callback. So we use input() and mimic dialog
" behaviour.
let prompt = a:text
for opt in a:options
let prompt .= ' ' . opt
endfor
let prompt .= ': '
try
let result = input( prompt, a:keys[ a:default_value - 1 ] )
catch /.*/
let result = -1
endtry
" Map the results to what the vim popup stuff would return (s:ConfirmCallback
" in popup.vim), i.e.:
" - 1-based index of selected item, or
" - -1 or 0 for cancellation
if result == ''
" User pressed ESC/ctrl-c
let result = -1
else
let index = 1
for k in a:keys
if k ==? result
let result = index
break
endif
let index += 1
endfor
if index > len( a:keys )
let result = -1
endif
endif
py3 __import__( 'vimspector', fromlist = [ 'utils' ] ).utils.ConfirmCallback(
\ int( vim.eval( 'a:confirm_id' ) ),
\ int( vim.eval( 'result' ) ) )
endfunction
" Boilerplate {{{ " Boilerplate {{{
let &cpoptions=s:save_cpo let &cpoptions=s:save_cpo
unlet s:save_cpo unlet s:save_cpo

View file

@ -12,6 +12,7 @@
" WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. " WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
" See the License for the specific language governing permissions and " See the License for the specific language governing permissions and
" limitations under the License. " limitations under the License.
scriptencoding utf-8
" Boilerplate {{{ " Boilerplate {{{
@ -32,6 +33,113 @@ function! vimspector#internal#popup#HideSplash( id ) abort
call popup_hide( a:id ) call popup_hide( a:id )
endfunction endfunction
let s:current_selection = 0
let s:selections = []
let s:text = []
function! s:UpdatePopup( id ) abort
let buf = copy( s:text )
call extend( buf, s:DrawButtons() )
call popup_settext( a:id, buf )
endfunction
function! s:ConfirmKeyFilter( keys, id, key ) abort
if a:key ==# "\<CR>"
call popup_close( a:id, s:current_selection + 1 )
return 1
elseif index( [ "\<Tab>", "\<Right>" ], a:key ) >= 0
let s:current_selection = ( s:current_selection + 1 ) % len( s:selections )
call s:UpdatePopup( a:id )
return 1
elseif index( [ "\<S-Tab>", "\<Left>" ], a:key ) >= 0
let s:current_selection = s:current_selection == 0
\ ? len( s:selections ) - 1: s:current_selection - 1
call s:UpdatePopup( a:id )
return 1
elseif a:key ==# "\<Esc>" || a:key ==# "\<C-c>"
call popup_close( a:id, -1 )
return 1
endif
let index = 1
for key in a:keys
if a:key ==? key
call popup_close( a:id, index )
return 1
endif
let index += 1
endfor
endfunction
function! s:ConfirmCallback( confirm_id, id, result ) abort
py3 __import__( 'vimspector', fromlist = [ 'utils' ] ).utils.ConfirmCallback(
\ int( vim.eval( 'a:confirm_id' ) ),
\ int( vim.eval( 'a:result' ) ) )
endfunction
function! s:SelectionPosition( idx ) abort
return a:idx == 0 ? 0 : len( join( s:selections[ : a:idx - 1 ], ' ' ) ) + 1
endfunction
function! s:DrawButtons() abort
return [ {
\ 'text': join( s:selections, ' ' ),
\ 'props': [
\ {
\ 'col': s:SelectionPosition( s:current_selection ) + 1,
\ 'length': len( s:selections[ s:current_selection ] ),
\ 'type': 'VimspectorSelectedItem'
\ },
\ ]
\ } ]
endfunction
function! vimspector#internal#popup#Confirm(
\ confirm_id,
\ text,
\ options,
\ default_value,
\ keys ) abort
silent! call prop_type_add( 'VimspectorSelectedItem', {
\ 'highlight': 'PMenuSel'
\ } )
let lines = split( a:text, "\n", v:true )
let buf = []
for line in lines
call add( buf, { 'text': line, 'props': [] } )
endfor
call add( buf, { 'text': '', 'props': [] } )
let s:selections = a:options
let s:current_selection = ( a:default_value - 1 )
let s:text = copy( buf )
call extend( buf, s:DrawButtons() )
let config = {
\ 'callback': function( 's:ConfirmCallback', [ a:confirm_id ] ),
\ 'filter': function( 's:ConfirmKeyFilter', [ a:keys ] ),
\ 'mapping': v:false,
\ }
let config = vimspector#internal#popup#SetBorderChars( config )
return popup_dialog( buf, config )
endfunction
function! vimspector#internal#popup#SetBorderChars( config ) abort
" When ambiwidth is single, use prettier characters for the border. This
" would look silly when ambiwidth is double.
if &ambiwidth ==# 'single' && &encoding ==? 'utf-8'
let a:config[ 'borderchars' ] = [ '─', '│', '─', '│', '╭', '╮', '┛', '╰' ]
endif
return a:config
endfunction
" Boilerplate {{{ " Boilerplate {{{
let &cpoptions=s:save_cpo let &cpoptions=s:save_cpo
unlet s:save_cpo unlet s:save_cpo

View file

@ -35,6 +35,8 @@ let s:mappings = get( g:, 'vimspector_enable_mappings', '' )
nnoremap <silent> <Plug>VimspectorContinue nnoremap <silent> <Plug>VimspectorContinue
\ :<c-u>call vimspector#Continue()<CR> \ :<c-u>call vimspector#Continue()<CR>
nnoremap <silent> <Plug>VimspectorLaunch
\ :<c-u>call vimspector#Launch( v:true )<CR>
nnoremap <silent> <Plug>VimspectorStop nnoremap <silent> <Plug>VimspectorStop
\ :<c-u>call vimspector#Stop()<CR> \ :<c-u>call vimspector#Stop()<CR>
nnoremap <silent> <Plug>VimspectorRestart nnoremap <silent> <Plug>VimspectorRestart
@ -84,6 +86,7 @@ if s:mappings ==# 'VISUAL_STUDIO'
nmap <S-F11> <Plug>VimspectorStepOut nmap <S-F11> <Plug>VimspectorStepOut
elseif s:mappings ==# 'HUMAN' elseif s:mappings ==# 'HUMAN'
nmap <F5> <Plug>VimspectorContinue nmap <F5> <Plug>VimspectorContinue
nmap <leader><F5> <Plug>VimspectorLaunch
nmap <F3> <Plug>VimspectorStop nmap <F3> <Plug>VimspectorStop
nmap <F4> <Plug>VimspectorRestart nmap <F4> <Plug>VimspectorRestart
nmap <F6> <Plug>VimspectorPause nmap <F6> <Plug>VimspectorPause

View file

@ -0,0 +1,51 @@
# vimspector - A multi-language debugging system for Vim
# Copyright 2021 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.
from vimspector.debug_session import DebugSession
from vimspector import utils, settings
class JavaDebugAdapter( object ):
def __init__( self, debug_session: DebugSession ):
self.debug_session = debug_session
def OnEvent_hotcodereplace( self, message ):
# Hack for java debug server hot-code-replace
body = message.get( 'body' ) or {}
if body.get( 'type' ) != 'hotcodereplace':
return
if body.get( 'changeType' ) == 'BUILD_COMPLETE':
def handler( result ):
if result == 1:
self.debug_session._connection.DoRequest( None, {
'command': 'redefineClasses',
'arguments': {},
} )
mode = settings.Get( 'java_hotcodereplace_mode' )
if mode == 'ask':
utils.Confirm( self.debug_session._api_prefix,
'Code has changed, hot reload?',
handler,
default_value = 1 )
elif mode == 'always':
self.debug_session._connection.DoRequest( None, {
'command': 'redefineClasses',
'arguments': {},
} )
elif body.get( 'message' ):
utils.UserMessage( 'Hot code replace: ' + body[ 'message' ] )

View file

@ -29,14 +29,14 @@ class PendingRequest( object ):
class DebugAdapterConnection( object ): class DebugAdapterConnection( object ):
def __init__( self, handler, send_func ): def __init__( self, handlers, send_func ):
self._logger = logging.getLogger( __name__ ) self._logger = logging.getLogger( __name__ )
utils.SetUpLogging( self._logger ) utils.SetUpLogging( self._logger )
self._Write = send_func self._Write = send_func
self._SetState( 'READ_HEADER' ) self._SetState( 'READ_HEADER' )
self._buffer = bytes() self._buffer = bytes()
self._handler = handler self._handlers = handlers
self._next_message_id = 0 self._next_message_id = 0
self._outstanding_requests = {} self._outstanding_requests = {}
@ -124,7 +124,7 @@ class DebugAdapterConnection( object ):
def Reset( self ): def Reset( self ):
self._Write = None self._Write = None
self._handler = None self._handlers = None
while self._outstanding_requests: while self._outstanding_requests:
_, request = self._outstanding_requests.popitem() _, request = self._outstanding_requests.popitem()
@ -237,7 +237,7 @@ class DebugAdapterConnection( object ):
def _OnMessageReceived( self, message ): def _OnMessageReceived( self, message ):
if not self._handler: if not self._handlers:
return return
if message[ 'type' ] == 'response': if message[ 'type' ] == 'response':
@ -270,25 +270,21 @@ class DebugAdapterConnection( object ):
self._logger.error( 'Request failed: {0}'.format( reason ) ) self._logger.error( 'Request failed: {0}'.format( reason ) )
if request.failure_handler: if request.failure_handler:
request.failure_handler( reason, message ) request.failure_handler( reason, message )
elif 'OnFailure' in dir( self._handler ):
self._handler.OnFailure( reason, request.msg, message )
else: else:
utils.UserMessage( 'Request failed: {0}'.format( reason ) ) for h in self._handlers:
if 'OnFailure' in dir( h ):
h.OnFailure( reason, request.msg, message )
elif message[ 'type' ] == 'event': elif message[ 'type' ] == 'event':
method = 'OnEvent_' + message[ 'event' ] method = 'OnEvent_' + message[ 'event' ]
if method in dir( self._handler ): for h in self._handlers:
getattr( self._handler, method )( message ) if method in dir( h ):
else: getattr( h, method )( message )
utils.UserMessage( 'Unhandled event: {0}'.format( message[ 'event' ] ),
persist = True )
elif message[ 'type' ] == 'request': elif message[ 'type' ] == 'request':
method = 'OnRequest_' + message[ 'command' ] method = 'OnRequest_' + message[ 'command' ]
if method in dir( self._handler ): for h in self._handlers:
getattr( self._handler, method )( message ) if method in dir( h ):
else: getattr( h, method )( message )
utils.UserMessage(
'Unhandled request: {0}'.format( message[ 'command' ] ),
persist = True )
def _KillTimer( request ): def _KillTimer( request ):

View file

@ -21,6 +21,7 @@ import shlex
import subprocess import subprocess
import functools import functools
import vim import vim
import importlib
from vimspector import ( breakpoints, from vimspector import ( breakpoints,
code, code,
@ -99,7 +100,7 @@ class DebugSession( object ):
return launch_config_file, configurations return launch_config_file, configurations
def Start( self, launch_variables = None ): def Start( self, force_choose=False, launch_variables = None ):
# We mutate launch_variables, so don't mutate the default argument. # We mutate launch_variables, so don't mutate the default argument.
# https://docs.python-guide.org/writing/gotchas/#mutable-default-arguments # https://docs.python-guide.org/writing/gotchas/#mutable-default-arguments
if launch_variables is None: if launch_variables is None:
@ -134,6 +135,11 @@ class DebugSession( object ):
if 'configuration' in launch_variables: if 'configuration' in launch_variables:
configuration_name = launch_variables.pop( 'configuration' ) configuration_name = launch_variables.pop( 'configuration' )
elif force_choose:
# Always display the menu
configuration_name = utils.SelectFromList(
'Which launch configuration?',
sorted( configurations.keys() ) )
elif ( len( configurations ) == 1 and elif ( len( configurations ) == 1 and
next( iter( configurations.values() ) ).get( "autoselect", True ) ): next( iter( configurations.values() ) ).get( "autoselect", True ) ):
configuration_name = next( iter( configurations.keys() ) ) configuration_name = next( iter( configurations.keys() ) )
@ -888,8 +894,21 @@ class DebugSession( object ):
self._splash_screen, self._splash_screen,
"Unable to start adapter" ) "Unable to start adapter" )
else: else:
if 'custom_handler' in self._adapter:
spec = self._adapter[ 'custom_handler' ]
if isinstance( spec, dict ):
module = spec[ 'module' ]
cls = spec[ 'class' ]
else:
module, cls = spec.rsplit( '.', 1 )
CustomHandler = getattr( importlib.import_module( module ), cls )
handlers = [ CustomHandler( self ), self ]
else:
handlers = [ self ]
self._connection = debug_adapter_connection.DebugAdapterConnection( self._connection = debug_adapter_connection.DebugAdapterConnection(
self, handlers,
lambda msg: utils.Call( lambda msg: utils.Call(
"vimspector#internal#{}#Send".format( self._connection_type ), "vimspector#internal#{}#Send".format( self._connection_type ),
msg ) ) msg ) )
@ -897,39 +916,58 @@ class DebugSession( object ):
self._logger.info( 'Debug Adapter Started' ) self._logger.info( 'Debug Adapter Started' )
def _StopDebugAdapter( self, interactive = False, callback = None ): def _StopDebugAdapter( self, interactive = False, callback = None ):
self._splash_screen = utils.DisplaySplash(
self._api_prefix,
self._splash_screen,
"Shutting down debug adapter..." )
def handler( *args ):
self._splash_screen = utils.HideSplash( self._api_prefix,
self._splash_screen )
if callback:
self._logger.debug( "Setting server exit handler before disconnect" )
assert not self._run_on_server_exit
self._run_on_server_exit = callback
vim.eval( 'vimspector#internal#{}#StopDebugSession()'.format(
self._connection_type ) )
arguments = {} arguments = {}
if ( interactive and
self._server_capabilities.get( 'supportTerminateDebuggee' ) ): def disconnect():
if self._stackTraceView.AnyThreadsRunning(): self._splash_screen = utils.DisplaySplash(
choice = utils.AskForInput( "Terminate debuggee [Y/N/default]? ", "" ) self._api_prefix,
if choice == "Y" or choice == "y": self._splash_screen,
"Shutting down debug adapter..." )
def handler( *args ):
self._splash_screen = utils.HideSplash( self._api_prefix,
self._splash_screen )
if callback:
self._logger.debug( "Setting server exit handler before disconnect" )
assert not self._run_on_server_exit
self._run_on_server_exit = callback
vim.eval( 'vimspector#internal#{}#StopDebugSession()'.format(
self._connection_type ) )
self._connection.DoRequest( handler, {
'command': 'disconnect',
'arguments': arguments,
}, failure_handler = handler, timeout = 5000 )
if not interactive:
disconnect()
elif not self._server_capabilities.get( 'supportTerminateDebuggee' ):
disconnect()
elif not self._stackTraceView.AnyThreadsRunning():
disconnect()
else:
def handle_choice( choice ):
if choice == 1:
# yes
arguments[ 'terminateDebuggee' ] = True arguments[ 'terminateDebuggee' ] = True
elif choice == "N" or choice == 'n': elif choice == 2:
# no
arguments[ 'terminateDebuggee' ] = False arguments[ 'terminateDebuggee' ] = False
elif choice <= 0:
# Abort
return
# Else, use server default
self._connection.DoRequest( handler, { disconnect()
'command': 'disconnect',
'arguments': arguments,
}, failure_handler = handler, timeout = 5000 )
# TODO: Use the 'tarminate' request if supportsTerminateRequest set utils.Confirm( self._api_prefix,
"Terminate debuggee?",
handle_choice,
default_value = 3,
options = [ '(Y)es', '(N)o', '(D)efault' ],
keys = [ 'y', 'n', 'd' ] )
def _PrepareAttach( self, adapter_config, launch_config ): def _PrepareAttach( self, adapter_config, launch_config ):

View file

@ -159,7 +159,8 @@ GADGETS = {
"port": "${DAPPort}", "port": "${DAPPort}",
"configuration": { "configuration": {
"cwd": "${workspaceRoot}" "cwd": "${workspaceRoot}"
} },
'custom_handler': 'vimspector.custom.java.JavaDebugAdapter'
} }
}, },
}, },

View file

@ -59,7 +59,10 @@ DEFAULTS = {
'expand_or_jump': [ '<CR>', '<2-LeftMouse>' ], 'expand_or_jump': [ '<CR>', '<2-LeftMouse>' ],
'focus_thread': [ '<leader><CR>' ], 'focus_thread': [ '<leader><CR>' ],
} }
} },
# Custom
'java_hotcodereplace_mode': 'ask',
} }

View file

@ -375,6 +375,44 @@ def AskForInput( prompt, default_value = None, completion = None ):
return None return None
CONFIRM = {}
CONFIRM_ID = 0
def ConfirmCallback( confirm_id, result ):
try:
handler = CONFIRM.pop( confirm_id )
except KeyError:
UserMessage( f"Internal error: unexpected callback id { confirm_id }",
persist = True,
error = True )
return
handler( result )
def Confirm( api_prefix,
prompt,
handler,
default_value = 2,
options: list = None,
keys: list = None ):
if not options:
options = [ '(Y)es', '(N)o' ]
if not keys:
keys = [ 'y', 'n' ]
global CONFIRM_ID
CONFIRM_ID += 1
CONFIRM[ CONFIRM_ID ] = handler
Call( f'vimspector#internal#{ api_prefix }popup#Confirm',
CONFIRM_ID,
prompt,
options,
default_value,
keys )
def AppendToBuffer( buf, line_or_lines, modified=False ): def AppendToBuffer( buf, line_or_lines, modified=False ):
line = 1 line = 1
try: try:
@ -403,8 +441,10 @@ def AppendToBuffer( buf, line_or_lines, modified=False ):
def ClearBuffer( buf ): def ClearBuffer( buf, modified = False ):
buf[ : ] = None buf[ : ] = None
if not modified:
buf.options[ 'modified' ] = False
def SetBufferContents( buf, lines, modified=False ): def SetBufferContents( buf, lines, modified=False ):

View file

@ -4,7 +4,7 @@
<artifactId>TestApplication</artifactId> <artifactId>TestApplication</artifactId>
<version>1</version> <version>1</version>
<properties> <properties>
<maven.compiler.source>8</maven.compiler.source> <maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target> <maven.compiler.target>11</maven.compiler.target>
</properties> </properties>
</project> </project>