command_request.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. # Copyright (C) 2013 Google Inc.
  2. #
  3. # This file is part of YouCompleteMe.
  4. #
  5. # YouCompleteMe is free software: you can redistribute it and/or modify
  6. # it under the terms of the GNU General Public License as published by
  7. # the Free Software Foundation, either version 3 of the License, or
  8. # (at your option) any later version.
  9. #
  10. # YouCompleteMe is distributed in the hope that it will be useful,
  11. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. # GNU General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU General Public License
  16. # along with YouCompleteMe. If not, see <http://www.gnu.org/licenses/>.
  17. from ycm.client.base_request import BaseRequest, BuildRequestData
  18. from ycm import vimsupport
  19. from ycmd.utils import ToUnicode
  20. DEFAULT_BUFFER_COMMAND = 'same-buffer'
  21. def _EnsureBackwardsCompatibility( arguments ):
  22. if arguments and arguments[ 0 ] == 'GoToDefinitionElseDeclaration':
  23. arguments[ 0 ] = 'GoTo'
  24. return arguments
  25. class CommandRequest( BaseRequest ):
  26. def __init__( self, arguments, extra_data = None, silent = False ):
  27. super( CommandRequest, self ).__init__()
  28. self._arguments = _EnsureBackwardsCompatibility( arguments )
  29. self._command = arguments and arguments[ 0 ]
  30. self._extra_data = extra_data
  31. self._response = None
  32. self._request_data = None
  33. self._response_future = None
  34. self._silent = silent
  35. def Start( self ):
  36. self._request_data = BuildRequestData()
  37. if self._extra_data:
  38. self._request_data.update( self._extra_data )
  39. self._request_data.update( {
  40. 'command_arguments': self._arguments
  41. } )
  42. self._response_future = self.PostDataToHandlerAsync(
  43. self._request_data,
  44. 'run_completer_command' )
  45. def Done( self ):
  46. return bool( self._response_future ) and self._response_future.done()
  47. def Response( self ):
  48. if self._response is None and self._response_future is not None:
  49. # Block
  50. self._response = self.HandleFuture( self._response_future,
  51. display_message = not self._silent )
  52. return self._response
  53. def RunPostCommandActionsIfNeeded( self,
  54. modifiers,
  55. buffer_command = DEFAULT_BUFFER_COMMAND ):
  56. # This is a blocking call if not Done()
  57. self.Response()
  58. if self._response is None:
  59. # An exception was raised and handled.
  60. return
  61. # If not a dictionary or a list, the response is necessarily a
  62. # scalar: boolean, number, string, etc. In this case, we print
  63. # it to the user.
  64. if not isinstance( self._response, ( dict, list ) ):
  65. return self._HandleBasicResponse()
  66. if 'fixits' in self._response:
  67. return self._HandleFixitResponse()
  68. if 'message' in self._response:
  69. return self._HandleMessageResponse()
  70. if 'detailed_info' in self._response:
  71. return self._HandleDetailedInfoResponse()
  72. # The only other type of response we understand is GoTo, and that is the
  73. # only one that we can't detect just by inspecting the response (it should
  74. # either be a single location or a list)
  75. return self._HandleGotoResponse( buffer_command, modifiers )
  76. def StringResponse( self ):
  77. # Retuns a supporable public API version of the response. The reason this
  78. # exists is that the ycmd API here is wonky as it originally only supported
  79. # text-responses and now has things like fixits and such.
  80. #
  81. # The supportable public API is basically any text-only response. All other
  82. # response types are returned as empty strings
  83. # This is a blocking call if not Done()
  84. self.Response()
  85. # Completer threw an error ?
  86. if self._response is None:
  87. return ""
  88. # If not a dictionary or a list, the response is necessarily a
  89. # scalar: boolean, number, string, etc. In this case, we print
  90. # it to the user.
  91. if not isinstance( self._response, ( dict, list ) ):
  92. return str( self._response )
  93. if 'message' in self._response:
  94. return self._response[ 'message' ]
  95. if 'detailed_info' in self._response:
  96. return self._response[ 'detailed_info' ]
  97. # The only other type of response we understand is 'fixits' and GoTo. We
  98. # don't provide string versions of them.
  99. return ""
  100. def _HandleGotoResponse( self, buffer_command, modifiers ):
  101. if isinstance( self._response, list ):
  102. vimsupport.SetQuickFixList(
  103. [ _BuildQfListItem( x ) for x in self._response ] )
  104. vimsupport.OpenQuickFixList( focus = True, autoclose = True )
  105. else:
  106. vimsupport.JumpToLocation( self._response[ 'filepath' ],
  107. self._response[ 'line_num' ],
  108. self._response[ 'column_num' ],
  109. modifiers,
  110. buffer_command )
  111. def _HandleFixitResponse( self ):
  112. if not len( self._response[ 'fixits' ] ):
  113. vimsupport.PostVimMessage( 'No fixits found for current line',
  114. warning = False )
  115. else:
  116. try:
  117. fixit_index = 0
  118. # If there is more than one fixit, we need to ask the user which one
  119. # should be applied.
  120. #
  121. # If there's only one, triggered by the FixIt subcommand (as opposed to
  122. # `RefactorRename`, for example) and whose `kind` is not `quicfix`, we
  123. # still need to as the user for confirmation.
  124. fixits = self._response[ 'fixits' ]
  125. if ( len( fixits ) > 1 or
  126. ( len( fixits ) == 1 and
  127. self._command == 'FixIt' and
  128. fixits[ 0 ].get( 'kind' ) != 'quickfix' ) ):
  129. fixit_index = vimsupport.SelectFromList(
  130. "FixIt suggestion(s) available at this location. "
  131. "Which one would you like to apply?",
  132. [ fixit[ 'text' ] for fixit in fixits ] )
  133. chosen_fixit = fixits[ fixit_index ]
  134. if chosen_fixit[ 'resolve' ]:
  135. self._request_data.update( { 'fixit': chosen_fixit } )
  136. response = self.PostDataToHandler( self._request_data,
  137. 'resolve_fixit' )
  138. fixits = response[ 'fixits' ]
  139. assert len( fixits ) == 1
  140. chosen_fixit = fixits[ 0 ]
  141. vimsupport.ReplaceChunks(
  142. chosen_fixit[ 'chunks' ],
  143. silent = self._command == 'Format' )
  144. except RuntimeError as e:
  145. vimsupport.PostVimMessage( str( e ) )
  146. def _HandleBasicResponse( self ):
  147. vimsupport.PostVimMessage( self._response, warning = False )
  148. def _HandleMessageResponse( self ):
  149. vimsupport.PostVimMessage( self._response[ 'message' ], warning = False )
  150. def _HandleDetailedInfoResponse( self ):
  151. vimsupport.WriteToPreviewWindow( self._response[ 'detailed_info' ] )
  152. def SendCommandRequestAsync( arguments, extra_data = None, silent = True ):
  153. request = CommandRequest( arguments,
  154. extra_data = extra_data,
  155. silent = silent )
  156. request.Start()
  157. # Don't block
  158. return request
  159. def SendCommandRequest( arguments,
  160. modifiers,
  161. buffer_command = DEFAULT_BUFFER_COMMAND,
  162. extra_data = None ):
  163. request = SendCommandRequestAsync( arguments,
  164. extra_data = extra_data,
  165. silent = False )
  166. # Block here to get the response
  167. request.RunPostCommandActionsIfNeeded( modifiers, buffer_command )
  168. return request.Response()
  169. def GetCommandResponse( arguments, extra_data = None ):
  170. request = SendCommandRequestAsync( arguments,
  171. extra_data = extra_data,
  172. silent = True )
  173. # Block here to get the response
  174. return request.StringResponse()
  175. def _BuildQfListItem( goto_data_item ):
  176. qf_item = {}
  177. if 'filepath' in goto_data_item:
  178. qf_item[ 'filename' ] = ToUnicode( goto_data_item[ 'filepath' ] )
  179. if 'description' in goto_data_item:
  180. qf_item[ 'text' ] = ToUnicode( goto_data_item[ 'description' ] )
  181. if 'line_num' in goto_data_item:
  182. qf_item[ 'lnum' ] = goto_data_item[ 'line_num' ]
  183. if 'column_num' in goto_data_item:
  184. # ycmd returns columns 1-based, and QuickFix lists require "byte offsets".
  185. # See :help getqflist and equivalent comment in
  186. # vimsupport.ConvertDiagnosticsToQfList.
  187. #
  188. # When the Vim help says "byte index", it really means "1-based column
  189. # number" (which is somewhat confusing). :help getqflist states "first
  190. # column is 1".
  191. qf_item[ 'col' ] = goto_data_item[ 'column_num' ]
  192. return qf_item