signature_help.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  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. def __init__( self,
  26. popup_win_id = None,
  27. state = INACTIVE ):
  28. self.popup_win_id = popup_win_id
  29. self.state = state
  30. self.anchor = None
  31. def _MakeSignatureHelpBuffer( signature_info ):
  32. active_parameter = int( signature_info.get( 'activeParameter', 0 ) )
  33. lines = []
  34. signatures = ( signature_info.get( 'signatures' ) or [] )
  35. for sig_index, signature in enumerate( signatures ):
  36. props = []
  37. sig_label = signature[ 'label' ]
  38. parameters = ( signature.get( 'parameters' ) or [] )
  39. for param_index, parameter in enumerate( parameters ):
  40. param_label = parameter[ 'label' ]
  41. begin = int( param_label[ 0 ] )
  42. end = int( param_label[ 1 ] )
  43. if param_index == active_parameter:
  44. props.append( {
  45. 'col': begin + 1, # 1-based
  46. 'length': end - begin,
  47. 'type': 'YCM-signature-help-current-argument'
  48. } )
  49. lines.append( {
  50. 'text': sig_label,
  51. 'props': props
  52. } )
  53. return lines
  54. @memoize
  55. def ShouldUseSignatureHelp():
  56. return ( vimsupport.VimHasFunctions( 'screenpos', 'pum_getpos' ) and
  57. vimsupport.VimSupportsPopupWindows() )
  58. def UpdateSignatureHelp( state, signature_info ): # noqa
  59. if not ShouldUseSignatureHelp():
  60. return state
  61. signatures = signature_info.get( 'signatures' ) or []
  62. if not signatures:
  63. if state.popup_win_id:
  64. # TODO/FIXME: Should we use popup_hide() instead ?
  65. vim.eval( "popup_close( {} )".format( state.popup_win_id ) )
  66. return SignatureHelpState( None, SignatureHelpState.INACTIVE )
  67. if state.state != SignatureHelpState.ACTIVE:
  68. state.anchor = vimsupport.CurrentLineAndColumn()
  69. state.state = SignatureHelpState.ACTIVE
  70. # Generate the buffer as a list of lines
  71. buf_lines = _MakeSignatureHelpBuffer( signature_info )
  72. screen_pos = vimsupport.ScreenPositionForLineColumnInWindow(
  73. vim.current.window,
  74. state.anchor[ 0 ] + 1, # anchor 0-based
  75. state.anchor[ 1 ] + 1 ) # anchor 0-based
  76. # Simulate 'flip' at the screen boundaries by using screenpos and hiding the
  77. # signature help menu if it overlaps the completion popup (pum).
  78. #
  79. # FIXME: revert to cursor-relative positioning and the 'flip' option when that
  80. # is implemented (if that is indeed better).
  81. # By default display above the anchor
  82. line = int( screen_pos[ 'row' ] ) - 1 # -1 to display above the cur line
  83. pos = "botleft"
  84. cursor_line = vimsupport.CurrentLineAndColumn()[ 0 ] + 1
  85. if int( screen_pos[ 'row' ] ) <= len( buf_lines ):
  86. # No room at the top, display below
  87. line = int( screen_pos[ 'row' ] ) + 1
  88. pos = "topleft"
  89. # Don't allow the popup to overlap the cursor
  90. if ( pos == 'topleft' and
  91. line < cursor_line and
  92. line + len( buf_lines ) >= cursor_line ):
  93. line = 0
  94. # Don't allow the popup to overlap the pum
  95. if line > 0 and GetIntValue( 'pumvisible()' ):
  96. pum_line = GetIntValue( 'pum_getpos().row' ) + 1
  97. if pos == 'botleft' and pum_line <= line:
  98. line = 0
  99. elif ( pos == 'topleft' and
  100. pum_line >= line and
  101. pum_line < ( line + len( buf_lines ) ) ):
  102. line = 0
  103. if line <= 0:
  104. # Nowhere to put it so hide it
  105. if state.popup_win_id:
  106. # TODO/FIXME: Should we use popup_hide() instead ?
  107. vim.eval( "popup_close( {} )".format( state.popup_win_id ) )
  108. return SignatureHelpState( None, SignatureHelpState.INACTIVE )
  109. if int( screen_pos[ 'curscol' ] ) <= 1:
  110. col = 1
  111. else:
  112. # -1 for padding,
  113. # -1 for the trigger character inserted (the anchor is set _after_ the
  114. # character is inserted, so we remove it).
  115. # FIXME: multi-byte characters would be wrong. Need to set anchor before
  116. # inserting the char ?
  117. col = int( screen_pos[ 'curscol' ] ) - 2
  118. if col <= 0:
  119. col = 1
  120. options = {
  121. "line": line,
  122. "col": col,
  123. "pos": pos,
  124. "wrap": 0,
  125. # NOTE: We *dont'* use "cursorline" here - that actually uses PMenuSel,
  126. # which is just too invasive for us (it's more selected item than actual
  127. # cursorline. So instead, we manually set 'cursorline' in the popup window
  128. # and enable sytax based on the current file syntax)
  129. "flip": 1,
  130. "padding": [ 0, 1, 0, 1 ], # Pad 1 char in X axis to match completion menu
  131. }
  132. if not state.popup_win_id:
  133. state.popup_win_id = GetIntValue( "popup_create( {}, {} )".format(
  134. json.dumps( buf_lines ),
  135. json.dumps( options ) ) )
  136. else:
  137. vim.eval( 'popup_settext( {}, {} )'.format(
  138. state.popup_win_id,
  139. json.dumps( buf_lines ) ) )
  140. # Should do nothing if already visible
  141. vim.eval( 'popup_move( {}, {} )'.format( state.popup_win_id,
  142. json.dumps( options ) ) )
  143. vim.eval( 'popup_show( {} )'.format( state.popup_win_id ) )
  144. active_signature = int( signature_info.get( 'activeSignature', 0 ) )
  145. vim.eval( "win_execute( {}, 'set syntax={} cursorline | "
  146. "call cursor( [ {}, 1 ] )' )".format(
  147. state.popup_win_id,
  148. utils.ToUnicode( vim.current.buffer.options[ 'syntax' ] ),
  149. active_signature + 1 ) )
  150. return state