From 1730660555e6c6216a76b0fdff197a8ac4030973 Mon Sep 17 00:00:00 2001 From: Strahinja Val Markovic Date: Fri, 20 Sep 2013 17:24:34 -0700 Subject: [PATCH] A (barely) working version of ycmd + client Still a lot of work to do. --- autoload/youcompleteme.vim | 22 +- cpp/ycm/tests/IdentifierCompleter_test.cpp | 12 + python/ycm/client/__init__.py | 0 python/ycm/client/base_request.py | 77 ++++ python/ycm/client/command_request.py | 67 ++++ python/ycm/client/completion_request.py | 74 ++++ python/ycm/client/event_notification.py | 42 +++ .../completers/all/identifier_completer.py | 27 +- python/ycm/completers/cpp/clang_completer.py | 76 ++-- python/ycm/completers/cs/cs_completer.py | 18 +- .../completers/general/filename_completer.py | 6 +- .../completers/general/ultisnips_completer.py | 4 +- .../ycm/completers/python/jedi_completer.py | 20 +- python/ycm/server/__init__.py | 0 python/ycm/server/default_settings.json | 1 + .../responses.py} | 2 + python/ycm/server/server.py | 168 +++++++++ python/ycm/server/server_state.py | 108 ++++++ python/ycm/server/tests/__init__.py | 0 python/ycm/server/tests/basic_test.py | 74 ++++ python/ycm/user_options_store.py | 15 + python/ycm/utils.py | 7 + python/ycm/youcompleteme.py | 332 ++++-------------- 23 files changed, 798 insertions(+), 354 deletions(-) create mode 100644 python/ycm/client/__init__.py create mode 100644 python/ycm/client/base_request.py create mode 100644 python/ycm/client/command_request.py create mode 100644 python/ycm/client/completion_request.py create mode 100644 python/ycm/client/event_notification.py create mode 100644 python/ycm/server/__init__.py create mode 100644 python/ycm/server/default_settings.json rename python/ycm/{server_responses.py => server/responses.py} (96%) create mode 100755 python/ycm/server/server.py create mode 100644 python/ycm/server/server_state.py create mode 100644 python/ycm/server/tests/__init__.py create mode 100644 python/ycm/server/tests/basic_test.py diff --git a/autoload/youcompleteme.vim b/autoload/youcompleteme.vim index 32731bbd..bb6b46aa 100644 --- a/autoload/youcompleteme.vim +++ b/autoload/youcompleteme.vim @@ -539,11 +539,6 @@ function! youcompleteme#Complete( findstart, base ) endif py request = ycm_state.CreateCompletionRequest() - if !pyeval( 'request.ShouldComplete()' ) - " for vim, -2 means not found but don't trigger an error message - " see :h complete-functions - return -2 - endif return pyeval( 'request.CompletionStartColumn()' ) else return s:CompletionsForQuery( a:base ) @@ -628,14 +623,15 @@ function! youcompleteme#OpenGoToList() endfunction -command! -nargs=* -complete=custom,youcompleteme#SubCommandsComplete - \ YcmCompleter call s:CompleterCommand() - - -function! youcompleteme#SubCommandsComplete( arglead, cmdline, cursorpos ) - return join( pyeval( 'ycm_state.GetFiletypeCompleter().DefinedSubcommands()' ), - \ "\n") -endfunction +" TODO: Make this work again +" command! -nargs=* -complete=custom,youcompleteme#SubCommandsComplete +" \ YcmCompleter call s:CompleterCommand() +" +" +" function! youcompleteme#SubCommandsComplete( arglead, cmdline, cursorpos ) +" return join( pyeval( 'ycm_state.GetFiletypeCompleter().DefinedSubcommands()' ), +" \ "\n") +" endfunction function! s:ForceCompile() diff --git a/cpp/ycm/tests/IdentifierCompleter_test.cpp b/cpp/ycm/tests/IdentifierCompleter_test.cpp index 959b8f53..d3c4d0dc 100644 --- a/cpp/ycm/tests/IdentifierCompleter_test.cpp +++ b/cpp/ycm/tests/IdentifierCompleter_test.cpp @@ -217,6 +217,18 @@ TEST( IdentifierCompleterTest, ShorterAndLowercaseWins ) { "STDIN_FILENO" ) ); } +TEST( IdentifierCompleterTest, AddIdentifiersToDatabaseFromBufferWorks ) { + IdentifierCompleter completer; + completer.AddIdentifiersToDatabaseFromBuffer( "foo foogoo ba", + "foo", + "/foo/bar", + false ); + + EXPECT_THAT( completer.CandidatesForQueryAndType( "oo", "foo" ), + ElementsAre( "foo", + "foogoo" ) ); +} + TEST( IdentifierCompleterTest, TagsEndToEndWorks ) { IdentifierCompleter completer; std::vector< std::string > tag_files; diff --git a/python/ycm/client/__init__.py b/python/ycm/client/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/python/ycm/client/base_request.py b/python/ycm/client/base_request.py new file mode 100644 index 00000000..650e512b --- /dev/null +++ b/python/ycm/client/base_request.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python +# +# Copyright (C) 2013 Strahinja Val Markovic +# +# 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 . + +import vim +import json +import requests +from ycm import vimsupport + +HEADERS = {'content-type': 'application/json'} + +class BaseRequest( object ): + def __init__( self ): + pass + + + def Start( self ): + pass + + + def Done( self ): + return True + + + def Response( self ): + return {} + + + def PostDataToHandler( self, data, handler ): + response = requests.post( _BuildUri( handler ), + data = json.dumps( data ), + headers = HEADERS ) + response.raise_for_status() + if response.text: + return response.json() + return None + + server_location = 'http://localhost:6666' + + +def BuildRequestData( start_column = None, query = None ): + line, column = vimsupport.CurrentLineAndColumn() + request_data = { + 'filetypes': vimsupport.CurrentFiletypes(), + 'line_num': line, + 'column_num': column, + 'start_column': start_column, + 'line_value': vim.current.line, + 'filepath': vim.current.buffer.name, + 'file_data': vimsupport.GetUnsavedAndCurrentBufferData() + } + + if query: + request_data[ 'query' ] = query + + return request_data + + +def _BuildUri( handler ): + return ''.join( [ BaseRequest.server_location, '/', handler ] ) + + diff --git a/python/ycm/client/command_request.py b/python/ycm/client/command_request.py new file mode 100644 index 00000000..77af8602 --- /dev/null +++ b/python/ycm/client/command_request.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python +# +# Copyright (C) 2013 Strahinja Val Markovic +# +# 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 . + +from ycm.client.base_request import BaseRequest, BuildRequestData + + +class CommandRequest( BaseRequest ): + class ServerResponse( object ): + def __init__( self ): + pass + + def Valid( self ): + return True + + def __init__( self, arguments, completer_target = None ): + super( CommandRequest, self ).__init__() + self._arguments = arguments + self._completer_target = ( completer_target if completer_target + else 'filetype_default' ) + # TODO: Handle this case. + # if completer_target == 'omni': + # completer = SERVER_STATE.GetOmniCompleter() + + def Start( self ): + request_data = BuildRequestData() + request_data.update( { + 'completer_target': self._completer_target, + 'command_arguments': self._arguments + } ) + self._response = self.PostDataToHandler( request_data, + 'run_completer_command' ) + + + def Response( self ): + # TODO: Call vimsupport.JumpToLocation if the user called a GoTo command... + # we may want to have specific subclasses of CommandRequest so that a + # GoToRequest knows it needs to jump after the data comes back. + # + # Also need to run the following on GoTo data: + # + # CAREFUL about line/column number 0-based/1-based confusion! + # + # defs = [] + # defs.append( {'filename': definition.module_path.encode( 'utf-8' ), + # 'lnum': definition.line, + # 'col': definition.column + 1, + # 'text': definition.description.encode( 'utf-8' ) } ) + # vim.eval( 'setqflist( %s )' % repr( defs ) ) + # vim.eval( 'youcompleteme#OpenGoToList()' ) + return self.ServerResponse() + diff --git a/python/ycm/client/completion_request.py b/python/ycm/client/completion_request.py new file mode 100644 index 00000000..b3ff943f --- /dev/null +++ b/python/ycm/client/completion_request.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python +# +# Copyright (C) 2013 Strahinja Val Markovic +# +# 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 . + +from ycm import base +from ycm import vimsupport +from ycm.client.base_request import BaseRequest, BuildRequestData + + +class CompletionRequest( BaseRequest ): + def __init__( self ): + super( CompletionRequest, self ).__init__() + + self._completion_start_column = base.CompletionStartColumn() + self._request_data = BuildRequestData( self._completion_start_column ) + + + # def ShouldComplete( self ): + # return ( self._do_filetype_completion or + # self._ycm_state.ShouldUseGeneralCompleter( self._request_data ) ) + + + def CompletionStartColumn( self ): + return self._completion_start_column + + + def Start( self, query ): + self._request_data[ 'query' ] = query + self._response = self.PostDataToHandler( self._request_data, + 'get_completions' ) + + + def Results( self ): + if not self._response: + return [] + try: + return [ _ConvertCompletionDataToVimData( x ) for x in self._response ] + except Exception as e: + vimsupport.PostVimMessage( str( e ) ) + return [] + + +def _ConvertCompletionDataToVimData( completion_data ): + # see :h complete-items for a description of the dictionary fields + vim_data = { + 'word' : completion_data[ 'insertion_text' ], + 'dup' : 1, + } + + if 'menu_text' in completion_data: + vim_data[ 'abbr' ] = completion_data[ 'menu_text' ] + if 'extra_menu_info' in completion_data: + vim_data[ 'menu' ] = completion_data[ 'extra_menu_info' ] + if 'kind' in completion_data: + vim_data[ 'kind' ] = completion_data[ 'kind' ] + if 'detailed_info' in completion_data: + vim_data[ 'info' ] = completion_data[ 'detailed_info' ] + + return vim_data diff --git a/python/ycm/client/event_notification.py b/python/ycm/client/event_notification.py new file mode 100644 index 00000000..c190e6e2 --- /dev/null +++ b/python/ycm/client/event_notification.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python +# +# Copyright (C) 2013 Strahinja Val Markovic +# +# 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 . + +from ycm.client.base_request import BaseRequest, BuildRequestData + + +class EventNotification( BaseRequest ): + def __init__( self, event_name, extra_data = None ): + super( EventNotification, self ).__init__() + self._event_name = event_name + self._extra_data = extra_data + + + def Start( self ): + request_data = BuildRequestData() + if self._extra_data: + request_data.update( self._extra_data ) + request_data[ 'event_name' ] = self._event_name + + self.PostDataToHandler( request_data, 'event_notification' ) + + +def SendEventNotificationAsync( event_name, extra_data = None ): + event = EventNotification( event_name, extra_data ) + event.Start() + diff --git a/python/ycm/completers/all/identifier_completer.py b/python/ycm/completers/all/identifier_completer.py index aa3d812b..d1b8359d 100644 --- a/python/ycm/completers/all/identifier_completer.py +++ b/python/ycm/completers/all/identifier_completer.py @@ -18,12 +18,14 @@ # along with YouCompleteMe. If not, see . import os +import logging import ycm_core from collections import defaultdict from ycm.completers.general_completer import GeneralCompleter # from ycm.completers.general import syntax_parse from ycm import utils -from ycm import server_responses +from ycm.utils import ToUtf8IfNeeded +from ycm.server import responses MAX_IDENTIFIER_COMPLETIONS_RETURNED = 10 SYNTAX_FILENAME = 'YCM_PLACEHOLDER_FOR_SYNTAX' @@ -36,6 +38,7 @@ class IdentifierCompleter( GeneralCompleter ): self.completer.EnableThreading() self.tags_file_last_mtime = defaultdict( int ) self.filetypes_with_keywords_loaded = set() + self._logger = logging.getLogger( __name__ ) def ShouldUseNow( self, request_data ): @@ -44,8 +47,8 @@ class IdentifierCompleter( GeneralCompleter ): def CandidatesForQueryAsync( self, request_data ): self.completions_future = self.completer.CandidatesForQueryAndTypeAsync( - utils.SanitizeQuery( request_data[ 'query' ] ), - request_data[ 'filetypes' ][ 0 ] ) + ToUtf8IfNeeded( utils.SanitizeQuery( request_data[ 'query' ] ) ), + ToUtf8IfNeeded( request_data[ 'filetypes' ][ 0 ] ) ) def AddIdentifier( self, identifier, request_data ): @@ -56,10 +59,11 @@ class IdentifierCompleter( GeneralCompleter ): return vector = ycm_core.StringVec() - vector.append( identifier ) + vector.append( ToUtf8IfNeeded( identifier ) ) + self._logger.info( 'Adding ONE buffer identifier for file: %s', filepath ) self.completer.AddIdentifiersToDatabase( vector, - filetype, - filepath ) + ToUtf8IfNeeded( filetype ), + ToUtf8IfNeeded( filepath ) ) def AddPreviousIdentifier( self, request_data ): @@ -88,10 +92,11 @@ class IdentifierCompleter( GeneralCompleter ): return text = request_data[ 'file_data' ][ filepath ][ 'contents' ] + self._logger.info( 'Adding buffer identifiers for file: %s', filepath ) self.completer.AddIdentifiersToDatabaseFromBufferAsync( - text, - filetype, - filepath, + ToUtf8IfNeeded( text ), + ToUtf8IfNeeded( filetype ), + ToUtf8IfNeeded( filepath ), collect_from_comments_and_strings ) @@ -110,7 +115,7 @@ class IdentifierCompleter( GeneralCompleter ): continue self.tags_file_last_mtime[ tag_file ] = current_mtime - absolute_paths_to_tag_files.append( tag_file ) + absolute_paths_to_tag_files.append( ToUtf8IfNeeded( tag_file ) ) if not absolute_paths_to_tag_files: return @@ -161,7 +166,7 @@ class IdentifierCompleter( GeneralCompleter ): completions = _RemoveSmallCandidates( completions, self.user_options[ 'min_num_identifier_candidate_chars' ] ) - return [ server_responses.BuildCompletionData( x ) for x in completions ] + return [ responses.BuildCompletionData( x ) for x in completions ] def _PreviousIdentifier( min_num_completion_start_chars, request_data ): diff --git a/python/ycm/completers/cpp/clang_completer.py b/python/ycm/completers/cpp/clang_completer.py index 98d9f5a0..4fbed782 100644 --- a/python/ycm/completers/cpp/clang_completer.py +++ b/python/ycm/completers/cpp/clang_completer.py @@ -20,7 +20,7 @@ from collections import defaultdict import ycm_core import logging -from ycm import server_responses +from ycm.server import responses from ycm import extra_conf_store from ycm.utils import ToUtf8IfNeeded from ycm.completers.completer import Completer @@ -72,9 +72,10 @@ class ClangCompleter( Completer ): continue unsaved_file = ycm_core.UnsavedFile() - unsaved_file.contents_ = contents - unsaved_file.length_ = len( contents ) - unsaved_file.filename_ = filename + utf8_contents = ToUtf8IfNeeded( contents ) + unsaved_file.contents_ = utf8_contents + unsaved_file.length_ = len( utf8_contents ) + unsaved_file.filename_ = ToUtf8IfNeeded( filename ) files.append( unsaved_file ) return files @@ -86,17 +87,17 @@ class ClangCompleter( Completer ): if not filename: return - if self.completer.UpdatingTranslationUnit( filename ): + if self.completer.UpdatingTranslationUnit( ToUtf8IfNeeded( filename ) ): self.completions_future = None self._logger.info( PARSING_FILE_MESSAGE ) - return server_responses.BuildDisplayMessageResponse( + return responses.BuildDisplayMessageResponse( PARSING_FILE_MESSAGE ) flags = self.flags.FlagsForFile( filename ) if not flags: self.completions_future = None self._logger.info( NO_COMPILE_FLAGS_MESSAGE ) - return server_responses.BuildDisplayMessageResponse( + return responses.BuildDisplayMessageResponse( NO_COMPILE_FLAGS_MESSAGE ) # TODO: sanitize query, probably in C++ code @@ -142,11 +143,11 @@ class ClangCompleter( Completer ): command = arguments[ 0 ] if command == 'GoToDefinition': - self._GoToDefinition( request_data ) + return self._GoToDefinition( request_data ) elif command == 'GoToDeclaration': - self._GoToDeclaration( request_data ) + return self._GoToDeclaration( request_data ) elif command == 'GoToDefinitionElseDeclaration': - self._GoToDefinitionElseDeclaration( request_data ) + return self._GoToDefinitionElseDeclaration( request_data ) elif command == 'ClearCompilationFlagCache': self._ClearCompilationFlagCache( request_data ) @@ -155,20 +156,20 @@ class ClangCompleter( Completer ): filename = request_data[ 'filepath' ] if not filename: self._logger.warning( INVALID_FILE_MESSAGE ) - return server_responses.BuildDisplayMessageResponse( + return responses.BuildDisplayMessageResponse( INVALID_FILE_MESSAGE ) flags = self.flags.FlagsForFile( filename ) if not flags: self._logger.info( NO_COMPILE_FLAGS_MESSAGE ) - return server_responses.BuildDisplayMessageResponse( + return responses.BuildDisplayMessageResponse( NO_COMPILE_FLAGS_MESSAGE ) - files = self.GetUnsavedFilesVector() + files = self.GetUnsavedFilesVector( request_data ) line = request_data[ 'line_num' ] + 1 column = request_data[ 'start_column' ] + 1 return getattr( self.completer, goto_function )( - filename, + ToUtf8IfNeeded( filename ), line, column, files, @@ -180,9 +181,9 @@ class ClangCompleter( Completer ): if not location or not location.IsValid(): raise RuntimeError( 'Can\'t jump to definition.' ) - return server_responses.BuildGoToResponse( location.filename_, - location.line_number_, - location.column_number_ ) + return responses.BuildGoToResponse( location.filename_, + location.line_number_, + location.column_number_ ) def _GoToDeclaration( self, request_data ): @@ -190,9 +191,9 @@ class ClangCompleter( Completer ): if not location or not location.IsValid(): raise RuntimeError( 'Can\'t jump to declaration.' ) - return server_responses.BuildGoToResponse( location.filename_, - location.line_number_, - location.column_number_ ) + return responses.BuildGoToResponse( location.filename_, + location.line_number_, + location.column_number_ ) def _GoToDefinitionElseDeclaration( self, request_data ): @@ -202,9 +203,9 @@ class ClangCompleter( Completer ): if not location or not location.IsValid(): raise RuntimeError( 'Can\'t jump to definition or declaration.' ) - return server_responses.BuildGoToResponse( location.filename_, - location.line_number_, - location.column_number_ ) + return responses.BuildGoToResponse( location.filename_, + location.line_number_, + location.column_number_ ) @@ -223,10 +224,10 @@ class ClangCompleter( Completer ): if not filename: self._logger.warning( INVALID_FILE_MESSAGE ) - return server_responses.BuildDisplayMessageResponse( + return responses.BuildDisplayMessageResponse( INVALID_FILE_MESSAGE ) - if self.completer.UpdatingTranslationUnit( filename ): + if self.completer.UpdatingTranslationUnit( ToUtf8IfNeeded( filename ) ): self.extra_parse_desired = True return @@ -234,11 +235,11 @@ class ClangCompleter( Completer ): if not flags: self.parse_future = None self._logger.info( NO_COMPILE_FLAGS_MESSAGE ) - return server_responses.BuildDisplayMessageResponse( + return responses.BuildDisplayMessageResponse( NO_COMPILE_FLAGS_MESSAGE ) self.parse_future = self.completer.UpdateTranslationUnitAsync( - filename, + ToUtf8IfNeeded( filename ), self.GetUnsavedFilesVector( request_data ), flags ) @@ -246,7 +247,8 @@ class ClangCompleter( Completer ): def OnBufferUnload( self, request_data ): - self.completer.DeleteCachesForFileAsync( request_data[ 'unloaded_buffer' ] ) + self.completer.DeleteCachesForFileAsync( + ToUtf8IfNeeded( request_data[ 'unloaded_buffer' ] ) ) def DiagnosticsForCurrentFileReady( self ): @@ -257,16 +259,18 @@ class ClangCompleter( Completer ): def GettingCompletions( self, request_data ): - return self.completer.UpdatingTranslationUnit( request_data[ 'filepath' ] ) + return self.completer.UpdatingTranslationUnit( + ToUtf8IfNeeded( request_data[ 'filepath' ] ) ) def GetDiagnosticsForCurrentFile( self, request_data ): filename = request_data[ 'filepath' ] if self.DiagnosticsForCurrentFileReady(): - diagnostics = self.completer.DiagnosticsForFile( filename ) + diagnostics = self.completer.DiagnosticsForFile( + ToUtf8IfNeeded( filename ) ) self.diagnostic_store = DiagnosticsToDiagStructure( diagnostics ) self.last_prepared_diagnostics = [ - server_responses.BuildDiagnosticData( x ) for x in + responses.BuildDiagnosticData( x ) for x in diagnostics[ : self.max_diagnostics_to_display ] ] self.parse_future = None @@ -282,12 +286,12 @@ class ClangCompleter( Completer ): current_file = request_data[ 'filepath' ] if not self.diagnostic_store: - return server_responses.BuildDisplayMessageResponse( + return responses.BuildDisplayMessageResponse( NO_DIAGNOSTIC_MESSAGE ) diagnostics = self.diagnostic_store[ current_file ][ current_line ] if not diagnostics: - return server_responses.BuildDisplayMessageResponse( + return responses.BuildDisplayMessageResponse( NO_DIAGNOSTIC_MESSAGE ) closest_diagnostic = None @@ -299,7 +303,7 @@ class ClangCompleter( Completer ): distance_to_closest_diagnostic = distance closest_diagnostic = diagnostic - return server_responses.BuildDisplayMessageResponse( + return responses.BuildDisplayMessageResponse( closest_diagnostic.long_formatted_text_ ) @@ -314,7 +318,7 @@ class ClangCompleter( Completer ): return '' flags = self.flags.FlagsForFile( filename ) or [] source = extra_conf_store.ModuleFileForSourceFile( filename ) - return server_responses.BuildDisplayMessageResponse( + return responses.BuildDisplayMessageResponse( 'Flags for {0} loaded from {1}:\n{2}'.format( filename, source, list( flags ) ) ) @@ -348,7 +352,7 @@ class ClangCompleter( Completer ): def ConvertCompletionData( completion_data ): - return server_responses.BuildCompletionData( + return responses.BuildCompletionData( insertion_text = completion_data.TextToInsertInBuffer(), menu_text = completion_data.MainCompletionText(), extra_menu_info = completion_data.ExtraMenuInfo(), diff --git a/python/ycm/completers/cs/cs_completer.py b/python/ycm/completers/cs/cs_completer.py index f63b3b67..20240ee8 100755 --- a/python/ycm/completers/cs/cs_completer.py +++ b/python/ycm/completers/cs/cs_completer.py @@ -22,13 +22,13 @@ import os from sys import platform import glob from ycm.completers.threaded_completer import ThreadedCompleter -from ycm import server_responses +from ycm.server import responses +from ycm import utils import urllib2 import urllib import urlparse import json import subprocess -import tempfile import logging @@ -44,7 +44,7 @@ class CsharpCompleter( ThreadedCompleter ): def __init__( self, user_options ): super( CsharpCompleter, self ).__init__( user_options ) self._omnisharp_port = None - self._logger = logging.getLogger(__name__) + self._logger = logging.getLogger( __name__ ) # if self.user_options[ 'auto_start_csharp_server' ]: # self._StartServer() @@ -62,7 +62,7 @@ class CsharpCompleter( ThreadedCompleter ): def ComputeCandidates( self, request_data ): - return [ server_responses.BuildCompletionData( + return [ responses.BuildCompletionData( completion[ 'CompletionText' ], completion[ 'DisplayText' ], completion[ 'Description' ] ) @@ -135,8 +135,8 @@ class CsharpCompleter( ThreadedCompleter ): command = [ omnisharp + ' -p ' + str( self._omnisharp_port ) + ' -s ' + path_to_solutionfile ] - filename_format = ( tempfile.gettempdir() + - '/omnisharp_{port}_{sln}_{std}.log' ) + filename_format = os.path.join( utils.PathToTempDir(), + 'omnisharp_{port}_{sln}_{std}.log' ) self._filename_stdout = filename_format.format( port=self._omnisharp_port, sln=solutionfile, std='stdout' ) @@ -169,9 +169,9 @@ class CsharpCompleter( ThreadedCompleter ): definition = self._GetResponse( '/gotodefinition', self._DefaultParameters( request_data ) ) if definition[ 'FileName' ] != None: - return server_responses.BuildGoToResponse( definition[ 'FileName' ], - definition[ 'Line' ], - definition[ 'Column' ] ) + return responses.BuildGoToResponse( definition[ 'FileName' ], + definition[ 'Line' ], + definition[ 'Column' ] ) else: raise RuntimeError( 'Can\'t jump to definition' ) diff --git a/python/ycm/completers/general/filename_completer.py b/python/ycm/completers/general/filename_completer.py index 985bda04..5d9eb0da 100644 --- a/python/ycm/completers/general/filename_completer.py +++ b/python/ycm/completers/general/filename_completer.py @@ -22,7 +22,7 @@ import re from ycm.completers.threaded_completer import ThreadedCompleter from ycm.completers.cpp.clang_completer import InCFamilyFile from ycm.completers.cpp.flags import Flags -from ycm import server_responses +from ycm.server import responses class FilenameCompleter( ThreadedCompleter ): """ @@ -146,7 +146,7 @@ def _GenerateCandidatesForPaths( absolute_paths ): is_dir = os.path.isdir( absolute_path ) completion_dicts.append( - server_responses.BuildCompletionData( basename, - '[Dir]' if is_dir else '[File]' ) ) + responses.BuildCompletionData( basename, + '[Dir]' if is_dir else '[File]' ) ) return completion_dicts diff --git a/python/ycm/completers/general/ultisnips_completer.py b/python/ycm/completers/general/ultisnips_completer.py index a66c6fdc..dfc37cd1 100644 --- a/python/ycm/completers/general/ultisnips_completer.py +++ b/python/ycm/completers/general/ultisnips_completer.py @@ -20,7 +20,7 @@ from ycm.completers.general_completer import GeneralCompleter from UltiSnips import UltiSnips_Manager -from ycm import server_responses +from ycm.server import responses class UltiSnipsCompleter( GeneralCompleter ): @@ -64,7 +64,7 @@ def _GetCandidates(): # UltiSnips_Manager._snips() returns a class instance where: # class.trigger - name of snippet trigger word ( e.g. defn or testcase ) # class.description - description of the snippet - return [ server_responses.BuildCompletionData( + return [ responses.BuildCompletionData( str( snip.trigger ), str( ' ' + snip.description.encode( 'utf-8' ) ) ) for snip in rawsnips ] diff --git a/python/ycm/completers/python/jedi_completer.py b/python/ycm/completers/python/jedi_completer.py index fdfa95d6..b64a95e4 100644 --- a/python/ycm/completers/python/jedi_completer.py +++ b/python/ycm/completers/python/jedi_completer.py @@ -20,7 +20,7 @@ # along with YouCompleteMe. If not, see . from ycm.completers.threaded_completer import ThreadedCompleter -from ycm import server_responses +from ycm.server import responses import sys from os.path import join, abspath, dirname @@ -65,7 +65,7 @@ class JediCompleter( ThreadedCompleter ): def ComputeCandidates( self, request_data ): script = self._GetJediScript( request_data ) - return [ server_responses.BuildCompletionData( + return [ responses.BuildCompletionData( str( completion.name ), str( completion.description ), str( completion.doc ) ) @@ -140,21 +140,21 @@ class JediCompleter( ThreadedCompleter ): else: raise RuntimeError( 'Builtin modules cannot be displayed.' ) else: - return server_responses.BuildGoToResponse( definition.module_path, - definition.line -1, - definition.column ) + return responses.BuildGoToResponse( definition.module_path, + definition.line -1, + definition.column ) else: # multiple definitions defs = [] for definition in definition_list: if definition.in_builtin_module(): - defs.append( server_responses.BuildDescriptionOnlyGoToResponse( + defs.append( responses.BuildDescriptionOnlyGoToResponse( 'Builting ' + definition.description ) ) else: defs.append( - server_responses.BuildGoToResponse( definition.module_path, - definition.line -1, - definition.column, - definition.description ) ) + responses.BuildGoToResponse( definition.module_path, + definition.line -1, + definition.column, + definition.description ) ) return defs diff --git a/python/ycm/server/__init__.py b/python/ycm/server/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/python/ycm/server/default_settings.json b/python/ycm/server/default_settings.json new file mode 100644 index 00000000..f6030fa1 --- /dev/null +++ b/python/ycm/server/default_settings.json @@ -0,0 +1 @@ +{ "filepath_completion_use_working_dir": 0, "min_num_of_chars_for_completion": 2, "semantic_triggers": {}, "collect_identifiers_from_comments_and_strings": 0, "filetype_specific_completion_to_disable": {}, "collect_identifiers_from_tags_files": 0, "extra_conf_globlist": [ "~\/repos\/*", "\/home\/strahinja\/googrepos\/*", "~\/local_googrepos\/*", "~\/.ycm_extra_conf.py" ], "global_ycm_extra_conf": "\/usr\/lib\/youcompleteme\/ycm_extra_conf.py", "confirm_extra_conf": 1, "complete_in_comments": 0, "complete_in_strings": 1, "min_num_identifier_candidate_chars": 0, "max_diagnostics_to_display": 30, "auto_stop_csharp_server": 1, "seed_identifiers_with_syntax": 0, "csharp_server_port": 2000, "filetype_whitelist": { "*": "1" }, "auto_start_csharp_server": 1, "filetype_blacklist": { "tagbar": "1", "notes": "1", "markdown": "1", "unite": "1", "text": "1" } } \ No newline at end of file diff --git a/python/ycm/server_responses.py b/python/ycm/server/responses.py similarity index 96% rename from python/ycm/server_responses.py rename to python/ycm/server/responses.py index f8414d9c..a6544d4b 100644 --- a/python/ycm/server_responses.py +++ b/python/ycm/server/responses.py @@ -18,6 +18,8 @@ # along with YouCompleteMe. If not, see . +# TODO: Move this file under server/ and rename it responses.py + def BuildGoToResponse( filepath, line_num, column_num, description = None ): response = { 'filepath': filepath, diff --git a/python/ycm/server/server.py b/python/ycm/server/server.py new file mode 100755 index 00000000..a9289427 --- /dev/null +++ b/python/ycm/server/server.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python +# +# Copyright (C) 2013 Strahinja Val Markovic +# +# 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 . + +import sys +import os + +# We want to have the YouCompleteMe/python directory on the Python PATH because +# all the code already assumes that it's there. This is a relic from before the +# client/server architecture. +# TODO: Fix things so that this is not needed anymore when we split ycmd into a +# separate repository. +sys.path.insert( 0, os.path.join( + os.path.dirname( os.path.abspath( __file__ ) ), + '../..' ) ) + +import logging +import time +import httplib +import json +import bottle +from bottle import run, request, response +import server_state +from ycm import extra_conf_store +from ycm import user_options_store +import argparse + +# num bytes for the request body buffer; request.json only works if the request +# size is less than this +bottle.Request.MEMFILE_MAX = 300 * 1024 + +user_options_store.LoadDefaults() +SERVER_STATE = server_state.ServerState( user_options_store.GetAll() ) + +LOGGER = logging.getLogger( __name__ ) +app = bottle.Bottle() + + +@app.post( '/event_notification' ) +def EventNotification(): + LOGGER.info( 'Received event notification') + request_data = request.json + event_name = request_data[ 'event_name' ] + LOGGER.debug( 'Event name: %s', event_name ) + + event_handler = 'On' + event_name + getattr( SERVER_STATE.GetGeneralCompleter(), event_handler )( request_data ) + + filetypes = request_data[ 'filetypes' ] + if SERVER_STATE.FiletypeCompletionUsable( filetypes ): + getattr( SERVER_STATE.GetFiletypeCompleter( filetypes ), + event_handler )( request_data ) + + if hasattr( extra_conf_store, event_handler ): + getattr( extra_conf_store, event_handler )( request_data ) + + # TODO: shut down the server on VimClose + + +@app.post( '/run_completer_command' ) +def RunCompleterCommand(): + LOGGER.info( 'Received command request') + request_data = request.json + completer_target = request_data[ 'completer_target' ] + + if completer_target == 'identifier': + completer = SERVER_STATE.GetGeneralCompleter() + else: + completer = SERVER_STATE.GetFiletypeCompleter() + + return _JsonResponse( + completer.OnUserCommand( request_data[ 'command_arguments' ], + request_data ) ) + + +@app.post( '/get_completions' ) +def GetCompletions(): + LOGGER.info( 'Received completion request') + request_data = request.json + do_filetype_completion = SERVER_STATE.ShouldUseFiletypeCompleter( + request_data ) + LOGGER.debug( 'Using filetype completion: %s', do_filetype_completion ) + filetypes = request_data[ 'filetypes' ] + completer = ( SERVER_STATE.GetFiletypeCompleter( filetypes ) if + do_filetype_completion else + SERVER_STATE.GetGeneralCompleter() ) + + # This is necessary so that general_completer_store fills up + # _current_query_completers. + # TODO: Fix this. + completer.ShouldUseNow( request_data ) + + # TODO: This should not be async anymore, server is multi-threaded + completer.CandidatesForQueryAsync( request_data ) + while not completer.AsyncCandidateRequestReady(): + time.sleep( 0.03 ) + return _JsonResponse( completer.CandidatesFromStoredRequest() ) + + +@app.route( '/user_options' ) +def UserOptions(): + global SERVER_STATE + + if request.method == 'GET': + LOGGER.info( 'Received user options GET request') + return SERVER_STATE.user_options + elif request.method == 'POST': + LOGGER.info( 'Received user options POST request') + data = request.json + SERVER_STATE = server_state.ServerState( data ) + user_options_store.SetAll( data ) + else: + response.status = httplib.BAD_REQUEST + + +@app.post( '/filetype_completion_available') +def FiletypeCompletionAvailable(): + LOGGER.info( 'Received filetype completion available request') + return _JsonResponse( SERVER_STATE.FiletypeCompletionAvailable( + request.json[ 'filetypes' ] ) ) + + +def _JsonResponse( data ): + response.set_header( 'Content-Type', 'application/json' ) + return json.dumps( data ) + + +def Main(): + global LOGGER + parser = argparse.ArgumentParser() + parser.add_argument( '--host', type = str, default = 'localhost', + help='server hostname') + parser.add_argument( '--port', type = int, default = 6666, + help='server port') + parser.add_argument( '--log', type = str, default = 'info', + help='log level, one of ' + '[debug|info|warning|error|critical]') + args = parser.parse_args() + + numeric_level = getattr( logging, args.log.upper(), None ) + if not isinstance( numeric_level, int ): + raise ValueError( 'Invalid log level: %s' % args.log ) + + logging.basicConfig( format = '%(asctime)s - %(levelname)s - %(message)s', + level = numeric_level ) + + LOGGER = logging.getLogger( __name__ ) + run( app = app, host = args.host, port = args.port, server='cherrypy' ) + + +if __name__ == "__main__": + Main() + diff --git a/python/ycm/server/server_state.py b/python/ycm/server/server_state.py new file mode 100644 index 00000000..a7f3eaa0 --- /dev/null +++ b/python/ycm/server/server_state.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python +# +# Copyright (C) 2013 Strahinja Val Markovic +# +# 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 . + +import imp +import os +from ycm.completers.general.general_completer_store import GeneralCompleterStore + + +class ServerState( object ): + def __init__( self, user_options ): + self._user_options = user_options + self._filetype_completers = {} + self._gencomp = GeneralCompleterStore( self._user_options ) + + + @property + def user_options( self ): + return self._user_options + + + def _GetFiletypeCompleterForFiletype( self, filetype ): + try: + return self._filetype_completers[ filetype ] + except KeyError: + pass + + module_path = _PathToFiletypeCompleterPluginLoader( filetype ) + + completer = None + supported_filetypes = [ filetype ] + if os.path.exists( module_path ): + module = imp.load_source( filetype, module_path ) + completer = module.GetCompleter( self._user_options ) + if completer: + supported_filetypes.extend( completer.SupportedFiletypes() ) + + for supported_filetype in supported_filetypes: + self._filetype_completers[ supported_filetype ] = completer + return completer + + + def GetFiletypeCompleter( self, current_filetypes ): + completers = [ self._GetFiletypeCompleterForFiletype( filetype ) + for filetype in current_filetypes ] + + for completer in completers: + if completer: + return completer + + return None + + + def FiletypeCompletionAvailable( self, filetypes ): + return bool( self.GetFiletypeCompleter( filetypes ) ) + + + def FiletypeCompletionUsable( self, filetypes ): + return ( self.CurrentFiletypeCompletionEnabled( filetypes ) and + self.FiletypeCompletionAvailable( filetypes ) ) + + + def ShouldUseGeneralCompleter( self, request_data ): + return self._gencomp.ShouldUseNow( request_data ) + + + def ShouldUseFiletypeCompleter( self, request_data ): + filetypes = request_data[ 'filetypes' ] + if self.FiletypeCompletionUsable( filetypes ): + return self.GetFiletypeCompleter( filetypes ).ShouldUseNow( request_data ) + return False + + + def GetGeneralCompleter( self ): + return self._gencomp + + + def CurrentFiletypeCompletionEnabled( self, current_filetypes ): + filetype_to_disable = self._user_options[ + 'filetype_specific_completion_to_disable' ] + return not all([ x in filetype_to_disable for x in current_filetypes ]) + + + +def _PathToCompletersFolder(): + dir_of_current_script = os.path.dirname( os.path.abspath( __file__ ) ) + return os.path.join( dir_of_current_script, '..', 'completers' ) + + +def _PathToFiletypeCompleterPluginLoader( filetype ): + return os.path.join( _PathToCompletersFolder(), filetype, 'hook.py' ) + + diff --git a/python/ycm/server/tests/__init__.py b/python/ycm/server/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/python/ycm/server/tests/basic_test.py b/python/ycm/server/tests/basic_test.py new file mode 100644 index 00000000..b7184fac --- /dev/null +++ b/python/ycm/server/tests/basic_test.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python +# +# Copyright (C) 2013 Strahinja Val Markovic +# +# 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 . + +from webtest import TestApp +from .. import server +from ..responses import BuildCompletionData +from nose.tools import ok_, eq_ +import bottle + +bottle.debug( True ) + + +def GetCompletions_IdentifierCompleterWorks_test(): + app = TestApp( server.app ) + event_data = { + 'event_name': 'FileReadyToParse', + 'filetypes': ['foo'], + 'filepath': '/foo/bar', + 'file_data': { + '/foo/bar': { + 'contents': 'foo foogoo ba', + 'filetypes': ['foo'] + } + } + } + + app.post_json( '/event_notification', event_data ) + + line_value = 'oo foo foogoo ba'; + completion_data = { + 'query': 'oo', + 'filetypes': ['foo'], + 'filepath': '/foo/bar', + 'line_num': 0, + 'column_num': 2, + 'start_column': 0, + 'line_value': line_value, + 'file_data': { + '/foo/bar': { + 'contents': line_value, + 'filetypes': ['foo'] + } + } + } + + eq_( [ BuildCompletionData( 'foo' ), + BuildCompletionData( 'foogoo' ) ], + app.post_json( '/get_completions', completion_data ).json ) + + +def FiletypeCompletionAvailable_Works_test(): + app = TestApp( server.app ) + request_data = { + 'filetypes': ['cpp'] + } + + ok_( app.post_json( '/filetype_completion_available', + request_data ).json ) diff --git a/python/ycm/user_options_store.py b/python/ycm/user_options_store.py index 759e63b9..b453f01e 100644 --- a/python/ycm/user_options_store.py +++ b/python/ycm/user_options_store.py @@ -17,6 +17,8 @@ # You should have received a copy of the GNU General Public License # along with YouCompleteMe. If not, see . +import json +import os from ycm.frozendict import frozendict _USER_OPTIONS = {} @@ -32,3 +34,16 @@ def GetAll(): def Value( key ): return _USER_OPTIONS[ key ] + + +def LoadDefaults(): + SetAll( _DefaultOptions() ) + + +def _DefaultOptions(): + settings_path = os.path.join( + os.path.dirname( os.path.abspath( __file__ ) ), + 'server/default_settings.json' ) + with open( settings_path ) as f: + return json.loads( f.read() ) + diff --git a/python/ycm/utils.py b/python/ycm/utils.py index 81e4bd77..d66a4fef 100644 --- a/python/ycm/utils.py +++ b/python/ycm/utils.py @@ -17,6 +17,9 @@ # You should have received a copy of the GNU General Public License # along with YouCompleteMe. If not, see . +import tempfile +import os + def IsIdentifierChar( char ): return char.isalnum() or char == '_' @@ -29,3 +32,7 @@ def ToUtf8IfNeeded( string_or_unicode ): if isinstance( string_or_unicode, unicode ): return string_or_unicode.encode( 'utf8' ) return string_or_unicode + + +def PathToTempDir(): + return os.path.join( tempfile.gettempdir(), 'ycm_temp' ) diff --git a/python/ycm/youcompleteme.py b/python/ycm/youcompleteme.py index ea600f44..b0b95773 100644 --- a/python/ycm/youcompleteme.py +++ b/python/ycm/youcompleteme.py @@ -17,176 +17,53 @@ # You should have received a copy of the GNU General Public License # along with YouCompleteMe. If not, see . -import imp import os import time import vim import ycm_core -import logging -import tempfile +import subprocess from ycm import vimsupport -from ycm import base -from ycm import extra_conf_store +from ycm import utils from ycm.completers.all.omni_completer import OmniCompleter -from ycm.completers.general.general_completer_store import GeneralCompleterStore - - -# TODO: Put the Request classes in separate files -class BaseRequest( object ): - def __init__( self ): - pass - - - def Start( self ): - pass - - - def Done( self ): - return True - - - def Response( self ): - return {} - - -class CompletionRequest( BaseRequest ): - def __init__( self, ycm_state ): - super( CompletionRequest, self ).__init__() - - self._completion_start_column = base.CompletionStartColumn() - self._ycm_state = ycm_state - self._request_data = _BuildRequestData( self._completion_start_column ) - self._do_filetype_completion = self._ycm_state.ShouldUseFiletypeCompleter( - self._request_data ) - self._completer = ( self._ycm_state.GetFiletypeCompleter() if - self._do_filetype_completion else - self._ycm_state.GetGeneralCompleter() ) - - - def ShouldComplete( self ): - return ( self._do_filetype_completion or - self._ycm_state.ShouldUseGeneralCompleter( self._request_data ) ) - - - def CompletionStartColumn( self ): - return self._completion_start_column - - - def Start( self, query ): - self._request_data[ 'query' ] = query - self._completer.CandidatesForQueryAsync( self._request_data ) - - def Done( self ): - return self._completer.AsyncCandidateRequestReady() - - - def Results( self ): - try: - return [ _ConvertCompletionDataToVimData( x ) - for x in self._completer.CandidatesFromStoredRequest() ] - except Exception as e: - vimsupport.PostVimMessage( str( e ) ) - return [] - - - -class CommandRequest( BaseRequest ): - class ServerResponse( object ): - def __init__( self ): - pass - - def Valid( self ): - return True - - def __init__( self, ycm_state, arguments, completer_target = None ): - super( CommandRequest, self ).__init__() - - if not completer_target: - completer_target = 'filetpe_default' - - if completer_target == 'omni': - self._completer = ycm_state.GetOmniCompleter() - elif completer_target == 'identifier': - self._completer = ycm_state.GetGeneralCompleter() - else: - self._completer = ycm_state.GetFiletypeCompleter() - self._arguments = arguments - - - def Start( self ): - self._completer.OnUserCommand( self._arguments, - _BuildRequestData() ) - - - def Response( self ): - # TODO: Call vimsupport.JumpToLocation if the user called a GoTo command... - # we may want to have specific subclasses of CommandRequest so that a - # GoToRequest knows it needs to jump after the data comes back. - # - # Also need to run the following on GoTo data: - # CAREFUL about line/column number 0-based/1-based confusion! - # - # defs = [] - # defs.append( {'filename': definition.module_path.encode( 'utf-8' ), - # 'lnum': definition.line, - # 'col': definition.column + 1, - # 'text': definition.description.encode( 'utf-8' ) } ) - # vim.eval( 'setqflist( %s )' % repr( defs ) ) - # vim.eval( 'youcompleteme#OpenGoToList()' ) - return self.ServerResponse() - - -class EventNotification( BaseRequest ): - def __init__( self, event_name, ycm_state, extra_data = None ): - super( EventNotification, self ).__init__() - - self._ycm_state = ycm_state - self._event_name = event_name - self._request_data = _BuildRequestData() - if extra_data: - self._request_data.update( extra_data ) - - - def Start( self ): - event_handler = 'On' + self._event_name - getattr( self._ycm_state.GetGeneralCompleter(), - event_handler )( self._request_data ) - - if self._ycm_state.FiletypeCompletionUsable(): - getattr( self._ycm_state.GetFiletypeCompleter(), - event_handler )( self._request_data ) - - if hasattr( extra_conf_store, event_handler ): - getattr( extra_conf_store, event_handler )( self._request_data ) - - - -def SendEventNotificationAsync( event_name, ycm_state, extra_data = None ): - event = EventNotification( event_name, ycm_state, extra_data ) - event.Start() +from ycm.client.base_request import BaseRequest +from ycm.client.command_request import CommandRequest +from ycm.client.completion_request import CompletionRequest +from ycm.client.event_notification import SendEventNotificationAsync +SERVER_PORT_RANGE_START = 10000 class YouCompleteMe( object ): def __init__( self, user_options ): - # TODO: This should go into the server - # TODO: Use more logging like we do in cs_completer - self._logfile = tempfile.NamedTemporaryFile() - logging.basicConfig( format='%(asctime)s - %(levelname)s - %(message)s', - filename=self._logfile.name, - level=logging.DEBUG ) - self._user_options = user_options - self._gencomp = GeneralCompleterStore( user_options ) self._omnicomp = OmniCompleter( user_options ) - self._filetype_completers = {} self._current_completion_request = None + server_port = SERVER_PORT_RANGE_START + os.getpid() + command = ''.join( [ 'python ', + _PathToServerScript(), + ' --port=', + str( server_port ) ] ) + + BaseRequest.server_location = 'http://localhost:' + str( server_port ) + + filename_format = os.path.join( utils.PathToTempDir(), + 'server_{port}_{std}.log' ) + + self._server_stdout = filename_format.format( port=server_port, + std='stdout' ) + self._server_stderr = filename_format.format( port=server_port, + std='stderr' ) + + with open( self._server_stderr, 'w' ) as fstderr: + with open( self._server_stdout, 'w' ) as fstdout: + subprocess.Popen( command, stdout=fstdout, stderr=fstderr, shell=True ) + def CreateCompletionRequest( self ): # We have to store a reference to the newly created CompletionRequest # because VimScript can't store a reference to a Python object across # function calls... Thus we need to keep this request somewhere. - self._current_completion_request = CompletionRequest( self ) + self._current_completion_request = CompletionRequest() return self._current_completion_request @@ -204,72 +81,19 @@ class YouCompleteMe( object ): return self._current_completion_request - def GetGeneralCompleter( self ): - return self._gencomp - - def GetOmniCompleter( self ): return self._omnicomp - def GetFiletypeCompleter( self ): - filetypes = vimsupport.CurrentFiletypes() - - completers = [ self.GetFiletypeCompleterForFiletype( filetype ) - for filetype in filetypes ] - - if not completers: - return None - - # Try to find a native completer first - for completer in completers: - if completer and completer is not self._omnicomp: - return completer - - # Return the omni completer for the first filetype - return completers[ 0 ] - - - def GetFiletypeCompleterForFiletype( self, filetype ): - try: - return self._filetype_completers[ filetype ] - except KeyError: - pass - - module_path = _PathToFiletypeCompleterPluginLoader( filetype ) - - completer = None - supported_filetypes = [ filetype ] - if os.path.exists( module_path ): - module = imp.load_source( filetype, module_path ) - completer = module.GetCompleter( self._user_options ) - if completer: - supported_filetypes.extend( completer.SupportedFiletypes() ) - else: - completer = self._omnicomp - - for supported_filetype in supported_filetypes: - self._filetype_completers[ supported_filetype ] = completer - return completer - - - def ShouldUseGeneralCompleter( self, request_data ): - return self._gencomp.ShouldUseNow( request_data ) - - - def ShouldUseFiletypeCompleter( self, request_data ): - if self.FiletypeCompletionUsable(): - return self.GetFiletypeCompleter().ShouldUseNow( request_data ) + def NativeFiletypeCompletionAvailable( self ): + # TODO: Talk to server about this. return False - def NativeFiletypeCompletionAvailable( self ): - completer = self.GetFiletypeCompleter() - return bool( completer ) and completer is not self._omnicomp - - - def FiletypeCompletionAvailable( self ): - return bool( self.GetFiletypeCompleter() ) + # TODO: This may not be needed at all when the server is ready. Evaluate this + # later. + # def FiletypeCompletionAvailable( self ): + # return bool( self.GetFiletypeCompleter() ) def NativeFiletypeCompletionUsable( self ): @@ -277,9 +101,11 @@ class YouCompleteMe( object ): self.NativeFiletypeCompletionAvailable() ) - def FiletypeCompletionUsable( self ): - return ( self.CurrentFiletypeCompletionEnabled() and - self.FiletypeCompletionAvailable() ) + # TODO: This may not be needed at all when the server is ready. Evaluate this + # later. + # def FiletypeCompletionUsable( self ): + # return ( self.CurrentFiletypeCompletionEnabled() and + # self.FiletypeCompletionAvailable() ) def OnFileReadyToParse( self ): @@ -290,51 +116,55 @@ class YouCompleteMe( object ): # TODO: make this work again # if self._user_options[ 'seed_identifiers_with_syntax' ]: - SendEventNotificationAsync( 'FileReadyToParse', self, extra_data ) + SendEventNotificationAsync( 'FileReadyToParse', extra_data ) def OnBufferUnload( self, deleted_buffer_file ): SendEventNotificationAsync( 'BufferUnload', - self, { 'unloaded_buffer': deleted_buffer_file } ) def OnBufferVisit( self ): - SendEventNotificationAsync( 'BufferVisit', self ) + SendEventNotificationAsync( 'BufferVisit' ) def OnInsertLeave( self ): - SendEventNotificationAsync( 'InsertLeave', self ) + SendEventNotificationAsync( 'InsertLeave' ) def OnVimLeave( self ): - SendEventNotificationAsync( 'VimLeave', self ) + SendEventNotificationAsync( 'VimLeave' ) def OnCurrentIdentifierFinished( self ): - SendEventNotificationAsync( 'CurrentIdentifierFinished', self ) + SendEventNotificationAsync( 'CurrentIdentifierFinished' ) + # TODO: Make this work again. def DiagnosticsForCurrentFileReady( self ): - if self.FiletypeCompletionUsable(): - return self.GetFiletypeCompleter().DiagnosticsForCurrentFileReady() + # if self.FiletypeCompletionUsable(): + # return self.GetFiletypeCompleter().DiagnosticsForCurrentFileReady() return False + # TODO: Make this work again. def GetDiagnosticsForCurrentFile( self ): - if self.FiletypeCompletionUsable(): - return self.GetFiletypeCompleter().GetDiagnosticsForCurrentFile() + # if self.FiletypeCompletionUsable(): + # return self.GetFiletypeCompleter().GetDiagnosticsForCurrentFile() return [] + # TODO: Make this work again. def GetDetailedDiagnostic( self ): - if self.FiletypeCompletionUsable(): - return self.GetFiletypeCompleter().GetDetailedDiagnostic() + # if self.FiletypeCompletionUsable(): + # return self.GetFiletypeCompleter().GetDetailedDiagnostic() + pass + # TODO: Make this work again. def GettingCompletions( self ): - if self.FiletypeCompletionUsable(): - return self.GetFiletypeCompleter().GettingCompletions() + # if self.FiletypeCompletionUsable(): + # return self.GetFiletypeCompleter().GettingCompletions() return False @@ -366,51 +196,13 @@ class YouCompleteMe( object ): return not all([ x in filetype_to_disable for x in filetypes ]) -def _PathToCompletersFolder(): - dir_of_current_script = os.path.dirname( os.path.abspath( __file__ ) ) - return os.path.join( dir_of_current_script, 'completers' ) - - -def _PathToFiletypeCompleterPluginLoader( filetype ): - return os.path.join( _PathToCompletersFolder(), filetype, 'hook.py' ) - - -def _BuildRequestData( start_column = None, query = None ): - line, column = vimsupport.CurrentLineAndColumn() - request_data = { - 'filetypes': vimsupport.CurrentFiletypes(), - 'line_num': line, - 'column_num': column, - 'start_column': start_column, - 'line_value': vim.current.line, - 'filepath': vim.current.buffer.name, - 'file_data': vimsupport.GetUnsavedAndCurrentBufferData() - } - - if query: - request_data[ 'query' ] = query - - return request_data - -def _ConvertCompletionDataToVimData( completion_data ): - # see :h complete-items for a description of the dictionary fields - vim_data = { - 'word' : completion_data[ 'insertion_text' ], - 'dup' : 1, - } - - if 'menu_text' in completion_data: - vim_data[ 'abbr' ] = completion_data[ 'menu_text' ] - if 'extra_menu_info' in completion_data: - vim_data[ 'menu' ] = completion_data[ 'extra_menu_info' ] - if 'kind' in completion_data: - vim_data[ 'kind' ] = completion_data[ 'kind' ] - if 'detailed_info' in completion_data: - vim_data[ 'info' ] = completion_data[ 'detailed_info' ] - - return vim_data def _GetTagFiles(): tag_files = vim.eval( 'tagfiles()' ) current_working_directory = os.getcwd() return [ os.path.join( current_working_directory, x ) for x in tag_files ] + + +def _PathToServerScript(): + dir_of_current_script = os.path.dirname( os.path.abspath( __file__ ) ) + return os.path.join( dir_of_current_script, 'server/server.py' )