youcompleteme.py 13 KB

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