diff --git a/README.md b/README.md index f336d23d2..e66bb297c 100644 --- a/README.md +++ b/README.md @@ -250,52 +250,20 @@ The following sections expand on the above brief overview. Vimspector requires: * One of: - * Vim 8.2 Huge build compiled with Python 3.6 or later - * Neovim 0.4.3 with Python 3.6 or later (experimental) + * Vim 8.2 Huge build compiled with Python 3.10 or later + * Neovim 0.8 with Python 3.10 or later (experimental) * One of the following operating systems: * Linux * macOS Mojave or later * Windows (experimental) -Why such a new vim? Well 2 reasons: +Which Linux versions? I only test on Ubuntu 20.04 and later and RHEL 7. -1. Because vimspector uses a lot of new Vim features -2. Because there are Vim bugs that vimspector triggers that will frustrate you - if you hit them. +### Neovim limitations -Why is neovim experimental? Because the author doesn't use neovim regularly, and -there are no regression tests for vimspector in neovim, so it may break -occasionally. Issue reports are handled on best-efforts basis, and PRs are -welcome to fix bugs. See also the next section describing differences for neovim -vs vim. - -Why is Windows support experimental? Because it's effort and it's not a priority -for the author. PRs are welcome to fix bugs. Windows will not be regularly -tested. - -Which Linux versions? I only test on Ubuntu 18.04 and later and RHEL 7. - -## Neovim differences - -neovim doesn't implement some features Vimspector relies on: - -* WinBar - used for the buttons at the top of the code window and for changing - the output window's current output. -* Prompt Buffers - used to send commands in the Console and add Watches. - (*Note*: prompt buffers are available in neovim nightly) -* Balloons - this allows for the variable evaluation popup to be displayed when - hovering the mouse. See below for how to create a keyboard mapping instead. - -Workarounds are in place as follows: - -* WinBar - There are [mappings](#mappings), - [`:VimspectorShowOutput`](#program-output) and - [`:VimspectorReset`](#closing-debugger) -* Prompt Buffers - There are [`:VimspectorEval`](#console) - and [`:VimspectorWatch`](#watches) -* Balloons - There is the `VimspectorBalloonEval` mapping. There is no -default mapping for this, so I recommend something like this to get variable -display in a popup: +neovim doesn't implement mouse hover balloons. Instead there is the +`VimspectorBalloonEval` mapping. There is no default mapping for this, so +I recommend something like this to get variable display in a popup: ```viml " mnemonic 'di' = 'debug inspect' (pick your own, if you prefer!) @@ -306,7 +274,7 @@ nmap di VimspectorBalloonEval xmap di VimspectorBalloonEval ``` -## Windows differences +### Windows differences The following features are not implemented for Windows: @@ -2455,6 +2423,17 @@ NOTE: This is a fairly advanced feature requiring some nontrivial vimscript. It's possible that this feature will be incorporated into Vimspector in future as it is a common requirement. +## Disabling the WinBar + +You can tell vimspector not to draw the WinBar (the toolbars in the code, +variables, output, etc. windows) by setting: + +```viml +let g:vimspector_enable_winbar=0 +``` + +The WinBar is in any case not displayed if the mouse is not enabled. + ## Advanced UI customisation > ***Please Note***: This customisation API is ***unstable***, meaning that it may diff --git a/autoload/vimspector/internal/neowinbar.vim b/autoload/vimspector/internal/neowinbar.vim new file mode 100644 index 000000000..945ddc952 --- /dev/null +++ b/autoload/vimspector/internal/neowinbar.vim @@ -0,0 +1,33 @@ +" 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! vimspector#internal#neowinbar#Do( idx, nclick, btn, mods ) abort + py3 __import__( "vimspector", + \ fromlist = [ "utils" ] ).utils.DoWinBarAction( + \ int( vim.eval( 'getmousepos().winid' ) ), + \ int( vim.eval( 'a:idx' ) ) ) +endfunction + +" Boilerplate {{{ +let &cpoptions=s:save_cpo +unlet s:save_cpo +" }}} + diff --git a/python3/vimspector/code.py b/python3/vimspector/code.py index 5fde1f57f..92d648cc2 100644 --- a/python3/vimspector/code.py +++ b/python3/vimspector/code.py @@ -48,30 +48,24 @@ def __init__( self, self._current_frame = None self._scratch_buffers = [] - with utils.LetCurrentWindow( self._window ): - if utils.UseWinBar(): - # Buggy neovim doesn't render correctly when the WinBar is defined: - # https://github.com/neovim/neovim/issues/12689 - vim.command( 'nnoremenu WinBar.■\\ Stop ' - ':call vimspector#Stop()' ) - vim.command( 'nnoremenu WinBar.▶\\ Cont ' - ':call vimspector#Continue()' ) - vim.command( 'nnoremenu WinBar.▷\\ Pause ' - ':call vimspector#Pause()' ) - vim.command( 'nnoremenu WinBar.↷\\ Next ' - ':call vimspector#StepSOver()' ) - vim.command( 'nnoremenu WinBar.→\\ Step ' - ':call vimspector#StepSInto()' ) - vim.command( 'nnoremenu WinBar.←\\ Out ' - ':call vimspector#StepSOut()' ) - vim.command( 'nnoremenu WinBar.↺ ' - ':call vimspector#Restart()' ) - vim.command( 'nnoremenu WinBar.✕ ' - ':call vimspector#Reset()' ) - + self._RenderWinBar() signs.DefineProgramCounterSigns() + def _RenderWinBar( self ): + with utils.LetCurrentWindow( self._window ): + if utils.UseWinBar(): + utils.SetWinBar( + ( '■ Stop', 'vimspector#Stop()', ), + ( '▶ Cont', 'vimspector#Continue()', ), + ( '▷ Pause', 'vimspector#Pause()', ), + ( '↷ Next', 'vimspector#StepSOver()', ), + ( '→ Step', 'vimspector#StepSInto()', ), + ( '← Out', 'vimspector#StepSOut()', ), + ( '↺', 'vimspector#Restart()', ), + ( '✕', 'vimspector#Reset()', ) + ) + def _UndisplayPC( self, clear_pc = True ): if clear_pc: self._current_frame = None @@ -138,7 +132,10 @@ def SetCurrentFrame( self, frame, should_jump_to_location ): with utils.LetCurrentWindow( self._window ): try: - utils.OpenFileInCurrentWindow( frame[ 'source' ][ 'path' ] ) + if utils.OpenFileInCurrentWindow( frame[ 'source' ][ 'path' ] ): + if utils.VimIsNeovim(): + # Sigh: https://github.com/neovim/neovim/issues/23165 + self._RenderWinBar() vim.command( 'doautocmd User VimspectorJumpedToFrame' ) except vim.error: self._logger.exception( 'Unexpected vim error opening file {}'.format( @@ -170,7 +167,6 @@ def SetCurrentFrame( self, frame, should_jump_to_location ): vim.current.buffer.options[ 'syntax' ] ) self._DisplayPC() - return True def Clear( self ): diff --git a/python3/vimspector/disassembly.py b/python3/vimspector/disassembly.py index 2bb6301a7..101258ed1 100644 --- a/python3/vimspector/disassembly.py +++ b/python3/vimspector/disassembly.py @@ -47,25 +47,9 @@ def __init__( self, window, api_prefix, render_event_emitter ): 'vimspectorPC': None, } - with utils.LetCurrentWindow( self._window ): - if utils.UseWinBar(): - vim.command( 'nnoremenu WinBar.■\\ Stop ' - ':call vimspector#Stop()' ) - vim.command( 'nnoremenu WinBar.▶\\ Cont ' - ':call vimspector#Continue()' ) - vim.command( 'nnoremenu WinBar.▷\\ Pause ' - ':call vimspector#Pause()' ) - vim.command( 'nnoremenu WinBar.↷\\ NextI ' - ':call vimspector#StepIOver()' ) - vim.command( 'nnoremenu WinBar.→\\ StepI ' - ':call vimspector#StepIInto()' ) - vim.command( 'nnoremenu WinBar.←\\ OutI ' - ':call vimspector#StepIOut()' ) - vim.command( 'nnoremenu WinBar.⟲: ' - ':call vimspector#Restart()' ) - vim.command( 'nnoremenu WinBar.✕ ' - ':call vimspector#Reset()' ) + self._RenderWinBar() + with utils.LetCurrentWindow( self._window ): vim.command( 'augroup VimspectorDisassembly' ) vim.command( 'autocmd!' ) vim.command( f'autocmd WinScrolled { utils.WindowID( self._window ) } ' @@ -75,6 +59,21 @@ def __init__( self, window, api_prefix, render_event_emitter ): signs.DefineProgramCounterSigns() + def _RenderWinBar( self ): + with utils.LetCurrentWindow( self._window ): + if utils.UseWinBar(): + utils.SetWinBar( + ( '■ Stop', 'vimspector#Stop()', ), + ( '▶ Cont', 'vimspector#Continue()', ), + ( '▷ Pause', 'vimspector#Pause()', ), + ( '↷ Next', 'vimspector#StepIOver()', ), + ( '→ Step', 'vimspector#StepIInto()', ), + ( '← Out', 'vimspector#StepIOut()', ), + ( '↺', 'vimspector#Restart()', ), + ( '✕', 'vimspector#Reset()', ) + ) + + def ConnectionClosed( self, connection: DebugAdapterConnection ): if connection != self.current_connection: return @@ -299,6 +298,8 @@ def _DrawInstructions( self, with utils.LetCurrentWindow( self._window ): utils.OpenFileInCurrentWindow( buf_name ) + if utils.VimIsNeovim(): + self._RenderWinBar() utils.SetUpUIWindow( self._window ) self._window.options[ 'signcolumn' ] = 'yes' diff --git a/python3/vimspector/output.py b/python3/vimspector/output.py index 38678832d..7a4d74744 100644 --- a/python3/vimspector/output.py +++ b/python3/vimspector/output.py @@ -114,8 +114,6 @@ def Reset( self ): def Clear( self ): for category, tab_buffer in self._buffers.items(): self._CleanUpBuffer( category, tab_buffer ) - - # FIXME: nunmenu the WinBar ? self._buffers = {} @@ -139,9 +137,7 @@ def WindowIsValid( self ): def UseWindow( self, win ): assert not self._window.valid self._window = win - # TODO: Sorting of the WinBar ? - for category, _ in self._buffers.items(): - self._RenderWinBar( category ) + self._RenderWinBar() def _ShowOutput( self, category ): @@ -153,6 +149,10 @@ def _ShowOutput( self, category ): vim.current.buffer = self._buffers[ category ].buf vim.command( 'normal! G' ) + if utils.VimIsNeovim(): + # Sigh: https://github.com/neovim/neovim/issues/23165 + self._RenderWinBar() + def ShowOutput( self, category ): self._ToggleFlag( category, False ) self._ShowOutput( category ) @@ -163,7 +163,7 @@ def _ToggleFlag( self, category, flag ): if self._window.valid: with utils.LetCurrentWindow( self._window ): - self._RenderWinBar( category ) + self._RenderWinBar() def RunJobWithOutput( self, category, cmd, **kwargs ): @@ -205,7 +205,6 @@ def _CreateBuffer( self, self._buffers[ category ] = TabBuffer( out, len( self._buffers ) ) self._buffers[ category ].is_job = True - self._RenderWinBar( category ) else: if category == 'Console': name = 'vimspector.Console' @@ -227,7 +226,7 @@ def _CreateBuffer( self, else: utils.SetUpHiddenBuffer( tab_buffer.buf, name ) - self._RenderWinBar( category ) + self._RenderWinBar() self._buffers[ category ].syntax = utils.SetSyntax( self._buffers[ category ].syntax, @@ -239,7 +238,7 @@ def _CreateBuffer( self, self._ShowOutput( category ) utils.CleanUpHiddenBuffer( buf_to_delete ) - def _RenderWinBar( self, category ): + def _RenderWinBar( self ): if not utils.UseWinBar(): return @@ -247,25 +246,18 @@ def _RenderWinBar( self, category ): return with utils.LetCurrentWindow( self._window ): - tab_buffer = self._buffers[ category ] - - try: - if tab_buffer.flag: - vim.command( 'nunmenu WinBar.{}'.format( utils.Escape( category ) ) ) - else: - vim.command( 'nunmenu WinBar.{}*'.format( utils.Escape( category ) ) ) - except vim.error as e: - # E329 means the menu doesn't exist; ignore that. - if 'E329' not in str( e ): - raise - - vim.command( - "nnoremenu 1.{0} WinBar.{1}{2} " - ":call vimspector#ShowOutputInWindow( {3}, '{1}' )".format( - tab_buffer.index, - utils.Escape( category ), - '*' if tab_buffer.flag else '', - utils.WindowID( self._window ) ) ) + tab_buffers = sorted( self._buffers.items(), + key = lambda i: i[ 1 ].index ) + + buttons = [] + for category, tab_buffer in tab_buffers: + buttons.append( + ( category + ( '*' if tab_buffer.flag else '' ), + "vimspector#ShowOutputInWindow( {}, '{}' )".format( + utils.WindowID( self._window ), + utils.Escape( category ) ) ) + ) + utils.SetWinBar( *buttons ) def GetCategories( self ): return list( self._buffers.keys() ) diff --git a/python3/vimspector/settings.py b/python3/vimspector/settings.py index e8059bace..998303735 100644 --- a/python3/vimspector/settings.py +++ b/python3/vimspector/settings.py @@ -37,6 +37,9 @@ 'terminal_maxheight': 15, 'terminal_minheight': 5, + # WinBar + 'enable_winbar': True, + # Session files 'session_file_name': '.vimspector.session', diff --git a/python3/vimspector/utils.py b/python3/vimspector/utils.py index 51b481f2c..0f9806cc6 100644 --- a/python3/vimspector/utils.py +++ b/python3/vimspector/utils.py @@ -110,13 +110,16 @@ def WindowForBuffer( buf ): def OpenFileInCurrentWindow( file_name ): buffer_number = BufferNumberForFile( file_name ) + if vim.current.buffer.number == buffer_number: + return False + try: vim.current.buffer = vim.buffers[ buffer_number ] except vim.error as e: if 'E325' not in str( e ): raise - return vim.buffers[ buffer_number ] + return True COMMAND_HANDLERS = {} @@ -505,12 +508,13 @@ def AppendToBuffer( buf, if not modified: buf.options[ 'modified' ] = False - HighlightTextSection( buf, - hl = hl, - start_line = line, - start_col = 1, - end_line = len( buf ), - end_col = len( buf[ -1 ] ) ) + if len( buf ) > 0: + HighlightTextSection( buf, + hl = hl, + start_line = line, + start_col = 1, + end_line = len( buf ), + end_col = len( buf[ -1 ] ) ) # Return the first Vim line number (1-based) that we just set. return line @@ -918,10 +922,47 @@ def GetWindowInfo( window ): return Call( 'getwininfo', WindowID( window ) )[ 0 ] +NVIM_WINBAR = {} + + +def SetWinBarOption( *args ): + window = vim.current.window + win_id = WindowID( window ) + + NVIM_WINBAR[ win_id ] = [] + winbar = [] + for idx, button in enumerate( args ): + button, action = button + winbar.append( '%#ToolbarButton#' + f'%{idx}@vimspector#internal#neowinbar#Do@ { button } %X' + '%*' ) + NVIM_WINBAR[ win_id ].append( action ) + + window.options[ 'winbar' ] = ' '.join( winbar ) + return True + + +def DoWinBarAction( win_id, idx ): + action = NVIM_WINBAR[ win_id ][ idx ] + vim.command( f':call { action }' ) + + +def SetWinBar( *args ): + if VimIsNeovim(): + return SetWinBarOption( *args ) + + vim.command( 'silent! nunmenu WinBar' ) + for idx, button in enumerate( args ): + button, action = button + button = button.replace( ' ', '\\ ' ) + vim.command( f'nnoremenu 1.{idx} ' + f'WinBar.{ button } ' + f':call {action}' ) + + def UseWinBar(): - # Buggy neovim doesn't render correctly when the WinBar is defined: - # https://github.com/neovim/neovim/issues/12689 - return not VimIsNeovim() and VimHasMouseSupport() + from vimspector import settings + return settings.Bool( 'enable_winbar' ) and VimHasMouseSupport() @memoize @@ -930,8 +971,8 @@ def VimIsNeovim(): def VimHasMouseSupport(): - mouse = vim.options[ 'mouse' ] - return b'a' in mouse or b'n' in mouse + mouse = ToUnicode( vim.options[ 'mouse' ] ) + return 'a' in mouse or 'n' in mouse class VisiblePosition: diff --git a/python3/vimspector/variables.py b/python3/vimspector/variables.py index 86eb0d69c..e741f6ba4 100644 --- a/python3/vimspector/variables.py +++ b/python3/vimspector/variables.py @@ -246,10 +246,10 @@ def __init__( self, session_id, variables_win, watches_win ): session_id ) ) with utils.LetCurrentWindow( variables_win ): if utils.UseWinBar(): - vim.command( 'nnoremenu 1.1 WinBar.Set ' - ':call vimspector#SetVariableValue()' ) - vim.command( 'nnoremenu 1.2 WinBar.Dump ' - ':call vimspector#ReadMemory()' ) + utils.SetWinBar( + ( 'Set', 'vimspector#SetVariableValue()', ), + ( 'Dump', 'vimspector#ReadMemory()' ) + ) AddExpandMappings( mappings ) # Set up the "Watches" buffer in the watches_win (and create a WinBar in @@ -271,16 +271,13 @@ def __init__( self, session_id, variables_win, watches_win ): f'nnoremap { mapping } :call vimspector#DeleteWatch()' ) if utils.UseWinBar(): - vim.command( 'nnoremenu 1.1 WinBar.Add ' - ':call vimspector#AddWatch()' ) - vim.command( 'nnoremenu 1.2 WinBar.Delete ' - ':call vimspector#DeleteWatch()' ) - vim.command( 'nnoremenu 1.3 WinBar.+/- ' - ':call vimspector#ExpandVariable()' ) - vim.command( 'nnoremenu 1.4 WinBar.Set ' - ':call vimspector#SetVariableValue()' ) - vim.command( 'nnoremenu 1.5 WinBar.Dump ' - ':call vimspector#ReadMemory()' ) + utils.SetWinBar( + ( 'Add', 'vimspector#AddWatch()', ), + ( 'Delete', 'vimspector#DeleteWatch()', ), + ( '+/-', 'vimspector#ExpandVariable()', ), + ( 'Set', 'vimspector#SetVariableValue()', ), + ( 'Dump', 'vimspector#ReadMemory()', ) + ) # Set the (global!) balloon expr if supported has_balloon = int( vim.eval( "has( 'balloon_eval' )" ) ) diff --git a/tests/ci/image/Dockerfile b/tests/ci/image/Dockerfile index 3856d0f71..2b032692a 100644 --- a/tests/ci/image/Dockerfile +++ b/tests/ci/image/Dockerfile @@ -12,7 +12,6 @@ RUN apt-get update && \ lsb-release \ ca-certificates \ software-properties-common && \ - add-apt-repository ppa:neovim-ppa/stable && \ curl -sL https://deb.nodesource.com/setup_18.x | bash - && \ apt-get update && \ apt-get -y dist-upgrade && \ @@ -33,10 +32,10 @@ RUN apt-get update && \ nodejs \ pkg-config \ lua5.1 \ - luajit \ - neovim && \ + luajit && \ pip3 install neovim + RUN ln -fs /usr/share/zoneinfo/Europe/London /etc/localtime && \ dpkg-reconfigure --frontend noninteractive tzdata @@ -69,6 +68,8 @@ RUN curl -LO https://github.com/love2d/love/releases/download/11.3/love-11.3-lin rm -rf love-11.3 && \ rm -f love-11.3-linux-src.tar.gz +RUN apt-get install -y ninja-build gettext cmake unzip curl + RUN apt-get -y autoremove ## cleanup of files from setup @@ -87,7 +88,20 @@ RUN mkdir -p $HOME/vim && \ git clone --depth 1 --branch ${VIM_VERSION} https://github.com/vim/vim && \ cd vim && \ make -j 4 && \ - make install + make install && \ + cd && \ + rm -rf $HOME/vim + +ARG NVIM_VERSION=v0.8.3 + +RUN mkdir -p $HOME/nvim && \ + cd $HOME/nvim && \ + git clone --depth 1 --branch ${NVIM_VERSION} https://github.com/neovim/neovim && \ + cd neovim && \ + make CMAKE_BUILD_TYPE=Release CMAKE_GENERTOR=Ninja -j 4 && \ + make install && \ + cd && \ + rm -rf $HOME/nvim # dotnet RUN curl -sSL https://dot.net/v1/dotnet-install.sh \ @@ -99,5 +113,4 @@ RUN curl -sSL https://dot.net/v1/dotnet-install.sh \ RUN pip install "git+https://github.com/puremourning/asciinema@exit_code#egg=asciinema" # clean up -RUN rm -rf ~/.cache && \ - rm -rf $HOME/vim +RUN rm -rf ~/.cache diff --git a/tests/disassembly.test.vim b/tests/disassembly.test.vim index 1471a030a..3e1a0f47f 100644 --- a/tests/disassembly.test.vim +++ b/tests/disassembly.test.vim @@ -4,6 +4,9 @@ let s:buf = '_vimspector_disassembly' " fixed position in this test require that. let s:dlines = 5 if has( 'nvim' ) + " Making these tests work with neovim is tricky because for some reason it + " always says "not enough room" whereas vim works with the same layout! + let g:vimspector_enable_winbar = 0 let s:offset = 1 else let s:offset = 0 diff --git a/tests/manual/image/Dockerfile b/tests/manual/image/Dockerfile index 56c7b4b22..ae661f273 100644 --- a/tests/manual/image/Dockerfile +++ b/tests/manual/image/Dockerfile @@ -27,3 +27,6 @@ ENV TEST_NO_RETRY 1 RUN go install github.com/go-delve/delve/cmd/dlv@latest ADD --chown=dev:dev .vim/ /home/dev/.vim/ +RUN mkdir -p /home/dev/.config/nvim && \ + cd /home/dev/.config/nvim && \ + ln -s /home/dev/.vim/vimrc init.vim diff --git a/tests/ui.test.vim b/tests/ui.test.vim index 66b0b2958..dd528faf2 100644 --- a/tests/ui.test.vim +++ b/tests/ui.test.vim @@ -23,7 +23,6 @@ function! SetUp_Test_StandardLayout() endfunction function! Test_StandardLayout() - call SkipNeovim() call s:StartDebugging() call vimspector#StepOver() @@ -537,7 +536,6 @@ function! Test_CloseOutput() endfunction function! Test_CloseOutput_Early() - call SkipNeovim() augroup TestCustomUI au! au User VimspectorUICreated @@ -677,7 +675,6 @@ function! TearDown_Test_NoMouseNoWinBar() endfunction function! Test_VimspectorJumpedToFrame() - call SkipNeovim() let s:ended = 0 let s:au_visited_buffers = {} @@ -731,7 +728,6 @@ function! Test_VimspectorJumpedToFrame() endfunction function! Test_DebugInfo_NotConnected() - call SkipNeovim() redir => debug_message VimspectorDebugInfo redir END @@ -744,7 +740,6 @@ function! Test_DebugInfo_NotConnected() endfunction function! Test_DebugInfo_Connected() - call SkipNeovim() call s:StartDebugging() " Just make sure there are no errors for now