From f194400aaf93dceee523d2805d830f5b8119d81f Mon Sep 17 00:00:00 2001 From: Shuangcheng-Ni <110970449+Shuangcheng-Ni@users.noreply.github.com> Date: Mon, 30 Jan 2023 18:03:53 +0800 Subject: [PATCH 1/3] Fix the problem of key bindings in finder buffers --- autoload/finder.vim | 879 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 879 insertions(+) create mode 100644 autoload/finder.vim diff --git a/autoload/finder.vim b/autoload/finder.vim new file mode 100644 index 0000000000..5172c738c9 --- /dev/null +++ b/autoload/finder.vim @@ -0,0 +1,879 @@ +" 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 + " override all the global mappings in the finder buffer + " by remapping each previously mapped key sequence to itself + " (still preserve the previously defined buffer-local mappings) + if exists( '*maplist' ) && exists( '*mapset' ) + let bufmapsave = maplist()->filter( 'v:val.buffer == 1' ) + for mapitem in map( + \ maplist()->filter( 'v:val.buffer == 0' ), + \ { _, val -> val->extend( #{ expr: 0, noremap: 1, rhs: val.lhs, buffer: 1, silent: 1 } ) } ) + call mapset( mapitem ) + endfor + for mapitem in bufmapsave + call mapset( mapitem ) + endfor + else + let bufmapsave = [] + let mapitems = [] + for mapitem in split( execute( 'map | map!' ), '\n\+' ) + let [ mapmode, lhs, attr; _ ] = split( mapitem, '\s\+', 1 ) + if attr =~ '^[*&]\?@' + call add( bufmapsave, lhs ) + else + call add( mapitems, [ mapmode, lhs ] ) + endif + endfor + for mapitem in mapitems + let [ mapmode, lhs ] = mapitem + if index( bufmapsave, lhs ) == -1 + let mapcmd = mapmode == '!' ? 'noremap!' : ( mapmode . 'noremap' ) + silent exec mapcmd . ' ' . lhs . ' ' . lhs + endif + endfor + endif + 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 From afc30350f851f0502fd7ad4919b496b0fcdb9cbf Mon Sep 17 00:00:00 2001 From: Shuangcheng-Ni <110970449+Shuangcheng-Ni@users.noreply.github.com> Date: Mon, 30 Jan 2023 19:32:02 +0800 Subject: [PATCH 2/3] Delete finder.vim --- autoload/finder.vim | 879 -------------------------------------------- 1 file changed, 879 deletions(-) delete mode 100644 autoload/finder.vim diff --git a/autoload/finder.vim b/autoload/finder.vim deleted file mode 100644 index 5172c738c9..0000000000 --- a/autoload/finder.vim +++ /dev/null @@ -1,879 +0,0 @@ -" 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 - " override all the global mappings in the finder buffer - " by remapping each previously mapped key sequence to itself - " (still preserve the previously defined buffer-local mappings) - if exists( '*maplist' ) && exists( '*mapset' ) - let bufmapsave = maplist()->filter( 'v:val.buffer == 1' ) - for mapitem in map( - \ maplist()->filter( 'v:val.buffer == 0' ), - \ { _, val -> val->extend( #{ expr: 0, noremap: 1, rhs: val.lhs, buffer: 1, silent: 1 } ) } ) - call mapset( mapitem ) - endfor - for mapitem in bufmapsave - call mapset( mapitem ) - endfor - else - let bufmapsave = [] - let mapitems = [] - for mapitem in split( execute( 'map | map!' ), '\n\+' ) - let [ mapmode, lhs, attr; _ ] = split( mapitem, '\s\+', 1 ) - if attr =~ '^[*&]\?@' - call add( bufmapsave, lhs ) - else - call add( mapitems, [ mapmode, lhs ] ) - endif - endfor - for mapitem in mapitems - let [ mapmode, lhs ] = mapitem - if index( bufmapsave, lhs ) == -1 - let mapcmd = mapmode == '!' ? 'noremap!' : ( mapmode . 'noremap' ) - silent exec mapcmd . ' ' . lhs . ' ' . lhs - endif - endfor - endif - 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 From 4b3f4f7ee08d63259b57302c70fe07695a72ea11 Mon Sep 17 00:00:00 2001 From: Shuangcheng-Ni <110970449+Shuangcheng-Ni@users.noreply.github.com> Date: Mon, 30 Jan 2023 19:33:04 +0800 Subject: [PATCH 3/3] Add files via upload --- autoload/youcompleteme/finder.vim | 32 +++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/autoload/youcompleteme/finder.vim b/autoload/youcompleteme/finder.vim index 1406312db7..5172c738c9 100644 --- a/autoload/youcompleteme/finder.vim +++ b/autoload/youcompleteme/finder.vim @@ -243,6 +243,38 @@ function! youcompleteme#finder#FindSymbol( scope ) abort autocmd WinLeave call s:Cancel() autocmd CmdLineEnter call s:Cancel() augroup END + " override all the global mappings in the finder buffer + " by remapping each previously mapped key sequence to itself + " (still preserve the previously defined buffer-local mappings) + if exists( '*maplist' ) && exists( '*mapset' ) + let bufmapsave = maplist()->filter( 'v:val.buffer == 1' ) + for mapitem in map( + \ maplist()->filter( 'v:val.buffer == 0' ), + \ { _, val -> val->extend( #{ expr: 0, noremap: 1, rhs: val.lhs, buffer: 1, silent: 1 } ) } ) + call mapset( mapitem ) + endfor + for mapitem in bufmapsave + call mapset( mapitem ) + endfor + else + let bufmapsave = [] + let mapitems = [] + for mapitem in split( execute( 'map | map!' ), '\n\+' ) + let [ mapmode, lhs, attr; _ ] = split( mapitem, '\s\+', 1 ) + if attr =~ '^[*&]\?@' + call add( bufmapsave, lhs ) + else + call add( mapitems, [ mapmode, lhs ] ) + endif + endfor + for mapitem in mapitems + let [ mapmode, lhs ] = mapitem + if index( bufmapsave, lhs ) == -1 + let mapcmd = mapmode == '!' ? 'noremap!' : ( mapmode . 'noremap' ) + silent exec mapcmd . ' ' . lhs . ' ' . lhs + endif + endfor + endif startinsert endfunction