youcompleteme.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400
  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 signal
  24. import base64
  25. from subprocess import PIPE
  26. from ycm import vimsupport
  27. from ycmd import utils
  28. from ycmd.request_wrap import RequestWrap
  29. from ycm.diagnostic_interface import DiagnosticInterface
  30. from ycm.omni_completer import OmniCompleter
  31. from ycm import syntax_parse
  32. from ycmd.completers.completer_utils import FiletypeCompleterExistsForFiletype
  33. from ycm.client.ycmd_keepalive import YcmdKeepalive
  34. from ycm.client.base_request import BaseRequest, BuildRequestData
  35. from ycm.client.command_request import SendCommandRequest
  36. from ycm.client.completion_request import CompletionRequest
  37. from ycm.client.omni_completion_request import OmniCompletionRequest
  38. from ycm.client.event_notification import ( SendEventNotificationAsync,
  39. EventNotification )
  40. from ycmd.responses import ServerError
  41. try:
  42. from UltiSnips import UltiSnips_Manager
  43. USE_ULTISNIPS_DATA = True
  44. except ImportError:
  45. USE_ULTISNIPS_DATA = False
  46. # We need this so that Requests doesn't end up using the local HTTP proxy when
  47. # talking to ycmd. Users should actually be setting this themselves when
  48. # configuring a proxy server on their machine, but most don't know they need to
  49. # or how to do it, so we do it for them.
  50. # Relevant issues:
  51. # https://github.com/Valloric/YouCompleteMe/issues/641
  52. # https://github.com/kennethreitz/requests/issues/879
  53. os.environ['no_proxy'] = '127.0.0.1,localhost'
  54. # Force the Python interpreter embedded in Vim (in which we are running) to
  55. # ignore the SIGINT signal. This helps reduce the fallout of a user pressing
  56. # Ctrl-C in Vim.
  57. signal.signal( signal.SIGINT, signal.SIG_IGN )
  58. HMAC_SECRET_LENGTH = 16
  59. NUM_YCMD_STDERR_LINES_ON_CRASH = 30
  60. SERVER_CRASH_MESSAGE_STDERR_FILE_DELETED = (
  61. 'The ycmd server SHUT DOWN (restart with :YcmRestartServer). '
  62. 'Logfile was deleted; set g:ycm_server_keep_logfiles to see errors '
  63. 'in the future.' )
  64. SERVER_CRASH_MESSAGE_STDERR_FILE = (
  65. 'The ycmd server SHUT DOWN (restart with :YcmRestartServer). ' +
  66. 'Stderr (last {0} lines):\n\n'.format( NUM_YCMD_STDERR_LINES_ON_CRASH ) )
  67. SERVER_CRASH_MESSAGE_SAME_STDERR = (
  68. 'The ycmd server SHUT DOWN (restart with :YcmRestartServer). '
  69. ' check console output for logs!' )
  70. SERVER_IDLE_SUICIDE_SECONDS = 10800 # 3 hours
  71. class YouCompleteMe( object ):
  72. def __init__( self, user_options ):
  73. self._user_options = user_options
  74. self._user_notified_about_crash = False
  75. self._diag_interface = DiagnosticInterface( user_options )
  76. self._omnicomp = OmniCompleter( user_options )
  77. self._latest_file_parse_request = None
  78. self._latest_completion_request = None
  79. self._server_stdout = None
  80. self._server_stderr = None
  81. self._server_popen = None
  82. self._filetypes_with_keywords_loaded = set()
  83. self._ycmd_keepalive = YcmdKeepalive()
  84. self._SetupServer()
  85. self._ycmd_keepalive.Start()
  86. def _SetupServer( self ):
  87. server_port = utils.GetUnusedLocalhostPort()
  88. # The temp options file is deleted by ycmd during startup
  89. with tempfile.NamedTemporaryFile( delete = False ) as options_file:
  90. hmac_secret = os.urandom( HMAC_SECRET_LENGTH )
  91. options_dict = dict( self._user_options )
  92. options_dict[ 'hmac_secret' ] = base64.b64encode( hmac_secret )
  93. json.dump( options_dict, options_file )
  94. options_file.flush()
  95. args = [ utils.PathToPythonInterpreter(),
  96. _PathToServerScript(),
  97. '--port={0}'.format( server_port ),
  98. '--options_file={0}'.format( options_file.name ),
  99. '--log={0}'.format( self._user_options[ 'server_log_level' ] ),
  100. '--idle_suicide_seconds={0}'.format(
  101. SERVER_IDLE_SUICIDE_SECONDS )]
  102. if not self._user_options[ 'server_use_vim_stdout' ]:
  103. filename_format = os.path.join( utils.PathToTempDir(),
  104. 'server_{port}_{std}.log' )
  105. self._server_stdout = filename_format.format( port = server_port,
  106. std = 'stdout' )
  107. self._server_stderr = filename_format.format( port = server_port,
  108. std = 'stderr' )
  109. args.append('--stdout={0}'.format( self._server_stdout ))
  110. args.append('--stderr={0}'.format( self._server_stderr ))
  111. if self._user_options[ 'server_keep_logfiles' ]:
  112. args.append('--keep_logfiles')
  113. self._server_popen = utils.SafePopen( args, stdout = PIPE, stderr = PIPE)
  114. BaseRequest.server_location = 'http://127.0.0.1:' + str( server_port )
  115. BaseRequest.hmac_secret = hmac_secret
  116. self._NotifyUserIfServerCrashed()
  117. def IsServerAlive( self ):
  118. returncode = self._server_popen.poll()
  119. # When the process hasn't finished yet, poll() returns None.
  120. return returncode is None
  121. def _NotifyUserIfServerCrashed( self ):
  122. if self._user_notified_about_crash or self.IsServerAlive():
  123. return
  124. self._user_notified_about_crash = True
  125. if self._server_stderr:
  126. try:
  127. with open( self._server_stderr, 'r' ) as server_stderr_file:
  128. error_output = ''.join( server_stderr_file.readlines()[
  129. : - NUM_YCMD_STDERR_LINES_ON_CRASH ] )
  130. vimsupport.PostMultiLineNotice( SERVER_CRASH_MESSAGE_STDERR_FILE +
  131. error_output )
  132. except IOError:
  133. vimsupport.PostVimMessage( SERVER_CRASH_MESSAGE_STDERR_FILE_DELETED )
  134. else:
  135. vimsupport.PostVimMessage( SERVER_CRASH_MESSAGE_SAME_STDERR )
  136. def ServerPid( self ):
  137. if not self._server_popen:
  138. return -1
  139. return self._server_popen.pid
  140. def _ServerCleanup( self ):
  141. if self.IsServerAlive():
  142. self._server_popen.terminate()
  143. def RestartServer( self ):
  144. vimsupport.PostVimMessage( 'Restarting ycmd server...' )
  145. self._user_notified_about_crash = False
  146. self._ServerCleanup()
  147. self._SetupServer()
  148. def CreateCompletionRequest( self, force_semantic = False ):
  149. request_data = BuildRequestData()
  150. if ( not self.NativeFiletypeCompletionAvailable() and
  151. self.CurrentFiletypeCompletionEnabled() ):
  152. wrapped_request_data = RequestWrap( request_data )
  153. if self._omnicomp.ShouldUseNow( wrapped_request_data ):
  154. self._latest_completion_request = OmniCompletionRequest(
  155. self._omnicomp, wrapped_request_data )
  156. return self._latest_completion_request
  157. self._AddExtraConfDataIfNeeded( request_data )
  158. if force_semantic:
  159. request_data[ 'force_semantic' ] = True
  160. self._latest_completion_request = CompletionRequest( request_data )
  161. return self._latest_completion_request
  162. def SendCommandRequest( self, arguments, completer ):
  163. if self.IsServerAlive():
  164. return SendCommandRequest( arguments, completer )
  165. def GetDefinedSubcommands( self ):
  166. if self.IsServerAlive():
  167. try:
  168. return BaseRequest.PostDataToHandler( BuildRequestData(),
  169. 'defined_subcommands' )
  170. except ServerError:
  171. return []
  172. else:
  173. return []
  174. def GetCurrentCompletionRequest( self ):
  175. return self._latest_completion_request
  176. def GetOmniCompleter( self ):
  177. return self._omnicomp
  178. def NativeFiletypeCompletionAvailable( self ):
  179. return any( [ FiletypeCompleterExistsForFiletype( x ) for x in
  180. vimsupport.CurrentFiletypes() ] )
  181. def NativeFiletypeCompletionUsable( self ):
  182. return ( self.CurrentFiletypeCompletionEnabled() and
  183. self.NativeFiletypeCompletionAvailable() )
  184. def OnFileReadyToParse( self ):
  185. self._omnicomp.OnFileReadyToParse( None )
  186. if not self.IsServerAlive():
  187. self._NotifyUserIfServerCrashed()
  188. extra_data = {}
  189. self._AddTagsFilesIfNeeded( extra_data )
  190. self._AddSyntaxDataIfNeeded( extra_data )
  191. self._AddExtraConfDataIfNeeded( extra_data )
  192. self._latest_file_parse_request = EventNotification( 'FileReadyToParse',
  193. extra_data )
  194. self._latest_file_parse_request.Start()
  195. def OnBufferUnload( self, deleted_buffer_file ):
  196. if not self.IsServerAlive():
  197. return
  198. SendEventNotificationAsync( 'BufferUnload',
  199. { 'unloaded_buffer': deleted_buffer_file } )
  200. def OnBufferVisit( self ):
  201. if not self.IsServerAlive():
  202. return
  203. extra_data = {}
  204. _AddUltiSnipsDataIfNeeded( extra_data )
  205. SendEventNotificationAsync( 'BufferVisit', extra_data )
  206. def OnInsertLeave( self ):
  207. if not self.IsServerAlive():
  208. return
  209. SendEventNotificationAsync( 'InsertLeave' )
  210. def OnCursorMoved( self ):
  211. self._diag_interface.OnCursorMoved()
  212. def OnVimLeave( self ):
  213. self._ServerCleanup()
  214. def OnCurrentIdentifierFinished( self ):
  215. if not self.IsServerAlive():
  216. return
  217. SendEventNotificationAsync( 'CurrentIdentifierFinished' )
  218. def DiagnosticsForCurrentFileReady( self ):
  219. return bool( self._latest_file_parse_request and
  220. self._latest_file_parse_request.Done() )
  221. def GetDiagnosticsFromStoredRequest( self, qflist_format = False ):
  222. if self.DiagnosticsForCurrentFileReady():
  223. diagnostics = self._latest_file_parse_request.Response()
  224. # We set the diagnostics request to None because we want to prevent
  225. # repeated refreshing of the buffer with the same diags. Setting this to
  226. # None makes DiagnosticsForCurrentFileReady return False until the next
  227. # request is created.
  228. self._latest_file_parse_request = None
  229. if qflist_format:
  230. return vimsupport.ConvertDiagnosticsToQfList( diagnostics )
  231. else:
  232. return diagnostics
  233. return []
  234. def UpdateDiagnosticInterface( self ):
  235. if not self.DiagnosticsForCurrentFileReady():
  236. return
  237. self._diag_interface.UpdateWithNewDiagnostics(
  238. self.GetDiagnosticsFromStoredRequest() )
  239. def ShowDetailedDiagnostic( self ):
  240. if not self.IsServerAlive():
  241. return
  242. try:
  243. debug_info = BaseRequest.PostDataToHandler( BuildRequestData(),
  244. 'detailed_diagnostic' )
  245. if 'message' in debug_info:
  246. vimsupport.EchoText( debug_info[ 'message' ] )
  247. except ServerError as e:
  248. vimsupport.PostVimMessage( str( e ) )
  249. def DebugInfo( self ):
  250. if self.IsServerAlive():
  251. debug_info = BaseRequest.PostDataToHandler( BuildRequestData(),
  252. 'debug_info' )
  253. else:
  254. debug_info = 'Server crashed, no debug info from server'
  255. debug_info += '\nServer running at: {0}'.format(
  256. BaseRequest.server_location )
  257. debug_info += '\nServer process ID: {0}'.format( self._server_popen.pid )
  258. if self._server_stderr or self._server_stdout:
  259. debug_info += '\nServer logfiles:\n {0}\n {1}'.format(
  260. self._server_stdout,
  261. self._server_stderr )
  262. return debug_info
  263. def CurrentFiletypeCompletionEnabled( self ):
  264. filetypes = vimsupport.CurrentFiletypes()
  265. filetype_to_disable = self._user_options[
  266. 'filetype_specific_completion_to_disable' ]
  267. if '*' in filetype_to_disable:
  268. return False
  269. else:
  270. return not all([ x in filetype_to_disable for x in filetypes ])
  271. def _AddSyntaxDataIfNeeded( self, extra_data ):
  272. if not self._user_options[ 'seed_identifiers_with_syntax' ]:
  273. return
  274. filetype = vimsupport.CurrentFiletypes()[ 0 ]
  275. if filetype in self._filetypes_with_keywords_loaded:
  276. return
  277. self._filetypes_with_keywords_loaded.add( filetype )
  278. extra_data[ 'syntax_keywords' ] = list(
  279. syntax_parse.SyntaxKeywordsForCurrentBuffer() )
  280. def _AddTagsFilesIfNeeded( self, extra_data ):
  281. def GetTagFiles():
  282. tag_files = vim.eval( 'tagfiles()' )
  283. # getcwd() throws an exception when the CWD has been deleted.
  284. try:
  285. current_working_directory = os.getcwd()
  286. except OSError:
  287. return []
  288. return [ os.path.join( current_working_directory, x ) for x in tag_files ]
  289. if not self._user_options[ 'collect_identifiers_from_tags_files' ]:
  290. return
  291. extra_data[ 'tag_files' ] = GetTagFiles()
  292. def _AddExtraConfDataIfNeeded( self, extra_data ):
  293. def BuildExtraConfData( extra_conf_vim_data ):
  294. return dict( ( expr, vimsupport.VimExpressionToPythonType( expr ) )
  295. for expr in extra_conf_vim_data )
  296. extra_conf_vim_data = self._user_options[ 'extra_conf_vim_data' ]
  297. if extra_conf_vim_data:
  298. extra_data[ 'extra_conf_data' ] = BuildExtraConfData(
  299. extra_conf_vim_data )
  300. def _PathToServerScript():
  301. dir_of_current_script = os.path.dirname( os.path.abspath( __file__ ) )
  302. return os.path.join( dir_of_current_script, '../../third_party/ycmd/ycmd' )
  303. def _AddUltiSnipsDataIfNeeded( extra_data ):
  304. if not USE_ULTISNIPS_DATA:
  305. return
  306. try:
  307. rawsnips = UltiSnips_Manager._snips( '', 1 )
  308. except:
  309. return
  310. # UltiSnips_Manager._snips() returns a class instance where:
  311. # class.trigger - name of snippet trigger word ( e.g. defn or testcase )
  312. # class.description - description of the snippet
  313. extra_data[ 'ultisnips_snippets' ] = [ { 'trigger': x.trigger,
  314. 'description': x.description
  315. } for x in rawsnips ]