signature_help.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. # Copyright (C) 2011-2018 YouCompleteMe contributors
  2. #
  3. # This file is part of YouCompleteMe.
  4. #
  5. # YouCompleteMe is free software: you can redistribute it and/or modify
  6. # it under the terms of the GNU General Public License as published by
  7. # the Free Software Foundation, either version 3 of the License, or
  8. # (at your option) any later version.
  9. #
  10. # YouCompleteMe is distributed in the hope that it will be useful,
  11. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. # GNU General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU General Public License
  16. # along with YouCompleteMe. If not, see <http://www.gnu.org/licenses/>.
  17. import vim
  18. import json
  19. from ycm import vimsupport
  20. from ycmd import utils
  21. from ycm.vimsupport import memoize, GetIntValue
  22. class SignatureHelpState:
  23. ACTIVE = 'ACTIVE'
  24. INACTIVE = 'INACTIVE'
  25. ACTIVE_SUPPRESSED = 'ACTIVE_SUPPRESSED'
  26. def __init__( self,
  27. popup_win_id = None,
  28. state = INACTIVE ):
  29. self.popup_win_id = popup_win_id
  30. self.state = state
  31. self.anchor = None
  32. def ToggleVisibility( self ):
  33. if self.state == 'ACTIVE':
  34. self.state = 'ACTIVE_SUPPRESSED'
  35. vim.eval( f'popup_hide( { self.popup_win_id } )' )
  36. elif self.state == 'ACTIVE_SUPPRESSED':
  37. self.state = 'ACTIVE'
  38. vim.eval( f'popup_show( { self.popup_win_id } )' )
  39. def IsActive( self ):
  40. if self.state in ( 'ACTIVE', 'ACTIVE_SUPPRESSED' ):
  41. return 'ACTIVE'
  42. return 'INACTIVE'
  43. def _MakeSignatureHelpBuffer( signature_info ):
  44. active_parameter = int( signature_info.get( 'activeParameter', 0 ) )
  45. lines = []
  46. signatures = ( signature_info.get( 'signatures' ) or [] )
  47. for sig_index, signature in enumerate( signatures ):
  48. props = []
  49. sig_label = signature[ 'label' ]
  50. parameters = ( signature.get( 'parameters' ) or [] )
  51. for param_index, parameter in enumerate( parameters ):
  52. param_label = parameter[ 'label' ]
  53. begin = int( param_label[ 0 ] )
  54. end = int( param_label[ 1 ] )
  55. if param_index == active_parameter:
  56. props.append( {
  57. 'col': begin + 1, # 1-based
  58. 'length': end - begin,
  59. 'type': 'YCM-signature-help-current-argument'
  60. } )
  61. lines.append( {
  62. 'text': sig_label,
  63. 'props': props
  64. } )
  65. return lines
  66. @memoize()
  67. def ShouldUseSignatureHelp():
  68. return ( vimsupport.VimHasFunctions( 'screenpos', 'pum_getpos' ) and
  69. vimsupport.VimSupportsPopupWindows() )
  70. def UpdateSignatureHelp( state, signature_info ): # noqa
  71. if not ShouldUseSignatureHelp():
  72. return state
  73. signatures = signature_info.get( 'signatures' ) or []
  74. if not signatures:
  75. if state.popup_win_id:
  76. # TODO/FIXME: Should we use popup_hide() instead ?
  77. vim.eval( f"popup_close( { state.popup_win_id } )" )
  78. return SignatureHelpState( None, SignatureHelpState.INACTIVE )
  79. if state.state == SignatureHelpState.INACTIVE:
  80. state.anchor = vimsupport.CurrentLineAndColumn()
  81. state.state = SignatureHelpState.ACTIVE
  82. # Generate the buffer as a list of lines
  83. buf_lines = _MakeSignatureHelpBuffer( signature_info )
  84. screen_pos = vimsupport.ScreenPositionForLineColumnInWindow(
  85. vim.current.window,
  86. state.anchor[ 0 ] + 1, # anchor 0-based
  87. state.anchor[ 1 ] + 1 ) # anchor 0-based
  88. # Simulate 'flip' at the screen boundaries by using screenpos and hiding the
  89. # signature help menu if it overlaps the completion popup (pum).
  90. #
  91. # FIXME: revert to cursor-relative positioning and the 'flip' option when that
  92. # is implemented (if that is indeed better).
  93. # By default display above the anchor
  94. line = int( screen_pos[ 'row' ] ) - 1 # -1 to display above the cur line
  95. pos = "botleft"
  96. cursor_line = vimsupport.CurrentLineAndColumn()[ 0 ] + 1
  97. if int( screen_pos[ 'row' ] ) <= len( buf_lines ):
  98. # No room at the top, display below
  99. line = int( screen_pos[ 'row' ] ) + 1
  100. pos = "topleft"
  101. # Don't allow the popup to overlap the cursor
  102. if ( pos == 'topleft' and
  103. line < cursor_line and
  104. line + len( buf_lines ) >= cursor_line ):
  105. line = 0
  106. # Don't allow the popup to overlap the pum
  107. if line > 0 and GetIntValue( 'pumvisible()' ):
  108. pum_line = GetIntValue( 'pum_getpos().row' ) + 1
  109. if pos == 'botleft' and pum_line <= line:
  110. line = 0
  111. elif ( pos == 'topleft' and
  112. pum_line >= line and
  113. pum_line < ( line + len( buf_lines ) ) ):
  114. line = 0
  115. if line <= 0:
  116. # Nowhere to put it so hide it
  117. if state.popup_win_id:
  118. # TODO/FIXME: Should we use popup_hide() instead ?
  119. vim.eval( f"popup_close( { state.popup_win_id } )" )
  120. return SignatureHelpState( None, SignatureHelpState.INACTIVE )
  121. if int( screen_pos[ 'curscol' ] ) <= 1:
  122. col = 1
  123. else:
  124. # -1 for padding,
  125. # -1 for the trigger character inserted (the anchor is set _after_ the
  126. # character is inserted, so we remove it).
  127. # FIXME: multi-byte characters would be wrong. Need to set anchor before
  128. # inserting the char ?
  129. col = int( screen_pos[ 'curscol' ] ) - 2
  130. if col <= 0:
  131. col = 1
  132. options = {
  133. "line": line,
  134. "col": col,
  135. "pos": pos,
  136. "wrap": 0,
  137. # NOTE: We *dont'* use "cursorline" here - that actually uses PMenuSel,
  138. # which is just too invasive for us (it's more selected item than actual
  139. # cursorline. So instead, we manually set 'cursorline' in the popup window
  140. # and enable syntax based on the current file syntax)
  141. "flip": 1,
  142. "padding": [ 0, 1, 0, 1 ], # Pad 1 char in X axis to match completion menu
  143. "hidden": int( state.state == SignatureHelpState.ACTIVE_SUPPRESSED )
  144. }
  145. if not state.popup_win_id:
  146. state.popup_win_id = GetIntValue(
  147. f'popup_create( { json.dumps( buf_lines ) }, '
  148. f'{ json.dumps( options ) } )' )
  149. else:
  150. vim.eval( f'popup_settext( { state.popup_win_id }, '
  151. f'{ json.dumps( buf_lines ) } )' )
  152. # Should do nothing if already visible
  153. vim.eval( f'popup_move( { state.popup_win_id }, { json.dumps( options ) } )' )
  154. if state.state == SignatureHelpState.ACTIVE:
  155. vim.eval( f'popup_show( { state.popup_win_id } )' )
  156. if vim.vars.get( 'ycm_signature_help_disable_syntax', False ):
  157. syntax = ''
  158. else:
  159. syntax = utils.ToUnicode( vim.current.buffer.options[ 'syntax' ] )
  160. active_signature = int( signature_info.get( 'activeSignature', 0 ) )
  161. vim.eval( f"win_execute( { state.popup_win_id }, "
  162. f"'set syntax={ syntax } cursorline | "
  163. f"call cursor( [ { active_signature + 1 }, 1 ] )' )" )
  164. return state