Ver código fonte

Reduce overhead of semantic highlighting (part 1)

Re-use the "optimal range" drawing used by inlay hints, i.e. only
re-request if the new visible range is not covered by the previously
requested visible range.

Essentially, semantic highlighting and inlay hints now use the (almost)
exact same code.

Still TODO (for both): record the _actual_ received range and compare
against that. The point is that we should only redraw if the new range
isn't within the last _drawn_ range. This is important for semantic
highlighting because servers might not support range highlighting and we
should therefore paint the whole buffer and only re-do that when it
changes.
Ben Jackson 1 ano atrás
pai
commit
25296a6ef8

+ 31 - 33
autoload/youcompleteme.vim

@@ -822,24 +822,29 @@ function! s:OnFileReadyToParse( ... )
           \ s:pollers.file_parse_response.wait_milliseconds,
           \ function( 's:PollFileParseResponse' ) )
 
-    call s:UpdateSemanticHighlighting( bufnr() )
+    call s:UpdateSemanticHighlighting( bufnr(), 1, 0 )
     call s:UpdateInlayHints( bufnr(), 1, 0 )
 
   endif
 endfunction
 
-function! s:UpdateSemanticHighlighting( bufnr ) abort
+function! s:UpdateSemanticHighlighting( bufnr, force, redraw_anyway ) abort
   call s:StopPoller( s:pollers.semantic_highlighting )
   if !s:is_neovim &&
         \ get( b:, 'ycm_enable_semantic_highlighting',
         \   get( g:, 'ycm_enable_semantic_highlighting', 0 ) )
 
-    py3 ycm_state.Buffer(
-          \ int( vim.eval( "a:bufnr" ) ) ).SendSemanticTokensRequest()
-    let s:pollers.semantic_highlighting.id = timer_start(
-          \ s:pollers.semantic_highlighting.wait_milliseconds,
-          \ function( 's:PollSemanticHighlighting', [ a:bufnr ] ) )
-
+    if py3eval(
+        \ 'ycm_state.Buffer( int( vim.eval( "a:bufnr" ) ) ).'
+        \ . 'semantic_highlighting.Request( '
+        \ . '  force=int( vim.eval( "a:force" ) ) )' )
+      let s:pollers.semantic_highlighting.id = timer_start(
+            \ s:pollers.semantic_highlighting.wait_milliseconds,
+            \ function( 's:PollSemanticHighlighting', [ a:bufnr ] ) )
+    elseif a:redraw_anyway
+      py3 ycm_state.Buffer(
+            \ int( vim.eval( "a:bufnr" ) ) ).semantic_highlighting.Refresh()
+    endif
   endif
 endfunction
 
@@ -850,7 +855,7 @@ function s:ShouldUseInlayHintsNow( bufnr )
         \   get( g:, 'ycm_enable_inlay_hints', 0 ) )
 endfunction
 
-function! s:UpdateInlayHints( bufnr, force, redraw_anyway )
+function! s:UpdateInlayHints( bufnr, force, redraw_anyway ) abort
   call s:StopPoller( s:pollers.inlay_hints )
 
   if s:ShouldUseInlayHintsNow( a:bufnr )
@@ -883,36 +888,29 @@ function! s:PollFileParseResponse( ... )
 endfunction
 
 
-function! s:PollSemanticHighlighting( bufnr, ... )
-  if !py3eval(
-      \ 'ycm_state.Buffer( int( vim.eval( "a:bufnr" ) ) )'
-      \ . '.SemanticTokensRequestReady()' )
-    let s:pollers.semantic_highlighting.id = timer_start(
-          \ s:pollers.semantic_highlighting.wait_milliseconds,
-          \ function( 's:PollSemanticHighlighting', [ a:bufnr ] ) )
-  elseif !py3eval(
-      \ 'ycm_state.Buffer( int( vim.eval( "a:bufnr" ) ) )'
-      \ . '.UpdateSemanticTokens()' )
-    let s:pollers.semantic_highlighting.id = timer_start(
-          \ s:pollers.semantic_highlighting.wait_milliseconds,
-          \ function( 's:PollSemanticHighlighting', [ a:bufnr ] ) )
-  endif
+function! s:PollSemanticHighlighting( bufnr, ... ) abort
+  return s:PollScrollable( a:bufnr, 'semantic_highlighting' )
+endfunction
+
+
+function! s:PollInlayHints( bufnr, ... ) abort
+  return s:PollScrollable( a:bufnr, 'inlay_hints' )
 endfunction
 
 
-function! s:PollInlayHints( bufnr, ... )
+function! s:PollScrollable( bufnr, scrollable, ... ) abort
   if !py3eval(
       \ 'ycm_state.Buffer( int( vim.eval( "a:bufnr" ) ) )'
-      \ . '.inlay_hints.Ready()' )
-    let s:pollers.inlay_hints.id = timer_start(
-          \ s:pollers.inlay_hints.wait_milliseconds,
-          \ function( 's:PollInlayHints', [ a:bufnr ] ) )
+      \ . '.' . a:scrollable . '.Ready()' )
+    let s:pollers[a:scrollable].id = timer_start(
+          \ s:pollers[a:scrollable].wait_milliseconds,
+          \ function( 's:PollScrollable', [ a:bufnr, a:scrollable ] ) )
   elseif ! py3eval(
       \ 'ycm_state.Buffer( int( vim.eval( "a:bufnr" ) ) )'
-      \ . '.inlay_hints.Update()' )
-    let s:pollers.inlay_hints.id = timer_start(
-          \ s:pollers.inlay_hints.wait_milliseconds,
-          \ function( 's:PollInlayHints', [ a:bufnr ] ) )
+      \ . '.' . a:scrollable . '.Update()' )
+    let s:pollers[ a:scrollable ].id = timer_start(
+          \ s:pollers[ a:scrollable ].wait_milliseconds,
+          \ function( 's:PollScrollable', [ a:bufnr, a:scrollable ] ) )
   endif
 endfunction
 
@@ -973,7 +971,7 @@ function! s:OnWinScrolled()
     return
   endif
   let bufnr = winbufnr( expand( '<afile>' ) )
-  call s:UpdateSemanticHighlighting( bufnr )
+  call s:UpdateSemanticHighlighting( bufnr, 0, 0 )
   call s:UpdateInlayHints( bufnr, 0, 0 )
 endfunction
 

+ 2 - 14
python/ycm/buffer.py

@@ -37,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.inlay_hints = InlayHints( bufnr, user_options )
+    self.semantic_highlighting = SemanticHighlighting( bufnr )
+    self.inlay_hints = InlayHints( bufnr )
     self.UpdateFromFileTypes( filetypes )
 
 
@@ -145,18 +145,6 @@ class Buffer:
     self._async_diags = False
 
 
-  def SendSemanticTokensRequest( self ):
-    self._semantic_highlighting.SendRequest()
-
-
-  def SemanticTokensRequestReady( self ):
-    return self._semantic_highlighting.IsResponseReady()
-
-
-  def UpdateSemanticTokens( self ):
-    return self._semantic_highlighting.Update()
-
-
   def _ChangedTick( self ):
     return vimsupport.GetBufferChangedTick( self._number )
 

+ 6 - 80
python/ycm/inlay_hints.py

@@ -20,6 +20,7 @@ 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
+from ycm import scrolling_range as sr
 
 
 HIGHLIGHT_GROUP = {
@@ -55,103 +56,28 @@ def Initialise():
   return True
 
 
-class InlayHints:
+class InlayHints( sr.ScrollingBufferRange ):
   """Stores the inlay hints state for a Vim buffer"""
 
-  # FIXME: Send a request per-disjoint range for this buffer rather than the
-  # maximal range. then collaate the results when all responses are returned
-  def __init__( self, bufnr, user_options ):
-    self._request = None
-    self._bufnr = bufnr
-    self.tick = -1
-    self._latest_inlay_hints = []
-    self._last_requested_range = None
-
-
-  def Request( self, force=False ):
-    if self._request and not self.Ready():
-      return True
-
-    # Check to see if the buffer ranges would actually change anything visible.
-    # This avoids a round-trip for every single line scroll event
-    if ( not force and
-         self.tick == vimsupport.GetBufferChangedTick( self._bufnr ) and
-         vimsupport.VisibleRangeOfBufferOverlaps(
-           self._bufnr,
-           self._last_requested_range ) ):
-      return False # don't poll
-
-    # We're requesting changes, so the existing results are now invalid
-    self._latest_inlay_hints = []
-    # FIXME: This call is duplicated in the call to VisibleRangeOfBufferOverlaps
-    #  - remove the expansion param
-    #  - look up the actual visible range, then call this function
-    #  - if not overlapping, do the factor expansion and request
-    self._last_requested_range = vimsupport.RangeVisibleInBuffer( self._bufnr )
-    self.tick = vimsupport.GetBufferChangedTick( self._bufnr )
 
+  def _NewRequest( self, request_range ):
     request_data = BuildRequestData( self._bufnr )
-    request_data.update( {
-      'range': self._last_requested_range
-    } )
-    self._request = InlayHintsRequest( request_data )
-    self._request.Start()
-    return True
-
-
-  def Ready( self ):
-    return self._request is not None and self._request.Done()
+    request_data[ 'range' ] = request_range
+    return InlayHintsRequest( request_data )
 
 
   def Clear( self ):
-    # ClearTextProperties is slow as it must scan the whole buffer
-    # we shouldn't use _last_requested_range, because the server is free to
-    # return a larger range, so we pick the first/last from the latest results
     types = [ 'YCM_INLAY_UNKNOWN', 'YCM_INLAY_PADDING' ] + [
       f'YCM_INLAY_{ prop_type }' for prop_type in HIGHLIGHT_GROUP.keys()
     ]
 
     tp.ClearTextProperties( self._bufnr, prop_types = types )
 
-  def Update( self ):
-    if not self._request:
-      # Nothing to update
-      return True
-
-    assert self.Ready()
-
-    # We're ready to use this response. Clear it (to avoid repeatedly
-    # re-polling).
-    self._latest_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.Request( force=True )
-      return False # poll again
-
-    self._Draw()
-
-    # No need to re-poll
-    return True
-
-
-  def Refresh( self ):
-    if self.tick != vimsupport.GetBufferChangedTick( self._bufnr ):
-      # stale data
-      return
-
-    if self._request is not None:
-      # request in progress; we''l handle refreshing when it's done.
-      return
-
-    self._Draw()
-
 
   def _Draw( self ):
     self.Clear()
 
-    for inlay_hint in self._latest_inlay_hints:
+    for inlay_hint in self._latest_response:
       if 'kind' not in inlay_hint:
         prop_type = 'YCM_INLAY_UNKNOWN'
       elif inlay_hint[ 'kind' ] not in HIGHLIGHT_GROUP:

+ 114 - 0
python/ycm/scrolling_range.py

@@ -0,0 +1,114 @@
+# Copyright (C) 2023, 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 abc
+
+from ycm import vimsupport
+
+
+class ScrollingBufferRange( object ):
+  """Abstraction used by inlay hints and semantic tokens to only request visible
+  ranges"""
+
+  # FIXME: Send a request per-disjoint range for this buffer rather than the
+  # maximal range. then collaate the results when all responses are returned
+  def __init__( self, bufnr ):
+    self._bufnr = bufnr
+    self._tick = -1
+    self._request = None
+    self._last_requested_range = None
+
+
+  def Ready( self ):
+    return self._request is not None and self._request.Done()
+
+
+  def Request( self, force=False ):
+    if self._request and not self.Ready():
+      return True
+
+    # Check to see if the buffer ranges would actually change anything visible.
+    # This avoids a round-trip for every single line scroll event
+    if ( not force and
+         self._tick == vimsupport.GetBufferChangedTick( self._bufnr ) and
+         vimsupport.VisibleRangeOfBufferOverlaps(
+           self._bufnr,
+           self._last_requested_range ) ):
+      return False # don't poll
+
+    # FIXME: This call is duplicated in the call to VisibleRangeOfBufferOverlaps
+    #  - remove the expansion param
+    #  - look up the actual visible range, then call this function
+    #  - if not overlapping, do the factor expansion and request
+    self._last_requested_range = vimsupport.RangeVisibleInBuffer( self._bufnr )
+    self._tick = vimsupport.GetBufferChangedTick( self._bufnr )
+
+    # We'll never use the last response again, so clear it
+    self._latest_response = None
+    self._request = self._NewRequest( self._last_requested_range )
+    self._request.Start()
+    return True
+
+
+  def Update( self ):
+    if not self._request:
+      # Nothing to update
+      return True
+
+    assert self.Ready()
+
+    # We're ready to use this response. Clear the request (to avoid repeatedly
+    # re-polling).
+    self._latest_response = self._request.Response()
+    self._request = None
+
+    if self._tick != vimsupport.GetBufferChangedTick( self._bufnr ):
+      # Buffer has changed, we should ignore the data and retry
+      self.Request( force=True )
+      return False # poll again
+
+    self._Draw()
+
+    # No need to re-poll
+    return True
+
+
+  def Refresh( self ):
+    if self._tick != vimsupport.GetBufferChangedTick( self._bufnr ):
+      # stale data
+      return
+
+    if self._request is not None:
+      # request in progress; we''l handle refreshing when it's done.
+      return
+
+    self._Draw()
+
+
+  # API; just implement the following, using self._bufnr and
+  # self._latest_response as required
+
+  @abc.abstractmethod
+  def _NewRequest( self, request_range ):
+    # prepare a new request_data and return it
+    pass
+
+
+  @abc.abstractmethod
+  def _Draw( self ):
+    # actuall paint the properties
+    pass

+ 9 - 39
python/ycm/semantic_highlighting.py

@@ -20,6 +20,7 @@ from ycm.client.semantic_tokens_request import SemanticTokensRequest
 from ycm.client.base_request import BuildRequestData
 from ycm import vimsupport
 from ycm import text_properties as tp
+from ycm import scrolling_range as sr
 
 
 HIGHLIGHT_GROUP = {
@@ -84,51 +85,23 @@ def NextPropID():
 
 
 
-class SemanticHighlighting:
+class SemanticHighlighting( sr.ScrollingBufferRange ):
   """Stores the semantic highlighting state for a Vim buffer"""
 
-  def __init__( self, bufnr, user_options ):
-    self._request = None
-    self._bufnr = bufnr
+  def __init__( self, bufnr ):
     self._prop_id = NextPropID()
-    self.tick = -1
+    super().__init__( bufnr )
 
 
-  def SendRequest( self ):
-    if self._request and not self.IsResponseReady():
-      return
-
-    self.tick = vimsupport.GetBufferChangedTick( self._bufnr )
-
+  def _NewRequest( self, request_range ):
     request: dict = BuildRequestData( self._bufnr )
-    request.update( {
-      'range': vimsupport.RangeVisibleInBuffer( self._bufnr )
-    } )
-    self._request = SemanticTokensRequest( request )
-    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
+    request[ 'range' ] = request_range
+    return SemanticTokensRequest( request )
 
-    assert self.IsResponseReady()
-
-    # We're ready to use this response. Clear it (to avoid repeatedly
-    # re-polling).
-    response = 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
 
+  def _Draw( self ):
     # We requested a snapshot
-    tokens = response.get( 'tokens', [] )
+    tokens = self._latest_response.get( 'tokens', [] )
 
     prev_prop_id = self._prop_id
     self._prop_id = NextPropID()
@@ -147,6 +120,3 @@ class SemanticHighlighting:
                           token[ 'range' ] )
 
     tp.ClearTextProperties( self._bufnr, prop_id = prev_prop_id )
-
-    # No need to re-poll
-    return True