" 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 " " Explanation for how this module works: " " The entry point is youcompleteme#finder#FindSymbol, which takes a scope " argument: " " * 'document' scope - search document symbols - GoToDocumentOutline " * 'workspace' scope - search workspace symbols - GoToSymbol " " In general, the approach is: " - create a prompt buffer, and display it to use for input of a query " - open up a popup to display the results in the middle of the screen " - as the query changes, re-query the server to get the newly filtered results " - as the server returns results, update the contents of the results-popup " - use a popup-filter to implement some interractivity with the results, such " as down/up, and select item " - when an item is selected, open its buffer in the window that was current " when the search started and jump to the selected item. " - then populate the quickfix list with any matching results and close the " popup and prompt buffer. " " However, there is an important wrinke/distinction that leads to the code being " a little more complicated than that: the 'search' mechanism for workspace and " document symbols is different. " " The reaosn for this differece is what the servers provide in response to the 2 " requests: " " * 'document' scope - we use ycmd's filtering and sorting. " GoToDocumentOutline (LSP documentSymbol) request does not take any filter " argument. It returns all symbols in the current document. Therefore, we use " ycmd's filter_and_sort_candidates endpoint to use our own fuzzy search on " the results. This is a _great_ experience, it's _super_ fast and familar to " YCM users. " * 'workspace' scope - we have to rely on the downstream server's filtering and " sorting. " GoToSymbol (LSP workspaceSymbol) request takes a query parameter to search " with. While the LSP spec states that an empty query means 'return " everything', no servers actually implement that, so we have to pass our " query down to the server. " " The result of this is that while a lot of the code is shared, there are " slightly different steps involved in the process: " " * for 'document' requests, as soon as the popup is opened, we issue a " GoToDocumentOutline request, storing the results as `raw_results`, then " then immediately start filtering it with filter_and_sort_candidates, storing " the filtered results in `results` " * for 'workspace' requests, as soon as the popup is opened, we issue a " GoToSymbol request with an empty query. This usually returns nothing, but if " any servers return anything then it would be stored in 'results'. As the " user types, we repeat the GoToSymbol request and update the 'results' " " In order to simplify the re-query code, we put the function used to filter " results in the state variable as 'query_func'. " " As the expereince of completely different filtering and sorting for workspace " vs document is so jarring, by default we actually pass all restuls from " 'workspace' search through filter_and_sort_candidates too. This isn't perfect " because it breaks the mental model when the server's filtering is dumb, but it " at least means that sorting is consistent. This can be disabled by setting " g:ycm_refilter_workspace_symbols to 0. " " The key functions are: " " - FindSymbol - initiate the request - open the popup/prompt buffer, set up " the state object to defaults and initiate the first request " (RequestDocumentSymbols for 'documnt' and SearchWorkspace for 'workspace' ) " " - RequestDocumentSymbols - perform the GoToDocumentOutline request and store " the results in 'raw_results' " " - SearchDocument - perform filter_and_sort_candidates request on the " 'raw_results', and store the results in 'results', then call " "HandleSymbolSearchResults" " " - SearchWorkspace - perform GoToSymbol request for all open filetypes, " and store the results in 'raw_results' as a dict mapping " filetype->results. Merge the results in to 'results', then call " "HandleSymbolSearchResults" " " - HandleSymbolSearchResults - redraw the popup with the 'results' " " - HandleKeyPress - handle a keypress while the popup is visible, intercepts " things to handle interractivity " " - PopupClosed - callback when the popup is closed. This openes the result in " the original window. " " 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 let s:prompt = 'Find Symbol: ' let s:find_symbol_status = {} " Entry point {{{ function! youcompleteme#finder#FindSymbol( scope ) abort if !py3eval( 'vimsupport.VimSupportsPopupWindows()' ) echo 'Sorry, this feature is not supported in your editor' 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 let s:find_symbol_status = { \ 'selected': -1, \ 'query': '', \ 'results': [], \ 'raw_results': v:none, \ 'all_filetypes': v:true, \ 'pending': [], \ 'winid': win_getid(), \ 'bufnr': bufnr(), \ 'prompt_bufnr': -1, \ 'prompt_winid': -1, \ 'filter': v:none, \ 'id': v:none, \ 'cursorline_match': v:none, \ 'spinner': 0, \ 'spinner_timer': -1, \ } let opts = { \ 'padding': [ 1, 2, 1, 2 ], \ 'wrap': 0, \ 'minwidth': &columns / 3 * 2, \ 'minheight': &lines / 3 * 2, \ 'maxwidth': &columns / 3 * 2, \ 'maxheight': &lines / 3 * 2, \ 'line': &lines / 6, \ 'col': &columns / 6, \ 'pos': 'topleft', \ 'drag': 1, \ 'resize': 1, \ 'close': 'button', \ 'border': [], \ 'callback': function( 's:PopupClosed' ), \ 'filter': function( 's:HandleKeyPress' ), \ 'highlight': 'Normal', \ } if &ambiwidth ==# 'single' && &encoding ==? 'utf-8' let opts[ 'borderchars' ] = [ '─', '│', '─', '│', '╭', '╮', '┛', '╰' ] endif if a:scope ==# 'document' let s:find_symbol_status.query_func = function( 's:SearchDocument' ) else let s:find_symbol_status.query_func = function( 's:SearchWorkspace' ) endif let s:find_symbol_status.id = popup_create( 'Type to query for stuff', opts ) " Kick off the request now if a:scope ==# 'document' call s:RequestDocumentSymbols() else call s:SearchWorkspace( '', v:true ) endif let bufnr = bufadd( '_ycm_filter_' ) silent call bufload( bufnr ) silent topleft 1split _ycm_filter_ " Disable ycm/completion in this buffer call setbufvar( bufnr, 'ycm_largefile', 1 ) let s:find_symbol_status.prompt_bufnr = bufnr let s:find_symbol_status.prompt_winid = win_getid() setlocal buftype=prompt noswapfile modifiable nomodified noreadonly setlocal nobuflisted bufhidden=delete textwidth=0 call prompt_setprompt( bufnr(), s:prompt ) augroup YCMPromptFindSymbol autocmd! autocmd TextChanged,TextChangedI call s:OnQueryTextChanged() autocmd WinLeave call s:Cancel() autocmd CmdLineEnter call s:Cancel() augroup END startinsert endfunction function! s:OnQueryTextChanged() abort if s:find_symbol_status.id < 0 return endif let bufnr = s:find_symbol_status.prompt_bufnr let query = getbufline( bufnr, '$' )[ 0 ] let s:find_symbol_status.query = query[ len( s:prompt ) : ] " really, re-query if we can call s:RequeryFinderPopup( v:true ) call win_execute( s:find_symbol_status.prompt_winid, 'setlocal nomodified' ) endfunction function! s:Cancel() abort if s:find_symbol_status.id < 0 return endif call popup_close( s:find_symbol_status.id, -1 ) endfunction " }}} " Popup and keyboard events {{{ function! s:HandleKeyPress( id, key ) abort let redraw = 0 let handled = 0 let requery = 0 " The input for the search/query is taken from the prompt buffer and the " TextChangedI event if a:key ==# "\" || \ a:key ==# "\" || \ a:key ==# "\" || \ a:key ==# "\" let s:find_symbol_status.selected += 1 " wrap if s:find_symbol_status.selected >= len( s:find_symbol_status.results ) let s:find_symbol_status.selected = 0 endif let redraw = 1 let handled = 1 elseif a:key ==# "\" || \ a:key ==# "\" || \ a:key ==# "\" || \ a:key ==# "\" let s:find_symbol_status.selected -= 1 " wrap if s:find_symbol_status.selected < 0 let s:find_symbol_status.selected = \ len( s:find_symbol_status.results ) - 1 endif let redraw = 1 let handled = 1 elseif a:key ==# "\" || a:key ==# "\" let s:find_symbol_status.selected += \ popup_getpos( s:find_symbol_status.id ).core_height " Don't wrap if s:find_symbol_status.selected >= len( s:find_symbol_status.results ) let s:find_symbol_status.selected = \ len( s:find_symbol_status.results ) - 1 endif let redraw = 1 let handled = 1 elseif a:key ==# "\" || a:key ==# "\" let s:find_symbol_status.selected -= \ popup_getpos( s:find_symbol_status.id ).core_height " Don't wrap if s:find_symbol_status.selected < 0 let s:find_symbol_status.selected = 0 endif let redraw = 1 let handled = 1 elseif a:key ==# "\" call popup_close( a:id, -1 ) let handled = 1 elseif a:key ==# "\" if s:find_symbol_status.selected >= 0 call popup_close( a:id, s:find_symbol_status.selected ) let handled = 1 endif elseif a:key ==# "\" || a:key ==# "\" let s:find_symbol_status.selected = 0 let redraw = 1 let handled = 1 elseif a:key ==# "\" || a:key ==# "\" let s:find_symbol_status.selected = len( s:find_symbol_status.results ) - 1 let redraw = 1 let handled = 1 elseif a:key ==# "\" " TOggle filetypes? let s:find_symbol_status.all_filetypes = !s:find_symbol_status.all_filetypes let redraw = 0 let requery = 1 let handled = 1 endif if requery call s:RequeryFinderPopup( v:true ) elseif redraw call s:RedrawFinderPopup() endif return handled endfunction " Handle the popup closing: jump to the selected item function! s:PopupClosed( id, selected ) abort stopinsert call win_gotoid( s:find_symbol_status.prompt_winid ) silent bwipe! " Return to original window call win_gotoid( s:find_symbol_status.winid ) if a:selected >= 0 let selected = s:find_symbol_status.results[ a:selected ] py3 vimsupport.JumpToLocation( \ filename = vimsupport.ToUnicode( vim.eval( 'selected.filepath' ) ), \ line = int( vim.eval( 'selected.line_num' ) ), \ column = int( vim.eval( 'selected.column_num' ) ), \ modifiers = '', \ command = 'same-buffer' \ ) if len( s:find_symbol_status.results ) > 1 " Also, populate the quickfix list py3 vimsupport.SetQuickFixList( \ [ vimsupport.BuildQfListItem( x ) for x in \ vim.eval( 's:find_symbol_status.results' ) ] ) " Emulate :echo, to avoid a redraw getting rid of the message. let txt = 'Added ' . len( getqflist() ) . ' entries to quickfix list.' call popup_notification( \ txt, \ { \ 'line': 1, \ 'col': &columns - len( txt ), \ 'padding': [ 0, 0, 0, 0 ], \ 'border': [ 0, 0, 0, 0 ], \ 'highlight': 'PMenu' \ } ) " But don't open it, as this could take up valuable actual screen space " py3 vimsupport.OpenQuickFixList() endif endif call s:EndRequest() let s:find_symbol_status.id = -1 endfunction "}}} " Results handling and re-query {{{ " Render a set of results returned from the filter/search function function! s:HandleSymbolSearchResults( results ) abort let s:find_symbol_status.results = [] if s:find_symbol_status.id < 0 " Popup was closed, ignore this event return endif let s:find_symbol_status.results = a:results call s:RedrawFinderPopup() " Re-query but no change in the query text call s:RequeryFinderPopup( v:false ) endfunction " Set the popup text function! s:RedrawFinderPopup() abort " Clamp selected. If there are any results, the first one is selected by " default let s:find_symbol_status.selected = max( [ \ s:find_symbol_status.selected, \ len( s:find_symbol_status.results ) > 0 ? 0 : -1 \ ] ) let s:find_symbol_status.selected = min( [ \ s:find_symbol_status.selected, \ len( s:find_symbol_status.results ) - 1 \ ] ) if empty( s:find_symbol_status.results ) call popup_settext( s:find_symbol_status.id, 'No results' ) let s:find_symbol_status.selected = -1 else let popup_width = popup_getpos( s:find_symbol_status.id ).core_width let buffer = [] let len_filetype = 0 for result in s:find_symbol_status.results let len_filetype = max( [ len_filetype, len( result[ 'filetype' ] ) ] ) endfor if len_filetype > 0 let filetype_sep = ' ' else let filetype_sep = '' endif let available_width = popup_width - len_filetype - len( filetype_sep ) for result in s:find_symbol_status.results " Calculate the text to use. Try and include the full path and line " number, (right aligned), but truncate if there isn't space for the " description and the file path. Include at least 8 spaces between them " (if there's room). if result->has_key( 'extra_data' ) 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 props = [ \ { 'col': 1, \ 'length': len( kind ) + 2, \ 'type': 'YCM-symbol-Normal' }, \ { 'col': len( kind ) + 3, \ 'length': len( name ), \ 'type': prop }, \ ] elseif result->has_key( 'description' ) let desc = result[ 'description' ] let props = [ \ { 'col': 1, 'length': len( desc ), 'type': 'YCM-symbol-Normal' }, \ ] else let desc = 'Invalid entry: ' . string( result ) let props = [] endif let line_num = result[ 'line_num' ] let path = fnamemodify( result[ 'filepath' ], ':.' ) \ .. ':' \ .. line_num let path_includes_line = 1 let spaces = available_width - len( desc ) - len( path ) let spacing = 4 if spaces < spacing let spaces = spacing let space_for_path = available_width - spacing - len( desc ) let path_includes_line = space_for_path - 3 > len( line_num ) + 1 if space_for_path > 3 let path = '...' . strpart( path, len( path ) - space_for_path + 3 ) elseif space_for_path <= 0 let path = '' else let path_includes_line = 0 let path = '...' endif endif let line = desc \ .. repeat( ' ', spaces ) \ .. path \ .. filetype_sep \ .. result[ 'filetype' ] if len( path ) > 0 if path_includes_line let props += [ \ { 'col': available_width - len( path ) + 1, \ 'length': len( path ) - len( line_num ), \ 'type': 'YCM-symbol-file' }, \ { 'col': available_width - len( line_num ) + 1, \ 'length': len( line_num ), \ 'type': 'YCM-symbol-line-num' }, \ ] else let props += [ \ { 'col': available_width - len( path ) + 1, \ 'length': len( path ), \ 'type': 'YCM-symbol-file' }, \ ] endif endif if len_filetype > 0 let props += [ \ { 'col': popup_width - len_filetype + len( filetype_sep ), \ 'length': len_filetype, \ 'type': 'YCM-symbol-filetype' }, \ ] endif call add( buffer, { 'text': line, 'props': props } ) endfor call popup_settext( s:find_symbol_status.id, buffer ) endif if s:find_symbol_status.selected > -1 " Move the cursor so that cursorline highlights the selected item. Also " scroll the window if the selected item is not in view. To make scrolling " feel natural we position the current line a the bottom of the window if " the new current line is below the current viewport, and at the top if the " new current line is above the viewport. let new_line = s:find_symbol_status.selected + 1 let pos = popup_getpos( s:find_symbol_status.id ) call win_execute( s:find_symbol_status.id, \ 'call cursor( [' . string( new_line ) . ', 1] )' ) if new_line < pos.firstline " New line is above the viewport, scroll so that this line is at the top " of the window. call win_execute( s:find_symbol_status.id, "normal z\" ) elseif new_line >= ( pos.firstline + pos.core_height ) " New line is below the viewport, scroll so that this line is at the " bottom of the window. call win_execute( s:find_symbol_status.id, ':normal z-' ) endif " Otherwise, new item is already displayed - don't scroll the window. if !getwinvar( s:find_symbol_status.id, '&cursorline' ) call win_execute( s:find_symbol_status.id, \ 'set cursorline cursorlineopt&' ) endif else call win_execute( s:find_symbol_status.id, 'set nocursorline' ) endif endfunction function! s:SetTitle() abort if s:find_symbol_status.spinner_timer > -1 let status = s:icon_spinner[ s:find_symbol_status.spinner ] else let status = s:icon_done endif call popup_setoptions( s:find_symbol_status.id, { \ 'title': ' [' . status . '] Search for symbol: ' \ . s:find_symbol_status.query . ' ' \ } ) endfunction " Re-query or re-filter by calling the filter function function! s:RequeryFinderPopup( new_query ) abort " Update the title even if we delay the query, as this makes the UI feel " snappy call s:SetTitle() call win_execute( s:find_symbol_status.winid, \ 'call s:find_symbol_status.query_func(' \ . 's:find_symbol_status.query,' \ . 'a:new_query )' ) endfunction function! s:ParseGoToResponse( filetype, results ) abort if type( a:results ) == v:t_none || empty( a:results ) let results = [] elseif type( a:results ) != v:t_list if type( a:results ) == v:t_dict && has_key( a:results, 'error' ) let results = [] else let results = [ a:results ] endif else let results = a:results endif call map( results, { _, r -> extend( r, { \ 'key': r->get( 'extra_data', {} )->get( 'name', r[ 'description' ] ), \ 'filetype': a:filetype \ } ) } ) return results endfunction " }}} " Spinner {{{ function! s:StartRequest() abort call s:EndRequest() let s:find_symbol_status.spinner = 0 let s:find_symbol_status.spinner_timer = timer_start( s:spinner_delay, \ function( 's:TickSpinner' ) ) call s:SetTitle() endfunction function! s:EndRequest() abort call timer_stop( s:find_symbol_status.spinner_timer ) let s:find_symbol_status.spinner_timer = -1 call s:SetTitle() endfunction function! s:TickSpinner( timer_id ) abort let s:find_symbol_status.spinner = ( s:find_symbol_status.spinner + 1 ) % \ len( s:icon_spinner ) let s:find_symbol_status.spinner_timer = timer_start( s:spinner_delay, \ function( 's:TickSpinner' ) ) call s:SetTitle() endfunction " }}} " Workspace search {{{ function! s:SearchWorkspace( query, new_query ) abort if a:new_query if s:find_symbol_status.raw_results is# v:none let raw_results = {} else let raw_results = copy( s:find_symbol_status.raw_results ) endif let s:find_symbol_status.raw_results = {} " FIXME: We might still get results for any pending results. There is no " cancellation mechanism implemented for the async request! let s:find_symbol_status.pending = [] if s:find_symbol_status.all_filetypes let ft_buffer_map = py3eval( 'vimsupport.AllOpenedFiletypes()' ) else let current_filetypes = py3eval( 'vimsupport.CurrentFiletypes()' ) let ft_buffer_map = {} for ft in current_filetypes let ft_buffer_map[ ft ] = [ bufnr() ] endfor endif for ft in keys( ft_buffer_map ) if !youcompleteme#filetypes#AllowedForFiletype( ft ) continue endif let s:find_symbol_status.raw_results[ ft ] = v:none if has_key( raw_results, ft ) && raw_results[ ft ] is# v:none call add( s:find_symbol_status.pending, \ [ ft, ft_buffer_map[ ft ][ 0 ] ] ) else call youcompleteme#GetRawCommandResponseAsync( \ function( 's:HandleWorkspaceSymbols', [ ft ] ), \ 'GoToSymbol', \ '--bufnr=' . ft_buffer_map[ ft ][ 0 ], \ 'ft=' . ft, \ a:query ) endif endfor if !empty( s:find_symbol_status.raw_results ) " We sent some requests call s:StartRequest() endif else " Just requery those completer filetypes that we're not currently waiting " for for [ ft, bufnr ] in copy( s:find_symbol_status.pending ) if s:find_symbol_status.raw_results[ ft ] isnot# v:none call filter( s:find_symbol_status.pending, { v -> v !=# ft } ) let s:find_symbol_status.raw_results[ ft ] = v:none call youcompleteme#GetRawCommandResponseAsync( \ function( 's:HandleWorkspaceSymbols', [ ft ] ), \ 'GoToSymbol', \ '--bufnr=' . bufnr, \ 'ft=' . ft, \ a:query ) endif endfor endif endfunction function! s:HandleWorkspaceSymbols( filetype, results ) abort let s:find_symbol_status.raw_results[ a:filetype ] = \ s:ParseGoToResponse( a:filetype, a:results ) " Collate the results from each filetype let results = [] let waiting = 0 for ft in keys( s:find_symbol_status.raw_results ) if s:find_symbol_status.raw_results[ ft ] is v:none let waiting = 1 continue endif call extend( results, s:find_symbol_status.raw_results[ ft ] ) endfor let query = s:find_symbol_status.query if g:ycm_refilter_workspace_symbols && !empty( results ) " This is kinda wonky, but seems to work well enough. " " We get the server to give us a result set, then use our own " filter_and_sort_candidates on the result set filtered by the server " " The reason for this is: " - server filterins will differ by server and this leads to horrible wonky " user experience " - ycmd filter is consistent, even if not perfect " - servers are supposed to return _all_ symbols if we request a query of " "" but not all servers actually do " " So as a compromise we let the server filter the results, then we " _refilter_ and sort them using ycmd's method. This provides consistency " with the filtering and sorting on the completion popup menu, with the " disadvantage of additional latency. " " We're not currently sure this is going to be perfecct, so we have a hidden " option to disable this re-filter/sort. " let results = py3eval( \ 'ycm_state.FilterAndSortItems( vim.eval( "results" ),' \ . ' "key",' \ . ' vim.eval( "query" ) )' ) endif if !waiting call s:EndRequest() endif eval s:HandleSymbolSearchResults( results ) endfunction " }}} " Document Search {{{ function! s:SearchDocument( query, new_query ) abort if !a:new_query return endif if type( s:find_symbol_status.raw_results ) == v:t_none call popup_settext( s:find_symbol_status.id, \ 'No symbols found in document' ) return endif " No spinner, because this is actually a synchronous call " Call filter_and_sort_candidates on the results (synchronously) let response = py3eval( \ 'ycm_state.FilterAndSortItems( ' \ . ' vim.eval( "s:find_symbol_status.raw_results" ),' \ . ' "key",' \ . ' vim.eval( "a:query" ) )' ) eval s:HandleSymbolSearchResults( response ) endfunction function! s:RequestDocumentSymbols() call s:StartRequest() call youcompleteme#GetRawCommandResponseAsync( \ function( 's:HandleDocumentSymbols' ), \ 'GoToDocumentOutline' ) endfunction function! s:HandleDocumentSymbols( results ) abort call s:EndRequest() let s:find_symbol_status.raw_results = s:ParseGoToResponse( '', a:results ) call s:SearchDocument( '', v:true ) endfunction " }}} " Utility for testing {{{ function! youcompleteme#finder#GetState() abort return s:find_symbol_status endfunction " }}} " This is basic vim plugin boilerplate let &cpoptions = s:save_cpo unlet s:save_cpo " vim: foldmethod=marker