Browse Source

Merge branch 'master' into inlay-hints

mergify[bot] 2 năm trước cách đây
mục cha
commit
8a2961159e

+ 4 - 0
README.md

@@ -1884,6 +1884,9 @@ input, and puts you in insert mode. This means that you can hit `<Esc>` to go
 into normal mode and use any other input commands that are supported in prompt
 buffers. As you type characters, the search is updated.
 
+Intially, results are queried from all open filetypes. You can hit `<C-f>` to
+switch to just the current filetype while the popup is open.
+
 While the popup is open, the following keys are intercepted:
 
 * `<C-j>`, `<Down>`, `<C-n>`, `<Tab>` - select the next item
@@ -1894,6 +1897,7 @@ While the popup is open, the following keys are intercepted:
 * `<End>`, `<kEnd>` - jump to last item
 * `<CR>` - jump to the selected item
 * `<C-c>` cancel/dismiss the popup
+* `<C-f>` - toggle results from all file types or just the current filetype
 
 The search is also cancelled if you leave the prompt buffer window at any time,
 so you can use window commands `<C-w>...` for example.

+ 57 - 54
autoload/youcompleteme.vim

@@ -61,6 +61,7 @@ let s:pollers = {
       \   'command': {
       \     'id': -1,
       \     'wait_milliseconds': 100,
+      \     'requests': {},
       \   },
       \   'semantic_highlighting': {
       \     'id': -1,
@@ -555,15 +556,6 @@ function! s:DisableOnLargeFile( buffer )
   return b:ycm_largefile
 endfunction
 
-function! s:HasAnyKey( dict, keys )
-  for key in a:keys
-    if has_key( a:dict, key )
-      return 1
-    endif
-  endfor
-  return 0
-endfunction
-
 function! s:PropertyTypeNotDefined( type )
   return exists( '*prop_type_add' ) &&
     \ index( prop_type_list(), a:type ) == -1
@@ -581,21 +573,13 @@ function! s:AllowedToCompleteInBuffer( buffer )
     let filetype = 'ycm_nofiletype'
   endif
 
-  let whitelist_allows = type( g:ycm_filetype_whitelist ) != v:t_dict ||
-        \ has_key( g:ycm_filetype_whitelist, '*' ) ||
-        \ s:HasAnyKey( g:ycm_filetype_whitelist, split( filetype, '\.' ) )
-  let blacklist_allows = type( g:ycm_filetype_blacklist ) != v:t_dict ||
-        \ !s:HasAnyKey( g:ycm_filetype_blacklist, split( filetype, '\.' ) )
-
-  let allowed = whitelist_allows && blacklist_allows
+  let allowed = youcompleteme#filetypes#AllowedForFiletype( filetype )
 
   if !allowed || s:DisableOnLargeFile( a:buffer )
     return 0
   endif
 
-  if allowed
-    let s:previous_allowed_buffer_number = bufnr( a:buffer )
-  endif
+  let s:previous_allowed_buffer_number = bufnr( a:buffer )
   return allowed
 endfunction
 
@@ -1429,16 +1413,17 @@ function! youcompleteme#GetCommandResponseAsync( callback, ... ) abort
     return
   endif
 
-  if s:pollers.command.id != -1
-    eval a:callback( '' )
-    return
-  endif
-
-  py3 ycm_state.SendCommandRequestAsync( vim.eval( "a:000" ) )
+  let request_id = py3eval(
+        \ 'ycm_state.SendCommandRequestAsync( vim.eval( "a:000" ) )' )
 
-  let s:pollers.command.id = timer_start(
-        \ s:pollers.command.wait_milliseconds,
-        \ function( 's:PollCommand', [ 'StringResponse', a:callback ] ) )
+  let s:pollers.command.requests[ request_id ] = {
+        \ 'response_func': 'StringResponse',
+        \ 'callback': a:callback
+        \ }
+  if s:pollers.command.id == -1
+    let s:pollers.command.id = timer_start( s:pollers.command.wait_milliseconds,
+                                          \ function( 's:PollCommands' ) )
+  endif
 endfunction
 
 
@@ -1453,39 +1438,57 @@ function! youcompleteme#GetRawCommandResponseAsync( callback, ... ) abort
     return
   endif
 
-  if s:pollers.command.id != -1
-    eval a:callback( { 'error': 'request in progress' } )
-    return
-  endif
+  let request_id = py3eval(
+        \ 'ycm_state.SendCommandRequestAsync( vim.eval( "a:000" ) )' )
 
-  py3 ycm_state.SendCommandRequestAsync( vim.eval( "a:000" ) )
-
-  let s:pollers.command.id = timer_start(
-        \ s:pollers.command.wait_milliseconds,
-        \ function( 's:PollCommand', [ 'Response', a:callback ] ) )
+  let s:pollers.command.requests[ request_id ] = {
+        \ 'response_func': 'Response',
+        \ 'callback': a:callback
+        \ }
+  if s:pollers.command.id == -1
+    let s:pollers.command.id = timer_start( s:pollers.command.wait_milliseconds,
+                                          \ function( 's:PollCommands' ) )
+  endif
 endfunction
 
 
-function! s:PollCommand( response_func, callback, id ) abort
-  if py3eval( 'ycm_state.GetCommandRequest() is None' )
-    " Possible in case of race conditions and things like RestartServer
-    " But particualrly in the tests
-    return
-  endif
-
-  if !py3eval( 'ycm_state.GetCommandRequest().Done()' )
-    let s:pollers.command.id = timer_start(
-          \ s:pollers.command.wait_milliseconds,
-          \ function( 's:PollCommand', [ a:response_func, a:callback ] ) )
-    return
-  endif
-
+function! s:PollCommands( timer_id ) abort
+  " Clear the timer id before calling the callback, as the callback might fire
+  " more requests
   call s:StopPoller( s:pollers.command )
 
-  let result = py3eval( 'ycm_state.GetCommandRequest().'
-                      \ .a:response_func . '()' )
+  " Must copy the requests because this loop is likely to modify it
+  let requests = copy( s:pollers.command.requests )
+  let poll_again = 0
+  for request_id in keys( requests )
+    let request = requests[ request_id ]
+    if py3eval( 'ycm_state.GetCommandRequest( int( vim.eval( "request_id" ) ) )'
+              \ . 'is None' )
+      " Possible in case of race conditions and things like RestartServer
+      " But particualrly in the tests
+      let result = v:none
+    elseif !py3eval( 'ycm_state.GetCommandRequest( '
+                   \ . 'int( vim.eval( "request_id" ) ) ).Done()' )
+      " Not ready yet, poll again and skip this one for now
+      let poll_again = 1
+      continue
+    else
+      let result = py3eval( 'ycm_state.GetCommandRequest( '
+                          \ . 'int( vim.eval( "request_id" ) ) ).'
+                          \ . request.response_func
+                          \ . '()' )
+    endif
+
+    " This request is done
+    call remove( s:pollers.command.requests, request_id )
+    py3 ycm_state.FlushCommandRequest( vim.eval( "request_id" ) )
+    call request[ 'callback' ]( result )
+  endfor
 
-  eval a:callback( result )
+  if poll_again && s:pollers.command.id == -1
+    let s:pollers.command.id = timer_start( s:pollers.command.wait_milliseconds,
+                                          \ function( 's:PollCommands' ) )
+  endif
 endfunction
 
 

+ 36 - 0
autoload/youcompleteme/filetypes.vim

@@ -0,0 +1,36 @@
+" Copyright (C) 2011-2018 YouCompleteMe contributors
+"
+" This file is part of YouCompleteMe.
+"
+" YouCompleteMe is free software: you can redistribute it and/or modify
+" it under the terms of the GNU General Public License as published by
+" the Free Software Foundation, either version 3 of the License, or
+" (at your option) any later version.
+"
+" YouCompleteMe is distributed in the hope that it will be useful,
+" but WITHOUT ANY WARRANTY; without even the implied warranty of
+" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+" GNU General Public License for more details.
+"
+" You should have received a copy of the GNU General Public License
+" along with YouCompleteMe.  If not, see <http://www.gnu.org/licenses/>.
+
+
+function! s:HasAnyKey( dict, keys ) abort
+  for key in a:keys
+    if has_key( a:dict, key )
+      return 1
+    endif
+  endfor
+  return 0
+endfunction
+
+function! youcompleteme#filetypes#AllowedForFiletype( filetype ) abort
+  let whitelist_allows = type( g:ycm_filetype_whitelist ) != v:t_dict ||
+        \ has_key( g:ycm_filetype_whitelist, '*' ) ||
+        \ s:HasAnyKey( g:ycm_filetype_whitelist, split( a:filetype, '\.' ) )
+  let blacklist_allows = type( g:ycm_filetype_blacklist ) != v:t_dict ||
+        \ !s:HasAnyKey( g:ycm_filetype_blacklist, split( a:filetype, '\.' ) )
+
+  return whitelist_allows && blacklist_allows
+endfunction

+ 197 - 75
autoload/youcompleteme/finder.vim

@@ -97,8 +97,10 @@ scriptencoding utf-8
 "    'raw_results', and store the results in 'results', then call
 "    "HandleSymbolSearchResults"
 "
-"  - SearchWorkspace - perform GoToSymbol request, 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'
 "
@@ -162,6 +164,7 @@ function! youcompleteme#finder#FindSymbol( scope ) abort
             \ { '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
@@ -171,8 +174,8 @@ function! youcompleteme#finder#FindSymbol( scope ) abort
         \ 'query': '',
         \ 'results': [],
         \ 'raw_results': v:none,
-        \ 'waiting': 0,
-        \ 'pending': 0,
+        \ 'all_filetypes': v:true,
+        \ 'pending': [],
         \ 'winid': win_getid(),
         \ 'bufnr': bufnr(),
         \ 'prompt_bufnr': -1,
@@ -216,12 +219,10 @@ function! youcompleteme#finder#FindSymbol( scope ) abort
   let s:find_symbol_status.id = popup_create( 'Type to query for stuff', opts )
 
   " Kick off the request now
-  let s:find_symbol_status.waiting = 1
   if a:scope ==# 'document'
-    call s:StartSpinner()
     call s:RequestDocumentSymbols()
   else
-    call s:SearchWorkspace( '' )
+    call s:SearchWorkspace( '', v:true )
   endif
 
   let bufnr = bufadd( '_ycm_filter_' )
@@ -253,9 +254,11 @@ function! s:OnQueryTextChanged() abort
   let bufnr = s:find_symbol_status.prompt_bufnr
   let query = getbufline( bufnr, '$' )[ 0 ]
   let s:find_symbol_status.query = query[ len( s:prompt ) : ]
-  let s:find_symbol_status.pending = 1
-  call s:RequeryFinderPopup()
-  setlocal nomodified
+
+  " really, re-query if we can
+  call s:RequeryFinderPopup( v:true )
+
+  call win_execute( s:find_symbol_status.prompt_winid, 'setlocal nomodified' )
 endfunction
 
 
@@ -273,6 +276,7 @@ endfunction
 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
@@ -334,9 +338,17 @@ function! s:HandleKeyPress( id, key ) abort
     let s:find_symbol_status.selected = len( s:find_symbol_status.results ) - 1
     let redraw = 1
     let handled = 1
+  elseif a:key ==# "\<C-f>"
+    " 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 redraw
+  if requery
+    call s:RequeryFinderPopup( v:true )
+  elseif redraw
     call s:RedrawFinderPopup()
   endif
 
@@ -389,7 +401,7 @@ function! s:PopupClosed( id, selected ) abort
   endif
 
 
-  call s:StopSpinner()
+  call s:EndRequest()
   let s:find_symbol_status.id = -1
 endfunction
 
@@ -399,9 +411,7 @@ endfunction
 
 " Render a set of results returned from the filter/search function
 function! s:HandleSymbolSearchResults( results ) abort
-  let s:find_symbol_status.waiting = 0
   let s:find_symbol_status.results = []
-  call s:StopSpinner()
 
   if s:find_symbol_status.id < 0
     " Popup was closed, ignore this event
@@ -410,7 +420,9 @@ function! s:HandleSymbolSearchResults( results ) abort
 
   let s:find_symbol_status.results = a:results
   call s:RedrawFinderPopup()
-  call s:RequeryFinderPopup()
+
+  " Re-query but no change in the query text
+  call s:RequeryFinderPopup( v:false )
 endfunction
 
 
@@ -435,6 +447,20 @@ function! s:RedrawFinderPopup() abort
 
     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
@@ -471,30 +497,54 @@ function! s:RedrawFinderPopup() abort
       let path = fnamemodify( result[ 'filepath' ], ':.' )
                \ .. ':'
                \ .. line_num
+      let path_includes_line = 1
 
-      let spaces = popup_width - len( desc ) - len( path )
-      let spacing = 8
+      let spaces = available_width - len( desc ) - len( path )
+      let spacing = 4
       if spaces < spacing
         let spaces = spacing
-        let path_len_to_use = popup_width - spacing - len( desc ) - 3
-        if path_len_to_use > 0
-          let path = '...' . strpart( path, len( path ) - path_len_to_use )
+        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 = '...:' .. line_num
+          let path_includes_line = 0
+          let path = '...'
         endif
       endif
-      let line = desc .. repeat( ' ', spaces ) .. path
-      call add( buffer, {
-            \ 'text': line,
-            \ 'props': props + [
-              \ { 'col': popup_width - len( path ) + 1,
+
+      let line = desc
+             \ .. repeat( ' ', spaces )
+             \ .. path
+             \ .. filetype_sep
+             \ .. result[ 'filetype' ]
+
+      if len( path ) > 0
+        let props += [
+              \ { 'col': available_width - len( path ) + 1,
               \   'length': len( path ) - len( line_num ),
               \   'type': 'YCM-symbol-file' },
-              \ { 'col': popup_width - len( line_num ) + 1,
-              \   'length': len( line_num ),
-              \   'type': 'YCM-symbol-line-num' }
               \ ]
-            \ } )
+        if path_includes_line
+          let props += [
+                \ { 'col': available_width - len( line_num ) + 1,
+                \   'length': len( line_num ),
+                \   'type': 'YCM-symbol-line-num' },
+                \ ]
+        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 )
@@ -548,28 +598,23 @@ endfunction
 
 
 " Re-query or re-filter by calling the filter function
-function! s:RequeryFinderPopup() abort
+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()
 
-  if s:find_symbol_status.waiting == 1
-    let s:find_symbol_status.pending = 1
-  elseif s:find_symbol_status.pending == 1
-    let s:find_symbol_status.pending = 0
-    let s:find_symbol_status.waiting = 1
-    call win_execute( s:find_symbol_status.winid,
-          \ 'call s:find_symbol_status.query_func('
-          \ . 's:find_symbol_status.query )' )
-  endif
+  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( results ) abort
+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 = {}
+      let results = []
     else
       let results = [ a:results ]
     endif
@@ -577,12 +622,10 @@ function! s:ParseGoToResponse( results ) abort
     let results = a:results
   endif
 
-  call map( results,
-        \ { i,v -> extend( v,
-              \ { 'key': v->get( 'extra_data',
-              \                  {} )->get( 'name',
-              \                             v[ 'description' ] ) } ) } )
-
+  call map( results, { _, r -> extend( r, {
+      \   'key': r->get( 'extra_data', {} )->get( 'name', r[ 'description' ] ),
+      \   'filetype': a:filetype
+      \ } ) } )
   return results
 endfunction
 
@@ -590,8 +633,8 @@ endfunction
 
 " Spinner {{{
 
-function! s:StartSpinner() abort
-  call s:StopSpinner()
+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,
@@ -600,8 +643,9 @@ function! s:StartSpinner() abort
   call s:SetTitle()
 endfunction
 
-function! s:StopSpinner() abort
+function! s:EndRequest() abort
   call timer_stop( s:find_symbol_status.spinner_timer )
+
   let s:find_symbol_status.spinner_timer = -1
 
   call s:SetTitle()
@@ -621,19 +665,90 @@ endfunction
 
 " Workspace search {{{
 
-function! s:SearchWorkspace( query ) abort
-  call s:StartSpinner()
+function! s:SearchWorkspace( query, new_query ) abort
 
-  let s:find_symbol_status.raw_results = v:none
-  call youcompleteme#GetRawCommandResponseAsync(
-        \ function( 's:HandleWorkspaceSymbols' ),
-        \ 'GoToSymbol',
-        \ a:query )
+  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( results ) abort
-  let results = s:ParseGoToResponse( a:results )
+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.
@@ -645,24 +760,26 @@ function! s:HandleWorkspaceSymbols( results ) abort
     "  - 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 no actual servers obey this part of the spec.
+    "  - 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.
+    " 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" ),'
-          \ . ' "description",'
-          \ . ' vim.eval( "s:find_symbol_status.query" ) )' )
+          \ 'ycm_state.FilterAndSortItems( vim.eval( "results" ),'
+          \ .                              ' "description",'
+          \ .                              ' vim.eval( "query" ) )' )
   endif
 
+  if !waiting
+    call s:EndRequest()
+  endif
   eval s:HandleSymbolSearchResults( results )
 endfunction
 
@@ -670,15 +787,18 @@ endfunction
 
 " Document Search {{{
 
-function! s:SearchDocument( query ) abort
+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 s:StopSpinner()
     call popup_settext( s:find_symbol_status.id,
           \ 'No symbols found in document' )
     return
   endif
 
-  call s:StartSpinner()
+  " No spinner, because this is actually a synchronous call
 
   " Call filter_and_sort_candidates on the results (synchronously)
   let response = py3eval(
@@ -692,6 +812,7 @@ endfunction
 
 
 function! s:RequestDocumentSymbols()
+  call s:StartRequest()
   call youcompleteme#GetRawCommandResponseAsync(
         \ function( 's:HandleDocumentSymbols' ),
         \ 'GoToDocumentOutline' )
@@ -699,8 +820,9 @@ endfunction
 
 
 function! s:HandleDocumentSymbols( results ) abort
-  let s:find_symbol_status.raw_results = s:ParseGoToResponse( a:results )
-  call s:SearchDocument( '' )
+  call s:EndRequest()
+  let s:find_symbol_status.raw_results = s:ParseGoToResponse( '', a:results )
+  call s:SearchDocument( '', v:true )
 endfunction
 
 " }}}

+ 6 - 1
python/ycm/client/command_request.py

@@ -37,10 +37,15 @@ class CommandRequest( BaseRequest ):
     self._request_data = None
     self._response_future = None
     self._silent = silent
+    self._bufnr = extra_data.pop( 'bufnr', None ) if extra_data else None
 
 
   def Start( self ):
-    self._request_data = BuildRequestData()
+    if self._bufnr is not None:
+      self._request_data = BuildRequestData( self._bufnr )
+    else:
+      self._request_data = BuildRequestData()
+
     if self._extra_data:
       self._request_data.update( self._extra_data )
     self._request_data.update( {

+ 2 - 1
python/ycm/tests/command_test.py

@@ -148,6 +148,7 @@ class CommandTest( TestCase ):
         '',
         'same-buffer',
         {
+          'completer_target': 'python',
           'options': {
             'tab_size': 2,
             'insert_spaces': True
@@ -156,7 +157,7 @@ class CommandTest( TestCase ):
       )
 
       with patch( 'ycm.youcompleteme.SendCommandRequest' ) as send_request:
-        ycm.SendCommandRequest( [ 'ft=ycm:ident', 'GoTo' ], '', False, 1, 1 )
+        ycm.SendCommandRequest( [ 'ft=python', 'GoTo' ], '', False, 1, 1 )
         send_request.assert_called_once_with( *expected_args )
 
       with patch( 'ycm.youcompleteme.SendCommandRequest' ) as send_request:

+ 10 - 0
python/ycm/vimsupport.py

@@ -863,6 +863,16 @@ def EscapeForVim( text ):
   return ToUnicode( text.replace( "'", "''" ) )
 
 
+def AllOpenedFiletypes():
+  """Returns a dict mapping filetype to list of buffer numbers for all open
+  buffers"""
+  filetypes = defaultdict( list )
+  for buffer in vim.buffers:
+    for filetype in FiletypesForBuffer( buffer ):
+      filetypes[ filetype ].append( buffer.number )
+  return filetypes
+
+
 def CurrentFiletypes():
   filetypes = vim.eval( "&filetype" )
   if not filetypes:

+ 27 - 14
python/ycm/youcompleteme.py

@@ -119,7 +119,8 @@ class YouCompleteMe:
     self._latest_completion_request = None
     self._latest_signature_help_request = None
     self._signature_help_available_requests = SigHelpAvailableByFileType()
-    self._latest_command_reqeust = None
+    self._command_requests = {}
+    self._next_command_request_id = 0
 
     self._signature_help_state = signature_help.SignatureHelpState()
     self._user_options = base.GetUserOptions( self._default_options )
@@ -375,21 +376,24 @@ class YouCompleteMe:
                                    has_range,
                                    start_line,
                                    end_line ):
-    final_arguments = []
-    for argument in arguments:
-      # The ft= option which specifies the completer when running a command is
-      # ignored because it has not been working for a long time. The option is
-      # still parsed to not break users that rely on it.
-      if argument.startswith( 'ft=' ):
-        continue
-      final_arguments.append( argument )
-
     extra_data = {
       'options': {
         'tab_size': vimsupport.GetIntValue( 'shiftwidth()' ),
         'insert_spaces': vimsupport.GetBoolValue( '&expandtab' )
       }
     }
+
+    final_arguments = []
+    for argument in arguments:
+      if argument.startswith( 'ft=' ):
+        extra_data[ 'completer_target' ] = argument[ 3: ]
+        continue
+      elif argument.startswith( '--bufnr=' ):
+        extra_data[ 'bufnr' ] = int( argument[ len( '--bufnr=' ): ] )
+        continue
+
+      final_arguments.append( argument )
+
     if has_range:
       extra_data.update( vimsupport.BuildRange( start_line, end_line ) )
     self._AddExtraConfDataIfNeeded( extra_data )
@@ -431,12 +435,21 @@ class YouCompleteMe:
       False,
       0,
       0 )
-    self._latest_command_reqeust = SendCommandRequestAsync( final_arguments,
-                                                            extra_data )
+
+    request_id = self._next_command_request_id
+    self._next_command_request_id += 1
+    self._command_requests[ request_id ] = SendCommandRequestAsync(
+      final_arguments,
+      extra_data )
+    return request_id
+
+
+  def GetCommandRequest( self, request_id ):
+    return self._command_requests.get( request_id )
 
 
-  def GetCommandRequest( self ):
-    return self._latest_command_reqeust
+  def FlushCommandRequest( self, request_id ):
+    self._command_requests.pop( request_id, None )
 
 
   def GetDefinedSubcommands( self ):

+ 314 - 15
test/finder.test.vim

@@ -32,11 +32,10 @@ function! Test_WorkspaceSymbol_Basic()
 
   function SelectItem( ... )
     let id = youcompleteme#finder#GetState().id
-    let o = popup_getoptions( id )
 
     call WaitForAssert( { ->
           \ assert_equal( ' [X] Search for symbol: thisisathing ',
-          \ o.title  ) },
+          \ popup_getoptions( id ).title  ) },
           \ 10000 )
 
     call WaitForAssert( { -> assert_equal( 1, line( '$', id ) ) } )
@@ -81,11 +80,10 @@ function! Test_DocumentSymbols_Basic()
 
   function SelectItem( ... )
     let id = youcompleteme#finder#GetState().id
-    let o = popup_getoptions( id )
 
     call WaitForAssert( { ->
           \ assert_equal( ' [X] Search for symbol: thisisathing ',
-          \ o.title  ) },
+          \ popup_getoptions( id ).title  ) },
           \ 10000 )
 
     call WaitForAssert( { -> assert_equal( 1, line( '$', id ) ) } )
@@ -136,11 +134,10 @@ function! Test_Cancel_DocumentSymbol()
 
   function SelectItem( ... )
     let id = youcompleteme#finder#GetState().id
-    let o = popup_getoptions( id )
 
     call WaitForAssert( { ->
           \ assert_equal( ' [X] Search for symbol: thisisathing ',
-          \ o.title  ) },
+          \ popup_getoptions( id ).title  ) },
           \ 10000 )
 
     call WaitForAssert( { -> assert_equal( 1, line( '$', id ) ) } )
@@ -189,11 +186,10 @@ function! Test_EmptySearch()
 
   function SelectNothing( ... )
     let id = youcompleteme#finder#GetState().id
-    let o = popup_getoptions( id )
 
     call WaitForAssert( { ->
           \ assert_equal( ' [X] Search for symbol: nothingshouldmatchthis ',
-          \ o.title  ) },
+          \ popup_getoptions( id ).title  ) },
           \ 10000 )
 
     call WaitForAssert( { -> assert_equal( 1, line( '$', id ) ) } )
@@ -205,13 +201,12 @@ function! Test_EmptySearch()
 
   function ChangeSearch( ... )
     let id = youcompleteme#finder#GetState().id
-    let o = popup_getoptions( id )
 
     " Hitting enter with nothing to select clears the prompt, because prompt
     " buffer
     call WaitForAssert( { ->
           \ assert_equal( ' [X] Search for symbol: notarealthing ',
-          \ o.title  ) },
+          \ popup_getoptions( id ).title  ) },
           \ 10000 )
     call assert_equal( 'No results', getbufline( winbufnr( id ), '$' )[ 0 ] )
 
@@ -223,11 +218,10 @@ function! Test_EmptySearch()
   let popup_id = -1
   function TestUpDownSelect( ... ) closure
     let popup_id = youcompleteme#finder#GetState().id
-    let o = popup_getoptions( popup_id )
 
     call WaitForAssert( { ->
           \ assert_equal( ' [X] Search for symbol: tiat ',
-          \ o.title  ) },
+          \ popup_getoptions( popup_id ).title  ) },
           \ 10000 )
     call WaitForAssert( { -> assert_equal( 2, line( '$', popup_id ) ) } )
 
@@ -450,11 +444,10 @@ function! Test_NoFileType_NoCompletionIn_PromptBuffer()
 
   function! CheckNoPopup( ... )
     let id = youcompleteme#finder#GetState().id
-    let o = popup_getoptions( id )
 
     call WaitForAssert( { ->
-            \ assert_equal( ' [X] Search for symbol: thisisathing ', o.title )
-          \ },
+            \ assert_equal( ' [X] Search for symbol: thisisathing ',
+            \ popup_getoptions( id ).title  ) },
           \ 10000 )
 
     call WaitForAssert( { -> assert_equal( 1, line( '$', id ) ) } )
@@ -481,3 +474,309 @@ function! Test_NoFileType_NoCompletionIn_PromptBuffer()
   delfunct! PutQuery
   delfunct! CheckNoPopup
 endfunction
+
+function! Test_MultipleFileTypes()
+  call youcompleteme#test#setup#OpenFile(
+        \ '/test/testdata/cpp/complete_with_sig_help.cc', {} )
+  split
+  call youcompleteme#test#setup#OpenFile( '/test/testdata/python/doc.py', {} )
+  wincmd w
+
+  let original_win = winnr()
+  let b = bufnr()
+  let l = winlayout()
+
+  function! PutQuery( ... )
+    " Wait for the current buffer to be a prompt buffer
+    call WaitForAssert( { -> assert_equal( 'prompt', &buftype ) } )
+    call WaitForAssert( { -> assert_equal( 'i', mode() ) } )
+
+    let popup_id = youcompleteme#finder#GetState().id
+    call WaitForAssert( { ->
+          \ assert_equal( ' [X] Search for symbol: thiswillnotmatchanything ',
+          \ popup_getoptions( popup_id ).title  ) },
+          \ 10000 )
+
+
+    call WaitForAssert( { -> assert_true(
+          \ youcompleteme#finder#GetState().id != -1 ) } )
+
+    let id = youcompleteme#finder#GetState().id
+    call assert_equal( 'No results', getbufline( winbufnr( id ), '$' )[ 0 ] )
+    call FeedAndCheckAgain( "\<C-u>thisisathing", funcref( 'CheckCpp' ) )
+  endfunction
+
+  function! CheckCpp( ... )
+    let popup_id = youcompleteme#finder#GetState().id
+
+    " Python can be _really_ slow
+    call WaitForAssert( { ->
+          \ assert_equal( ' [X] Search for symbol: thisisathing ',
+          \ popup_getoptions( popup_id ).title  ) },
+          \ 10000 )
+
+    call WaitForAssert( { -> assert_equal( 1, line( '$', popup_id ) ) } )
+    call assert_equal( 0, youcompleteme#finder#GetState().selected )
+    call assert_equal( 'this_is_a_thing',
+          \ youcompleteme#finder#GetState().results[
+          \   youcompleteme#finder#GetState().selected ].extra_data.name )
+
+    " Wait for the current buffer to be a prompt buffer
+    call WaitForAssert( { -> assert_equal( 'prompt', &buftype ) } )
+    call WaitForAssert( { -> assert_equal( 'i', mode() ) } )
+
+    call FeedAndCheckAgain(
+          \ "\<C-u>Really_Long_Method",
+          \ funcref( 'CheckPython' ) )
+  endfunction
+
+  function! CheckPython( ... )
+    let popup_id = youcompleteme#finder#GetState().id
+
+    " Python can be _really_ slow
+    call WaitForAssert( { ->
+          \ assert_equal( ' [X] Search for symbol: Really_Long_Method ',
+          \ popup_getoptions( popup_id ).title ) },
+          \ 10000 )
+
+    call WaitForAssert( { -> assert_equal( 2, line( '$', popup_id ) ) },
+                      \ 10000 )
+    call assert_equal( 0, youcompleteme#finder#GetState().selected )
+    call assert_equal( 'def Really_Long_Method',
+          \ youcompleteme#finder#GetState().results[
+          \   youcompleteme#finder#GetState().selected ].description )
+
+    " Toggle single-filetype mode
+    call FeedAndCheckAgain( "\<C-f>", funcref( 'CheckCppAgain' ) )
+  endfunction
+
+  function! CheckCppAgain( ... )
+    let popup_id = youcompleteme#finder#GetState().id
+
+    " Python can be _really_ slow
+    call WaitForAssert( { ->
+          \ assert_equal( ' [X] Search for symbol: Really_Long_Method ',
+          \ popup_getoptions( popup_id ).title ) },
+          \ 10000 )
+
+    call WaitForAssert( { -> assert_true(
+          \ youcompleteme#finder#GetState().id != -1 ) } )
+
+    let id = youcompleteme#finder#GetState().id
+    call assert_equal( 'No results', getbufline( winbufnr( id ), '$' )[ 0 ] )
+
+    " And back to multiple filetypes
+    call FeedAndCheckAgain( "\<C-f>", funcref( 'CheckPythonAgain' ) )
+  endfunction
+
+  function! CheckPythonAgain( ... )
+    let popup_id = youcompleteme#finder#GetState().id
+
+    " Python can be _really_ slow
+    call WaitForAssert( { ->
+          \ assert_equal( ' [X] Search for symbol: Really_Long_Method ',
+          \ popup_getoptions( popup_id ).title ) },
+          \ 10000 )
+
+    call WaitForAssert( { -> assert_equal( 2, line( '$', popup_id ) ) },
+                      \ 10000 )
+    call assert_equal( 0, youcompleteme#finder#GetState().selected )
+    call assert_equal( 'def Really_Long_Method',
+          \ youcompleteme#finder#GetState().results[
+          \   youcompleteme#finder#GetState().selected ].description )
+
+    call feedkeys( "\<C-c>" )
+  endfunction
+
+
+  " <Leader> is \ - this calls <Plug>(YCMFindSymbolInWorkspace)
+  call FeedAndCheckMain( '\\wthiswillnotmatchanything', funcref( 'PutQuery' ) )
+
+  call WaitForAssert( { -> assert_equal( l, winlayout() ) } )
+  call WaitForAssert( { -> assert_equal( original_win, winnr() ) } )
+  call assert_equal( b, bufnr() )
+endfunction
+
+function! Test_MultipleFileTypes_CurrentNotSemantic()
+  call youcompleteme#test#setup#OpenFile(
+        \ '/test/testdata/cpp/complete_with_sig_help.cc', {} )
+  split
+  call youcompleteme#test#setup#OpenFile( '/test/testdata/python/doc.py', {} )
+  split
+  " Current buffer is a ycm_nofiletype, which ycm is blacklisted in
+  " but otherwise we behave the same as before with the exception that we open
+  " the python file in the current window
+
+  let original_win = winnr()
+  let b = bufnr()
+  let l = winlayout()
+
+  function! PutQuery( ... )
+    " Wait for the current buffer to be a prompt buffer
+    call WaitForAssert( { -> assert_equal( 'prompt', &buftype ) } )
+    call WaitForAssert( { -> assert_equal( 'i', mode() ) } )
+
+    call WaitForAssert( { -> assert_true(
+          \ youcompleteme#finder#GetState().id != -1 ) } )
+
+    let popup_id = youcompleteme#finder#GetState().id
+    call WaitForAssert( { ->
+          \ assert_equal( ' [X] Search for symbol: thiswillnotmatchanything ',
+          \ popup_getoptions( popup_id ).title  ) },
+          \ 10000 )
+
+
+    let id = youcompleteme#finder#GetState().id
+    call assert_equal( 'No results', getbufline( winbufnr( id ), '$' )[ 0 ] )
+    call FeedAndCheckAgain( "\<C-u>thisisathing", funcref( 'CheckCpp' ) )
+  endfunction
+
+  function! CheckCpp( ... )
+    let popup_id = youcompleteme#finder#GetState().id
+
+    " Python can be _really_ slow
+    call WaitForAssert( { ->
+          \ assert_equal( ' [X] Search for symbol: thisisathing ',
+          \ popup_getoptions( popup_id ).title  ) },
+          \ 10000 )
+
+    call WaitForAssert( { -> assert_equal( 1, line( '$', popup_id ) ) } )
+    call assert_equal( 0, youcompleteme#finder#GetState().selected )
+    call assert_equal( 'this_is_a_thing',
+          \ youcompleteme#finder#GetState().results[
+          \   youcompleteme#finder#GetState().selected ].extra_data.name )
+
+    " Wait for the current buffer to be a prompt buffer
+    call WaitForAssert( { -> assert_equal( 'prompt', &buftype ) } )
+    call WaitForAssert( { -> assert_equal( 'i', mode() ) } )
+
+    call FeedAndCheckAgain(
+          \ "\<C-u>Really_Long_Method",
+          \ funcref( 'CheckPython' ) )
+  endfunction
+
+  function! CheckPython( ... )
+    let popup_id = youcompleteme#finder#GetState().id
+
+    " Python can be _really_ slow
+    call WaitForAssert( { ->
+          \ assert_equal( ' [X] Search for symbol: Really_Long_Method ',
+          \ popup_getoptions( popup_id ).title ) },
+          \ 10000 )
+
+    call WaitForAssert( { -> assert_equal( 2, line( '$', popup_id ) ) },
+                      \ 10000 )
+    call assert_equal( 0, youcompleteme#finder#GetState().selected )
+    call assert_equal( 'def Really_Long_Method',
+          \ youcompleteme#finder#GetState().results[
+          \   youcompleteme#finder#GetState().selected ].description )
+
+    call feedkeys( "\<CR>")
+  endfunction
+
+
+  " <Leader> is \ - this calls <Plug>(YCMFindSymbolInWorkspace)
+  call FeedAndCheckMain( '\\wthiswillnotmatchanything', funcref( 'PutQuery' ) )
+
+  " We pop up a notification with some text in it
+  if exists( '*popup_list' )
+    call assert_equal( 1, len( popup_list() ) )
+  endif
+
+  " Old vim doesn't have popup_list, so hit-test the top-right corner which is
+  " where we pup the popu
+  let notification_id = popup_locate( 1, &columns - 1 )
+  call assert_equal( [ 'Added 2 entries to quickfix list.' ],
+                   \ getbufline( winbufnr( notification_id ), 1, '$' ) )
+  " Wait for the notification to clear
+  call WaitForAssert(
+        \ { -> assert_equal( {}, popup_getpos( notification_id ) ) },
+        \ 10000 )
+
+  call WaitForAssert( { -> assert_equal( l, winlayout() ) } )
+  call WaitForAssert( { -> assert_equal( original_win, winnr() ) } )
+  call assert_equal( bufnr( 'doc.py' ), bufnr() )
+  call assert_equal( [ 0, 16, 5, 0 ], getpos( '.' ) )
+endfunction
+
+function! Test_WorkspaceSymbol_NormalModeChange()
+  call youcompleteme#test#setup#OpenFile(
+        \ '/test/testdata/cpp/complete_with_sig_help.cc', {} )
+
+  let original_win = winnr()
+  let b = bufnr()
+  let l = winlayout()
+
+  let popup_id = -1
+
+  function! PutQuery( ... )
+    " Wait for the current buffer to be a prompt buffer
+    call WaitForAssert( { -> assert_equal( 'prompt', &buftype ) } )
+    call WaitForAssert( { -> assert_equal( 'i', mode() ) } )
+
+    call WaitForAssert( { -> assert_true(
+          \ youcompleteme#finder#GetState().id != -1 ) } )
+
+    let popup_id = youcompleteme#finder#GetState().id
+    call WaitForAssert( { ->
+          \ assert_equal( ' [X] Search for symbol: thiswillnotmatchanything ',
+          \ popup_getoptions( popup_id ).title  ) },
+          \ 10000 )
+
+    let id = youcompleteme#finder#GetState().id
+    call assert_equal( 'No results', getbufline( winbufnr( id ), '$' )[ 0 ] )
+    call FeedAndCheckAgain( "\<C-u>thisisathing", funcref( 'ChangeQuery' ) )
+  endfunction
+
+  function ChangeQuery( ... )
+    let id = youcompleteme#finder#GetState().id
+
+    call WaitForAssert( { ->
+          \ assert_equal( ' [X] Search for symbol: thisisathing ',
+          \ popup_getoptions( id ).title  ) },
+          \ 10000 )
+
+    call WaitForAssert( { -> assert_equal( 1, line( '$', id ) ) } )
+    call assert_equal( 0, youcompleteme#finder#GetState().selected )
+    call assert_equal( 'this_is_a_thing',
+          \ youcompleteme#finder#GetState().results[
+          \   youcompleteme#finder#GetState().selected ].extra_data.name )
+
+    " Wait for the current buffer to be a prompt buffer
+    call WaitForAssert( { -> assert_equal( 'prompt', &buftype ) } )
+    call WaitForAssert( { -> assert_equal( 'i', mode() ) } )
+
+    call FeedAndCheckAgain( "\<Esc>bcwthatisathing",
+                          \ funcref( 'SelectNewItem' ) )
+  endfunction
+
+  function SelectNewItem( ... )
+    let id = youcompleteme#finder#GetState().id
+
+    call WaitForAssert( { ->
+          \ assert_equal( ' [X] Search for symbol: thatisathing ',
+          \ popup_getoptions( id ).title  ) },
+          \ 10000 )
+
+    call WaitForAssert( { -> assert_equal( 1, line( '$', id ) ) } )
+    call assert_equal( 0, youcompleteme#finder#GetState().selected )
+    call assert_equal( 'that_is_a_thing',
+          \ youcompleteme#finder#GetState().results[
+          \   youcompleteme#finder#GetState().selected ].extra_data.name )
+
+    call feedkeys( "\<CR>" )
+  endfunction
+
+  " <Leader> is \ - this calls <Plug>(YCMFindSymbolInWorkspace)
+  call FeedAndCheckMain( '\\wthiswillnotmatchanything', funcref( 'PutQuery' ) )
+
+  call WaitForAssert( { -> assert_equal( l, winlayout() ) } )
+  call WaitForAssert( { -> assert_equal( original_win, winnr() ) } )
+  call assert_equal( b, bufnr() )
+  call assert_equal( [ 0, 5, 28, 0 ], getpos( '.' ) )
+
+  delfunct PutQuery
+  delfunct SelectNewItem
+  delfunct ChangeQuery
+  silent %bwipe!
+endfunction

+ 8 - 4
test/lib/autoload/youcompleteme/test/commands.vim

@@ -1,8 +1,10 @@
 function! youcompleteme#test#commands#WaitForCommandRequestComplete() abort
   call WaitForAssert( { ->
         \ assert_true( py3eval(
-        \     'ycm_state.GetCommandRequest() is not None and '
-        \   . 'ycm_state.GetCommandRequest().Done()' ) )
+        \     'ycm_state.GetCommandRequest( '
+        \   . '  ycm_state._next_command_request_id - 1 ) is not None and '
+        \   . 'ycm_state.GetCommandRequest(  '
+        \   . '  ycm_state._next_command_request_id - 1 ).Done()' ) )
         \ } )
 
   call WaitForAssert( { ->
@@ -14,8 +16,10 @@ endfunction
 function! youcompleteme#test#commands#CheckNoCommandRequest() abort
   call WaitForAssert( { ->
         \ assert_true( py3eval(
-        \     'ycm_state.GetCommandRequest() is None or '
-        \   . 'ycm_state.GetCommandRequest().Done()' ) )
+        \     'ycm_state.GetCommandRequest( '
+        \   . '  ycm_state._next_command_request_id - 1 ) is None or '
+        \   . 'ycm_state.GetCommandRequest( '
+        \   . '  ycm_state._next_command_request_id - 1 ).Done()' ) )
         \ } )
 
   call WaitForAssert( { ->