youcompleteme.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578
  1. #!/usr/bin/env python
  2. #
  3. # Copyright (C) 2011, 2012 Google Inc.
  4. #
  5. # This file is part of YouCompleteMe.
  6. #
  7. # YouCompleteMe is free software: you can redistribute it and/or modify
  8. # it under the terms of the GNU General Public License as published by
  9. # the Free Software Foundation, either version 3 of the License, or
  10. # (at your option) any later version.
  11. #
  12. # YouCompleteMe is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. # GNU General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU General Public License
  18. # along with YouCompleteMe. If not, see <http://www.gnu.org/licenses/>.
  19. import os
  20. import vim
  21. import tempfile
  22. import json
  23. import re
  24. import signal
  25. import base64
  26. from subprocess import PIPE
  27. from ycm import vimsupport
  28. from ycmd import utils
  29. from ycmd.request_wrap import RequestWrap
  30. from ycm.diagnostic_interface import DiagnosticInterface
  31. from ycm.omni_completer import OmniCompleter
  32. from ycm import syntax_parse
  33. from ycm.client.ycmd_keepalive import YcmdKeepalive
  34. from ycm.client.base_request import BaseRequest, BuildRequestData
  35. from ycm.client.completer_available_request import SendCompleterAvailableRequest
  36. from ycm.client.command_request import SendCommandRequest
  37. from ycm.client.completion_request import ( CompletionRequest,
  38. ConvertCompletionDataToVimData )
  39. from ycm.client.omni_completion_request import OmniCompletionRequest
  40. from ycm.client.event_notification import ( SendEventNotificationAsync,
  41. EventNotification )
  42. from ycmd.responses import ServerError
  43. try:
  44. from UltiSnips import UltiSnips_Manager
  45. USE_ULTISNIPS_DATA = True
  46. except ImportError:
  47. USE_ULTISNIPS_DATA = False
  48. def PatchNoProxy():
  49. current_value = os.environ.get('no_proxy', '')
  50. additions = '127.0.0.1,localhost'
  51. os.environ['no_proxy'] = ( additions if not current_value
  52. else current_value + ',' + additions )
  53. # We need this so that Requests doesn't end up using the local HTTP proxy when
  54. # talking to ycmd. Users should actually be setting this themselves when
  55. # configuring a proxy server on their machine, but most don't know they need to
  56. # or how to do it, so we do it for them.
  57. # Relevant issues:
  58. # https://github.com/Valloric/YouCompleteMe/issues/641
  59. # https://github.com/kennethreitz/requests/issues/879
  60. PatchNoProxy()
  61. # Force the Python interpreter embedded in Vim (in which we are running) to
  62. # ignore the SIGINT signal. This helps reduce the fallout of a user pressing
  63. # Ctrl-C in Vim.
  64. signal.signal( signal.SIGINT, signal.SIG_IGN )
  65. HMAC_SECRET_LENGTH = 16
  66. NUM_YCMD_STDERR_LINES_ON_CRASH = 30
  67. SERVER_CRASH_MESSAGE_STDERR_FILE_DELETED = (
  68. 'The ycmd server SHUT DOWN (restart with :YcmRestartServer). '
  69. 'Logfile was deleted; set g:ycm_server_keep_logfiles to see errors '
  70. 'in the future.' )
  71. SERVER_CRASH_MESSAGE_STDERR_FILE = (
  72. 'The ycmd server SHUT DOWN (restart with :YcmRestartServer). ' +
  73. 'Stderr (last {0} lines):\n\n'.format( NUM_YCMD_STDERR_LINES_ON_CRASH ) )
  74. SERVER_CRASH_MESSAGE_SAME_STDERR = (
  75. 'The ycmd server SHUT DOWN (restart with :YcmRestartServer). '
  76. ' check console output for logs!' )
  77. SERVER_IDLE_SUICIDE_SECONDS = 10800 # 3 hours
  78. class YouCompleteMe( object ):
  79. def __init__( self, user_options ):
  80. self._user_options = user_options
  81. self._user_notified_about_crash = False
  82. self._diag_interface = DiagnosticInterface( user_options )
  83. self._omnicomp = OmniCompleter( user_options )
  84. self._latest_file_parse_request = None
  85. self._latest_completion_request = None
  86. self._server_stdout = None
  87. self._server_stderr = None
  88. self._server_popen = None
  89. self._filetypes_with_keywords_loaded = set()
  90. self._ycmd_keepalive = YcmdKeepalive()
  91. self._SetupServer()
  92. self._ycmd_keepalive.Start()
  93. self._complete_done_hooks = {
  94. 'cs': lambda( self ): self._OnCompleteDone_Csharp()
  95. }
  96. def _SetupServer( self ):
  97. self._available_completers = {}
  98. server_port = utils.GetUnusedLocalhostPort()
  99. # The temp options file is deleted by ycmd during startup
  100. with tempfile.NamedTemporaryFile( delete = False ) as options_file:
  101. hmac_secret = os.urandom( HMAC_SECRET_LENGTH )
  102. options_dict = dict( self._user_options )
  103. options_dict[ 'hmac_secret' ] = base64.b64encode( hmac_secret )
  104. json.dump( options_dict, options_file )
  105. options_file.flush()
  106. args = [ utils.PathToPythonInterpreter(),
  107. _PathToServerScript(),
  108. '--port={0}'.format( server_port ),
  109. '--options_file={0}'.format( options_file.name ),
  110. '--log={0}'.format( self._user_options[ 'server_log_level' ] ),
  111. '--idle_suicide_seconds={0}'.format(
  112. SERVER_IDLE_SUICIDE_SECONDS )]
  113. if not self._user_options[ 'server_use_vim_stdout' ]:
  114. filename_format = os.path.join( utils.PathToTempDir(),
  115. 'server_{port}_{std}.log' )
  116. self._server_stdout = filename_format.format( port = server_port,
  117. std = 'stdout' )
  118. self._server_stderr = filename_format.format( port = server_port,
  119. std = 'stderr' )
  120. args.append('--stdout={0}'.format( self._server_stdout ))
  121. args.append('--stderr={0}'.format( self._server_stderr ))
  122. if self._user_options[ 'server_keep_logfiles' ]:
  123. args.append('--keep_logfiles')
  124. self._server_popen = utils.SafePopen( args, stdin_windows = PIPE,
  125. stdout = PIPE, stderr = PIPE)
  126. BaseRequest.server_location = 'http://127.0.0.1:' + str( server_port )
  127. BaseRequest.hmac_secret = hmac_secret
  128. self._NotifyUserIfServerCrashed()
  129. def IsServerAlive( self ):
  130. returncode = self._server_popen.poll()
  131. # When the process hasn't finished yet, poll() returns None.
  132. return returncode is None
  133. def _NotifyUserIfServerCrashed( self ):
  134. if self._user_notified_about_crash or self.IsServerAlive():
  135. return
  136. self._user_notified_about_crash = True
  137. if self._server_stderr:
  138. try:
  139. with open( self._server_stderr, 'r' ) as server_stderr_file:
  140. error_output = ''.join( server_stderr_file.readlines()[
  141. : - NUM_YCMD_STDERR_LINES_ON_CRASH ] )
  142. vimsupport.PostMultiLineNotice( SERVER_CRASH_MESSAGE_STDERR_FILE +
  143. error_output )
  144. except IOError:
  145. vimsupport.PostVimMessage( SERVER_CRASH_MESSAGE_STDERR_FILE_DELETED )
  146. else:
  147. vimsupport.PostVimMessage( SERVER_CRASH_MESSAGE_SAME_STDERR )
  148. def ServerPid( self ):
  149. if not self._server_popen:
  150. return -1
  151. return self._server_popen.pid
  152. def _ServerCleanup( self ):
  153. if self.IsServerAlive():
  154. self._server_popen.terminate()
  155. def RestartServer( self ):
  156. vimsupport.PostVimMessage( 'Restarting ycmd server...' )
  157. self._user_notified_about_crash = False
  158. self._ServerCleanup()
  159. self._SetupServer()
  160. def CreateCompletionRequest( self, force_semantic = False ):
  161. request_data = BuildRequestData()
  162. if ( not self.NativeFiletypeCompletionAvailable() and
  163. self.CurrentFiletypeCompletionEnabled() ):
  164. wrapped_request_data = RequestWrap( request_data )
  165. if self._omnicomp.ShouldUseNow( wrapped_request_data ):
  166. self._latest_completion_request = OmniCompletionRequest(
  167. self._omnicomp, wrapped_request_data )
  168. return self._latest_completion_request
  169. request_data[ 'working_dir' ] = os.getcwd()
  170. self._AddExtraConfDataIfNeeded( request_data )
  171. if force_semantic:
  172. request_data[ 'force_semantic' ] = True
  173. self._latest_completion_request = CompletionRequest( request_data )
  174. return self._latest_completion_request
  175. def SendCommandRequest( self, arguments, completer ):
  176. if self.IsServerAlive():
  177. return SendCommandRequest( arguments, completer )
  178. def GetDefinedSubcommands( self ):
  179. if self.IsServerAlive():
  180. try:
  181. return BaseRequest.PostDataToHandler( BuildRequestData(),
  182. 'defined_subcommands' )
  183. except ServerError:
  184. return []
  185. else:
  186. return []
  187. def GetCurrentCompletionRequest( self ):
  188. return self._latest_completion_request
  189. def GetOmniCompleter( self ):
  190. return self._omnicomp
  191. def FiletypeCompleterExistsForFiletype( self, filetype ):
  192. try:
  193. return self._available_completers[ filetype ]
  194. except KeyError:
  195. pass
  196. exists_completer = ( self.IsServerAlive() and
  197. bool( SendCompleterAvailableRequest( filetype ) ) )
  198. self._available_completers[ filetype ] = exists_completer
  199. return exists_completer
  200. def NativeFiletypeCompletionAvailable( self ):
  201. return any( [ self.FiletypeCompleterExistsForFiletype( x ) for x in
  202. vimsupport.CurrentFiletypes() ] )
  203. def NativeFiletypeCompletionUsable( self ):
  204. return ( self.CurrentFiletypeCompletionEnabled() and
  205. self.NativeFiletypeCompletionAvailable() )
  206. def OnFileReadyToParse( self ):
  207. self._omnicomp.OnFileReadyToParse( None )
  208. if not self.IsServerAlive():
  209. self._NotifyUserIfServerCrashed()
  210. extra_data = {}
  211. self._AddTagsFilesIfNeeded( extra_data )
  212. self._AddSyntaxDataIfNeeded( extra_data )
  213. self._AddExtraConfDataIfNeeded( extra_data )
  214. self._latest_file_parse_request = EventNotification( 'FileReadyToParse',
  215. extra_data )
  216. self._latest_file_parse_request.Start()
  217. def OnBufferUnload( self, deleted_buffer_file ):
  218. if not self.IsServerAlive():
  219. return
  220. SendEventNotificationAsync( 'BufferUnload',
  221. { 'unloaded_buffer': deleted_buffer_file } )
  222. def OnBufferVisit( self ):
  223. if not self.IsServerAlive():
  224. return
  225. extra_data = {}
  226. _AddUltiSnipsDataIfNeeded( extra_data )
  227. SendEventNotificationAsync( 'BufferVisit', extra_data )
  228. def OnInsertLeave( self ):
  229. if not self.IsServerAlive():
  230. return
  231. SendEventNotificationAsync( 'InsertLeave' )
  232. def OnCursorMoved( self ):
  233. self._diag_interface.OnCursorMoved()
  234. def OnVimLeave( self ):
  235. self._ServerCleanup()
  236. def OnCurrentIdentifierFinished( self ):
  237. if not self.IsServerAlive():
  238. return
  239. SendEventNotificationAsync( 'CurrentIdentifierFinished' )
  240. def OnCompleteDone( self ):
  241. complete_done_actions = self.GetCompleteDoneHooks()
  242. for action in complete_done_actions:
  243. action(self)
  244. def GetCompleteDoneHooks( self ):
  245. filetypes = vimsupport.CurrentFiletypes()
  246. for key, value in self._complete_done_hooks.iteritems():
  247. if key in filetypes:
  248. yield value
  249. def GetCompletionsUserMayHaveCompleted( self ):
  250. latest_completion_request = self.GetCurrentCompletionRequest()
  251. if not latest_completion_request or not latest_completion_request.Done():
  252. return []
  253. completions = latest_completion_request.RawResponse()
  254. result = self._FilterToMatchingCompletions( completions, True )
  255. result = list( result )
  256. if result:
  257. return result
  258. if self._HasCompletionsThatCouldBeCompletedWithMoreText( completions ):
  259. # Since the way that YCM works leads to CompleteDone called on every
  260. # character, return blank if the completion might not be done. This won't
  261. # match if the completion is ended with typing a non-keyword character.
  262. return []
  263. result = self._FilterToMatchingCompletions( completions, False )
  264. return list( result )
  265. def _FilterToMatchingCompletions( self, completions, full_match_only ):
  266. self._PatchBasedOnVimVersion()
  267. return self._FilterToMatchingCompletions( completions, full_match_only)
  268. def _HasCompletionsThatCouldBeCompletedWithMoreText( self, completions ):
  269. self._PatchBasedOnVimVersion()
  270. return self._HasCompletionsThatCouldBeCompletedWithMoreText( completions )
  271. def _PatchBasedOnVimVersion( self ):
  272. if vimsupport.VimVersionAtLeast( "7.4.774" ):
  273. self._HasCompletionsThatCouldBeCompletedWithMoreText = \
  274. self._HasCompletionsThatCouldBeCompletedWithMoreText_NewerVim
  275. self._FilterToMatchingCompletions = \
  276. self._FilterToMatchingCompletions_NewerVim
  277. else:
  278. self._FilterToMatchingCompletions = \
  279. self._FilterToMatchingCompletions_OlderVim
  280. self._HasCompletionsThatCouldBeCompletedWithMoreText = \
  281. self._HasCompletionsThatCouldBeCompletedWithMoreText_OlderVim
  282. def _FilterToMatchingCompletions_NewerVim( self, completions,
  283. full_match_only ):
  284. """ Filter to completions matching the item Vim said was completed """
  285. completed = vimsupport.GetVariableValue( 'v:completed_item' )
  286. for completion in completions:
  287. item = ConvertCompletionDataToVimData( completion )
  288. match_keys = ( [ "word", "abbr", "menu", "info" ] if full_match_only
  289. else [ 'word' ] )
  290. matcher = lambda key: completed.get( key, "" ) == item.get( key, "" )
  291. if all( [ matcher( i ) for i in match_keys ] ):
  292. yield completion
  293. def _FilterToMatchingCompletions_OlderVim( self, completions,
  294. full_match_only ):
  295. """ Filter to completions matching the buffer text """
  296. if full_match_only:
  297. return # Only supported in 7.4.774+
  298. # No support for multiple line completions
  299. text = vimsupport.TextBeforeCursor()
  300. for completion in completions:
  301. word = completion[ "insertion_text" ]
  302. # Trim complete-ending character if needed
  303. text = re.sub( r"[^a-zA-Z0-9_]$", "", text )
  304. buffer_text = text[ -1 * len( word ) : ]
  305. if buffer_text == word:
  306. yield completion
  307. def _HasCompletionsThatCouldBeCompletedWithMoreText_NewerVim( self,
  308. completions ):
  309. completed_item = vimsupport.GetVariableValue( 'v:completed_item' )
  310. completed_word = completed_item[ 'word' ]
  311. if not completed_word:
  312. return False
  313. # Sometime CompleteDone is called after the next character is inserted
  314. # If so, use inserted character to filter possible completions further
  315. text = vimsupport.TextBeforeCursor()
  316. reject_exact_match = True
  317. if text and text[ -1 ] != completed_word[ -1 ]:
  318. reject_exact_match = False
  319. completed_word += text[ -1 ]
  320. for completion in completions:
  321. word = ConvertCompletionDataToVimData( completion )[ 'word' ]
  322. if reject_exact_match and word == completed_word:
  323. continue
  324. if word.startswith( completed_word ):
  325. return True
  326. return False
  327. def _HasCompletionsThatCouldBeCompletedWithMoreText_OlderVim( self,
  328. completions ):
  329. # No support for multiple line completions
  330. text = vimsupport.TextBeforeCursor()
  331. for completion in completions:
  332. word = ConvertCompletionDataToVimData( completion )[ 'word' ]
  333. for i in range( 1, len( word ) - 1 ): # Excluding full word
  334. if text[ -1 * i : ] == word[ : i ]:
  335. return True
  336. return False
  337. def _OnCompleteDone_Csharp( self ):
  338. completions = self.GetCompletionsUserMayHaveCompleted()
  339. namespaces = [ self._GetRequiredNamespaceImport( c )
  340. for c in completions ]
  341. namespaces = [ n for n in namespaces if n ]
  342. if not namespaces:
  343. return
  344. if len( namespaces ) > 1:
  345. choices = [ "{0} {1}".format( i + 1, n )
  346. for i,n in enumerate( namespaces ) ]
  347. choice = vimsupport.PresentDialog( "Insert which namespace:", choices )
  348. if choice < 0:
  349. return
  350. namespace = namespaces[ choice ]
  351. else:
  352. namespace = namespaces[ 0 ]
  353. vimsupport.InsertNamespace( namespace )
  354. def _GetRequiredNamespaceImport( self, completion ):
  355. if ( "extra_data" not in completion
  356. or "required_namespace_import" not in completion[ "extra_data" ] ):
  357. return None
  358. return completion[ "extra_data" ][ "required_namespace_import" ]
  359. def DiagnosticsForCurrentFileReady( self ):
  360. return bool( self._latest_file_parse_request and
  361. self._latest_file_parse_request.Done() )
  362. def GetDiagnosticsFromStoredRequest( self, qflist_format = False ):
  363. if self.DiagnosticsForCurrentFileReady():
  364. diagnostics = self._latest_file_parse_request.Response()
  365. # We set the diagnostics request to None because we want to prevent
  366. # repeated refreshing of the buffer with the same diags. Setting this to
  367. # None makes DiagnosticsForCurrentFileReady return False until the next
  368. # request is created.
  369. self._latest_file_parse_request = None
  370. if qflist_format:
  371. return vimsupport.ConvertDiagnosticsToQfList( diagnostics )
  372. else:
  373. return diagnostics
  374. return []
  375. def UpdateDiagnosticInterface( self ):
  376. if ( self.DiagnosticsForCurrentFileReady() and
  377. self.NativeFiletypeCompletionUsable() ):
  378. self._diag_interface.UpdateWithNewDiagnostics(
  379. self.GetDiagnosticsFromStoredRequest() )
  380. def ShowDetailedDiagnostic( self ):
  381. if not self.IsServerAlive():
  382. return
  383. try:
  384. debug_info = BaseRequest.PostDataToHandler( BuildRequestData(),
  385. 'detailed_diagnostic' )
  386. if 'message' in debug_info:
  387. vimsupport.EchoText( debug_info[ 'message' ] )
  388. except ServerError as e:
  389. vimsupport.PostVimMessage( str( e ) )
  390. def DebugInfo( self ):
  391. if self.IsServerAlive():
  392. debug_info = BaseRequest.PostDataToHandler( BuildRequestData(),
  393. 'debug_info' )
  394. else:
  395. debug_info = 'Server crashed, no debug info from server'
  396. debug_info += '\nServer running at: {0}'.format(
  397. BaseRequest.server_location )
  398. debug_info += '\nServer process ID: {0}'.format( self._server_popen.pid )
  399. if self._server_stderr or self._server_stdout:
  400. debug_info += '\nServer logfiles:\n {0}\n {1}'.format(
  401. self._server_stdout,
  402. self._server_stderr )
  403. return debug_info
  404. def CurrentFiletypeCompletionEnabled( self ):
  405. filetypes = vimsupport.CurrentFiletypes()
  406. filetype_to_disable = self._user_options[
  407. 'filetype_specific_completion_to_disable' ]
  408. if '*' in filetype_to_disable:
  409. return False
  410. else:
  411. return not any([ x in filetype_to_disable for x in filetypes ])
  412. def _AddSyntaxDataIfNeeded( self, extra_data ):
  413. if not self._user_options[ 'seed_identifiers_with_syntax' ]:
  414. return
  415. filetype = vimsupport.CurrentFiletypes()[ 0 ]
  416. if filetype in self._filetypes_with_keywords_loaded:
  417. return
  418. self._filetypes_with_keywords_loaded.add( filetype )
  419. extra_data[ 'syntax_keywords' ] = list(
  420. syntax_parse.SyntaxKeywordsForCurrentBuffer() )
  421. def _AddTagsFilesIfNeeded( self, extra_data ):
  422. def GetTagFiles():
  423. tag_files = vim.eval( 'tagfiles()' )
  424. # getcwd() throws an exception when the CWD has been deleted.
  425. try:
  426. current_working_directory = os.getcwd()
  427. except OSError:
  428. return []
  429. return [ os.path.join( current_working_directory, x ) for x in tag_files ]
  430. if not self._user_options[ 'collect_identifiers_from_tags_files' ]:
  431. return
  432. extra_data[ 'tag_files' ] = GetTagFiles()
  433. def _AddExtraConfDataIfNeeded( self, extra_data ):
  434. def BuildExtraConfData( extra_conf_vim_data ):
  435. return dict( ( expr, vimsupport.VimExpressionToPythonType( expr ) )
  436. for expr in extra_conf_vim_data )
  437. extra_conf_vim_data = self._user_options[ 'extra_conf_vim_data' ]
  438. if extra_conf_vim_data:
  439. extra_data[ 'extra_conf_data' ] = BuildExtraConfData(
  440. extra_conf_vim_data )
  441. def _PathToServerScript():
  442. dir_of_current_script = os.path.dirname( os.path.abspath( __file__ ) )
  443. return os.path.join( dir_of_current_script, '../../third_party/ycmd/ycmd' )
  444. def _AddUltiSnipsDataIfNeeded( extra_data ):
  445. if not USE_ULTISNIPS_DATA:
  446. return
  447. try:
  448. rawsnips = UltiSnips_Manager._snips( '', 1 )
  449. except:
  450. return
  451. # UltiSnips_Manager._snips() returns a class instance where:
  452. # class.trigger - name of snippet trigger word ( e.g. defn or testcase )
  453. # class.description - description of the snippet
  454. extra_data[ 'ultisnips_snippets' ] = [ { 'trigger': x.trigger,
  455. 'description': x.description
  456. } for x in rawsnips ]