From b92a89f0d49611e9476d8825c7d65c700505a387 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Fri, 26 Feb 2021 19:21:40 +0000 Subject: [PATCH 01/10] Add a way to have adapter specific message handlers --- python3/vimspector/custom/java.py | 34 +++++++++++++++++++ .../vimspector/debug_adapter_connection.py | 32 ++++++++--------- python3/vimspector/debug_session.py | 16 ++++++++- python3/vimspector/gadgets.py | 3 +- support/test/java/test_project/pom.xml | 4 +-- 5 files changed, 67 insertions(+), 22 deletions(-) create mode 100644 python3/vimspector/custom/java.py diff --git a/python3/vimspector/custom/java.py b/python3/vimspector/custom/java.py new file mode 100644 index 0000000..ebfec35 --- /dev/null +++ b/python3/vimspector/custom/java.py @@ -0,0 +1,34 @@ +# 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 + + +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 + if utils.Call( 'confirm', + 'Code has changed, hot reload?', + '&Yes,&No', + 1, + 'Question' ) == 1: + self.debug_session._connection.DoRequest( None, { + 'command': 'redefineClasses', + 'arguments': {}, + } ) diff --git a/python3/vimspector/debug_adapter_connection.py b/python3/vimspector/debug_adapter_connection.py index 283c40c..df2ef13 100644 --- a/python3/vimspector/debug_adapter_connection.py +++ b/python3/vimspector/debug_adapter_connection.py @@ -29,14 +29,14 @@ class PendingRequest( object ): class DebugAdapterConnection( object ): - def __init__( self, handler, send_func ): + def __init__( self, handlers, send_func ): self._logger = logging.getLogger( __name__ ) utils.SetUpLogging( self._logger ) self._Write = send_func self._SetState( 'READ_HEADER' ) self._buffer = bytes() - self._handler = handler + self._handlers = handlers self._next_message_id = 0 self._outstanding_requests = {} @@ -124,7 +124,7 @@ class DebugAdapterConnection( object ): def Reset( self ): self._Write = None - self._handler = None + self._handlers = None while self._outstanding_requests: _, request = self._outstanding_requests.popitem() @@ -237,7 +237,7 @@ class DebugAdapterConnection( object ): def _OnMessageReceived( self, message ): - if not self._handler: + if not self._handlers: return if message[ 'type' ] == 'response': @@ -270,25 +270,21 @@ class DebugAdapterConnection( object ): self._logger.error( 'Request failed: {0}'.format( reason ) ) if request.failure_handler: request.failure_handler( reason, message ) - elif 'OnFailure' in dir( self._handler ): - self._handler.OnFailure( reason, request.msg, message ) 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': method = 'OnEvent_' + message[ 'event' ] - if method in dir( self._handler ): - getattr( self._handler, method )( message ) - else: - utils.UserMessage( 'Unhandled event: {0}'.format( message[ 'event' ] ), - persist = True ) + for h in self._handlers: + if method in dir( h ): + getattr( h, method )( message ) elif message[ 'type' ] == 'request': method = 'OnRequest_' + message[ 'command' ] - if method in dir( self._handler ): - getattr( self._handler, method )( message ) - else: - utils.UserMessage( - 'Unhandled request: {0}'.format( message[ 'command' ] ), - persist = True ) + for h in self._handlers: + if method in dir( h ): + getattr( h, method )( message ) def _KillTimer( request ): diff --git a/python3/vimspector/debug_session.py b/python3/vimspector/debug_session.py index 4d8c816..31b4342 100644 --- a/python3/vimspector/debug_session.py +++ b/python3/vimspector/debug_session.py @@ -21,6 +21,7 @@ import shlex import subprocess import functools import vim +import importlib from vimspector import ( breakpoints, code, @@ -888,8 +889,21 @@ class DebugSession( object ): self._splash_screen, "Unable to start adapter" ) 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, + handlers, lambda msg: utils.Call( "vimspector#internal#{}#Send".format( self._connection_type ), msg ) ) diff --git a/python3/vimspector/gadgets.py b/python3/vimspector/gadgets.py index d1b6872..5c61074 100644 --- a/python3/vimspector/gadgets.py +++ b/python3/vimspector/gadgets.py @@ -159,7 +159,8 @@ GADGETS = { "port": "${DAPPort}", "configuration": { "cwd": "${workspaceRoot}" - } + }, + 'custom_handler': 'vimspector.custom.java.JavaDebugAdapter' } }, }, diff --git a/support/test/java/test_project/pom.xml b/support/test/java/test_project/pom.xml index 890e7e8..e6dc4d3 100644 --- a/support/test/java/test_project/pom.xml +++ b/support/test/java/test_project/pom.xml @@ -4,7 +4,7 @@ TestApplication 1 - 8 - 8 + 11 + 11 From afb912dd089a6cfbbadfad2a55531ed00226889a Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Tue, 2 Mar 2021 10:48:55 +0000 Subject: [PATCH 02/10] Print hotcodereplace messages --- python3/vimspector/custom/java.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/python3/vimspector/custom/java.py b/python3/vimspector/custom/java.py index ebfec35..0dfa89c 100644 --- a/python3/vimspector/custom/java.py +++ b/python3/vimspector/custom/java.py @@ -23,12 +23,18 @@ class JavaDebugAdapter( object ): def OnEvent_hotcodereplace( self, message ): # Hack for java debug server hot-code-replace - if utils.Call( 'confirm', - 'Code has changed, hot reload?', - '&Yes,&No', - 1, - 'Question' ) == 1: - self.debug_session._connection.DoRequest( None, { - 'command': 'redefineClasses', - 'arguments': {}, - } ) + body = message.get( 'body' ) or {} + + if body.get( 'type' ) != 'hotcodereplace': + return + + if body.get( 'changeType' ) == 'BUILD_COMPLETE': + if utils.AskForInput( 'Code has changed, hot reload? [Y/N] ', + 'Y' ).upper()[ 0 ] == 'Y': + self.debug_session._connection.DoRequest( None, { + 'command': 'redefineClasses', + 'arguments': {}, + } ) + elif body.get( 'message' ): + utils.UserMessage( 'Hot code replace: ' + body[ 'message' ] ) + From 63fd3165fb48fa0c5c8dd916aa3c3ae54878d304 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Thu, 4 Mar 2021 21:44:54 +0000 Subject: [PATCH 03/10] Use popup for confirmations (note these have to be async) --- autoload/vimspector/internal/neopopup.vim | 22 ++++++++ autoload/vimspector/internal/popup.vim | 31 +++++++++++ python3/vimspector/custom/java.py | 17 +++--- python3/vimspector/debug_session.py | 63 ++++++++++++++--------- python3/vimspector/utils.py | 29 ++++++++++- 5 files changed, 129 insertions(+), 33 deletions(-) diff --git a/autoload/vimspector/internal/neopopup.vim b/autoload/vimspector/internal/neopopup.vim index fe5fe05..cc25020 100644 --- a/autoload/vimspector/internal/neopopup.vim +++ b/autoload/vimspector/internal/neopopup.vim @@ -80,6 +80,28 @@ function! vimspector#internal#neopopup#HideSplash( id ) abort unlet s:db[ a:id ] endfunction +function! vimspector#internal#neopopup#Confirm( confirm_id, + \ text, + \ default_value ) abort + let result = confirm( a:text, '&Yes &No &Default', 3 ) + + " Map the results to what popup_menu_filter would return (ok s:ConfirmCallback + " in popup.vim) + if result == 2 + " No is represented as 0 + let result = 0 + elseif result == 0 + " User pressed ESC/ctrl-c + let result = -1 + elseif result == 3 + " Default + let result = a:default_value + endif + + py3 __import__( 'vimspector', fromlist = [ 'utils' ] ).utils.ConfirmCallback( + \ int( vim.eval( 'a:confirm_id' ) ), + \ int( vim.eval( 'result' ) ) ) +endfunction " Boilerplate {{{ let &cpoptions=s:save_cpo unlet s:save_cpo diff --git a/autoload/vimspector/internal/popup.vim b/autoload/vimspector/internal/popup.vim index fc8820b..e8658ab 100644 --- a/autoload/vimspector/internal/popup.vim +++ b/autoload/vimspector/internal/popup.vim @@ -32,6 +32,37 @@ function! vimspector#internal#popup#HideSplash( id ) abort call popup_hide( a:id ) endfunction +function! s:YesNoDefaultFilter( default_value, id, key ) abort + if a:key ==# "\" || a:key ==# 'D' || a:key ==# 'd' + call popup_close( a:id, a:default_value ) + endif + + return popup_filter_yesno( a:id, a:key ) +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! vimspector#internal#popup#Confirm( + \ confirm_id, + \ text, + \ default_value ) abort + let text = a:text + if type( a:text ) != v:t_list + let text = [ a:text ] + endif + + call extend( text, [ '', '(Y)es (N)o (D)efault' ] ) + + return popup_dialog( text, { + \ 'callback': function( 's:ConfirmCallback', [ a:confirm_id ] ), + \ 'filter': function( 's:YesNoDefaultFilter', [ a:default_value ] ) + \ } ) +endfunction + " Boilerplate {{{ let &cpoptions=s:save_cpo unlet s:save_cpo diff --git a/python3/vimspector/custom/java.py b/python3/vimspector/custom/java.py index 0dfa89c..83705c1 100644 --- a/python3/vimspector/custom/java.py +++ b/python3/vimspector/custom/java.py @@ -29,12 +29,15 @@ class JavaDebugAdapter( object ): return if body.get( 'changeType' ) == 'BUILD_COMPLETE': - if utils.AskForInput( 'Code has changed, hot reload? [Y/N] ', - 'Y' ).upper()[ 0 ] == 'Y': - self.debug_session._connection.DoRequest( None, { - 'command': 'redefineClasses', - 'arguments': {}, - } ) + def handler( result ): + if result == 1: + self.debug_session._connection.DoRequest( None, { + 'command': 'redefineClasses', + 'arguments': {}, + } ) + + utils.Confirm( self.debug_session._api_prefix, + 'Code has changed, hot reload?', + handler ) elif body.get( 'message' ): utils.UserMessage( 'Hot code replace: ' + body[ 'message' ] ) - diff --git a/python3/vimspector/debug_session.py b/python3/vimspector/debug_session.py index 31b4342..ae16706 100644 --- a/python3/vimspector/debug_session.py +++ b/python3/vimspector/debug_session.py @@ -911,39 +911,52 @@ class DebugSession( object ): self._logger.info( 'Debug Adapter Started' ) def _StopDebugAdapter( self, interactive = False, callback = None ): - self._splash_screen = utils.DisplaySplash( - self._api_prefix, - self._splash_screen, - "Shutting down debug adapter..." ) + def disconnect( arguments = {} ): + 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 ) + 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 + 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 ) ) + vim.eval( 'vimspector#internal#{}#StopDebugSession()'.format( + self._connection_type ) ) - arguments = {} - if ( interactive and - self._server_capabilities.get( 'supportTerminateDebuggee' ) ): - if self._stackTraceView.AnyThreadsRunning(): - choice = utils.AskForInput( "Terminate debuggee [Y/N/default]? ", "" ) - if choice == "Y" or choice == "y": + self._connection.DoRequest( handler, { + 'command': 'disconnect', + '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 ): + arguments = {} + if choice == 1: arguments[ 'terminateDebuggee' ] = True - elif choice == "N" or choice == 'n': + elif choice == 0: arguments[ 'terminateDebuggee' ] = False + elif choice == -1: + # Abort + return - self._connection.DoRequest( handler, { - 'command': 'disconnect', - 'arguments': arguments, - }, failure_handler = handler, timeout = 5000 ) + disconnect( arguments ) - # TODO: Use the 'tarminate' request if supportsTerminateRequest set + utils.Confirm( self._api_prefix, + "Terminate debuggee?", + handle_choice, + default_value = 3 ) def _PrepareAttach( self, adapter_config, launch_config ): diff --git a/python3/vimspector/utils.py b/python3/vimspector/utils.py index d8dd345..c65a184 100644 --- a/python3/vimspector/utils.py +++ b/python3/vimspector/utils.py @@ -375,6 +375,31 @@ def AskForInput( prompt, default_value = None, completion = 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 = 1 ): + global CONFIRM_ID + CONFIRM_ID += 1 + CONFIRM[ CONFIRM_ID ] = handler + Call( f'vimspector#internal#{ api_prefix }popup#Confirm', + CONFIRM_ID, + prompt, + default_value ) + + def AppendToBuffer( buf, line_or_lines, modified=False ): line = 1 try: @@ -403,8 +428,10 @@ def AppendToBuffer( buf, line_or_lines, modified=False ): -def ClearBuffer( buf ): +def ClearBuffer( buf, modified = False ): buf[ : ] = None + if not modified: + buf.options[ 'modified' ] = False def SetBufferContents( buf, lines, modified=False ): From 85bb8594ab916358a5605c259f89b036a119d6b9 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Fri, 5 Mar 2021 21:09:39 +0000 Subject: [PATCH 04/10] Allow forcing selection from the menu with F5 --- autoload/vimspector.vim | 4 ++-- plugin/vimspector.vim | 3 +++ python3/vimspector/debug_session.py | 7 ++++++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/autoload/vimspector.vim b/autoload/vimspector.vim index a547e5c..a0b6c42 100644 --- a/autoload/vimspector.vim +++ b/autoload/vimspector.vim @@ -41,11 +41,11 @@ function! s:Enabled() abort return s:enabled endfunction -function! vimspector#Launch() abort +function! vimspector#Launch( ... ) abort if !s:Enabled() return endif - py3 _vimspector_session.Start() + py3 _vimspector_session.Start( *vim.eval( 'a:000' ) ) endfunction function! vimspector#LaunchWithSettings( settings ) abort diff --git a/plugin/vimspector.vim b/plugin/vimspector.vim index 37509a7..75e2baa 100644 --- a/plugin/vimspector.vim +++ b/plugin/vimspector.vim @@ -35,6 +35,8 @@ let s:mappings = get( g:, 'vimspector_enable_mappings', '' ) nnoremap VimspectorContinue \ :call vimspector#Continue() +nnoremap VimspectorLaunch + \ :call vimspector#Launch( v:true ) nnoremap VimspectorStop \ :call vimspector#Stop() nnoremap VimspectorRestart @@ -84,6 +86,7 @@ if s:mappings ==# 'VISUAL_STUDIO' nmap VimspectorStepOut elseif s:mappings ==# 'HUMAN' nmap VimspectorContinue + nmap VimspectorLaunch nmap VimspectorStop nmap VimspectorRestart nmap VimspectorPause diff --git a/python3/vimspector/debug_session.py b/python3/vimspector/debug_session.py index ae16706..12e4563 100644 --- a/python3/vimspector/debug_session.py +++ b/python3/vimspector/debug_session.py @@ -100,7 +100,7 @@ class DebugSession( object ): 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. # https://docs.python-guide.org/writing/gotchas/#mutable-default-arguments if launch_variables is None: @@ -135,6 +135,11 @@ class DebugSession( object ): if 'configuration' in launch_variables: 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 next( iter( configurations.values() ) ).get( "autoselect", True ) ): configuration_name = next( iter( configurations.keys() ) ) From 6b74e584d53b284a35c72f7ab5e8f1f2095fe7ea Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Sat, 6 Mar 2021 20:24:24 +0000 Subject: [PATCH 05/10] Create a messagebox-like interface in vim --- autoload/vimspector/internal/balloon.vim | 6 +- autoload/vimspector/internal/neopopup.vim | 3 + autoload/vimspector/internal/popup.vim | 88 ++++++++++++++++++++--- python3/vimspector/utils.py | 6 +- 4 files changed, 87 insertions(+), 16 deletions(-) diff --git a/autoload/vimspector/internal/balloon.vim b/autoload/vimspector/internal/balloon.vim index f7ed9d7..b607e51 100644 --- a/autoload/vimspector/internal/balloon.vim +++ b/autoload/vimspector/internal/balloon.vim @@ -71,11 +71,7 @@ function! vimspector#internal#balloon#CreateTooltip( is_hover, ... ) abort \ 'callback': 'vimspector#internal#balloon#CloseCallback', \ } - " 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 config[ 'borderchars' ] = [ '─', '│', '─', '│', '╭', '╮', '┛', '╰' ] - endif + let config = vimspector#internal#popup#SetBorderChars( config ) if a:is_hover let config[ 'filter' ] = 'vimspector#internal#balloon#MouseFilter' diff --git a/autoload/vimspector/internal/neopopup.vim b/autoload/vimspector/internal/neopopup.vim index cc25020..189c78c 100644 --- a/autoload/vimspector/internal/neopopup.vim +++ b/autoload/vimspector/internal/neopopup.vim @@ -83,6 +83,9 @@ endfunction function! vimspector#internal#neopopup#Confirm( confirm_id, \ text, \ default_value ) 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(). let result = confirm( a:text, '&Yes &No &Default', 3 ) " Map the results to what popup_menu_filter would return (ok s:ConfirmCallback diff --git a/autoload/vimspector/internal/popup.vim b/autoload/vimspector/internal/popup.vim index e8658ab..5978987 100644 --- a/autoload/vimspector/internal/popup.vim +++ b/autoload/vimspector/internal/popup.vim @@ -32,9 +32,32 @@ function! vimspector#internal#popup#HideSplash( id ) abort call popup_hide( a:id ) endfunction +let s:current_selection = 0 +let s:selections = [] +let s:text = [] + +function! s:UpdatePopup( id ) + let buf = copy( s:text ) + call extend( buf, s:DrawButtons() ) + call popup_settext( a:id, buf ) +endfunction + function! s:YesNoDefaultFilter( default_value, id, key ) abort - if a:key ==# "\" || a:key ==# 'D' || a:key ==# 'd' + if a:key ==# "\" + call popup_close( a:id, s:current_selection + 1 ) + return 1 + elseif a:key ==# 'D' || a:key ==# 'd' call popup_close( a:id, a:default_value ) + return 1 + elseif index( [ "\", "\" ], a:key ) >= 0 + let s:current_selection = ( s:current_selection + 1 ) % len( s:selections ) + call s:UpdatePopup( a:id ) + return 1 + elseif index( [ "\", "\" ], 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 endif return popup_filter_yesno( a:id, a:key ) @@ -46,23 +69,68 @@ function! s:ConfirmCallback( confirm_id, id, result ) abort \ 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 ) abort - let text = a:text - if type( a:text ) != v:t_list - let text = [ a:text ] + + 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:YesNoDefaultFilter', [ a:default_value ] ), + \ 'mapping': v:false, + \ } + let config = vimspector#internal#popup#SetBorderChars( config ) + + return popup_dialog( buf, config ) +endfunction + +function! vimspector#internal#popup#SetBorderChars( config ) + " 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 - call extend( text, [ '', '(Y)es (N)o (D)efault' ] ) - - return popup_dialog( text, { - \ 'callback': function( 's:ConfirmCallback', [ a:confirm_id ] ), - \ 'filter': function( 's:YesNoDefaultFilter', [ a:default_value ] ) - \ } ) + return a:config endfunction + " Boilerplate {{{ let &cpoptions=s:save_cpo unlet s:save_cpo diff --git a/python3/vimspector/utils.py b/python3/vimspector/utils.py index c65a184..ca0b80a 100644 --- a/python3/vimspector/utils.py +++ b/python3/vimspector/utils.py @@ -390,13 +390,17 @@ def ConfirmCallback( confirm_id, result ): handler( result ) -def Confirm( api_prefix, prompt, handler, default_value = 1 ): +def Confirm( api_prefix, prompt, handler, default_value = 3, options = None ): global CONFIRM_ID + if not options: + options = [ '(Y)es', '(N)o', '(D)efault' ] + CONFIRM_ID += 1 CONFIRM[ CONFIRM_ID ] = handler Call( f'vimspector#internal#{ api_prefix }popup#Confirm', CONFIRM_ID, prompt, + options, default_value ) From 154e727b9693144439b37735b9e4f0aa44d58c16 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Thu, 11 Mar 2021 22:38:01 +0000 Subject: [PATCH 06/10] Make confirm dialog take arbitrary keys Confirm now takes the list of options, list of keys to select them and the default value. Returned values are always a 1-based index into the list (like SelectFromList) or -1 to mean esc/ctrl-c. This uses a nice popup dialog in vim and a crappy input on neovim. --- autoload/vimspector/internal/neopopup.vim | 41 ++++++++++++++++++----- autoload/vimspector/internal/popup.vim | 22 ++++++++---- python3/vimspector/custom/java.py | 16 ++++++--- python3/vimspector/debug_session.py | 10 ++++-- python3/vimspector/settings.py | 5 ++- python3/vimspector/utils.py | 16 ++++++--- 6 files changed, 82 insertions(+), 28 deletions(-) diff --git a/autoload/vimspector/internal/neopopup.vim b/autoload/vimspector/internal/neopopup.vim index 189c78c..9f35914 100644 --- a/autoload/vimspector/internal/neopopup.vim +++ b/autoload/vimspector/internal/neopopup.vim @@ -82,29 +82,52 @@ endfunction function! vimspector#internal#neopopup#Confirm( confirm_id, \ text, - \ default_value ) abort + \ 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(). - let result = confirm( a:text, '&Yes &No &Default', 3 ) + let prompt = a:text + for opt in a:options + let prompt .= ' ' . opt + endfor + let prompt .= ': ' + + " 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. + try + let result = input( prompt, a:keys[ a:default_value - 1 ] ) + catch /.*/ + let result = -1 + endtry " Map the results to what popup_menu_filter would return (ok s:ConfirmCallback " in popup.vim) - if result == 2 - " No is represented as 0 - let result = 0 - elseif result == 0 + if result == '' " User pressed ESC/ctrl-c let result = -1 - elseif result == 3 - " Default - let result = a:default_value + else + let index = 1 + for k in a:keys + if k ==? result + let result = index + break + endif + let index += 2 + 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 {{{ let &cpoptions=s:save_cpo unlet s:save_cpo diff --git a/autoload/vimspector/internal/popup.vim b/autoload/vimspector/internal/popup.vim index 5978987..db2c2fd 100644 --- a/autoload/vimspector/internal/popup.vim +++ b/autoload/vimspector/internal/popup.vim @@ -42,13 +42,10 @@ function! s:UpdatePopup( id ) call popup_settext( a:id, buf ) endfunction -function! s:YesNoDefaultFilter( default_value, id, key ) abort +function! s:ConfirmKeyFilter( keys, id, key ) abort if a:key ==# "\" call popup_close( a:id, s:current_selection + 1 ) return 1 - elseif a:key ==# 'D' || a:key ==# 'd' - call popup_close( a:id, a:default_value ) - return 1 elseif index( [ "\", "\" ], a:key ) >= 0 let s:current_selection = ( s:current_selection + 1 ) % len( s:selections ) call s:UpdatePopup( a:id ) @@ -58,9 +55,19 @@ function! s:YesNoDefaultFilter( default_value, id, key ) abort \ ? len( s:selections ) - 1: s:current_selection - 1 call s:UpdatePopup( a:id ) return 1 + elseif a:key ==# "\" || a:key ==# "\" + call popup_close( a:id, -1 ) + return 1 endif - return popup_filter_yesno( a:id, a:key ) + 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 @@ -90,7 +97,8 @@ function! vimspector#internal#popup#Confirm( \ confirm_id, \ text, \ options, - \ default_value ) abort + \ default_value, + \ keys ) abort silent! call prop_type_add( 'VimspectorSelectedItem', { \ 'highlight': 'PMenuSel' @@ -112,7 +120,7 @@ function! vimspector#internal#popup#Confirm( let config = { \ 'callback': function( 's:ConfirmCallback', [ a:confirm_id ] ), - \ 'filter': function( 's:YesNoDefaultFilter', [ a:default_value ] ), + \ 'filter': function( 's:ConfirmKeyFilter', [ a:keys ] ), \ 'mapping': v:false, \ } let config = vimspector#internal#popup#SetBorderChars( config ) diff --git a/python3/vimspector/custom/java.py b/python3/vimspector/custom/java.py index 83705c1..c2a2264 100644 --- a/python3/vimspector/custom/java.py +++ b/python3/vimspector/custom/java.py @@ -14,7 +14,7 @@ # limitations under the License. from vimspector.debug_session import DebugSession -from vimspector import utils +from vimspector import utils, settings class JavaDebugAdapter( object ): @@ -36,8 +36,16 @@ class JavaDebugAdapter( object ): 'arguments': {}, } ) - utils.Confirm( self.debug_session._api_prefix, - 'Code has changed, hot reload?', - handler ) + 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' ] ) diff --git a/python3/vimspector/debug_session.py b/python3/vimspector/debug_session.py index 12e4563..255e072 100644 --- a/python3/vimspector/debug_session.py +++ b/python3/vimspector/debug_session.py @@ -949,10 +949,12 @@ class DebugSession( object ): def handle_choice( choice ): arguments = {} if choice == 1: + # yes arguments[ 'terminateDebuggee' ] = True - elif choice == 0: + elif choice == 2: + # no arguments[ 'terminateDebuggee' ] = False - elif choice == -1: + else: # Abort return @@ -961,7 +963,9 @@ class DebugSession( object ): utils.Confirm( self._api_prefix, "Terminate debuggee?", handle_choice, - default_value = 3 ) + default_value = 3, + options = [ '(Y)es', '(N)o', '(D)efault' ], + keys = [ 'y', 'n', 'd' ] ) def _PrepareAttach( self, adapter_config, launch_config ): diff --git a/python3/vimspector/settings.py b/python3/vimspector/settings.py index e9e76ea..a060543 100644 --- a/python3/vimspector/settings.py +++ b/python3/vimspector/settings.py @@ -59,7 +59,10 @@ DEFAULTS = { 'expand_or_jump': [ '', '<2-LeftMouse>' ], 'focus_thread': [ '' ], } - } + }, + + # Custom + 'java_hotcodereplace_mode': 'ask', } diff --git a/python3/vimspector/utils.py b/python3/vimspector/utils.py index ca0b80a..53ebd3b 100644 --- a/python3/vimspector/utils.py +++ b/python3/vimspector/utils.py @@ -390,18 +390,26 @@ def ConfirmCallback( confirm_id, result ): handler( result ) -def Confirm( api_prefix, prompt, handler, default_value = 3, options = None ): - global CONFIRM_ID +def Confirm( api_prefix, + prompt, + handler, + default_value = 2, + options: list = None, + keys: list = None ): if not options: - options = [ '(Y)es', '(N)o', '(D)efault' ] + 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 ) + default_value, + keys ) def AppendToBuffer( buf, line_or_lines, modified=False ): From efc5c7686638efc4c047bf0f58ae58fbff0bd393 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Thu, 11 Mar 2021 22:45:59 +0000 Subject: [PATCH 07/10] Fix lints --- autoload/vimspector/internal/popup.vim | 5 +++-- python3/vimspector/utils.py | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/autoload/vimspector/internal/popup.vim b/autoload/vimspector/internal/popup.vim index db2c2fd..083fdf5 100644 --- a/autoload/vimspector/internal/popup.vim +++ b/autoload/vimspector/internal/popup.vim @@ -12,6 +12,7 @@ " 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. +scriptencoding utf-8 " Boilerplate {{{ @@ -36,7 +37,7 @@ let s:current_selection = 0 let s:selections = [] let s:text = [] -function! s:UpdatePopup( id ) +function! s:UpdatePopup( id ) abort let buf = copy( s:text ) call extend( buf, s:DrawButtons() ) call popup_settext( a:id, buf ) @@ -128,7 +129,7 @@ function! vimspector#internal#popup#Confirm( return popup_dialog( buf, config ) endfunction -function! vimspector#internal#popup#SetBorderChars( config ) +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' diff --git a/python3/vimspector/utils.py b/python3/vimspector/utils.py index 53ebd3b..5f836fc 100644 --- a/python3/vimspector/utils.py +++ b/python3/vimspector/utils.py @@ -378,6 +378,7 @@ def AskForInput( prompt, default_value = None, completion = None ): CONFIRM = {} CONFIRM_ID = 0 + def ConfirmCallback( confirm_id, result ): try: handler = CONFIRM.pop( confirm_id ) From c05335c79987dc3ab19f30c477ad1b435e1ab9bf Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Tue, 16 Mar 2021 19:15:56 +0000 Subject: [PATCH 08/10] Make default option work for terminate debugee --- python3/vimspector/debug_session.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/python3/vimspector/debug_session.py b/python3/vimspector/debug_session.py index 255e072..156cbfd 100644 --- a/python3/vimspector/debug_session.py +++ b/python3/vimspector/debug_session.py @@ -954,9 +954,10 @@ class DebugSession( object ): elif choice == 2: # no arguments[ 'terminateDebuggee' ] = False - else: + elif choice <= 0: # Abort return + # Else, use server default disconnect( arguments ) From a39017dd06869bdb2b38b30ec21f41cb684a8e8b Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Fri, 19 Mar 2021 18:23:48 +0000 Subject: [PATCH 09/10] Fix actually sending terminateDebugee, and fix neovim 1-based index reporting --- autoload/vimspector/internal/neopopup.vim | 10 ++++++---- python3/vimspector/debug_session.py | 9 +++++---- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/autoload/vimspector/internal/neopopup.vim b/autoload/vimspector/internal/neopopup.vim index 9f35914..eb5a0dc 100644 --- a/autoload/vimspector/internal/neopopup.vim +++ b/autoload/vimspector/internal/neopopup.vim @@ -103,8 +103,10 @@ function! vimspector#internal#neopopup#Confirm( confirm_id, let result = -1 endtry - " Map the results to what popup_menu_filter would return (ok s:ConfirmCallback - " in popup.vim) + " 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 @@ -115,10 +117,10 @@ function! vimspector#internal#neopopup#Confirm( confirm_id, let result = index break endif - let index += 2 + let index += 1 endfor - if index >= len( a:keys ) + if index > len( a:keys ) let result = -1 endif endif diff --git a/python3/vimspector/debug_session.py b/python3/vimspector/debug_session.py index 156cbfd..2f132f9 100644 --- a/python3/vimspector/debug_session.py +++ b/python3/vimspector/debug_session.py @@ -916,7 +916,9 @@ class DebugSession( object ): self._logger.info( 'Debug Adapter Started' ) def _StopDebugAdapter( self, interactive = False, callback = None ): - def disconnect( arguments = {} ): + arguments = {} + + def disconnect(): self._splash_screen = utils.DisplaySplash( self._api_prefix, self._splash_screen, @@ -936,7 +938,7 @@ class DebugSession( object ): self._connection.DoRequest( handler, { 'command': 'disconnect', - 'arguments': {}, + 'arguments': arguments, }, failure_handler = handler, timeout = 5000 ) if not interactive: @@ -947,7 +949,6 @@ class DebugSession( object ): disconnect() else: def handle_choice( choice ): - arguments = {} if choice == 1: # yes arguments[ 'terminateDebuggee' ] = True @@ -959,7 +960,7 @@ class DebugSession( object ): return # Else, use server default - disconnect( arguments ) + disconnect() utils.Confirm( self._api_prefix, "Terminate debuggee?", From 1414f261a1700515eb67c429aa681cbd84eca4e5 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Fri, 19 Mar 2021 22:59:27 +0000 Subject: [PATCH 10/10] Documentation for hot code replace --- README.md | 18 +++++++++++++++++- autoload/vimspector/internal/neopopup.vim | 8 ++++---- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 2152a9c..952d07f 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,7 @@ For detailed explanatin of the `.vimspector.json` format, see the * [Console autocompletion](#console-autocompletion) * [Log View](#log-view) * [Closing debugger](#closing-debugger) + * [Terminate debuggee](#terminate-debuggee) * [Debug profile configuration](#debug-profile-configuration) * [C, C , Rust, etc.](#c-c-rust-etc) * [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) * [JavaScript, TypeScript, etc.](#javascript-typescript-etc) * [Java](#java) + * [Hot code replace](#hot-code-replace) * [Usage with YouCompleteMe](#usage-with-youcompleteme) * [Other LSP clients](#other-lsp-clients) * [Lua](#lua) @@ -91,7 +93,7 @@ For detailed explanatin of the `.vimspector.json` format, see the * [Example](#example) * [FAQ](#faq) - + @@ -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 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 * Set up [YCM for java][YcmJava]. diff --git a/autoload/vimspector/internal/neopopup.vim b/autoload/vimspector/internal/neopopup.vim index eb5a0dc..a734ca3 100644 --- a/autoload/vimspector/internal/neopopup.vim +++ b/autoload/vimspector/internal/neopopup.vim @@ -87,16 +87,16 @@ function! vimspector#internal#neopopup#Confirm( confirm_id, \ 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(). + " 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 .= ': ' - " 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. try let result = input( prompt, a:keys[ a:default_value - 1 ] ) catch /.*/