# Copyright (C) 2011-2018 YouCompleteMe contributors # # This file is part of YouCompleteMe. # # YouCompleteMe is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # YouCompleteMe is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with YouCompleteMe. If not, see . import vim import json from ycm import vimsupport from ycmd import utils from ycm.vimsupport import memoize, GetIntValue class SignatureHelpState: ACTIVE = 'ACTIVE' INACTIVE = 'INACTIVE' ACTIVE_SUPPRESSED = 'ACTIVE_SUPPRESSED' def __init__( self, popup_win_id = None, state = INACTIVE ): self.popup_win_id = popup_win_id self.state = state self.anchor = None def ToggleVisibility( self ): if self.state == 'ACTIVE': self.state = 'ACTIVE_SUPPRESSED' vim.eval( f'popup_hide( { self.popup_win_id } )' ) elif self.state == 'ACTIVE_SUPPRESSED': self.state = 'ACTIVE' vim.eval( f'popup_show( { self.popup_win_id } )' ) def IsActive( self ): if self.state in ( 'ACTIVE', 'ACTIVE_SUPPRESSED' ): return 'ACTIVE' return 'INACTIVE' def _MakeSignatureHelpBuffer( signature_info ): active_parameter = int( signature_info.get( 'activeParameter', 0 ) ) lines = [] signatures = ( signature_info.get( 'signatures' ) or [] ) for sig_index, signature in enumerate( signatures ): props = [] sig_label = signature[ 'label' ] parameters = ( signature.get( 'parameters' ) or [] ) for param_index, parameter in enumerate( parameters ): param_label = parameter[ 'label' ] begin = int( param_label[ 0 ] ) end = int( param_label[ 1 ] ) if param_index == active_parameter: props.append( { 'col': begin + 1, # 1-based 'length': end - begin, 'type': 'YCM-signature-help-current-argument' } ) lines.append( { 'text': sig_label, 'props': props } ) return lines @memoize() def ShouldUseSignatureHelp(): return ( vimsupport.VimHasFunctions( 'screenpos', 'pum_getpos' ) and vimsupport.VimSupportsPopupWindows() ) def UpdateSignatureHelp( state, signature_info ): # noqa if not ShouldUseSignatureHelp(): return state signatures = signature_info.get( 'signatures' ) or [] if not signatures: if state.popup_win_id: # TODO/FIXME: Should we use popup_hide() instead ? vim.eval( f"popup_close( { state.popup_win_id } )" ) return SignatureHelpState( None, SignatureHelpState.INACTIVE ) if state.state == SignatureHelpState.INACTIVE: state.anchor = vimsupport.CurrentLineAndColumn() state.state = SignatureHelpState.ACTIVE # Generate the buffer as a list of lines buf_lines = _MakeSignatureHelpBuffer( signature_info ) screen_pos = vimsupport.ScreenPositionForLineColumnInWindow( vim.current.window, state.anchor[ 0 ] + 1, # anchor 0-based state.anchor[ 1 ] + 1 ) # anchor 0-based # Simulate 'flip' at the screen boundaries by using screenpos and hiding the # signature help menu if it overlaps the completion popup (pum). # # FIXME: revert to cursor-relative positioning and the 'flip' option when that # is implemented (if that is indeed better). # By default display above the anchor line = int( screen_pos[ 'row' ] ) - 1 # -1 to display above the cur line pos = "botleft" cursor_line = vimsupport.CurrentLineAndColumn()[ 0 ] + 1 if int( screen_pos[ 'row' ] ) <= len( buf_lines ): # No room at the top, display below line = int( screen_pos[ 'row' ] ) + 1 pos = "topleft" # Don't allow the popup to overlap the cursor if ( pos == 'topleft' and line < cursor_line and line + len( buf_lines ) >= cursor_line ): line = 0 # Don't allow the popup to overlap the pum if line > 0 and GetIntValue( 'pumvisible()' ): pum_line = GetIntValue( 'pum_getpos().row' ) + 1 if pos == 'botleft' and pum_line <= line: line = 0 elif ( pos == 'topleft' and pum_line >= line and pum_line < ( line + len( buf_lines ) ) ): line = 0 if line <= 0: # Nowhere to put it so hide it if state.popup_win_id: # TODO/FIXME: Should we use popup_hide() instead ? vim.eval( f"popup_close( { state.popup_win_id } )" ) return SignatureHelpState( None, SignatureHelpState.INACTIVE ) if int( screen_pos[ 'curscol' ] ) <= 1: col = 1 else: # -1 for padding, # -1 for the trigger character inserted (the anchor is set _after_ the # character is inserted, so we remove it). # FIXME: multi-byte characters would be wrong. Need to set anchor before # inserting the char ? col = int( screen_pos[ 'curscol' ] ) - 2 if col <= 0: col = 1 options = { "line": line, "col": col, "pos": pos, "wrap": 0, # NOTE: We *dont'* use "cursorline" here - that actually uses PMenuSel, # which is just too invasive for us (it's more selected item than actual # cursorline. So instead, we manually set 'cursorline' in the popup window # and enable syntax based on the current file syntax) "flip": 1, "padding": [ 0, 1, 0, 1 ], # Pad 1 char in X axis to match completion menu "hidden": int( state.state == SignatureHelpState.ACTIVE_SUPPRESSED ) } if not state.popup_win_id: state.popup_win_id = GetIntValue( f'popup_create( { json.dumps( buf_lines ) }, ' f'{ json.dumps( options ) } )' ) else: vim.eval( f'popup_settext( { state.popup_win_id }, ' f'{ json.dumps( buf_lines ) } )' ) # Should do nothing if already visible vim.eval( f'popup_move( { state.popup_win_id }, { json.dumps( options ) } )' ) if state.state == SignatureHelpState.ACTIVE: vim.eval( f'popup_show( { state.popup_win_id } )' ) syntax = utils.ToUnicode( vim.current.buffer.options[ 'syntax' ] ) active_signature = int( signature_info.get( 'activeSignature', 0 ) ) vim.eval( f"win_execute( { state.popup_win_id }, " f"'set syntax={ syntax } cursorline | " f"call cursor( [ { active_signature + 1 }, 1 ] )' )" ) return state