Skip to content

Commit 25296a6

Browse files
committed
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.
1 parent 0d387ad commit 25296a6

File tree

5 files changed

+162
-166
lines changed

5 files changed

+162
-166
lines changed

autoload/youcompleteme.vim

Lines changed: 31 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -822,24 +822,29 @@ function! s:OnFileReadyToParse( ... )
822822
\ s:pollers.file_parse_response.wait_milliseconds,
823823
\ function( 's:PollFileParseResponse' ) )
824824

825-
call s:UpdateSemanticHighlighting( bufnr() )
825+
call s:UpdateSemanticHighlighting( bufnr(), 1, 0 )
826826
call s:UpdateInlayHints( bufnr(), 1, 0 )
827827

828828
endif
829829
endfunction
830830

831-
function! s:UpdateSemanticHighlighting( bufnr ) abort
831+
function! s:UpdateSemanticHighlighting( bufnr, force, redraw_anyway ) abort
832832
call s:StopPoller( s:pollers.semantic_highlighting )
833833
if !s:is_neovim &&
834834
\ get( b:, 'ycm_enable_semantic_highlighting',
835835
\ get( g:, 'ycm_enable_semantic_highlighting', 0 ) )
836836

837-
py3 ycm_state.Buffer(
838-
\ int( vim.eval( "a:bufnr" ) ) ).SendSemanticTokensRequest()
839-
let s:pollers.semantic_highlighting.id = timer_start(
840-
\ s:pollers.semantic_highlighting.wait_milliseconds,
841-
\ function( 's:PollSemanticHighlighting', [ a:bufnr ] ) )
842-
837+
if py3eval(
838+
\ 'ycm_state.Buffer( int( vim.eval( "a:bufnr" ) ) ).'
839+
\ . 'semantic_highlighting.Request( '
840+
\ . ' force=int( vim.eval( "a:force" ) ) )' )
841+
let s:pollers.semantic_highlighting.id = timer_start(
842+
\ s:pollers.semantic_highlighting.wait_milliseconds,
843+
\ function( 's:PollSemanticHighlighting', [ a:bufnr ] ) )
844+
elseif a:redraw_anyway
845+
py3 ycm_state.Buffer(
846+
\ int( vim.eval( "a:bufnr" ) ) ).semantic_highlighting.Refresh()
847+
endif
843848
endif
844849
endfunction
845850

@@ -850,7 +855,7 @@ function s:ShouldUseInlayHintsNow( bufnr )
850855
\ get( g:, 'ycm_enable_inlay_hints', 0 ) )
851856
endfunction
852857

853-
function! s:UpdateInlayHints( bufnr, force, redraw_anyway )
858+
function! s:UpdateInlayHints( bufnr, force, redraw_anyway ) abort
854859
call s:StopPoller( s:pollers.inlay_hints )
855860

856861
if s:ShouldUseInlayHintsNow( a:bufnr )
@@ -883,36 +888,29 @@ function! s:PollFileParseResponse( ... )
883888
endfunction
884889

885890

886-
function! s:PollSemanticHighlighting( bufnr, ... )
887-
if !py3eval(
888-
\ 'ycm_state.Buffer( int( vim.eval( "a:bufnr" ) ) )'
889-
\ . '.SemanticTokensRequestReady()' )
890-
let s:pollers.semantic_highlighting.id = timer_start(
891-
\ s:pollers.semantic_highlighting.wait_milliseconds,
892-
\ function( 's:PollSemanticHighlighting', [ a:bufnr ] ) )
893-
elseif !py3eval(
894-
\ 'ycm_state.Buffer( int( vim.eval( "a:bufnr" ) ) )'
895-
\ . '.UpdateSemanticTokens()' )
896-
let s:pollers.semantic_highlighting.id = timer_start(
897-
\ s:pollers.semantic_highlighting.wait_milliseconds,
898-
\ function( 's:PollSemanticHighlighting', [ a:bufnr ] ) )
899-
endif
891+
function! s:PollSemanticHighlighting( bufnr, ... ) abort
892+
return s:PollScrollable( a:bufnr, 'semantic_highlighting' )
893+
endfunction
894+
895+
896+
function! s:PollInlayHints( bufnr, ... ) abort
897+
return s:PollScrollable( a:bufnr, 'inlay_hints' )
900898
endfunction
901899

902900

903-
function! s:PollInlayHints( bufnr, ... )
901+
function! s:PollScrollable( bufnr, scrollable, ... ) abort
904902
if !py3eval(
905903
\ 'ycm_state.Buffer( int( vim.eval( "a:bufnr" ) ) )'
906-
\ . '.inlay_hints.Ready()' )
907-
let s:pollers.inlay_hints.id = timer_start(
908-
\ s:pollers.inlay_hints.wait_milliseconds,
909-
\ function( 's:PollInlayHints', [ a:bufnr ] ) )
904+
\ . '.' . a:scrollable . '.Ready()' )
905+
let s:pollers[a:scrollable].id = timer_start(
906+
\ s:pollers[a:scrollable].wait_milliseconds,
907+
\ function( 's:PollScrollable', [ a:bufnr, a:scrollable ] ) )
910908
elseif ! py3eval(
911909
\ 'ycm_state.Buffer( int( vim.eval( "a:bufnr" ) ) )'
912-
\ . '.inlay_hints.Update()' )
913-
let s:pollers.inlay_hints.id = timer_start(
914-
\ s:pollers.inlay_hints.wait_milliseconds,
915-
\ function( 's:PollInlayHints', [ a:bufnr ] ) )
910+
\ . '.' . a:scrollable . '.Update()' )
911+
let s:pollers[ a:scrollable ].id = timer_start(
912+
\ s:pollers[ a:scrollable ].wait_milliseconds,
913+
\ function( 's:PollScrollable', [ a:bufnr, a:scrollable ] ) )
916914
endif
917915
endfunction
918916

@@ -973,7 +971,7 @@ function! s:OnWinScrolled()
973971
return
974972
endif
975973
let bufnr = winbufnr( expand( '<afile>' ) )
976-
call s:UpdateSemanticHighlighting( bufnr )
974+
call s:UpdateSemanticHighlighting( bufnr, 0, 0 )
977975
call s:UpdateInlayHints( bufnr, 0, 0 )
978976
endfunction
979977

python/ycm/buffer.py

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ def __init__( self, bufnr, user_options, filetypes ):
3737
self._diag_interface = DiagnosticInterface( bufnr, user_options )
3838
self._open_loclist_on_ycm_diags = user_options[
3939
'open_loclist_on_ycm_diags' ]
40-
self._semantic_highlighting = SemanticHighlighting( bufnr, user_options )
41-
self.inlay_hints = InlayHints( bufnr, user_options )
40+
self.semantic_highlighting = SemanticHighlighting( bufnr )
41+
self.inlay_hints = InlayHints( bufnr )
4242
self.UpdateFromFileTypes( filetypes )
4343

4444

@@ -145,18 +145,6 @@ def UpdateFromFileTypes( self, filetypes ):
145145
self._async_diags = False
146146

147147

148-
def SendSemanticTokensRequest( self ):
149-
self._semantic_highlighting.SendRequest()
150-
151-
152-
def SemanticTokensRequestReady( self ):
153-
return self._semantic_highlighting.IsResponseReady()
154-
155-
156-
def UpdateSemanticTokens( self ):
157-
return self._semantic_highlighting.Update()
158-
159-
160148
def _ChangedTick( self ):
161149
return vimsupport.GetBufferChangedTick( self._number )
162150

python/ycm/inlay_hints.py

Lines changed: 6 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from ycm.client.base_request import BuildRequestData
2121
from ycm import vimsupport
2222
from ycm import text_properties as tp
23+
from ycm import scrolling_range as sr
2324

2425

2526
HIGHLIGHT_GROUP = {
@@ -55,103 +56,28 @@ def Initialise():
5556
return True
5657

5758

58-
class InlayHints:
59+
class InlayHints( sr.ScrollingBufferRange ):
5960
"""Stores the inlay hints state for a Vim buffer"""
6061

61-
# FIXME: Send a request per-disjoint range for this buffer rather than the
62-
# maximal range. then collaate the results when all responses are returned
63-
def __init__( self, bufnr, user_options ):
64-
self._request = None
65-
self._bufnr = bufnr
66-
self.tick = -1
67-
self._latest_inlay_hints = []
68-
self._last_requested_range = None
69-
70-
71-
def Request( self, force=False ):
72-
if self._request and not self.Ready():
73-
return True
74-
75-
# Check to see if the buffer ranges would actually change anything visible.
76-
# This avoids a round-trip for every single line scroll event
77-
if ( not force and
78-
self.tick == vimsupport.GetBufferChangedTick( self._bufnr ) and
79-
vimsupport.VisibleRangeOfBufferOverlaps(
80-
self._bufnr,
81-
self._last_requested_range ) ):
82-
return False # don't poll
83-
84-
# We're requesting changes, so the existing results are now invalid
85-
self._latest_inlay_hints = []
86-
# FIXME: This call is duplicated in the call to VisibleRangeOfBufferOverlaps
87-
# - remove the expansion param
88-
# - look up the actual visible range, then call this function
89-
# - if not overlapping, do the factor expansion and request
90-
self._last_requested_range = vimsupport.RangeVisibleInBuffer( self._bufnr )
91-
self.tick = vimsupport.GetBufferChangedTick( self._bufnr )
9262

63+
def _NewRequest( self, request_range ):
9364
request_data = BuildRequestData( self._bufnr )
94-
request_data.update( {
95-
'range': self._last_requested_range
96-
} )
97-
self._request = InlayHintsRequest( request_data )
98-
self._request.Start()
99-
return True
100-
101-
102-
def Ready( self ):
103-
return self._request is not None and self._request.Done()
65+
request_data[ 'range' ] = request_range
66+
return InlayHintsRequest( request_data )
10467

10568

10669
def Clear( self ):
107-
# ClearTextProperties is slow as it must scan the whole buffer
108-
# we shouldn't use _last_requested_range, because the server is free to
109-
# return a larger range, so we pick the first/last from the latest results
11070
types = [ 'YCM_INLAY_UNKNOWN', 'YCM_INLAY_PADDING' ] + [
11171
f'YCM_INLAY_{ prop_type }' for prop_type in HIGHLIGHT_GROUP.keys()
11272
]
11373

11474
tp.ClearTextProperties( self._bufnr, prop_types = types )
11575

116-
def Update( self ):
117-
if not self._request:
118-
# Nothing to update
119-
return True
120-
121-
assert self.Ready()
122-
123-
# We're ready to use this response. Clear it (to avoid repeatedly
124-
# re-polling).
125-
self._latest_inlay_hints = self._request.Response()
126-
self._request = None
127-
128-
if self.tick != vimsupport.GetBufferChangedTick( self._bufnr ):
129-
# Buffer has changed, we should ignore the data and retry
130-
self.Request( force=True )
131-
return False # poll again
132-
133-
self._Draw()
134-
135-
# No need to re-poll
136-
return True
137-
138-
139-
def Refresh( self ):
140-
if self.tick != vimsupport.GetBufferChangedTick( self._bufnr ):
141-
# stale data
142-
return
143-
144-
if self._request is not None:
145-
# request in progress; we''l handle refreshing when it's done.
146-
return
147-
148-
self._Draw()
149-
15076

15177
def _Draw( self ):
15278
self.Clear()
15379

154-
for inlay_hint in self._latest_inlay_hints:
80+
for inlay_hint in self._latest_response:
15581
if 'kind' not in inlay_hint:
15682
prop_type = 'YCM_INLAY_UNKNOWN'
15783
elif inlay_hint[ 'kind' ] not in HIGHLIGHT_GROUP:

python/ycm/scrolling_range.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# Copyright (C) 2023, 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+
18+
import abc
19+
20+
from ycm import vimsupport
21+
22+
23+
class ScrollingBufferRange( object ):
24+
"""Abstraction used by inlay hints and semantic tokens to only request visible
25+
ranges"""
26+
27+
# FIXME: Send a request per-disjoint range for this buffer rather than the
28+
# maximal range. then collaate the results when all responses are returned
29+
def __init__( self, bufnr ):
30+
self._bufnr = bufnr
31+
self._tick = -1
32+
self._request = None
33+
self._last_requested_range = None
34+
35+
36+
def Ready( self ):
37+
return self._request is not None and self._request.Done()
38+
39+
40+
def Request( self, force=False ):
41+
if self._request and not self.Ready():
42+
return True
43+
44+
# Check to see if the buffer ranges would actually change anything visible.
45+
# This avoids a round-trip for every single line scroll event
46+
if ( not force and
47+
self._tick == vimsupport.GetBufferChangedTick( self._bufnr ) and
48+
vimsupport.VisibleRangeOfBufferOverlaps(
49+
self._bufnr,
50+
self._last_requested_range ) ):
51+
return False # don't poll
52+
53+
# FIXME: This call is duplicated in the call to VisibleRangeOfBufferOverlaps
54+
# - remove the expansion param
55+
# - look up the actual visible range, then call this function
56+
# - if not overlapping, do the factor expansion and request
57+
self._last_requested_range = vimsupport.RangeVisibleInBuffer( self._bufnr )
58+
self._tick = vimsupport.GetBufferChangedTick( self._bufnr )
59+
60+
# We'll never use the last response again, so clear it
61+
self._latest_response = None
62+
self._request = self._NewRequest( self._last_requested_range )
63+
self._request.Start()
64+
return True
65+
66+
67+
def Update( self ):
68+
if not self._request:
69+
# Nothing to update
70+
return True
71+
72+
assert self.Ready()
73+
74+
# We're ready to use this response. Clear the request (to avoid repeatedly
75+
# re-polling).
76+
self._latest_response = self._request.Response()
77+
self._request = None
78+
79+
if self._tick != vimsupport.GetBufferChangedTick( self._bufnr ):
80+
# Buffer has changed, we should ignore the data and retry
81+
self.Request( force=True )
82+
return False # poll again
83+
84+
self._Draw()
85+
86+
# No need to re-poll
87+
return True
88+
89+
90+
def Refresh( self ):
91+
if self._tick != vimsupport.GetBufferChangedTick( self._bufnr ):
92+
# stale data
93+
return
94+
95+
if self._request is not None:
96+
# request in progress; we''l handle refreshing when it's done.
97+
return
98+
99+
self._Draw()
100+
101+
102+
# API; just implement the following, using self._bufnr and
103+
# self._latest_response as required
104+
105+
@abc.abstractmethod
106+
def _NewRequest( self, request_range ):
107+
# prepare a new request_data and return it
108+
pass
109+
110+
111+
@abc.abstractmethod
112+
def _Draw( self ):
113+
# actuall paint the properties
114+
pass

0 commit comments

Comments
 (0)