diff --git a/README.md b/README.md index de59c43987..9edf24b960 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,7 @@ Contents - [Diagnostic Display](#diagnostic-display) - [Diagnostic Highlighting Groups](#diagnostic-highlighting-groups) - [Symbol Search](#symbol-search) + - [Type/Call Hierarchy](#typecall-hierarchy) - [Commands](#commands) - [YcmCompleter subcommands](#ycmcompleter-subcommands) - [GoTo Commands](#goto-commands) @@ -677,6 +678,8 @@ Quick Feature Summary * Code formatting (`Format`) * Semantic highlighting * Inlay hints +* Type hierarchy +* Call hierarchy ### C♯ @@ -720,6 +723,8 @@ Quick Feature Summary * Type information for identifiers (`GetType`) * Code formatting (`Format`) * Management of `gopls` server instance +* Inlay hints +* Call hierarchy ### JavaScript and TypeScript @@ -741,6 +746,7 @@ Quick Feature Summary * Organize imports (`OrganizeImports`) * Management of `TSServer` server instance * Inlay hints +* Call hierarchy ### Rust @@ -759,6 +765,7 @@ Quick Feature Summary * Management of `rust-analyzer` server instance * Semantic highlighting * Inlay hints +* Call hierarchy ### Java @@ -782,6 +789,9 @@ Quick Feature Summary * Execute custom server command (`ExecuteCommand `) * Management of `jdt.ls` server instance * Semantic highlighting +* Inlay hints +* Type hierarchy +* Call hierarchy User Guide ---------- @@ -913,10 +923,6 @@ Ctrl-l is not a suggestion, just an example. ### Semantic highlighting -**NOTE**: This feature is highly experimental and offered in the hope that it is -useful. It shall not be considered stable; if you find issues with it, feel free -to report them, however. - Semantic highlighting is the process where the buffer text is coloured according to the underlying semantic type of the word, rather than classic syntax highlighting based on regular expressions. This can be powerful additional data @@ -1883,6 +1889,61 @@ so you can use window commands `...` for example. for that, or use a window command (e.g. `j`) or the mouse to leave the prompt buffer window. +### Type/Call Hierarchy + +***This feature requires Vim and is not supported in Neovim*** + +**NOTE**: This feature is highly experimental and offered in the hope that it is +useful. Please help us by reporting issues and offering feedback. + +YCM provides a way to view and navigate hierarchies. The following hierarchies +are supported: + +* Type hierachy `(YCMTypeHierarchy)`: Display subtypes and supertypes + of the symbol under cursor. Expand down to subtypes and up to supertypes. +* Call hierarchy `(YCMCallHierarchy)`: Display callees and callers of + the symbol under cursor. Expand down to callers and up to callees. + +Take a look at this [![asciicast](https://asciinema.org/a/659925.svg)](https://asciinema.org/a/659925) +for brief demo. + +Hierarchy UI can be initiated by mapping something to the indicated plug +mappings, for example: + +```viml +nmap yth (YCMTypeHierarchy) +nmap ych (YCMCallHierarchy) +``` + +This opens a "modal" popup showing the current element in the hierarchy tree. +The current tree root is aligned to the left and child and parent nodes are +expanded to the right. Expand the tree "down" with ` and "up" with +``. + +The "root" of the tree can be re-focused to the selected item with +`` and further `` will show the parents of the selected item. This +can take a little getting used to, but it's particularly important with multiple +inheritance where a "child" of the current root may actually have other, +invisible, parent links. `` on that row will show these by setting the +display root to the selected item. + +When the hierarchy is displayed, the following keys are intercepted: + +* ``: Drill into the hierarchy at the selected item: expand and show + children of the selected item. +* ``: Show parents of the selected item. When applied to sub-types, this + will re-root the tree at that type, so that all parent types (are displayed). + Similar for callers. +* ``: Jump to the symbol currently selected. +* ``, ``, ``, `j`: Select the next item +* ``, ``, ``, `k`; Select the previous item +* Any other key: closes the popup without jumping to any location + +**Note:** you might think the call hierarchy tree is inverted, but we think +this way round is more intuitive because this is the typical way that call +stacks are displayed (with the current function at the top, and its callers +below). + Commands -------- @@ -2102,6 +2163,9 @@ Supported in filetypes: `c, cpp, objc, objcpp, cuda, go, java, rust` #### The `GoToCallers` and `GoToCallees` subcommands +Note: A much more powerful call and type hierarchy can be viewd interactively. +See [interactive type and call hierarchy](#interactive-type-and-call-hierarchy). + Populate the quickfix list with the callers, or callees respectively, of the function associated with the current cursor position. The semantics of this differ depending on the filetype and language server. @@ -3738,9 +3802,7 @@ let g:ycm_language_server = [] ### The `g:ycm_disable_signature_help` option This option allows you to disable all signature help for all completion engines. -There is no way to disable it per-completer. This option is _reserved_, meaning -that while signature help support remains experimental, its values and meaning -may change and it may be removed in a future version. +There is no way to disable it per-completer. Default: `0` diff --git a/autoload/youcompleteme.vim b/autoload/youcompleteme.vim index 0e137506ee..3ca046b20b 100644 --- a/autoload/youcompleteme.vim +++ b/autoload/youcompleteme.vim @@ -1763,6 +1763,11 @@ endfunction silent! nnoremap (YCMToggleInlayHints) \ call ToggleInlayHints() +silent! nnoremap (YCMTypeHierarchy) + \ call youcompleteme#hierarchy#StartRequest( 'type' ) +silent! nnoremap (YCMCallHierarchy) + \ call youcompleteme#hierarchy#StartRequest( 'call' ) + " This is basic vim plugin boilerplate let &cpo = s:save_cpo unlet s:save_cpo diff --git a/autoload/youcompleteme/finder.vim b/autoload/youcompleteme/finder.vim index bab8c6e24d..d492d1f9dd 100644 --- a/autoload/youcompleteme/finder.vim +++ b/autoload/youcompleteme/finder.vim @@ -113,35 +113,6 @@ scriptencoding utf-8 " The other functions are utility for the most part and handle things like " TextChangedI event, starting/stopping drawing the spinner and such. -let s:highlight_group_for_symbol_kind = { - \ 'Array': 'Identifier', - \ 'Boolean': 'Boolean', - \ 'Class': 'Structure', - \ 'Constant': 'Constant', - \ 'Constructor': 'Function', - \ 'Enum': 'Structure', - \ 'EnumMember': 'Identifier', - \ 'Event': 'Identifier', - \ 'Field': 'Identifier', - \ 'Function': 'Function', - \ 'Interface': 'Structure', - \ 'Key': 'Identifier', - \ 'Method': 'Function', - \ 'Module': 'Include', - \ 'Namespace': 'Type', - \ 'Null': 'Keyword', - \ 'Number': 'Number', - \ 'Object': 'Structure', - \ 'Operator': 'Operator', - \ 'Package': 'Include', - \ 'Property': 'Identifier', - \ 'String': 'String', - \ 'Struct': 'Structure', - \ 'TypeParameter': 'Typedef', - \ 'Variable': 'Identifier', - \ } -let s:initialized_text_properties = v:false - let s:icon_spinner = [ '/', '-', '\', '|', '/', '-', '\', '|' ] let s:icon_done = 'X' let s:spinner_delay = 100 @@ -156,18 +127,7 @@ function! youcompleteme#finder#FindSymbol( scope ) abort return endif - if !s:initialized_text_properties - call prop_type_add( 'YCM-symbol-Normal', { 'highlight': 'Normal' } ) - for k in keys( s:highlight_group_for_symbol_kind ) - call prop_type_add( - \ 'YCM-symbol-' . k, - \ { 'highlight': s:highlight_group_for_symbol_kind[ k ] } ) - endfor - call prop_type_add( 'YCM-symbol-file', { 'highlight': 'Comment' } ) - call prop_type_add( 'YCM-symbol-filetype', { 'highlight': 'Special' } ) - call prop_type_add( 'YCM-symbol-line-num', { 'highlight': 'Number' } ) - let s:initialized_text_properties = v:true - endif + call youcompleteme#symbol#InitSymbolProperties() let s:find_symbol_status = { \ 'selected': -1, @@ -470,11 +430,7 @@ function! s:RedrawFinderPopup() abort let kind = result[ 'extra_data' ][ 'kind' ] let name = result[ 'extra_data' ][ 'name' ] let desc = kind .. ': ' .. name - if s:highlight_group_for_symbol_kind->has_key( kind ) - let prop = 'YCM-symbol-' . kind - else - let prop = 'YCM-symbol-Normal' - endif + let prop = youcompleteme#symbol#GetPropForSymbolKind( kind ) let props = [ \ { 'col': 1, \ 'length': len( kind ) + 2, diff --git a/autoload/youcompleteme/hierarchy.vim b/autoload/youcompleteme/hierarchy.vim new file mode 100644 index 0000000000..d5b3563bd9 --- /dev/null +++ b/autoload/youcompleteme/hierarchy.vim @@ -0,0 +1,250 @@ +" Copyright (C) 2021 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 . + +" This is basic vim plugin boilerplate +let s:save_cpo = &cpoptions +set cpoptions&vim + +scriptencoding utf-8 + +let s:popup_id = -1 +let s:lines_and_handles = v:null +" 1-based index of the selected item in the popup +" -1 means none set +" 0 means nothing, (Invalid) +let s:select = -1 +let s:kind = '' + +let s:ingored_keys = [ + \ "\", + \ "\", + \ ] + +function! youcompleteme#hierarchy#StartRequest( kind ) + if !py3eval( 'vimsupport.VimSupportsPopupWindows()' ) + echo 'Sorry, this feature is not supported in your editor' + return + endif + + call youcompleteme#symbol#InitSymbolProperties() + py3 ycm_state.ResetCurrentHierarchy() + py3 from ycm.client.command_request import GetRawCommandResponse + if a:kind == 'call' + let lines_and_handles = py3eval( + \ 'ycm_state.InitializeCurrentHierarchy( GetRawCommandResponse( ' . + \ '[ "CallHierarchy" ], False ), ' . + \ 'vim.eval( "a:kind" ) )' ) + else + let lines_and_handles = py3eval( + \ 'ycm_state.InitializeCurrentHierarchy( GetRawCommandResponse( ' . + \ '[ "TypeHierarchy" ], False ), ' . + \ 'vim.eval( "a:kind" ) )' ) + endif + if len( lines_and_handles ) + let s:lines_and_handles = lines_and_handles + let s:kind = a:kind + let s:select = 1 + call s:SetUpMenu() + endif +endfunction + +function! s:MenuFilter( winid, key ) + if a:key == "\" + " Root changes if we're showing super-tree of a sub-tree of the root + " (indicated by the handle being positive) + let will_change_root = s:lines_and_handles[ s:select - 1 ][ 1 ] > 0 + call popup_close( + \ s:popup_id, + \ [ s:select - 1, 'resolve_up', will_change_root ] ) + return 1 + endif + if a:key == "\" + " Root changes if we're showing sub-tree of a super-tree of the root + " (indicated by the handle being negative) + let will_change_root = s:lines_and_handles[ s:select - 1 ][ 1 ] < 0 + call popup_close( + \ s:popup_id, + \ [ s:select - 1, 'resolve_down', will_change_root ] ) + return 1 + endif + if a:key == "\" + call popup_close( s:popup_id, [ s:select - 1, 'jump', v:none ] ) + return 1 + endif + if a:key == "\" || a:key == "\" || a:key == "\" || a:key == "k" + let s:select -= 1 + if s:select < 1 + let s:select = 1 + endif + call win_execute( s:popup_id, + \ 'call cursor( [' . string( s:select ) . ', 1 ] )' ) + call win_execute( s:popup_id, + \ 'set cursorline cursorlineopt&' ) + return 1 + endif + if a:key == "\" || a:key == "\" || a:key == "\" || a:key == "j" + let s:select += 1 + if s:select > len( s:lines_and_handles ) + let s:select = len( s:lines_and_handles ) + endif + call win_execute( s:popup_id, + \ 'call cursor( [' . string( s:select ) . ', 1 ] )' ) + call win_execute( s:popup_id, + \ 'set cursorline cursorlineopt&' ) + return 1 + endif + if index( s:ingored_keys, a:key ) >= 0 + return 0 + endif + " Close the popup on any other key press + call popup_close( s:popup_id, [ s:select - 1, 'cancel', v:none ] ) + if a:key == "\" || a:key == "\" + return 1 + endif + return 0 +endfunction + +function! s:MenuCallback( winid, result ) + let operation = a:result[ 1 ] + let selection = a:result[ 0 ] + if operation == 'resolve_down' + call s:ResolveItem( selection, 'down', a:result[ 2 ] ) + elseif operation == 'resolve_up' + call s:ResolveItem( selection, 'up', a:result[ 2 ] ) + else + if operation == 'jump' + let handle = s:lines_and_handles[ selection ][ 1 ] + py3 ycm_state.JumpToHierarchyItem( vimsupport.GetIntValue( "handle" ) ) + endif + py3 ycm_state.ResetCurrentHierarchy() + let s:kind = '' + let s:select = 1 + endif +endfunction + +function! s:SetUpMenu() + let opts = #{ + \ filter: funcref( 's:MenuFilter' ), + \ callback: funcref( 's:MenuCallback' ), + \ wrap: 0, + \ minwidth: &columns * 90/100, + \ maxwidth: &columns * 90/100, + \ maxheight: &lines * 75/100, + \ scrollbar: 1, + \ padding: [ 0, 0, 0, 0 ], + \ highlight: 'Normal', + \ border: [], + \ } + if &ambiwidth ==# 'single' && &encoding ==? 'utf-8' + let opts[ 'borderchars' ] = [ '─', '│', '─', '│', '╭', '╮', '╯', '╰' ] + endif + + let s:popup_id = popup_create( [], opts ) + let menu_lines = [] + let popup_width = popup_getpos( s:popup_id ).core_width + let tabstop = popup_width / 3 + for [ item, handle ] in s:lines_and_handles + let indent = repeat( ' ', item.indent ) + let name = indent + \ .. item.icon + \ .. item.kind + \ .. ': ' .. item.symbol + " -2 because: + " 0-based index + " 1 for the tab character + let trunc_name = name[ : tabstop - 2 ] + let props = [] + let name_pfx_len = len( indent ) + len( item.icon ) + len( item.kind ) + 2 + if len( trunc_name ) > name_pfx_len + let props += [ + \ { + \ 'col': name_pfx_len + 1, + \ 'length': len( trunc_name ) - name_pfx_len, + \ 'type': youcompleteme#symbol#GetPropForSymbolKind( item.kind ), + \ } + \ ] + endif + + let file_name = item.filepath .. ':' .. item.line_num + let trunc_path = file_name[ : tabstop - 2 ] + if len(trunc_path) > 0 + let props += [ + \ { + \ 'col': len(trunc_name) + 2, + \ 'length': min( [ len(trunc_path), len( item.filepath ) ] ), + \ 'type': 'YCM-symbol-file' + \ } + \ ] + if len(trunc_path) > len(item.filepath) + 1 + let props += [ + \ { + \ 'col': len(trunc_name) + 2 + len(item.filepath) + 1, + \ 'length': min( [ len(trunc_path), len( item.line_num ) ] ), + \ 'type': 'YCM-symbol-line-num' + \ } + \ ] + endif + endif + + let trunc_desc = item.description[ : tabstop - 2 ] + + let line = trunc_name + \ . "\t" + \ .. trunc_path + \ . "\t" + \ .. trunc_desc + call add( menu_lines, { 'text': line, 'props': props } ) + endfor + call win_execute( s:popup_id, + \ 'setlocal tabstop=' . tabstop ) + call popup_settext( s:popup_id, menu_lines ) + call win_execute( s:popup_id, + \ 'call cursor( [' . string( s:select ) . ', 1 ] )' ) + call win_execute( s:popup_id, + \ 'set cursorline cursorlineopt&' ) +endfunction + +function! s:ResolveItem( choice, direction, will_change_root ) + let handle = s:lines_and_handles[ a:choice ][ 1 ] + if py3eval( + \ 'ycm_state.ShouldResolveItem( vimsupport.GetIntValue( "handle" ), vim.eval( "a:direction" ) )' ) + let lines_and_handles_with_offset = py3eval( + \ 'ycm_state.UpdateCurrentHierarchy( ' . + \ 'vimsupport.GetIntValue( "handle" ), ' . + \ 'vim.eval( "a:direction" ) )' ) + let s:lines_and_handles = lines_and_handles_with_offset[ 0 ] + if a:will_change_root + " When re-rooting the tree, put the cursor on the new "root" item, as this + " helps with orientation. This behaviour is consistent with an expansion + " where we _don't_ re-root the tree, so feels more natural than anything + " else. + " The new root is the element with indent of 0. + " let s:select = 1 + indexof( s:lines_and_handles, + " \ { i, v -> v[0].indent == 0 } ) + let s:select = 1 + for item in s:lines_and_handles + if item[0].indent == 0 + break + endif + let s:select += 1 + endfor + else + let s:select += lines_and_handles_with_offset[ 1 ] + endif + endif + call s:SetUpMenu() +endfunction diff --git a/autoload/youcompleteme/symbol.vim b/autoload/youcompleteme/symbol.vim new file mode 100644 index 0000000000..f34120cdbd --- /dev/null +++ b/autoload/youcompleteme/symbol.vim @@ -0,0 +1,71 @@ +" Copyright (C) 2024 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 . + + +let s:highlight_group_for_symbol_kind = { + \ 'Array': 'Identifier', + \ 'Boolean': 'Boolean', + \ 'Class': 'Structure', + \ 'Constant': 'Constant', + \ 'Constructor': 'Function', + \ 'Enum': 'Structure', + \ 'EnumMember': 'Identifier', + \ 'Event': 'Identifier', + \ 'Field': 'Identifier', + \ 'Function': 'Function', + \ 'Interface': 'Structure', + \ 'Key': 'Identifier', + \ 'Method': 'Function', + \ 'Module': 'Include', + \ 'Namespace': 'Type', + \ 'Null': 'Keyword', + \ 'Number': 'Number', + \ 'Object': 'Structure', + \ 'Operator': 'Operator', + \ 'Package': 'Include', + \ 'Property': 'Identifier', + \ 'String': 'String', + \ 'Struct': 'Structure', + \ 'TypeParameter': 'Typedef', + \ 'Variable': 'Identifier', + \ } +let s:initialized_text_properties = v:false + +function! youcompleteme#symbol#InitSymbolProperties() abort + if !s:initialized_text_properties + call prop_type_add( 'YCM-symbol-Normal', { 'highlight': 'Normal' } ) + for k in keys( s:highlight_group_for_symbol_kind ) + call prop_type_add( + \ 'YCM-symbol-' . k, + \ { 'highlight': s:highlight_group_for_symbol_kind[ k ] } ) + endfor + call prop_type_add( 'YCM-symbol-file', { 'highlight': 'Comment' } ) + call prop_type_add( 'YCM-symbol-filetype', { 'highlight': 'Special' } ) + call prop_type_add( 'YCM-symbol-line-num', { 'highlight': 'Number' } ) + let s:initialized_text_properties = v:true + endif +endfunction + +function! youcompleteme#symbol#GetPropForSymbolKind( kind ) abort + if s:highlight_group_for_symbol_kind->has_key( a:kind ) + return 'YCM-symbol-' . a:kind + endif + + return 'YCM-symbol-Normal' +endfunction + + diff --git a/python/ycm/client/base_request.py b/python/ycm/client/base_request.py index 7c47fe32d0..707339612a 100644 --- a/python/ycm/client/base_request.py +++ b/python/ycm/client/base_request.py @@ -167,7 +167,7 @@ def _MakeRequest( data, handler, method, timeout, payload ): else: headers = BaseRequest._ExtraHeaders( method, request_uri ) if payload: - request_uri += ToBytes( f'?{urlencode( payload )}' ) + request_uri += ToBytes( f'?{ urlencode( payload ) }' ) _logger.debug( 'GET %s (%s)\n%s', request_uri, payload, headers ) return urlopen( @@ -249,6 +249,26 @@ def BuildRequestData( buffer_number = None ): } +def BuildRequestDataForLocation( file : str, line : int, column : int ): + buffer_number = vimsupport.GetBufferNumberForFilename( + file, + create_buffer_if_needed = True ) + try: + vim.eval( f'bufload( "{ file }" )' ) + except vim.error as e: + if 'E325' not in str( e ): + raise + buffer = vim.buffers[ buffer_number ] + file_data = vimsupport.GetUnsavedAndSpecifiedBufferData( buffer, file ) + return { + 'filepath': file, + 'line_num': line, + 'column_num': column, + 'working_dir': GetCurrentDirectory(), + 'file_data': file_data + } + + def _JsonFromFuture( future ): try: response = future.result() @@ -308,6 +328,7 @@ def _BuildUri( handler ): def MakeServerException( data ): + _logger.debug( 'Server exception: %s', data ) if data[ 'exception' ][ 'TYPE' ] == UnknownExtraConf.__name__: return UnknownExtraConf( data[ 'exception' ][ 'extra_conf_file' ] ) diff --git a/python/ycm/client/command_request.py b/python/ycm/client/command_request.py index ff29963ff3..a9045686d8 100644 --- a/python/ycm/client/command_request.py +++ b/python/ycm/client/command_request.py @@ -15,7 +15,9 @@ # You should have received a copy of the GNU General Public License # along with YouCompleteMe. If not, see . -from ycm.client.base_request import BaseRequest, BuildRequestData +from ycm.client.base_request import ( BaseRequest, + BuildRequestData, + BuildRequestDataForLocation ) from ycm import vimsupport DEFAULT_BUFFER_COMMAND = 'same-buffer' @@ -28,7 +30,11 @@ def _EnsureBackwardsCompatibility( arguments ): class CommandRequest( BaseRequest ): - def __init__( self, arguments, extra_data = None, silent = False ): + def __init__( self, + arguments, + extra_data = None, + silent = False, + location = None ): super( CommandRequest, self ).__init__() self._arguments = _EnsureBackwardsCompatibility( arguments ) self._command = arguments and arguments[ 0 ] @@ -38,10 +44,13 @@ def __init__( self, arguments, extra_data = None, silent = False ): self._response_future = None self._silent = silent self._bufnr = extra_data.pop( 'bufnr', None ) if extra_data else None + self._location = location def Start( self ): - if self._bufnr is not None: + if self._location is not None: + self._request_data = BuildRequestDataForLocation( *self._location ) + elif self._bufnr is not None: self._request_data = BuildRequestData( self._bufnr ) else: self._request_data = BuildRequestData() @@ -206,10 +215,14 @@ def _HandleDetailedInfoResponse( self, modifiers ): modifiers ) -def SendCommandRequestAsync( arguments, extra_data = None, silent = True ): +def SendCommandRequestAsync( arguments, + extra_data = None, + silent = True, + location = None ): request = CommandRequest( arguments, extra_data = extra_data, - silent = silent ) + silent = silent, + location = location ) request.Start() # Don't block return request @@ -218,12 +231,14 @@ def SendCommandRequestAsync( arguments, extra_data = None, silent = True ): def SendCommandRequest( arguments, modifiers, buffer_command = DEFAULT_BUFFER_COMMAND, - extra_data = None ): + extra_data = None, + skip_post_command_action = False ): request = SendCommandRequestAsync( arguments, extra_data = extra_data, silent = False ) # Block here to get the response - request.RunPostCommandActionsIfNeeded( modifiers, buffer_command ) + if not skip_post_command_action: + request.RunPostCommandActionsIfNeeded( modifiers, buffer_command ) return request.Response() @@ -233,3 +248,11 @@ def GetCommandResponse( arguments, extra_data = None ): silent = True ) # Block here to get the response return request.StringResponse() + + +def GetRawCommandResponse( arguments, silent, location = None ): + request = SendCommandRequestAsync( arguments, + extra_data = None, + silent = silent, + location = location ) + return request.Response() diff --git a/python/ycm/hierarchy_tree.py b/python/ycm/hierarchy_tree.py new file mode 100644 index 0000000000..2485ce47e0 --- /dev/null +++ b/python/ycm/hierarchy_tree.py @@ -0,0 +1,213 @@ +# Copyright (C) 2024 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 . + +from typing import Optional, List +from ycm import vimsupport +import os + + +class HierarchyNode: + def __init__( self, data, distance : int ): + self._references : Optional[ List[ int ] ] = None + self._data = data + self._distance_from_root = distance + + + def ToRootLocation( self, subindex : int ): + if location := self._data.get( 'root_location' ): + file = location[ 'filepath' ] + line = location[ 'line_num' ] + column = location[ 'column_num' ] + return file, line, column + else: + return self.ToLocation( subindex ) + + + def ToLocation( self, subindex : int ): + location = self._data[ 'locations' ][ subindex ] + line = location[ 'line_num' ] + column = location[ 'column_num' ] + file = location[ 'filepath' ] + return file, line, column + + +MAX_HANDLES_PER_INDEX = 1000000 + + +def handle_to_index( handle : int ): + return abs( handle ) // MAX_HANDLES_PER_INDEX + + +def handle_to_location_index( handle : int ): + return abs( handle ) % MAX_HANDLES_PER_INDEX + + +def make_handle( index : int, location_index : int ): + return index * MAX_HANDLES_PER_INDEX + location_index + + +class HierarchyTree: + def __init__( self ): + self._up_nodes : List[ HierarchyNode ] = [] + self._down_nodes : List[ HierarchyNode ] = [] + self._kind : str = '' + + def SetRootNode( self, items, kind : str ): + if items: + assert len( items ) == 1 + self._root_node_indices = [ 0 ] + self._down_nodes.append( HierarchyNode( items[ 0 ], 0 ) ) + self._up_nodes.append( HierarchyNode( items[ 0 ], 0 ) ) + self._kind = kind + return self.HierarchyToLines() + return [] + + + def UpdateHierarchy( self, handle : int, items, direction : str ): + current_index = handle_to_index( handle ) + nodes = self._down_nodes if direction == 'down' else self._up_nodes + if items: + nodes.extend( [ + HierarchyNode( item, + nodes[ current_index ]._distance_from_root + 1 ) + for item in items ] ) + nodes[ current_index ]._references = list( + range( len( nodes ) - len( items ), len( nodes ) ) ) + else: + nodes[ current_index ]._references = [] + + + def Reset( self ): + self._down_nodes = [] + self._up_nodes = [] + self._kind = '' + + + def _HierarchyToLinesHelper( self, refs, use_down_nodes ): + partial_result = [] + nodes = self._down_nodes if use_down_nodes else self._up_nodes + for index in refs: + next_node = nodes[ index ] + indent = 2 * next_node._distance_from_root + if index == 0: + can_expand = ( self._down_nodes[ 0 ]._references is None or + self._up_nodes[ 0 ]._references is None ) + else: + can_expand = next_node._references is None + symbol = '+' if can_expand else '-' + name = next_node._data[ 'name' ] + kind = next_node._data[ 'kind' ] + if use_down_nodes: + partial_result.extend( [ + ( + { + 'indent': indent, + 'icon': symbol, + 'symbol': name, + 'kind': kind, + 'filepath': os.path.split( l[ 'filepath' ] )[ 1 ], + 'line_num': str( l[ 'line_num' ] ), + 'description': l.get( 'description', '' ).strip(), + }, + make_handle( index, location_index ) + ) + for location_index, l in enumerate( next_node._data[ 'locations' ] ) + ] ) + else: + partial_result.extend( [ + ( + { + 'indent': indent, + 'icon': symbol, + 'symbol': name, + 'kind': kind, + 'filepath': os.path.split( l[ 'filepath' ] )[ 1 ], + 'line_num': str( l[ 'line_num' ] ), + 'description': l.get( 'description', '' ).strip(), + }, + make_handle( index, location_index ) * -1 + ) + for location_index, l in enumerate( next_node._data[ 'locations' ] ) + ] ) + if next_node._references: + partial_result.extend( + self._HierarchyToLinesHelper( + next_node._references, use_down_nodes ) ) + return partial_result + + def HierarchyToLines( self ): + down_lines = self._HierarchyToLinesHelper( [ 0 ], True ) + up_lines = self._HierarchyToLinesHelper( [ 0 ], False ) + up_lines.reverse() + return up_lines + down_lines[ 1: ] + + + def JumpToItem( self, handle : int, command ): + node_index = handle_to_index( handle ) + location_index = handle_to_location_index( handle ) + if handle >= 0: + node = self._down_nodes[ node_index ] + else: + node = self._up_nodes[ node_index ] + file, line, column = node.ToLocation( location_index ) + vimsupport.JumpToLocation( file, line, column, '', command ) + + + def ShouldResolveItem( self, handle : int, direction : str ): + node_index = handle_to_index( handle ) + if ( ( handle >= 0 and direction == 'down' ) or + ( handle <= 0 and direction == 'up' ) ): + if direction == 'down': + node = self._down_nodes[ node_index ] + else: + node = self._up_nodes[ node_index ] + return node._references is None + return True + + + def ResolveArguments( self, handle : int, direction : str ): + node_index = handle_to_index( handle ) + if self._kind == 'call': + direction = 'outgoing' if direction == 'up' else 'incoming' + else: + direction = 'supertypes' if direction == 'up' else 'subtypes' + if handle >= 0: + node = self._down_nodes[ node_index ] + else: + node = self._up_nodes[ node_index ] + return [ + f'Resolve{ self._kind.title() }HierarchyItem', + node._data, + direction + ] + + + def HandleToRootLocation( self, handle : int ): + node_index = handle_to_index( handle ) + + if handle >= 0: + node = self._down_nodes[ node_index ] + else: + node = self._up_nodes[ node_index ] + + location_index = handle_to_location_index( handle ) + return node.ToRootLocation( location_index ) + + + def UpdateChangesRoot( self, handle : int, direction : str ): + return ( ( handle < 0 and direction == 'down' ) or + ( handle > 0 and direction == 'up' ) ) diff --git a/python/ycm/tests/command_test.py b/python/ycm/tests/command_test.py index 89b4f350f9..52ce467030 100644 --- a/python/ycm/tests/command_test.py +++ b/python/ycm/tests/command_test.py @@ -47,7 +47,7 @@ def test_SendCommandRequest_ExtraConfVimData_Works( self, ycm ): 'extra_conf_data': has_entries( { 'tempname()': '_TEMP_FILE_' } ), - } ) + } ), ) ) @@ -71,7 +71,7 @@ def test_SendCommandRequest_ExtraConfData_UndefinedValue( self, ycm ): 'tab_size': 2, 'insert_spaces': True, } ) - } ) + } ), ) ) @@ -102,7 +102,7 @@ def test_SendCommandRequest_BuildRange_NoVisualMarks( self, ycm, *args ): 'column_num': 12 } } - } + }, ) @@ -135,7 +135,7 @@ def test_SendCommandRequest_BuildRange_VisualMarks( self, ycm, *args ): 'column_num': 9 } } - } + }, ) @@ -153,7 +153,7 @@ def test_SendCommandRequest_IgnoreFileTypeOption( self, ycm, *args ): 'tab_size': 2, 'insert_spaces': True }, - } + }, ) with patch( 'ycm.youcompleteme.SendCommandRequest' ) as send_request: diff --git a/python/ycm/tests/test_utils.py b/python/ycm/tests/test_utils.py index 7209481d6e..17025af9cc 100644 --- a/python/ycm/tests/test_utils.py +++ b/python/ycm/tests/test_utils.py @@ -35,8 +35,8 @@ BUFNR_REGEX = re.compile( - '^bufnr\\(\'(?P.+)\'(, ([01]))?\\)$' ) -BUFWINNR_REGEX = re.compile( '^bufwinnr\\((?P[0-9]+)\\)$' ) + '^bufnr\\( \'(?P.+)\'(, ([01]))? \\)$' ) +BUFWINNR_REGEX = re.compile( '^bufwinnr\\( (?P[0-9]+) \\)$' ) BWIPEOUT_REGEX = re.compile( '^(?:silent! )bwipeout!? (?P[0-9]+)$' ) GETBUFVAR_REGEX = re.compile( diff --git a/python/ycm/vimsupport.py b/python/ycm/vimsupport.py index 826a5b59fc..f84cfc9201 100644 --- a/python/ycm/vimsupport.py +++ b/python/ycm/vimsupport.py @@ -152,8 +152,8 @@ def GetUnsavedAndSpecifiedBufferData( included_buffer, included_filepath ): def GetBufferNumberForFilename( filename, create_buffer_if_needed = False ): realpath = os.path.realpath( filename ) return MADEUP_FILENAME_TO_BUFFER_NUMBER.get( realpath, GetIntValue( - f"bufnr('{ EscapeForVim( realpath ) }', " - f"{ int( create_buffer_if_needed ) })" ) ) + f"bufnr( '{ EscapeForVim( realpath ) }', " + f"{ int( create_buffer_if_needed ) } )" ) ) def GetCurrentBufferFilepath(): @@ -163,7 +163,7 @@ def GetCurrentBufferFilepath(): def BufferIsVisible( buffer_number ): if buffer_number < 0: return False - window_number = GetIntValue( f"bufwinnr({ buffer_number })" ) + window_number = GetIntValue( f"bufwinnr( { buffer_number } )" ) return window_number != -1 @@ -259,7 +259,7 @@ def VisibleRangeOfBufferOverlaps( bufnr, expanded_range ): def CaptureVimCommand( command ): - return vim.eval( f"execute( '{EscapeForVim(command)}', 'silent!' )" ) + return vim.eval( f"execute( '{ EscapeForVim( command ) }', 'silent!' )" ) def GetSignsInBuffer( buffer_number ): @@ -345,7 +345,7 @@ def GetTextProperties( buffer_number ): else: properties = [] for line_number in range( len( vim.buffers[ buffer_number ] ) ): - vim_props = vim.eval( f'prop_list( {line_number + 1}, ' + vim_props = vim.eval( f'prop_list( { line_number + 1 }, ' f'{{ "bufnr": { buffer_number } }} )' ) properties.extend( DiagnosticProperty( @@ -818,9 +818,9 @@ def PresentDialog( message, choices, default_choice_index = 0 ): [Y]es, (N)o, May(b)e:""" message = EscapeForVim( ToUnicode( message ) ) choices = EscapeForVim( ToUnicode( '\n'.join( choices ) ) ) - to_eval = ( f"confirm('{ message }', " - f"'{ choices }', " - f"{ default_choice_index + 1 })" ) + to_eval = ( f"confirm( '{ message }', " + f"'{ choices }', " + f"{ default_choice_index + 1 } )" ) try: return GetIntValue( to_eval ) - 1 except KeyboardInterrupt: @@ -1258,7 +1258,7 @@ def OpenFileInPreviewWindow( filename, modifiers ): """ Open the supplied filename in the preview window """ if modifiers: modifiers = ' ' + modifiers - vim.command( f'silent!{modifiers} pedit! { filename }' ) + vim.command( f'silent!{ modifiers } pedit! { filename }' ) def WriteToPreviewWindow( message, modifiers ): @@ -1353,7 +1353,7 @@ def OpenFilename( filename, options = {} ): # Open the file. try: - vim.command( f'{ options.get( "mods", "") }' + vim.command( f'{ options.get( "mods", "" ) }' f'{ size }' f'{ command } ' f'{ filename }' ) diff --git a/python/ycm/youcompleteme.py b/python/ycm/youcompleteme.py index dd9e30a144..9e60ae87bd 100644 --- a/python/ycm/youcompleteme.py +++ b/python/ycm/youcompleteme.py @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2018 YouCompleteMe contributors +# Copyright (C) 2011-2024 YouCompleteMe contributors # # This file is part of YouCompleteMe. # @@ -29,12 +29,14 @@ from ycmd.request_wrap import RequestWrap from ycm.omni_completer import OmniCompleter from ycm import syntax_parse +from ycm.hierarchy_tree import HierarchyTree from ycm.client.ycmd_keepalive import YcmdKeepalive from ycm.client.base_request import BaseRequest, BuildRequestData from ycm.client.completer_available_request import SendCompleterAvailableRequest from ycm.client.command_request import ( SendCommandRequest, SendCommandRequestAsync, - GetCommandResponse ) + GetCommandResponse, + GetRawCommandResponse ) from ycm.client.completion_request import CompletionRequest from ycm.client.resolve_completion_request import ResolveCompletionItem from ycm.client.signature_help_request import ( SignatureHelpRequest, @@ -108,6 +110,58 @@ def __init__( self, default_options = {} ): self._SetUpLogging() self._SetUpServer() self._ycmd_keepalive.Start() + self._current_hierarchy = HierarchyTree() + + + def InitializeCurrentHierarchy( self, items, kind ): + return self._current_hierarchy.SetRootNode( items, kind ) + + + def UpdateCurrentHierarchy( self, handle : int, direction : str ): + if not self._current_hierarchy.UpdateChangesRoot( handle, direction ): + items = self._ResolveHierarchyItem( handle, direction ) + self._current_hierarchy.UpdateHierarchy( handle, items, direction ) + + if items is not None and direction == 'up': + offset = sum( len( item[ 'locations' ] ) for item in items ) + else: + offset = 0 + + return self._current_hierarchy.HierarchyToLines(), offset + else: + location = self._current_hierarchy.HandleToRootLocation( handle ) + kind = self._current_hierarchy._kind + self._current_hierarchy.Reset() + items = GetRawCommandResponse( + [ f'{ kind.title() }Hierarchy' ], + silent = False, + location = location + ) + # [ 0 ] chooses the data for the 1st (and only) line. + # [ 1 ] chooses only the handle + handle = self.InitializeCurrentHierarchy( items, kind )[ 0 ][ 1 ] + return self.UpdateCurrentHierarchy( handle, direction ) + + + def _ResolveHierarchyItem( self, handle : int, direction : str ): + return GetRawCommandResponse( + self._current_hierarchy.ResolveArguments( handle, direction ), + silent = False + ) + + + def ShouldResolveItem( self, handle : int, direction : str ): + return self._current_hierarchy.ShouldResolveItem( handle, direction ) + + + def ResetCurrentHierarchy( self ): + self._current_hierarchy.Reset() + + + def JumpToHierarchyItem( self, handle ): + self._current_hierarchy.JumpToItem( + handle, + self._user_options[ 'goto_buffer_command' ] ) def _SetUpServer( self ): @@ -431,7 +485,10 @@ def GetCommandResponse( self, arguments ): return GetCommandResponse( final_arguments, extra_data ) - def SendCommandRequestAsync( self, arguments ): + def SendCommandRequestAsync( self, + arguments, + silent = True, + location = None ): final_arguments, extra_data = self._GetCommandRequestArguments( arguments, False, @@ -442,7 +499,9 @@ def SendCommandRequestAsync( self, arguments ): self._next_command_request_id += 1 self._command_requests[ request_id ] = SendCommandRequestAsync( final_arguments, - extra_data ) + extra_data, + silent, + location = location ) return request_id diff --git a/test/hierarchies.test.vim b/test/hierarchies.test.vim new file mode 100644 index 0000000000..e9a51a12cf --- /dev/null +++ b/test/hierarchies.test.vim @@ -0,0 +1,158 @@ +function! SetUp() + let g:ycm_auto_hover = 1 + 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_Call_Hierarchy() + call youcompleteme#test#setup#OpenFile( '/test/testdata/cpp/hierarchies.cc', {} ) + call cursor( [ 1, 5 ] ) + + call youcompleteme#hierarchy#StartRequest( 'call' ) + call WaitForAssert( { -> assert_equal( len( popup_list() ), 1 ) } ) + " Check that `+Function f` is at the start of the only line in the popup. + call WaitForAssert( { -> assert_equal( len( getbufline( winbufnr( popup_list()[ 0 ] ), 1, '$' ) ), 1 ) } ) + call assert_match( '^+Function: f', getbufline( winbufnr( popup_list()[ 0 ] ), 1 )[ 0 ] ) + + call feedkeys( "\", "xt" ) + " Check that f's callers are present. + call WaitForAssert( { -> assert_equal( len( getbufline( winbufnr( popup_list()[ 0 ] ), 1, '$' ) ), 4 ) } ) + call assert_match( '^+Function: f.*:1', getbufline( winbufnr( popup_list()[ 0 ] ), 1 )[ 0 ] ) + call assert_match( '^ +Function: g.*:4', getbufline( winbufnr( popup_list()[ 0 ] ), 2 )[ 0 ] ) + call assert_match( '^ +Function: g.*:4', getbufline( winbufnr( popup_list()[ 0 ] ), 3 )[ 0 ] ) + call assert_match( '^ +Function: h.*:9', getbufline( winbufnr( popup_list()[ 0 ] ), 4 )[ 0 ] ) + + call feedkeys( "\\", "xt" ) + " Check that g's callers are present. + call WaitForAssert( { -> assert_equal( len( getbufline( winbufnr( popup_list()[ 0 ] ), 1, '$' ) ), 5 ) } ) + call assert_match( '^+Function: f.*:1', getbufline( winbufnr( popup_list()[ 0 ] ), 1 )[ 0 ] ) + call assert_match( '^ -Function: g.*:4', getbufline( winbufnr( popup_list()[ 0 ] ), 2 )[ 0 ] ) + call assert_match( '^ -Function: g.*:4', getbufline( winbufnr( popup_list()[ 0 ] ), 3 )[ 0 ] ) + call assert_match( '^ +Function: h.*:8', getbufline( winbufnr( popup_list()[ 0 ] ), 4 )[ 0 ] ) + call assert_match( '^ +Function: h.*:9', getbufline( winbufnr( popup_list()[ 0 ] ), 5 )[ 0 ] ) + + " silent, because h has no incoming calls. + silent call feedkeys( "\\\", "xt" ) + " Check that 1st h's callers are present. + call WaitForAssert( { -> assert_equal( len( getbufline( winbufnr( popup_list()[ 0 ] ), 1, '$' ) ), 5 ) } ) + call assert_match( '^+Function: f.*:1', getbufline( winbufnr( popup_list()[ 0 ] ), 1 )[ 0 ] ) + call assert_match( '^ -Function: g.*:4', getbufline( winbufnr( popup_list()[ 0 ] ), 2 )[ 0 ] ) + call assert_match( '^ -Function: g.*:4', getbufline( winbufnr( popup_list()[ 0 ] ), 3 )[ 0 ] ) + call assert_match( '^ -Function: h.*:8', getbufline( winbufnr( popup_list()[ 0 ] ), 4 )[ 0 ] ) + call assert_match( '^ +Function: h.*:9', getbufline( winbufnr( popup_list()[ 0 ] ), 5 )[ 0 ] ) + + " silent, because h has no incoming calls. + silent call feedkeys( "\\", "xt" ) + " Check that 2nd h's callers are present. + call WaitForAssert( { -> assert_equal( len( getbufline( winbufnr( popup_list()[ 0 ] ), 1, '$' ) ), 5 ) } ) + call assert_match( '^+Function: f.*:1', getbufline( winbufnr( popup_list()[ 0 ] ), 1 )[ 0 ] ) + call assert_match( '^ -Function: g.*:4', getbufline( winbufnr( popup_list()[ 0 ] ), 2 )[ 0 ] ) + call assert_match( '^ -Function: g.*:4', getbufline( winbufnr( popup_list()[ 0 ] ), 3 )[ 0 ] ) + call assert_match( '^ -Function: h.*:8', getbufline( winbufnr( popup_list()[ 0 ] ), 4 )[ 0 ] ) + call assert_match( '^ -Function: h.*:9', getbufline( winbufnr( popup_list()[ 0 ] ), 5 )[ 0 ] ) + + " silent, because clangd does not support outgoing calls. + silent call feedkeys( "\\\\\", "xt" ) + " Try to access callees of f. + call WaitForAssert( { -> assert_equal( len( getbufline( winbufnr( popup_list()[ 0 ] ), 1, '$' ) ), 5 ) } ) + call assert_match( '^-Function: f.*:1', getbufline( winbufnr( popup_list()[ 0 ] ), 1 )[ 0 ] ) + call assert_match( '^ -Function: g.*:4', getbufline( winbufnr( popup_list()[ 0 ] ), 2 )[ 0 ] ) + call assert_match( '^ -Function: g.*:4', getbufline( winbufnr( popup_list()[ 0 ] ), 3 )[ 0 ] ) + call assert_match( '^ -Function: h.*:8', getbufline( winbufnr( popup_list()[ 0 ] ), 4 )[ 0 ] ) + call assert_match( '^ -Function: h.*:9', getbufline( winbufnr( popup_list()[ 0 ] ), 5 )[ 0 ] ) + + " silent, because clangd does not support outgoing calls. + silent call feedkeys( "\\\\\", "xt" ) + " Re-root at h. + call WaitForAssert( { -> assert_equal( len( getbufline( winbufnr( popup_list()[ 0 ] ), 1, '$' ) ), 1 ) } ) + call assert_match( '^+Function: h', getbufline( winbufnr( popup_list()[ 0 ] ), 1 )[0] ) + + " silent, because clangd does not support outgoing calls. + silent call feedkeys( "\\", "xt" ) + " Expansion after re-rooting works. + " NOTE: Clangd does not support outgoing calls, hence, we are stuck at just h. + call WaitForAssert( { -> assert_equal( len( getbufline( winbufnr( popup_list()[ 0 ] ), 1, '$' ) ), 1 ) } ) + call assert_match( '^-Function: h', getbufline( winbufnr( popup_list()[ 0 ] ), 1 )[ 0 ] ) + + call feedkeys( "\", "xt" ) + " Make sure it is closed. + call WaitForAssert( { -> assert_equal( len( popup_list() ), 0 ) } ) + + %bwipe! +endfunction + +function! Test_Type_Hierarchy() + call youcompleteme#test#setup#OpenFile( '/test/testdata/cpp/hierarchies.cc', {} ) + call cursor( [ 13, 8 ] ) + + call youcompleteme#hierarchy#StartRequest( 'type' ) + call WaitForAssert( { -> assert_equal( len( popup_list() ), 1 ) } ) + " Check that `+Struct: B1` is at the start of the only line in the popup. + call WaitForAssert( { -> assert_equal( len( getbufline( winbufnr( popup_list()[ 0 ] ), 1, '$' ) ), 1 ) } ) + call assert_match( '^+Struct: B1', getbufline( winbufnr( popup_list()[ 0 ] ), 1 )[ 0 ] ) + + call feedkeys( "\", "xt" ) + " Check that B1's subtypes are present. + call WaitForAssert( { -> assert_equal( len( getbufline( winbufnr( popup_list()[ 0 ] ), 1, '$' ) ), 2 ) } ) + call assert_match( '^+Struct: B1.*:13', getbufline( winbufnr( popup_list()[ 0 ] ), 1 )[ 0 ] ) + call assert_match( '^ +Struct: D1.*:16', getbufline( winbufnr( popup_list()[ 0 ] ), 2 )[ 0 ] ) + + " silent, because D1 has no subtypes. + silent call feedkeys( "\\", "xt" ) + " Try to access D1's subtypes. + call WaitForAssert( { -> assert_equal( len( getbufline( winbufnr( popup_list()[ 0 ] ), 1, '$' ) ), 2 ) } ) + call assert_match( '^+Struct: B1.*:13', getbufline( winbufnr( popup_list()[ 0 ] ), 1 )[ 0 ] ) + call assert_match( '^ -Struct: D1.*:16', getbufline( winbufnr( popup_list()[ 0 ] ), 2 )[ 0 ] ) + + call feedkeys( "\\", "xt" ) + " Check that B1's supertypes are present. + call WaitForAssert( { -> assert_equal( len( getbufline( winbufnr( popup_list()[ 0 ] ), 1, '$' ) ), 3 ) } ) + call assert_match( '^ +Struct: B0.*:12', getbufline( winbufnr( popup_list()[ 0 ] ), 1 )[ 0 ] ) + call assert_match( '^-Struct: B1.*:13', getbufline( winbufnr( popup_list()[ 0 ] ), 2 )[ 0 ] ) + call assert_match( '^ -Struct: D1.*:16', getbufline( winbufnr( popup_list()[ 0 ] ), 3 )[ 0 ] ) + + " silent, because there are no supertypes of B0. + silent call feedkeys( "\\", "xt" ) + " Try to access B0's supertypes. + call WaitForAssert( { -> assert_equal( len( getbufline( winbufnr( popup_list()[ 0 ] ), 1, '$' ) ), 3 ) } ) + call assert_match( '^ -Struct: B0.*:12', getbufline( winbufnr( popup_list()[ 0 ] ), 1 )[ 0 ] ) + call assert_match( '^-Struct: B1.*:13', getbufline( winbufnr( popup_list()[ 0 ] ), 2 )[ 0 ] ) + call assert_match( '^ -Struct: D1.*:16', getbufline( winbufnr( popup_list()[ 0 ] ), 3 )[ 0 ] ) + + call feedkeys( "\", "xt" ) + " Re-root at B0: supertypes->subtypes. + call WaitForAssert( { -> assert_equal( len( getbufline( winbufnr( popup_list()[ 0 ] ), 1, '$' ) ), 4 ) } ) + call assert_match( '^+Struct: B0.*:12', getbufline( winbufnr( popup_list()[ 0 ] ), 1 )[ 0 ] ) + call assert_match( '^ +Struct: B1.*:13', getbufline( winbufnr( popup_list()[ 0 ] ), 2 )[ 0 ] ) + call assert_match( '^ +Struct: D0.*:15', getbufline( winbufnr( popup_list()[ 0 ] ), 3 )[ 0 ] ) + call assert_match( '^ +Struct: D1.*:16', getbufline( winbufnr( popup_list()[ 0 ] ), 4 )[ 0 ] ) + + call feedkeys( "\\\\", "xt" ) + " Re-root at D1: subtypes->supertypes. + call WaitForAssert( { -> assert_equal( len( getbufline( winbufnr( popup_list()[ 0 ] ), 1, '$' ) ), 3 ) } ) + call assert_match( '^ +Struct: B0.*:12', getbufline( winbufnr( popup_list()[ 0 ] ), 1 )[ 0 ] ) + call assert_match( '^ +Struct: B1.*:13', getbufline( winbufnr( popup_list()[ 0 ] ), 2 )[ 0 ] ) + call assert_match( '^+Struct: D1.*:16', getbufline( winbufnr( popup_list()[ 0 ] ), 3 )[ 0 ] ) + + " silent, because there are no subtypes of D1. + silent call feedkeys( "\\\", "xt" ) + " Expansion after re-rooting works. + call WaitForAssert( { -> assert_equal( len( getbufline( winbufnr( popup_list()[ 0 ] ), 1, '$' ) ), 4 ) } ) + call assert_match( '^ +Struct: B0.*:12', getbufline( winbufnr( popup_list()[ 0 ] ), 1 )[ 0 ] ) + call assert_match( '^ +Struct: B0.*:12', getbufline( winbufnr( popup_list()[ 0 ] ), 2 )[ 0 ] ) + call assert_match( '^ -Struct: B1.*:13', getbufline( winbufnr( popup_list()[ 0 ] ), 3 )[ 0 ] ) + call assert_match( '^-Struct: D1.*:16', getbufline( winbufnr( popup_list()[ 0 ] ), 4 )[ 0 ] ) + + call feedkeys( "\", "xt" ) + " Make sure it is closed. + call WaitForAssert( { -> assert_equal( len( popup_list() ), 0 ) } ) + + %bwipe! +endfunction diff --git a/test/hover.test.vim b/test/hover.test.vim index f19df03db3..e430980540 100644 --- a/test/hover.test.vim +++ b/test/hover.test.vim @@ -71,7 +71,7 @@ let s:cpp_lifetime = { \ '', \ 'Type: char', \ 'Offset: 16 bytes', - \ 'Size: 1 byte (+7 bytes padding)', + \ 'Size: 1 byte (+7 bytes padding), alignment 1 byte', \ 'nobody will live > 128 years', \ '', \ '// In PointInTime', diff --git a/test/testdata/cpp/hierarchies.cc b/test/testdata/cpp/hierarchies.cc new file mode 100644 index 0000000000..f2af7513de --- /dev/null +++ b/test/testdata/cpp/hierarchies.cc @@ -0,0 +1,16 @@ +int f(); + +int g() { + return f() + f(); +} + +int h() { + int x = g(); + return f() + x; +} + +struct B0 {}; +struct B1 : B0 {}; + +struct D0 : B0 {}; +struct D1 : B0, B1 {}; diff --git a/third_party/ycmd b/third_party/ycmd index b63d2e86c6..e81c15e9ee 160000 --- a/third_party/ycmd +++ b/third_party/ycmd @@ -1 +1 @@ -Subproject commit b63d2e86c62d39c20284badfd4bfed6e8797d134 +Subproject commit e81c15e9eeda987c1d04e9efefa24a92b13ad6d4