Prechádzať zdrojové kódy

WIP: Add support for inlay hints

First cut of just spamming them in there. Very inefficient, doesn't
quite work yet
Ben Jackson 2 rokov pred
rodič
commit
93003daf96

+ 32 - 0
autoload/youcompleteme.vim

@@ -66,6 +66,10 @@ let s:pollers = {
       \     'id': -1,
       \     'wait_milliseconds': 100,
       \   },
+      \   'inlay_hints': {
+      \     'id': -1,
+      \     'wait_milliseconds': 100,
+      \   },
       \ }
 let s:buftype_blacklist = {
       \   'help': 1,
@@ -270,6 +274,7 @@ try:
   # Import the modules used in this file.
   from ycm import base, vimsupport, youcompleteme
   from ycm import semantic_highlighting as ycm_semantic_highlighting
+  from ycm import inlay_hints as ycm_inlay_hints
 
   if 'ycm_state' in globals():
     # If re-initializing, pretend that we shut down
@@ -295,6 +300,7 @@ try:
 
   ycm_state = youcompleteme.YouCompleteMe( default_options )
   ycm_semantic_highlighting.Initialise()
+  ycm_inlay_hints.Initialise()
 except Exception as error:
   # We don't use PostVimMessage or EchoText from the vimsupport module because
   # importing this module may fail.
@@ -773,6 +779,18 @@ function! s:OnFileReadyToParse( ... )
             \ function( 's:PollSemanticHighlighting' ) )
 
     endif
+
+    call s:StopPoller( s:pollers.inlay_hints )
+    if !s:is_neovim &&
+          \ get( b:, 'ycm_enable_inlay_hints',
+          \   get( g:, 'ycm_enable_inlay_hints', 0 ) )
+
+      py3 ycm_state.CurrentBuffer().SendInlayHintsRequest()
+      let s:pollers.inlay_hints.id = timer_start(
+            \ s:pollers.inlay_hints.wait_milliseconds,
+            \ function( 's:PollInlayHints' ) )
+
+    endif
   endif
 endfunction
 
@@ -805,6 +823,20 @@ function! s:PollSemanticHighlighting( ... )
 endfunction
 
 
+function! s:PollInlayHints( ... )
+  if !py3eval( 'ycm_state.CurrentBuffer().InlayHintsReady()' )
+    let s:pollers.inlay_hints.id = timer_start(
+          \ s:pollers.inlay_hints.wait_milliseconds,
+          \ function( 's:PollInlayHints' ) )
+  elseif ! py3eval( 'ycm_state.CurrentBuffer().UpdateInlayHints()' )
+    let s:pollers.inlay_hints.id = timer_start(
+          \ s:pollers.inlay_hints.wait_milliseconds,
+          \ function( 's:PollInlayHints' ) )
+  endif
+endfunction
+
+
+
 function! s:SendKeys( keys )
   " By default keys are added to the end of the typeahead buffer. If there are
   " already keys in the buffer, they will be processed first and may change

+ 15 - 2
python/ycm/buffer.py

@@ -19,6 +19,7 @@ from ycm import vimsupport
 from ycm.client.event_notification import EventNotification
 from ycm.diagnostic_interface import DiagnosticInterface
 from ycm.semantic_highlighting import SemanticHighlighting
+from ycm.inlay_hints import InlayHints
 
 
 # Emulates Vim buffer
@@ -36,8 +37,8 @@ class Buffer:
     self._diag_interface = DiagnosticInterface( bufnr, user_options )
     self._open_loclist_on_ycm_diags = user_options[
                                         'open_loclist_on_ycm_diags' ]
-    self._semantic_highlighting = SemanticHighlighting( bufnr,
-                                                        user_options )
+    self._semantic_highlighting = SemanticHighlighting( bufnr, user_options )
+    self._inlay_hints = InlayHints( bufnr, user_options )
     self.UpdateFromFileTypes( filetypes )
 
 
@@ -148,6 +149,18 @@ class Buffer:
     return self._semantic_highlighting.Update()
 
 
+  def SendInlayHintsRequest( self ):
+    self._inlay_hints.SendRequest()
+
+
+  def InlayHintsReady( self ):
+    return self._inlay_hints.IsResponseReady()
+
+
+  def UpdateInlayHints( self ):
+    return self._inlay_hints.Update()
+
+
   def _ChangedTick( self ):
     return vimsupport.GetBufferChangedTick( self._number )
 

+ 64 - 0
python/ycm/client/inlay_hints_request.py

@@ -0,0 +1,64 @@
+# Copyright (C) 2022, 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/>.
+
+
+import logging
+from ycm.client.base_request import ( BaseRequest, DisplayServerException,
+                                      MakeServerException )
+
+_logger = logging.getLogger( __name__ )
+
+
+# FIXME: This is copy/pasta from SemanticTokensRequest - abstract a
+# SimpleAsyncRequest base that does all of this generically
+class InlayHintsRequest( BaseRequest ):
+  def __init__( self, request_data ):
+    super().__init__()
+    self.request_data = request_data
+    self._response_future = None
+
+
+  def Start( self ):
+    self._response_future = self.PostDataToHandlerAsync( self.request_data,
+                                                         'inlay_hints' )
+
+  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( 'inlay_hints' ) or []

+ 135 - 0
python/ycm/inlay_hints.py

@@ -0,0 +1,135 @@
+# Copyright (C) 2022, 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 ycm.client.inlay_hints_request import InlayHintsRequest
+from ycm.client.base_request import BuildRequestData
+from ycm import vimsupport
+from ycm import text_properties as tp
+import vim
+from ycmd.utils import ToBytes
+
+
+HIGHLIGHT_GROUP = {
+  # 1-based inedexes
+  0: '',
+  1: 'Comment',        # Type
+  2: 'Comment'   # Parameter
+}
+REPORTED_MISSING_TYPES = set()
+
+
+def Initialise():
+  if vimsupport.VimIsNeovim():
+    return
+
+  props = tp.GetTextPropertyTypes()
+  if 'YCM_INLAY_UNKNOWN' not in props:
+    tp.AddTextPropertyType( 'YCM_INLAY_UNKNOWN', highlight = 'Comment' )
+
+  for token_type, group in HIGHLIGHT_GROUP.items():
+    prop = f'YCM_INLAY_{ token_type }'
+    if prop not in props and vimsupport.GetIntValue(
+        f"hlexists( '{ vimsupport.EscapeForVim( group ) }' )" ):
+      tp.AddTextPropertyType( prop, highlight = group )
+
+
+class InlayHints:
+  """Stores the inlay hints state for a Vim buffer"""
+
+  def __init__( self, bufnr, user_options ):
+    self._request = None
+    self._bufnr = bufnr
+    self._prop_ids = set()
+    self.tick = -1
+
+
+  def SendRequest( self ):
+    if self._request and not self.IsResponseReady():
+      return
+
+    self.tick = vimsupport.GetBufferChangedTick( self._bufnr )
+
+    # TODO: How to determine the range to display ? Should we do the range
+    # visible in "all" windows? We're doing this per-buffer, but perhaps it
+    # should actually be per-window; that might ultimately be a better model
+    # but the resulting properties are per-buffer, not per-window.
+    #
+    # Perhaps the maximal range of visible windows or something.
+    request_data = BuildRequestData( self._bufnr )
+    request_data.update( {
+      'range': {
+        'start': {
+          'line_num': 1,
+          'column_num': 1
+        },
+        'end': {
+          'line_num': max( len( vim.buffers[ self._bufnr ] ), 1 ),
+          'column_num': len( ToBytes( vim.buffers[ self._bufnr ][ -1 ] ) ) + 1
+        }
+      }
+    } )
+    self._request = InlayHintsRequest( request_data )
+    self._request.Start()
+
+  def IsResponseReady( self ):
+    return self._request is not None and self._request.Done()
+
+  def Update( self ):
+    if not self._request:
+      # Nothing to update
+      return True
+
+    assert self.IsResponseReady()
+
+    # We're ready to use this response. Clear it (to avoid repeatedly
+    # re-polling).
+    inlay_hints = self._request.Response()
+    self._request = None
+
+    if self.tick != vimsupport.GetBufferChangedTick( self._bufnr ):
+      # Buffer has changed, we should ignore the data and retry
+      self.SendRequest()
+      return False # poll again
+
+    for prop_id in self._prop_ids:
+      tp.ClearTextProperties( self._bufnr, prop_id )
+
+    self._prop_ids.clear()
+
+    for inlay_hint in inlay_hints:
+      if 'kind' not in  inlay_hint:
+        prop_type = 'YCM_INLAY_UNKNOWN'
+      elif inlay_hint[ 'kind' ] not in HIGHLIGHT_GROUP:
+        prop_type = 'YCM_INLAY_UNKNOWN'
+      else:
+        prop_type = 'YCM_INLAY_' + str( inlay_hint[ 'kind' ] )
+
+      self._prop_ids.add(
+        tp.AddTextProperty( self._bufnr,
+                            None,
+                            prop_type,
+                            {
+                              'start': inlay_hint[ 'position' ],
+                              'end': inlay_hint[ 'position' ],
+                            },
+                            {
+                              'text': inlay_hint[ 'label' ]
+                            } ) )
+
+    # No need to re-poll
+    return True

+ 10 - 52
python/ycm/semantic_highlighting.py

@@ -19,10 +19,7 @@
 from ycm.client.semantic_tokens_request import SemanticTokensRequest
 from ycm.client.base_request import BuildRequestData
 from ycm import vimsupport
-from ycmd import utils
-
-import vim
-import json
+from ycm import text_properties as tp
 
 
 HIGHLIGHT_GROUP = {
@@ -59,15 +56,15 @@ def Initialise():
   if vimsupport.VimIsNeovim():
     return
 
-  props = GetTextPropertyTypes()
+  props = tp.GetTextPropertyTypes()
   if 'YCM_HL_UNKNOWN' not in props:
-    AddTextPropertyType( 'YCM_HL_UNKNOWN', highlight = 'WarningMsg' )
+    tp.AddTextPropertyType( 'YCM_HL_UNKNOWN', highlight = 'WarningMsg' )
 
   for token_type, group in HIGHLIGHT_GROUP.items():
     prop = f'YCM_HL_{ token_type }'
     if prop not in props and vimsupport.GetIntValue(
         f"hlexists( '{ vimsupport.EscapeForVim( group ) }' )" ):
-      AddTextPropertyType( prop, highlight = group )
+      tp.AddTextPropertyType( prop, highlight = group )
 
 
 # "arbitrary" base id
@@ -99,7 +96,7 @@ class SemanticHighlighting:
 
     self.tick = vimsupport.GetBufferChangedTick( self._bufnr )
 
-    self._request = SemanticTokensRequest( BuildRequestData() )
+    self._request = SemanticTokensRequest( BuildRequestData( self._bufnr ) )
     self._request.Start()
 
   def IsResponseReady( self ):
@@ -136,52 +133,13 @@ class SemanticHighlighting:
             f"Missing property type for { token[ 'type' ] }" )
         continue
       prop_type = f"YCM_HL_{ token[ 'type' ] }"
-      AddTextProperty( self._bufnr, self._prop_id, prop_type, token[ 'range' ] )
+      tp.AddTextProperty( self._bufnr,
+                          self._prop_id,
+                          prop_type,
+                          token[ 'range' ] )
 
-    ClearTextProperties( self._bufnr, prev_prop_id )
+    tp.ClearTextProperties( self._bufnr, prev_prop_id )
 
     # No need to re-poll
     return True
 
-
-# FIXME/TODO: Merge this with vimsupport funcitons, added after these were
-# written. It's not trivial, as those vimsupport functions are a bit fiddly.
-# They also support neovim, but we don't.
-def AddTextPropertyType( name, **kwargs ):
-  props = {
-    'highlight': 'Ignore',
-    'combine': False,
-    'start_incl': False,
-    'end_incl': False,
-    'priority': 10
-  }
-  props.update( kwargs )
-
-  vim.eval( f"prop_type_add( '{ vimsupport.EscapeForVim( name ) }', "
-            f"               { json.dumps( kwargs ) } )" )
-
-
-def GetTextPropertyTypes( *args, **kwargs ):
-  return [ utils.ToUnicode( p ) for p in vim.eval( 'prop_type_list()' ) ]
-
-
-def AddTextProperty( bufnr, prop_id, prop_type, range ):
-  props = {
-    'end_lnum': range[ 'end' ][ 'line_num' ],
-    'end_col': range[ 'end' ][ 'column_num' ],
-    'bufnr': bufnr,
-    'id': prop_id,
-    'type': prop_type
-  }
-  vim.eval( f"prop_add( { range[ 'start' ][ 'line_num' ] },"
-            f"          { range[ 'start' ][ 'column_num' ] },"
-            f"          { json.dumps( props ) } )" )
-
-
-def ClearTextProperties( bufnr, prop_id ):
-  props = {
-    'id': prop_id,
-    'bufnr': bufnr,
-    'all': 1,
-  }
-  vim.eval( f"prop_remove( { json.dumps( props ) } )" )

+ 74 - 0
python/ycm/text_properties.py

@@ -0,0 +1,74 @@
+# Copyright (C) 2020, 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 ycm import vimsupport
+from ycmd import utils
+
+import vim
+import json
+
+
+# FIXME/TODO: Merge this with vimsupport funcitons, added after these were
+# written. It's not trivial, as those vimsupport functions are a bit fiddly.
+# They also support neovim, but we don't.
+def AddTextPropertyType( name, **kwargs ):
+  props = {
+    'highlight': 'Ignore',
+    'combine': False,
+    'start_incl': False,
+    'end_incl': False,
+    'priority': 10
+  }
+  props.update( kwargs )
+
+  vim.eval( f"prop_type_add( '{ vimsupport.EscapeForVim( name ) }', "
+            f"               { json.dumps( kwargs ) } )" )
+
+
+def GetTextPropertyTypes( *args, **kwargs ):
+  return [ utils.ToUnicode( p ) for p in vim.eval( 'prop_type_list()' ) ]
+
+
+def AddTextProperty( bufnr,
+                     prop_id,
+                     prop_type,
+                     range,
+                     extra_args: dict = None ):
+  props = {
+    'end_lnum': range[ 'end' ][ 'line_num' ],
+    'end_col': range[ 'end' ][ 'column_num' ],
+    'bufnr': bufnr,
+    'type': prop_type
+  }
+  if prop_id is not None:
+    props[ 'id' ]: prop_id
+  if extra_args:
+    props.update( extra_args )
+  return vim.eval( f"prop_add( { range[ 'start' ][ 'line_num' ] },"
+                   f"          { range[ 'start' ][ 'column_num' ] },"
+                   f"          { json.dumps( props ) } )" )
+
+
+def ClearTextProperties( bufnr, prop_id ):
+  props = {
+    'id': prop_id,
+    'bufnr': bufnr,
+    'all': 1,
+  }
+  vim.eval( f"prop_remove( { json.dumps( props ) } )" )
+