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.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/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 fe5fe05..a734ca3 100644 --- a/autoload/vimspector/internal/neopopup.vim +++ b/autoload/vimspector/internal/neopopup.vim @@ -80,6 +80,56 @@ function! vimspector#internal#neopopup#HideSplash( id ) abort unlet s:db[ a:id ] 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 {{{ 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..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 {{{ @@ -32,6 +33,113 @@ 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 ) 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 ==# "\" + call popup_close( a:id, s:current_selection + 1 ) + 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 + elseif a:key ==# "\" || a:key ==# "\" + 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 {{{ let &cpoptions=s:save_cpo unlet s:save_cpo 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/custom/java.py b/python3/vimspector/custom/java.py new file mode 100644 index 0000000..c2a2264 --- /dev/null +++ b/python3/vimspector/custom/java.py @@ -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' ] ) 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..2f132f9 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, @@ -99,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: @@ -134,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() ) ) @@ -888,8 +894,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 ) ) @@ -897,39 +916,58 @@ 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 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 = {} - 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": + + def disconnect(): + 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 ) ) + + 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 - elif choice == "N" or choice == 'n': + elif choice == 2: + # no arguments[ 'terminateDebuggee' ] = False + elif choice <= 0: + # Abort + return + # Else, use server default - self._connection.DoRequest( handler, { - 'command': 'disconnect', - 'arguments': arguments, - }, failure_handler = handler, timeout = 5000 ) + disconnect() - # 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 ): diff --git a/python3/vimspector/gadgets.py b/python3/vimspector/gadgets.py index 6974367..528f60c 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/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 d8dd345..5f836fc 100644 --- a/python3/vimspector/utils.py +++ b/python3/vimspector/utils.py @@ -375,6 +375,44 @@ 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 = 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 ): line = 1 try: @@ -403,8 +441,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 ): 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