Browse Source

Support for signature help

This uses Vim's new popup windows (where available). When the functions
we require are not available, we simply don't send the request and thus
never attempt to show any signatures.

The biggest challenge is placement of the popup, which is essentially
manual for us. Note that the popup position passed to Vim must be the
_screen_ position, not a buffer position. So we attempt to mimic
the way text properties follow text, by anchoring the popup to a
specific character in a specific buffer. We use the 'screenpos' function
for this.

The idea is to place the popup:

* imemdiately above the triggering character (named the 'anchor'), not
  overlapping the cursor if there is room above for all the items.
* otherwise, immediately below the triggering character.

Complications include:

* Ensuring there is room. If there is no room, just don't display.
* Avoiding overlapping the PUM. This is tricky as the pum is displayed
  asynchronously. We use the CompleteChanged event to try and recaculate
  our popup position when it moves. Again, if we can't display without
  overlapping, just don't display.

The rationale for not displaying is that signature help is secondary
information that may also be abailabel from the preview window (or
preview popup), so should not interfere with the primary completion
mechanism.
Ben Jackson 5 years ago
parent
commit
4e7797c89b

+ 116 - 1
autoload/youcompleteme.vim

@@ -25,13 +25,19 @@ let s:force_semantic = 0
 let s:completion_stopped = 0
 " These two variables are initialized in youcompleteme#Enable.
 let s:default_completion = {}
-let s:completion = {}
+let s:completion = s:default_completion
+let s:default_signature_help = {}
+let s:signature_help = s:default_completion
 let s:previous_allowed_buffer_number = 0
 let s:pollers = {
       \   'completion': {
       \     'id': -1,
       \     'wait_milliseconds': 10
       \   },
+      \   'signature_help': {
+      \     'id': -1,
+      \     'wait_milliseconds': 10
+      \   },
       \   'file_parse_response': {
       \     'id': -1,
       \     'wait_milliseconds': 100
@@ -142,6 +148,9 @@ function! youcompleteme#Enable()
     autocmd InsertLeave * call s:OnInsertLeave()
     autocmd VimLeave * call s:OnVimLeave()
     autocmd CompleteDone * call s:OnCompleteDone()
+    if exists( '##CompleteChanged' )
+      autocmd CompleteChanged * call s:OnCompleteChanged()
+    endif
     autocmd BufEnter,WinEnter * call s:UpdateMatches()
   augroup END
 
@@ -153,6 +162,28 @@ function! youcompleteme#Enable()
 
   let s:default_completion = s:Pyeval( 'vimsupport.NO_COMPLETIONS' )
   let s:completion = s:default_completion
+
+  if exists( '*prop_type_add' ) && exists( '*prop_type_delete' )
+    call prop_type_delete( 'YCM-signature-help-current-argument' )
+    call prop_type_delete( 'YCM-signature-help-current-signature' )
+    call prop_type_delete( 'YCM-signature-help-signature' )
+
+    call prop_type_add( 'YCM-signature-help-current-argument', {
+          \   'highlight': 'PMenuSel',
+          \   'combine':   0,
+          \   'priority':  50,
+          \ } )
+    call prop_type_add( 'YCM-signature-help-current-signature', {
+          \   'highlight': 'PMenu',
+          \   'combine':   0,
+          \   'priority':  40,
+          \ } )
+    call prop_type_add( 'YCM-signature-help-signature', {
+          \   'highlight': 'PMenuSbar',
+          \   'combine':   0,
+          \   'priority':  40,
+          \ } )
+  endif
 endfunction
 
 
@@ -534,6 +565,16 @@ function! s:OnCompleteDone()
   endif
 
   exec s:python_command "ycm_state.OnCompleteDone()"
+  call s:UpdateSignatureHelp()
+endfunction
+
+
+function! s:OnCompleteChanged()
+  if !s:AllowedToCompleteInCurrentBuffer()
+    return
+  endif
+
+  call s:UpdateSignatureHelp()
 endfunction
 
 
@@ -618,6 +659,9 @@ function! s:OnFileReadyToParse( ... )
   " We only want to send a new FileReadyToParse event notification if the buffer
   " has changed since the last time we sent one, or if forced.
   if force_parsing || s:Pyeval( "ycm_state.NeedsReparse()" )
+    " We switched buffers or somethuing, so claer.
+    " FIXME: sig hekp should be buffer local?
+    call s:ClearSignatureHelp()
     exec s:python_command "ycm_state.OnFileReadyToParse()"
 
     call timer_stop( s:pollers.file_parse_response.id )
@@ -669,6 +713,9 @@ function! s:OnInsertChar()
 
   call timer_stop( s:pollers.completion.id )
   call s:CloseCompletionMenu()
+
+  " TODO: Do we really need this here?
+  call timer_stop( s:pollers.signature_help.id )
 endfunction
 
 
@@ -678,6 +725,9 @@ function! s:OnDeleteChar( key )
   endif
 
   call timer_stop( s:pollers.completion.id )
+  "
+  " TODO: Do we really need this here?
+  call timer_stop( s:pollers.signature_help.id )
   if pumvisible()
     return "\<C-y>" . a:key
   endif
@@ -687,6 +737,9 @@ endfunction
 
 function! s:StopCompletion( key )
   call timer_stop( s:pollers.completion.id )
+
+  call s:ClearSignatureHelp()
+
   if pumvisible()
     let s:completion_stopped = 1
     return "\<C-y>"
@@ -740,6 +793,9 @@ function! s:OnTextChangedInsertMode()
     " Immediately call previous completion to avoid flickers.
     call s:Complete()
     call s:RequestCompletion()
+
+    call s:UpdateSignatureHelp()
+    call s:RequestSignatureHelp()
   endif
 
   exec s:python_command "ycm_state.OnCursorMoved()"
@@ -765,6 +821,8 @@ function! s:OnInsertLeave()
         \ g:ycm_autoclose_preview_window_after_insertion
     call s:ClosePreviewWindowIfNeeded()
   endif
+
+  call s:ClearSignatureHelp()
 endfunction
 
 
@@ -868,6 +926,38 @@ function! s:PollCompletion( ... )
 endfunction
 
 
+function! s:ShouldUseSignatureHelp()
+  return s:Pyeval( 'vimsupport.VimSupportsPopupWindows()' )
+endfunction
+
+
+function! s:RequestSignatureHelp()
+  if !s:ShouldUseSignatureHelp()
+    return
+  endif
+
+  exec s:python_command "ycm_state.SendSignatureHelpRequest()"
+  call s:PollSignatureHelp()
+endfunction
+
+
+function! s:PollSignatureHelp( ... )
+  if !s:ShouldUseSignatureHelp()
+    return
+  endif
+
+  if !s:Pyeval( 'ycm_state.SignatureHelpRequestReady()' )
+    let s:pollers.signature_help.id = timer_start(
+          \ s:pollers.signature_help.wait_milliseconds,
+          \ function( 's:PollSignatureHelp' ) )
+    return
+  endif
+
+  let s:signature_help = s:Pyeval( 'ycm_state.GetSignatureHelpResponse()' )
+  call s:UpdateSignatureHelp()
+endfunction
+
+
 function! s:Complete()
   " Do not call user's completion function if the start column is after the
   " current column or if there are no candidates. Close the completion menu
@@ -885,6 +975,8 @@ function! s:Complete()
     " text until he explicitly chooses to replace it with a completion.
     call s:SendKeys( "\<C-X>\<C-U>\<C-P>" )
   endif
+  " Displaying or hiding the PUM might mean we need to hide the sig help
+  call s:UpdateSignatureHelp()
 endfunction
 
 
@@ -912,6 +1004,27 @@ function! youcompleteme#CompleteFunc( findstart, base )
 endfunction
 
 
+function! s:UpdateSignatureHelp()
+  if !s:ShouldUseSignatureHelp()
+    return
+  endif
+
+  call s:Pyeval(
+        \ 'ycm_state.UpdateSignatureHelp( vim.eval( "s:signature_help" ) )' )
+endfunction
+
+
+function! s:ClearSignatureHelp()
+  if !s:ShouldUseSignatureHelp()
+    return
+  endif
+
+  call timer_stop( s:pollers.signature_help.id )
+  let s:signature_help = s:default_signature_help
+  call s:Pyeval( 'ycm_state.ClearSignatureHelp()' )
+endfunction
+
+
 function! youcompleteme#ServerPid()
   return s:Pyeval( 'ycm_state.ServerPid()' )
 endfunction
@@ -951,6 +1064,8 @@ function! s:RestartServer()
   call timer_stop( s:pollers.receive_messages.id )
   let s:pollers.receive_messages.id = -1
 
+  call s:ClearSignatureHelp()
+
   call timer_stop( s:pollers.server_ready.id )
   let s:pollers.server_ready.id = timer_start(
         \ s:pollers.server_ready.wait_milliseconds,

+ 15 - 4
python/ycm/client/base_request.py

@@ -107,13 +107,22 @@ class BaseRequest( object ):
                           handler,
                           timeout = _READ_TIMEOUT_SEC,
                           display_message = True,
-                          truncate_message = False ):
+                          truncate_message = False,
+                          payload = None ):
     return self.HandleFuture(
-        BaseRequest._TalkToHandlerAsync( '', handler, 'GET', timeout ),
+        self.GetDataFromHandlerAsync( handler, timeout, payload ),
         display_message,
         truncate_message )
 
 
+  def GetDataFromHandlerAsync( self,
+                               handler,
+                               timeout = _READ_TIMEOUT_SEC,
+                               payload = None ):
+    return BaseRequest._TalkToHandlerAsync(
+        '', handler, 'GET', timeout, payload )
+
+
   # This is the blocking version of the method. See below for async.
   # |timeout| is num seconds to tolerate no response from server before giving
   # up; see Requests docs for details (we just pass the param along).
@@ -147,7 +156,8 @@ class BaseRequest( object ):
   def _TalkToHandlerAsync( data,
                            handler,
                            method,
-                           timeout = _READ_TIMEOUT_SEC ):
+                           timeout = _READ_TIMEOUT_SEC,
+                           payload = None ):
     request_uri = _BuildUri( handler )
     if method == 'POST':
       sent_data = _ToUtf8Json( data )
@@ -169,7 +179,8 @@ class BaseRequest( object ):
     return BaseRequest.Session().get(
       request_uri,
       headers = headers,
-      timeout = ( _CONNECT_TIMEOUT_SEC, timeout ) )
+      timeout = ( _CONNECT_TIMEOUT_SEC, timeout ),
+      params = payload )
 
 
   @staticmethod

+ 106 - 0
python/ycm/client/signature_help_request.py

@@ -0,0 +1,106 @@
+# Copyright (C) 2019 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 <http://www.gnu.org/licenses/>.
+
+from __future__ import unicode_literals
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+# Not installing aliases from python-future; it's unreliable and slow.
+from builtins import *  # noqa
+
+import logging
+from ycm.client.base_request import ( BaseRequest, DisplayServerException,
+                                      MakeServerException )
+
+_logger = logging.getLogger( __name__ )
+
+
+class SigHelpAvailableByFileType( dict ):
+  def __missing__( self, filetype ):
+    request = SignatureHelpAvailableRequest( filetype )
+    self[ filetype ] = request
+    return request
+
+
+class SignatureHelpRequest( BaseRequest ):
+  def __init__( self, request_data ):
+    super( SignatureHelpRequest, self ).__init__()
+    self.request_data = request_data
+    self._response_future = None
+
+
+  def Start( self ):
+    self._response_future = self.PostDataToHandlerAsync( self.request_data,
+                                                         'signature_help' )
+
+
+  def Done( self ):
+    return bool( self._response_future ) and self._response_future.done()
+
+
+  def Reset( self ):
+    self._response_future = None
+
+
+  def Response( self ):
+    if not self._response_future:
+      return {}
+
+    response = self.HandleFuture( self._response_future,
+                                  truncate_message = True )
+    if not response:
+      return {}
+
+    # Vim may not be able to convert the 'errors' entry to its internal format
+    # so we remove it from the response.
+    errors = response.pop( 'errors', [] )
+    for e in errors:
+      exception = MakeServerException( e )
+      _logger.error( exception )
+      DisplayServerException( exception, truncate_message = True )
+
+    return response.get( 'signature_help' ) or {}
+
+
+class SignatureHelpAvailableRequest( BaseRequest ):
+  def __init__( self, filetype ):
+    super( SignatureHelpAvailableRequest, self ).__init__()
+    self._response_future = None
+    self.Start( filetype )
+
+
+  def Done( self ):
+    return bool( self._response_future ) and self._response_future.done()
+
+
+  def Response( self ):
+    if not self._response_future:
+      return None
+
+    response = self.HandleFuture( self._response_future,
+                                  truncate_message = True )
+
+    if not response:
+      return None
+
+    return response[ 'available' ]
+
+
+  def Start( self, filetype ):
+    self._response_future = self.GetDataFromHandlerAsync(
+      'signature_help_available',
+      payload = { 'subserver': filetype } )

+ 191 - 0
python/ycm/signature_help.py

@@ -0,0 +1,191 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+from __future__ import unicode_literals
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+# Not installing aliases from python-future; it's unreliable and slow.
+from builtins import *  # noqa
+
+import vim
+import json
+from ycm import vimsupport
+from ycm.vimsupport import GetIntValue
+
+
+class SignatureHelpState( object ):
+  ACTIVE = 'ACTIVE'
+  INACTIVE = 'INACTIVE'
+
+  def __init__( self,
+                popup_win_id = None,
+                state = INACTIVE ):
+    self.popup_win_id = popup_win_id
+    self.state = state
+    self.anchor = None
+
+
+def _MakeSignatureHelpBuffer( signature_info ):
+  active_signature = int( signature_info.get( 'activeSignature', 0 ) )
+  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' ]
+    if sig_index == active_signature:
+      props.append( {
+        'col': 1,
+        'length': len( sig_label ),
+        'type': 'YCM-signature-help-current-signature'
+      } )
+    else:
+      props.append( {
+        'col': 1,
+        'length': len( sig_label ),
+        'type': 'YCM-signature-help-signature'
+      } )
+
+    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
+
+
+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( "popup_close( {} )".format( state.popup_win_id ) )
+    return SignatureHelpState( None, SignatureHelpState.INACTIVE )
+
+  if state.state != SignatureHelpState.ACTIVE:
+    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( vim.eval( '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( "popup_close( {} )".format( 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,
+    "flip": 1,
+    "padding": [ 0, 1, 0, 1 ], # Pad 1 char in X axis to match completion menu
+  }
+
+  if not state.popup_win_id:
+    state.popup_win_id = GetIntValue( vim.eval( "popup_create( {}, {} )".format(
+      json.dumps( buf_lines ),
+      json.dumps( options ) ) ) )
+  else:
+    vim.eval( 'popup_settext( {}, {} )'.format(
+      state.popup_win_id,
+      json.dumps( buf_lines ) ) )
+
+  # Should do nothing if already visible
+  vim.eval( 'popup_move( {}, {} )'.format( state.popup_win_id,
+                                           json.dumps( options ) ) )
+  vim.eval( 'popup_show( {} )'.format( state.popup_win_id ) )
+
+  return state

+ 2 - 1
python/ycm/tests/__init__.py

@@ -59,7 +59,8 @@ DEFAULT_CLIENT_OPTIONS = {
   'g:ycm_semantic_triggers': {},
   'g:ycm_filetype_specific_completion_to_disable': { 'gitcommit': 1 },
   'g:ycm_max_num_candidates': 50,
-  'g:ycm_max_diagnostics_to_display': 30
+  'g:ycm_max_diagnostics_to_display': 30,
+  'g:ycm_disable_signature_help': 0,
 }
 
 

+ 45 - 0
python/ycm/tests/signature_help_test.py

@@ -0,0 +1,45 @@
+# coding: utf-8
+#
+# Copyright (C) 2019 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 <http://www.gnu.org/licenses/>.
+
+# Intentionally not importing unicode_literals!
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+# Not installing aliases from python-future; it's unreliable and slow.
+from builtins import *  # noqa
+
+from hamcrest import ( assert_that,
+                       empty )
+from ycm import signature_help as sh
+
+
+def MakeSignatureHelpBuffer_Empty_test():
+  assert_that( sh._MakeSignatureHelpBuffer( {} ), empty() )
+  assert_that( sh._MakeSignatureHelpBuffer( {
+    'activeSignature': 0,
+    'activeParameter': 0,
+    'signatures': []
+  } ), empty() )
+  assert_that( sh._MakeSignatureHelpBuffer( {
+    'activeSignature': 0,
+    'activeParameter': 0,
+  } ), empty() )
+  assert_that( sh._MakeSignatureHelpBuffer( {
+    'signatures': []
+  } ), empty() )

+ 28 - 0
python/ycm/vimsupport.py

@@ -1258,3 +1258,31 @@ def AutoCloseOnCurrentBuffer( name ):
                'if bufnr( "%" ) == expand( "<abuf>" ) | q | endif '
                '| autocmd! {}'.format( name ) )
   vim.command( 'augroup END' )
+
+
+def VimSupportsPopupWindows():
+  return VimHasFunctions( 'popup_create',
+                          'popup_move',
+                          'popup_hide',
+                          'popup_settext',
+                          'popup_show',
+                          'popup_close',
+                          'prop_add',
+                          'prop_type_add' )
+
+
+def VimHasFunctions( *functions ):
+  return all( ( bool( GetIntValue( vim.eval( 'exists( "*{}" )'.format( f ) ) ) )
+                for f in functions ) )
+
+
+def WinIDForWindow( window ):
+  return GetIntValue( 'win_getid( {}, {} )'.format( window.number,
+                                                    window.tabpage.number ) )
+
+
+def ScreenPositionForLineColumnInWindow( window, line, column ):
+  return vim.eval( 'screenpos( {}, {}, {} )'.format(
+      WinIDForWindow( window ),
+      line,
+      column ) )

+ 64 - 1
python/ycm/youcompleteme.py

@@ -31,7 +31,7 @@ import signal
 import vim
 from subprocess import PIPE
 from tempfile import NamedTemporaryFile
-from ycm import base, paths, vimsupport
+from ycm import base, paths, signature_help, vimsupport
 from ycm.buffer import ( BufferDict,
                          DIAGNOSTIC_UI_FILETYPES,
                          DIAGNOSTIC_UI_ASYNC_FILETYPES )
@@ -44,6 +44,8 @@ from ycm.client.base_request import BaseRequest, BuildRequestData
 from ycm.client.completer_available_request import SendCompleterAvailableRequest
 from ycm.client.command_request import SendCommandRequest
 from ycm.client.completion_request import CompletionRequest
+from ycm.client.signature_help_request import SignatureHelpRequest
+from ycm.client.signature_help_request import SigHelpAvailableByFileType
 from ycm.client.debug_info_request import ( SendDebugInfoRequest,
                                             FormatDebugInfoResponse )
 from ycm.client.omni_completion_request import OmniCompletionRequest
@@ -113,6 +115,9 @@ class YouCompleteMe( object ):
     self._omnicomp = None
     self._buffers = None
     self._latest_completion_request = None
+    self._latest_signature_help_request = None
+    self._signature_help_available_requests = SigHelpAvailableByFileType()
+    self._signature_help_state = signature_help.SignatureHelpState()
     self._logger = logging.getLogger( 'ycm' )
     self._client_logfile = None
     self._server_stdout = None
@@ -292,6 +297,7 @@ class YouCompleteMe( object ):
   def SendCompletionRequest( self, force_semantic = False ):
     request_data = BuildRequestData()
     request_data[ 'force_semantic' ] = force_semantic
+
     if not self.NativeFiletypeCompletionUsable():
       wrapped_request_data = RequestWrap( request_data )
       if self._omnicomp.ShouldUseNow( wrapped_request_data ):
@@ -317,6 +323,58 @@ class YouCompleteMe( object ):
     return response
 
 
+  def SendSignatureHelpRequest( self ):
+    filetype = vimsupport.CurrentFiletypes()[ 0 ]
+    if not self._signature_help_available_requests[ filetype ].Done():
+      return
+
+    sig_help_available = self._signature_help_available_requests[
+        filetype ].Response()
+    if sig_help_available == 'NO':
+      return
+
+    if sig_help_available == 'PENDING':
+      # Send another /signature_help_available request
+      self._signature_help_available_requests[ filetype ].Start( filetype )
+      return
+
+    if not self.NativeFiletypeCompletionUsable():
+      return
+
+    if not self._latest_completion_request:
+      return
+
+    request_data = self._latest_completion_request.request_data.copy()
+    request_data[ 'signature_help_state' ] = self._signature_help_state.state
+
+    self._AddExtraConfDataIfNeeded( request_data )
+
+    self._latest_signature_help_request = SignatureHelpRequest(
+      request_data )
+    self._latest_signature_help_request.Start()
+
+
+  def SignatureHelpRequestReady( self ):
+    return bool( self._latest_signature_help_request and
+                 self._latest_signature_help_request.Done() )
+
+
+  def GetSignatureHelpResponse( self ):
+    return self._latest_signature_help_request.Response()
+
+
+  def ClearSignatureHelp( self ):
+    self.UpdateSignatureHelp( {} )
+    if self._latest_signature_help_request:
+      self._latest_signature_help_request.Reset()
+
+
+  def UpdateSignatureHelp( self, signature_info ):
+    self._signature_help_state = signature_help.UpdateSignatureHelp(
+      self._signature_help_state,
+      signature_info )
+
+
   def SendCommandRequest( self,
                           arguments,
                           modifiers,
@@ -470,6 +528,11 @@ class YouCompleteMe( object ):
 
 
   def OnBufferVisit( self ):
+    filetype = vimsupport.CurrentFiletypes()[ 0 ]
+    # The constructor of dictionary values starts the request,
+    # so the line below fires a new request only if the dictionary
+    # value is accessed for the first time.
+    self._signature_help_available_requests[ filetype ].Done()
     extra_data = {}
     self._AddUltiSnipsDataIfNeeded( extra_data )
     SendEventNotificationAsync( 'BufferVisit', extra_data = extra_data )

+ 48 - 0
test/completion.test.vim

@@ -0,0 +1,48 @@
+function! SetUp()
+  let g:ycm_use_clangd = 1
+  let g:ycm_confirm_extra_conf = 0
+  let g:ycm_auto_trigger = 1
+  let g:ycm_keep_logfiles = 1
+  let g:ycm_log_level = 'DEBUG'
+
+  call youcompleteme#test#setup#SetUp()
+endfunction
+
+function! TearDown()
+  call youcompleteme#test#setup#CleanUp()
+endfunction
+
+function! Test_Compl_After_Trigger()
+  call youcompleteme#test#setup#OpenFile(
+        \ '/third_party/ycmd/ycmd/tests/clangd/testdata/basic.cpp' )
+
+  call setpos( '.', [ 0, 11, 6 ] )
+
+  " Required to trigger TextChangedI
+  " https://github.com/vim/vim/issues/4665#event-2480928194
+  call test_override( 'char_avail', 1 )
+
+  " Must do the checks in a timer callback because we need to stay in insert
+  " mode until done.
+  function! Check( id ) closure
+    call WaitForAssert( {->
+          \ assert_true( pyxeval( 'ycm_state.GetCurrentCompletionRequest() is not None' ) )
+          \ } )
+    call WaitForAssert( {->
+          \ assert_true( pyxeval( 'ycm_state.CompletionRequestReady()' ) )
+          \ } )
+    redraw
+    call WaitForAssert( {->
+          \ assert_true( pumvisible(), 'pumvisible()' )
+          \ }, 10000 )
+    call feedkeys( "\<ESC>" )
+  endfunction
+
+  call timer_start( 500, funcref( 'Check' ) )
+  call feedkeys( 'cl.', 'ntx!' )
+  " Checks run in insert mode, then exit insert mode.
+  call assert_false( pumvisible(), 'pumvisible()' )
+
+  call test_override( 'ALL', 0 )
+  %bwipeout!
+endfunctio

+ 9 - 9
test/lib/autoload/youcompleteme/test/setup.vim

@@ -1,9 +1,14 @@
 
-function! youcompleteme#test#setup#SetUp( setup_f ) abort
+function! youcompleteme#test#setup#SetUp() abort
   if exists ( 'g:loaded_youcompleteme' )
     unlet g:loaded_youcompleteme
   endif
 
+  if pyxeval( "'ycm_state' in globals()" )
+    pyx ycm_state.OnVimLeave()
+    pyx del ycm_state
+  endif
+
   source $PWD/vimrc
 
   " This is a bit of a hack
@@ -20,13 +25,6 @@ function! youcompleteme#test#setup#SetUp( setup_f ) abort
 endfunction
 
 function! youcompleteme#test#setup#CleanUp() abort
-  pyx <<EOF
-
-if 'ycm_state' in globals():
-  ycm_state.OnVimLeave()
-  del ycm_state
-
-EOF
 endfunction
 
 function! youcompleteme#test#setup#OpenFile( f ) abort
@@ -40,7 +38,9 @@ function! youcompleteme#test#setup#OpenFile( f ) abort
         \ } )
 
   " Need to wait for the server to be ready. The best way to do this is to
-  " force compile and diagnostics
+  " force compile and diagnostics, though this only works for the c-based
+  " completers. For python and others, we actually need to parse the debug info
+  " to check the server state.
   YcmForceCompileAndDiagnostics
 
   " Sometimes, that's just not enough to ensure stuff works

+ 472 - 31
test/signature_help.test.vim

@@ -1,3 +1,68 @@
+let s:timer_interval = 2000
+
+function! s:_ClearSigHelp()
+  pythonx _sh_state = sh.UpdateSignatureHelp( _sh_state, {} )
+  call assert_true( pyxeval( '_sh_state.popup_win_id is None' ),
+        \ 'win id none with emtpy' )
+  unlet! s:popup_win_id
+endfunction
+
+function s:_GetSigHelpWinID()
+  call WaitForAssert( {->
+        \   assert_true(
+        \     pyxeval(
+        \       'ycm_state.SignatureHelpRequestReady()'
+        \     ),
+        \     'sig help request reqdy'
+        \   )
+        \ } )
+  call WaitForAssert( {->
+        \   assert_true(
+        \     pyxeval(
+        \       'ycm_state._signature_help_state.popup_win_id is not None'
+        \     ),
+        \     'popup_win_id'
+        \   )
+        \ } )
+  let s:popup_win_id = pyxeval( 'ycm_state._signature_help_state.popup_win_id' )
+  return s:popup_win_id
+endfunction
+
+function! s:_CheckPopupPosition( winid, pos )
+  redraw
+  let actual_pos = popup_getpos( a:winid )
+  let ret = 0
+  if a:pos->empty()
+    return assert_true( actual_pos->empty(), 'popup pos empty' )
+  endif
+  for c in keys( a:pos )
+    if !has_key( actual_pos, c )
+      let ret += 1
+      call assert_report( 'popup with ID '
+                        \ . string( a:winid )
+                        \ . ' has no '
+                        \ . c
+                        \ . ' in: '
+                        \ . string( actual_pos ) )
+    else
+      let ret += assert_equal( a:pos[ c ],
+                             \ actual_pos[ c ],
+                             \ c . ' in: ' . string( actual_pos ) )
+    endif
+  endfor
+  return ret
+endfunction
+
+function! s:_CheckSigHelpAtPos( sh, cursor, pos )
+  call setpos( '.', [ 0 ] + a:cursor )
+  redraw
+  pythonx _sh_state = sh.UpdateSignatureHelp( _sh_state,
+                                            \ vim.eval( 'a:sh' ) )
+  redraw
+  let winid = pyxeval( '_sh_state.popup_win_id' )
+  call s:_CheckPopupPosition( winid, a:pos )
+endfunction
+
 function! SetUp()
   let g:ycm_use_clangd = 1
   let g:ycm_confirm_extra_conf = 0
@@ -5,10 +70,13 @@ function! SetUp()
   let g:ycm_keep_logfiles = 1
   let g:ycm_log_level = 'DEBUG'
 
-  call youcompleteme#test#setup#SetUp( v:none )
+  call youcompleteme#test#setup#SetUp()
+  pythonx from ycm import signature_help as sh
+  pythonx _sh_state = sh.SignatureHelpState()
 endfunction
 
-function! ClearDown()
+function! TearDown()
+  call s:_ClearSigHelp()
   call youcompleteme#test#setup#CleanUp()
 endfunction
 
@@ -36,91 +104,464 @@ endfunction
 "   %bwipeout!
 " endfunction
 
-function! Test_Compl_After_Trigger()
+function! Test_Enough_Screen_Space()
+  call assert_true( &lines >= 25,
+                  \ &lines . " is not enough rows. need 25." )
+  call assert_true( &columns >= 80,
+                  \ &columns . " is not enough columns. need 80." )
+endfunction
+
+function! Test_Signatures_After_Trigger()
   call youcompleteme#test#setup#OpenFile(
-        \ '/third_party/ycmd/ycmd/tests/clangd/testdata/basic.cpp' )
+        \ '/third_party/ycmd/ycmd/tests/clangd/testdata/general_fallback'
+        \ . '/make_drink.cc' )
 
-  call setpos( '.', [ 0, 11, 6 ] )
+  call setpos( '.', [ 0, 7, 13 ] )
 
   " Required to trigger TextChangedI
   " https://github.com/vim/vim/issues/4665#event-2480928194
   call test_override( 'char_avail', 1 )
 
   " Must do the checks in a timer callback because we need to stay in insert
-  " mode until done.
+  " mode until done. Use a func because it's big enough (a lambda is a little
+  " neater in many contexts).
   function! Check( id ) closure
     call WaitForAssert( {->
-          \ assert_true( pyxeval( 'ycm_state.GetCurrentCompletionRequest() is not None' ) )
+          \   assert_true(
+          \     pyxeval(
+          \       'ycm_state.SignatureHelpRequestReady()'
+          \     ),
+          \     'sig help request reqdy'
+          \   )
           \ } )
     call WaitForAssert( {->
-          \ assert_true( pyxeval( 'ycm_state.CompletionRequestReady()' ) )
+          \   assert_true(
+          \     pyxeval(
+          \       "bool( ycm_state.GetSignatureHelpResponse()[ 'signatures' ] )"
+          \     ),
+          \     'sig help request reqdy'
+          \   )
+          \ } )
+    call WaitForAssert( {->
+          \   assert_true(
+          \     pyxeval(
+          \       'ycm_state._signature_help_state.popup_win_id is not None'
+          \     ),
+          \     'popup_win_id'
+          \   )
           \ } )
-    call assert_true( pumvisible(), 'pumvisible()' )
+
+    let popup_win_id = pyxeval( 'ycm_state._signature_help_state.popup_win_id' )
+    let pos = win_screenpos( popup_win_id )
+    call assert_false( pos == [ 0, 0 ] )
+
+    " Exit insert mode to ensure the test continues
+    call test_override( 'ALL', 0 )
     call feedkeys( "\<ESC>" )
   endfunction
 
-  call timer_start( 100, funcref( 'Check' ) )
-  call feedkeys( 'cl.', 'ntx!' )
-  " Checks run in insert mode, then exit insert mode.
+  call assert_false( pyxeval( 'ycm_state.SignatureHelpRequestReady()' ) )
+  call timer_start( s:timer_interval, funcref( 'Check' ) )
+  call feedkeys( 'cl(', 'ntx!' )
   call assert_false( pumvisible(), 'pumvisible()' )
 
+  call WaitForAssert( {->
+        \   assert_true(
+        \     pyxeval(
+        \       'ycm_state._signature_help_state.popup_win_id is None'
+        \     ),
+        \     'popup_win_id'
+        \   )
+        \ } )
+
   call test_override( 'ALL', 0 )
   %bwipeout!
-endfunctio
+  delfunc! Check
+endfunction
 
-function! Test_Signatures_After_Trigger()
+function! Test_Signatures_With_PUM_NoSigns()
   call youcompleteme#test#setup#OpenFile(
         \ '/third_party/ycmd/ycmd/tests/clangd/testdata/general_fallback'
         \ . '/make_drink.cc' )
 
+  " Make sure that error signs don't shift the window
+  setlocal signcolumn=no
+
   call setpos( '.', [ 0, 7, 13 ] )
 
   " Required to trigger TextChangedI
   " https://github.com/vim/vim/issues/4665#event-2480928194
   call test_override( 'char_avail', 1 )
 
+  function Check2( id ) closure
+    call WaitForAssert( {-> assert_true( pumvisible() ) } )
+    call WaitForAssert( {-> assert_notequal( [], complete_info().items ) } )
+    call assert_equal( 7, pum_getpos().row )
+    redraw
+
+    " NOTE: anchor is 0-based
+    call assert_equal( 6,
+                     \ pyxeval( 'ycm_state._signature_help_state.anchor[0]' ) )
+    call assert_equal( 13,
+                     \ pyxeval( 'ycm_state._signature_help_state.anchor[1]' ) )
+
+
+    " Popup is shifted due to 80 column screen
+    call s:_CheckPopupPosition( s:_GetSigHelpWinID(),
+                              \ { 'line': 5, 'col': 5 } )
+
+    call test_override( 'ALL', 0 )
+    call feedkeys( "\<ESC>", 't' )
+  endfunction
+
   " Must do the checks in a timer callback because we need to stay in insert
   " mode until done.
   function! Check( id ) closure
-    redraw
     call WaitForAssert( {->
           \   assert_true(
           \     pyxeval(
-          \       'ycm_state.SignatureHelpRequestReady()'
+          \       'ycm_state._signature_help_state.popup_win_id is not None'
           \     ),
-          \     'sign help request reqdy'
+          \     'popup_win_id'
           \   )
           \ } )
+    " Popup is shifted left due to 80 char screen
+    call s:_CheckPopupPosition( s:_GetSigHelpWinID(),
+                              \ { 'line': 5, 'col': 5 } )
+
+    call timer_start( s:timer_interval, funcref( 'Check2' ) )
+    call feedkeys( ' TypeOfD', 't' )
+  endfunction
+
+  call assert_false( pyxeval( 'ycm_state.SignatureHelpRequestReady()' ) )
+  call timer_start( s:timer_interval, funcref( 'Check' ) )
+  call feedkeys( 'C(', 'ntx!' )
+
+  call WaitForAssert( {->
+        \   assert_true(
+        \     pyxeval(
+        \       'ycm_state._signature_help_state.popup_win_id is None'
+        \     ),
+        \     'popup_win_id'
+        \   )
+        \ } )
+
+  call test_override( 'ALL', 0 )
+  %bwipeout!
+  delfunc! Check
+  delfunc! Check2
+endfunction
+
+function! Test_Signatures_With_PUM_Signs()
+  call youcompleteme#test#setup#OpenFile(
+        \ '/third_party/ycmd/ycmd/tests/clangd/testdata/general_fallback'
+        \ . '/make_drink.cc' )
+
+  " Make sure that sign causes the popup to shift
+  setlocal signcolumn=auto
+
+  call setpos( '.', [ 0, 7, 13 ] )
+
+  " Required to trigger TextChangedI
+  " https://github.com/vim/vim/issues/4665#event-2480928194
+  call test_override( 'char_avail', 1 )
+
+  function Check2( id ) closure
+    call WaitForAssert( {-> assert_true( pumvisible() ) } )
+    call WaitForAssert( {-> assert_notequal( [], complete_info().items ) } )
+    call assert_equal( 7, pum_getpos().row )
+    redraw
+
+    " NOTE: anchor is 0-based
+    call assert_equal( 6,
+                     \ pyxeval( 'ycm_state._signature_help_state.anchor[0]' ) )
+    call assert_equal( 13,
+                     \ pyxeval( 'ycm_state._signature_help_state.anchor[1]' ) )
+
+
+    " Sign column is shown, popup shifts to the right 2 screen columns
+    " Then shifts back due to 80 character screen width
+    " FIXME: This test was supposed to show the shifting right. Write another
+    " one which uses a much smaller popup to do that.
+    call s:_CheckPopupPosition( s:_GetSigHelpWinID(),
+                              \ { 'line': 5, 'col': 5 } )
+
+    call test_override( 'ALL', 0 )
+    call feedkeys( "\<ESC>", 't' )
+  endfunction
+
+  " Must do the checks in a timer callback because we need to stay in insert
+  " mode until done.
+  function! Check( id ) closure
     call WaitForAssert( {->
           \   assert_true(
           \     pyxeval(
-          \       "bool( ycm_state.GetSignatureHelpResponse()[ 'signatures' ] )"
+          \       'ycm_state._signature_help_state.popup_win_id is not None'
           \     ),
-          \     'sign help request reqdy'
+          \     'popup_win_id'
           \   )
           \ } )
+    " Popup is shifted left due to 80 char screen
+    call s:_CheckPopupPosition( s:_GetSigHelpWinID(),
+                              \ { 'line': 5, 'col': 5 } )
+
+    call timer_start( s:timer_interval, funcref( 'Check2' ) )
+    call feedkeys( ' TypeOfD', 't' )
+  endfunction
+
+  call assert_false( pyxeval( 'ycm_state.SignatureHelpRequestReady()' ) )
+  call timer_start( s:timer_interval, funcref( 'Check' ) )
+  call feedkeys( 'C(', 'ntx!' )
+
+  call WaitForAssert( {->
+        \   assert_true(
+        \     pyxeval(
+        \       'ycm_state._signature_help_state.popup_win_id is None'
+        \     ),
+        \     'popup_win_id'
+        \   )
+        \ } )
+
+  call test_override( 'ALL', 0 )
+  %bwipeout!
+  delfunc! Check
+  delfunc! Check2
+endfunction
+
+function! Test_Placement_Simple()
+  call assert_true( &lines >= 25, "Enough rows" )
+  call assert_true( &columns >= 25, "Enough columns" )
+
+  let X = join( map( range( 0, &columns - 1 ), {->'X'} ), '' )
+
+  for i in range( 0, &lines )
+    call append( line('$'), X )
+  endfor
+
+  " Delete the blank line that is always added to a buffer
+  0delete
+
+  call s:_ClearSigHelp()
+
+  let v_sh = {
+        \   'activeSignature': 0,
+        \   'activeParameter': 0,
+        \   'signatures': [
+        \     { 'label': 'test function', 'parameters': [] }
+        \   ]
+        \ }
+
+  " When displayed in the middle with plenty of space
+  call s:_CheckSigHelpAtPos( v_sh, [ 10, 3 ], {
+        \ 'line': 9,
+        \ 'col': 1
+        \ } )
+  " Confirm that anchoring works (i.e. it doesn't move!)
+  call s:_CheckSigHelpAtPos( v_sh, [ 20, 10 ], {
+        \ 'line': 9,
+        \ 'col': 1
+        \ } )
+  call s:_ClearSigHelp()
+
+  " Window slides from left of screen
+  call s:_CheckSigHelpAtPos( v_sh, [ 10, 2 ], {
+        \ 'line': 9,
+        \ 'col': 1,
+        \ } )
+  call s:_ClearSigHelp()
+
+  " Window slides from left of screen
+  call s:_CheckSigHelpAtPos( v_sh, [ 10, 1 ], {
+        \ 'line': 9,
+        \ 'col': 1,
+        \ } )
+  call s:_ClearSigHelp()
+
+  " Cursor at top-left of window
+  call s:_CheckSigHelpAtPos( v_sh, [ 1, 1 ], {
+        \ 'line': 2,
+        \ 'col': 1,
+        \ } )
+  call s:_ClearSigHelp()
+
+  " Cursor at top-right of window
+  call s:_CheckSigHelpAtPos( v_sh, [ 1, &columns ], {
+        \ 'line': 2,
+        \ 'col': &columns - len( "test function" ) - 1,
+        \ } )
+  call s:_ClearSigHelp()
+
+  " Bottom-left of window
+  call s:_CheckSigHelpAtPos( v_sh, [ &lines + 1, 1 ], {
+        \ 'line': &lines - 2,
+        \ 'col': 1,
+        \ } )
+  call s:_ClearSigHelp()
+
+  " Bottom-right of window
+  call s:_CheckSigHelpAtPos( v_sh, [ &lines + 1, &columns ], {
+        \ 'line': &lines - 2,
+        \ 'col': &columns - len( "test function" ) - 1,
+        \ } )
+  call s:_ClearSigHelp()
+
+  call popup_clear()
+  %bwipeout!
+endfunction
+
+function! Test_Placement_MultiLine()
+  call assert_true( &lines >= 25, "Enough rows" )
+  call assert_true( &columns >= 25, "Enough columns" )
+
+  let X = join( map( range( 0, &columns - 1 ), {->'X'} ), '' )
+
+  for i in range( 0, &lines )
+    call append( line('$'), X )
+  endfor
+
+  " Delete the blank line that is always added to a buffer
+  0delete
+
+  call s:_ClearSigHelp()
+
+  let v_sh = {
+        \   'activeSignature': 0,
+        \   'activeParameter': 0,
+        \   'signatures': [
+        \     { 'label': 'test function', 'parameters': [] },
+        \     { 'label': 'toast function', 'parameters': [
+        \         { 'label': [ 0, 5 ] }
+        \     ] },
+        \   ]
+        \ }
+
+  " When displayed in the middle with plenty of space
+  call s:_CheckSigHelpAtPos( v_sh, [ 10, 3 ], {
+        \ 'line': 8,
+        \ 'col': 1
+        \ } )
+  " Confirm that anchoring works (i.e. it doesn't move!)
+  call s:_CheckSigHelpAtPos( v_sh, [ 20, 10 ], {
+        \ 'line': 8,
+        \ 'col': 1
+        \ } )
+  call s:_ClearSigHelp()
+
+  " Window slides from left of screen
+  call s:_CheckSigHelpAtPos( v_sh, [ 10, 2 ], {
+        \ 'line': 8,
+        \ 'col': 1,
+        \ } )
+  call s:_ClearSigHelp()
+
+  " Window slides from left of screen
+  call s:_CheckSigHelpAtPos( v_sh, [ 10, 1 ], {
+        \ 'line': 8,
+        \ 'col': 1,
+        \ } )
+  call s:_ClearSigHelp()
+
+  " Cursor at top-left of window
+  call s:_CheckSigHelpAtPos( v_sh, [ 1, 1 ], {
+        \ 'line': 2,
+        \ 'col': 1,
+        \ } )
+  call s:_ClearSigHelp()
+
+  " Cursor at top-right of window
+  call s:_CheckSigHelpAtPos( v_sh, [ 1, &columns ], {
+        \ 'line': 2,
+        \ 'col': &columns - len( "toast function" ) - 1,
+        \ } )
+  call s:_ClearSigHelp()
+
+  " Bottom-left of window
+  call s:_CheckSigHelpAtPos( v_sh, [ &lines + 1, 1 ], {
+        \ 'line': &lines - 3,
+        \ 'col': 1,
+        \ } )
+  call s:_ClearSigHelp()
+
+  " Bottom-right of window
+  call s:_CheckSigHelpAtPos( v_sh, [ &lines + 1, &columns ], {
+        \ 'line': &lines - 3,
+        \ 'col': &columns - len( "toast function" ) - 1,
+        \ } )
+  call s:_ClearSigHelp()
+
+  call popup_clear()
+  %bwipeout!
+endfunction
+
+function! Test_Signatures_TopLine()
+  call youcompleteme#test#setup#OpenFile( 'test/testdata/python/test.py' )
+  call setpos( '.', [ 0, 1, 24 ] )
+  call test_override( 'char_avail', 1 )
+
+  function! Check( id ) closure
+    call s:_CheckPopupPosition( s:_GetSigHelpWinID(), { 'line': 2, 'col': 23 } )
+    call test_override( 'ALL', 0 )
+    call feedkeys( "\<ESC>" )
+  endfunction
+
+  call timer_start( s:timer_interval, funcref( 'Check' ) )
+  call feedkeys( 'cl(', 'ntx!' )
+
+  call test_override( 'ALL', 0 )
+  %bwipeout!
+  delfun! Check
+endfunction
+
+function! Test_Signatures_TopLineWithPUM()
+  call youcompleteme#test#setup#OpenFile( 'test/testdata/python/test.py' )
+  call setpos( '.', [ 0, 1, 24 ] )
+  call test_override( 'char_avail', 1 )
+
+  function! CheckSigHelpAndTriggerCompletion( id ) closure
+    " Popup placed below the cursor
+    call s:_CheckPopupPosition( s:_GetSigHelpWinID(), { 'line': 2, 'col': 23 } )
+
+    " Push more characters into the typeahead buffer to trigger insert mode
+    " completion.
+    "
+    " Nte for some reason the first semantic response can take quite some time,
+    " and if our timer fires before then, the test just fails. so we take 2
+    " seconds here.
+    call timer_start( s:timer_interval,
+                    \ funcref( 'CheckCompletionVisibleAndSigHelpHidden' ) )
+    call feedkeys( " os.", 't' )
+  endfunction
+
+  function! CheckCompletionVisibleAndSigHelpHidden( id ) closure
+    " Completion popup now visible, overlapping where the sig help popup was
+    redraw
+    call WaitForAssert( {-> assert_true( pumvisible() ) } )
+    call assert_equal( 1, get( pum_getpos(), 'row', -1 ) )
+    call assert_equal( 28, get( pum_getpos(), 'col', -1 ) )
+    " so we hide the sig help popup.
     call WaitForAssert( {->
           \   assert_true(
           \     pyxeval(
-          \       'ycm_state._signature_help_state.popup_win_id is not None'
+          \       'ycm_state._signature_help_state.popup_win_id is None'
           \     ),
           \     'popup_win_id'
           \   )
           \ } )
+    call s:_CheckPopupPosition( s:popup_win_id, {} )
 
-    let popup_win_id = pyxeval( 'ycm_state._signature_help_state.popup_win_id' )
-    let pos = win_screenpos( popup_win_id )
-    call assert_false( pos == [ 0, 0 ] )
-
-    " Exit insert mode to ensure the test continues
-    call feedkeys( "\<ESC>" )
+    " We're done in insert mode now.
+    call test_override( 'ALL', 0 )
+    call feedkeys( "\<ESC>", 't' )
   endfunction
 
-  call assert_false( pyxeval( 'ycm_state.SignatureHelpRequestReady()' ) )
-  call timer_start( 500, funcref( 'Check' ) )
-  call feedkeys( 'cl(', 'ntx!' )
-  call assert_false( pumvisible(), 'pumvisible()' )
+  " Edit the line and trigger signature help
+  call timer_start( s:timer_interval,
+                  \ funcref( 'CheckSigHelpAndTriggerCompletion' ) )
+  call feedkeys( 'C(', 'ntx!' )
 
   call test_override( 'ALL', 0 )
   %bwipeout!
-endfunctio
+
+  delfunc! CheckSigHelpAndTriggerCompletion
+  delfunc! CheckCompletionVisibleAndSigHelpHidden
+endfunction

+ 1 - 0
test/testdata/python/test.py

@@ -0,0 +1 @@
+import os; os.path.join( os.path.dirname( __file__ ) )

+ 2 - 0
test/vimrc

@@ -1,6 +1,8 @@
+runtime defaults.vim
 let g:ycm_test_plugin_dir = expand( '<sfile>:p:h:h' )
 set mouse=a
 set lines=30
+set columns=80
 
 let g:ycm_confirm_extra_conf=0
 

+ 1 - 1
third_party/ycmd

@@ -1 +1 @@
-Subproject commit 3365e2d44817d127596f59f70a6240507eb4b0bc
+Subproject commit 6f3e2ac5217c11a731e233c196b89932d9a17bbe