From 6acc381262e08dddc065755bacfac84620e7a81d Mon Sep 17 00:00:00 2001 From: Strahinja Val Markovic Date: Sat, 7 Sep 2013 17:39:52 -0700 Subject: [PATCH] Identifier completer now fully decoupled from Vim --- .../completers/all/identifier_completer.py | 139 +++++++++++------- .../all/tests/identifier_completer_test.py | 127 ++++++++++++++++ python/ycm/completers/completer.py | 10 +- python/ycm/completers/cpp/clang_completer.py | 6 +- .../general/general_completer_store.py | 20 +-- .../completers/general/ultisnips_completer.py | 4 +- python/ycm/youcompleteme.py | 99 +++++++++---- 7 files changed, 300 insertions(+), 105 deletions(-) create mode 100644 python/ycm/completers/all/tests/identifier_completer_test.py diff --git a/python/ycm/completers/all/identifier_completer.py b/python/ycm/completers/all/identifier_completer.py index 196ff85a..aa3d812b 100644 --- a/python/ycm/completers/all/identifier_completer.py +++ b/python/ycm/completers/all/identifier_completer.py @@ -18,12 +18,10 @@ # along with YouCompleteMe. If not, see . import os -import vim import ycm_core from collections import defaultdict from ycm.completers.general_completer import GeneralCompleter -from ycm.completers.general import syntax_parse -from ycm import vimsupport +# from ycm.completers.general import syntax_parse from ycm import utils from ycm import server_responses @@ -50,9 +48,9 @@ class IdentifierCompleter( GeneralCompleter ): request_data[ 'filetypes' ][ 0 ] ) - def AddIdentifier( self, identifier ): - filetype = vim.eval( "&filetype" ) - filepath = vim.eval( "expand('%:p')" ) + def AddIdentifier( self, identifier, request_data ): + filetype = request_data[ 'filetypes' ][ 0 ] + filepath = request_data[ 'filepath' ] if not filetype or not filepath or not identifier: return @@ -64,23 +62,20 @@ class IdentifierCompleter( GeneralCompleter ): filepath ) - def AddPreviousIdentifier( self ): - self.AddIdentifier( _PreviousIdentifier( self.user_options[ - 'min_num_of_chars_for_completion' ] ) ) + def AddPreviousIdentifier( self, request_data ): + self.AddIdentifier( + _PreviousIdentifier( + self.user_options[ 'min_num_of_chars_for_completion' ], + request_data ), + request_data ) - def AddIdentifierUnderCursor( self ): - cursor_identifier = vim.eval( 'expand("")' ) + def AddIdentifierUnderCursor( self, request_data ): + cursor_identifier = _GetCursorIdentifier( request_data ) if not cursor_identifier: return - stripped_cursor_identifier = ''.join( ( x for x in - cursor_identifier if - utils.IsIdentifierChar( x ) ) ) - if not stripped_cursor_identifier: - return - - self.AddIdentifier( stripped_cursor_identifier ) + self.AddIdentifier( cursor_identifier, request_data ) def AddBufferIdentifiers( self, request_data ): @@ -100,26 +95,22 @@ class IdentifierCompleter( GeneralCompleter ): collect_from_comments_and_strings ) - def AddIdentifiersFromTagFiles( self ): - tag_files = vim.eval( 'tagfiles()' ) - current_working_directory = os.getcwd() + def AddIdentifiersFromTagFiles( self, tag_files ): absolute_paths_to_tag_files = ycm_core.StringVec() for tag_file in tag_files: - absolute_tag_file = os.path.join( current_working_directory, - tag_file ) try: - current_mtime = os.path.getmtime( absolute_tag_file ) + current_mtime = os.path.getmtime( tag_file ) except: continue - last_mtime = self.tags_file_last_mtime[ absolute_tag_file ] + last_mtime = self.tags_file_last_mtime[ tag_file ] # We don't want to repeatedly process the same file over and over; we only # process if it's changed since the last time we looked at it if current_mtime <= last_mtime: continue - self.tags_file_last_mtime[ absolute_tag_file ] = current_mtime - absolute_paths_to_tag_files.append( absolute_tag_file ) + self.tags_file_last_mtime[ tag_file ] = current_mtime + absolute_paths_to_tag_files.append( tag_file ) if not absolute_paths_to_tag_files: return @@ -128,41 +119,37 @@ class IdentifierCompleter( GeneralCompleter ): absolute_paths_to_tag_files ) - def AddIdentifiersFromSyntax( self ): - filetype = vim.eval( "&filetype" ) - if filetype in self.filetypes_with_keywords_loaded: - return + # def AddIdentifiersFromSyntax( self ): + # filetype = vim.eval( "&filetype" ) + # if filetype in self.filetypes_with_keywords_loaded: + # return - self.filetypes_with_keywords_loaded.add( filetype ) + # self.filetypes_with_keywords_loaded.add( filetype ) - keyword_set = syntax_parse.SyntaxKeywordsForCurrentBuffer() - keywords = ycm_core.StringVec() - for keyword in keyword_set: - keywords.append( keyword ) + # keyword_set = syntax_parse.SyntaxKeywordsForCurrentBuffer() + # keywords = ycm_core.StringVec() + # for keyword in keyword_set: + # keywords.append( keyword ) - filepath = SYNTAX_FILENAME + filetype - self.completer.AddIdentifiersToDatabase( keywords, - filetype, - filepath ) + # filepath = SYNTAX_FILENAME + filetype + # self.completer.AddIdentifiersToDatabase( keywords, + # filetype, + # filepath ) def OnFileReadyToParse( self, request_data ): self.AddBufferIdentifiers( request_data ) - - # TODO: make these work again - # if self.user_options[ 'collect_identifiers_from_tags_files' ]: - # self.AddIdentifiersFromTagFiles() - - # if self.user_options[ 'seed_identifiers_with_syntax' ]: - # self.AddIdentifiersFromSyntax() + if 'tag_files' in request_data: + self.AddIdentifiersFromTagFiles( request_data[ 'tag_files' ] ) + #self.AddIdentifiersFromSyntax() - def OnInsertLeave( self ): - self.AddIdentifierUnderCursor() + def OnInsertLeave( self, request_data ): + self.AddIdentifierUnderCursor( request_data ) - def OnCurrentIdentifierFinished( self ): - self.AddPreviousIdentifier() + def OnCurrentIdentifierFinished( self, request_data ): + self.AddPreviousIdentifier( request_data ) def CandidatesFromStoredRequest( self ): @@ -177,10 +164,13 @@ class IdentifierCompleter( GeneralCompleter ): return [ server_responses.BuildCompletionData( x ) for x in completions ] -def _PreviousIdentifier( min_num_completion_start_chars ): - line_num, column_num = vimsupport.CurrentLineAndColumn() - buffer = vim.current.buffer - line = buffer[ line_num ] +def _PreviousIdentifier( min_num_completion_start_chars, request_data ): + line_num = request_data[ 'line_num' ] + column_num = request_data[ 'column_num' ] + filepath = request_data[ 'filepath' ] + contents_per_line = ( + request_data[ 'file_data' ][ filepath ][ 'contents' ].split( '\n' ) ) + line = contents_per_line[ line_num ] end_column = column_num @@ -190,7 +180,7 @@ def _PreviousIdentifier( min_num_completion_start_chars ): # Look at the previous line if we reached the end of the current one if end_column == 0: try: - line = buffer[ line_num - 1] + line = contents_per_line[ line_num - 1 ] except: return "" end_column = len( line ) @@ -214,3 +204,40 @@ def _RemoveSmallCandidates( candidates, min_num_candidate_size_chars ): return [ x for x in candidates if len( x ) >= min_num_candidate_size_chars ] + +# This is meant to behave like 'expand(" 0 and utils.IsIdentifierChar( line[ + identifier_start - 1 ] ): + identifier_start -= 1 + return identifier_start + + + def FindIdentifierEnd( line, valid_char_column ): + identifier_end = valid_char_column + while identifier_end < len( line ) - 1 and utils.IsIdentifierChar( line[ + identifier_end + 1 ] ): + identifier_end += 1 + return identifier_end + 1 + + column_num = request_data[ 'column_num' ] + line = request_data[ 'line_value' ] + + try: + valid_char_column = FindFirstValidChar( line, column_num ) + return line[ FindIdentifierStart( line, valid_char_column ) : + FindIdentifierEnd( line, valid_char_column ) ] + except: + return '' + diff --git a/python/ycm/completers/all/tests/identifier_completer_test.py b/python/ycm/completers/all/tests/identifier_completer_test.py new file mode 100644 index 00000000..532773cd --- /dev/null +++ b/python/ycm/completers/all/tests/identifier_completer_test.py @@ -0,0 +1,127 @@ +#!/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 nose.tools import eq_ +from ycm.completers.all import identifier_completer + + +def GetCursorIdentifier_StartOfLine_test(): + eq_( 'foo', + identifier_completer._GetCursorIdentifier( + { + 'column_num': 0, + 'line_value': 'foo' + } ) ) + + eq_( 'fooBar', + identifier_completer._GetCursorIdentifier( + { + 'column_num': 0, + 'line_value': 'fooBar' + } ) ) + + +def GetCursorIdentifier_EndOfLine_test(): + eq_( 'foo', + identifier_completer._GetCursorIdentifier( + { + 'column_num': 2, + 'line_value': 'foo' + } ) ) + + +def GetCursorIdentifier_PastEndOfLine_test(): + eq_( '', + identifier_completer._GetCursorIdentifier( + { + 'column_num': 10, + 'line_value': 'foo' + } ) ) + + +def GetCursorIdentifier_NegativeColumn_test(): + eq_( '', + identifier_completer._GetCursorIdentifier( + { + 'column_num': -10, + 'line_value': 'foo' + } ) ) + + +def GetCursorIdentifier_StartOfLine_StopsAtNonIdentifierChar_test(): + eq_( 'foo', + identifier_completer._GetCursorIdentifier( + { + 'column_num': 0, + 'line_value': 'foo(goo)' + } ) ) + + +def GetCursorIdentifier_AtNonIdentifier_test(): + eq_( 'goo', + identifier_completer._GetCursorIdentifier( + { + 'column_num': 3, + 'line_value': 'foo(goo)' + } ) ) + + +def GetCursorIdentifier_WalksForwardForIdentifier_test(): + eq_( 'foo', + identifier_completer._GetCursorIdentifier( + { + 'column_num': 0, + 'line_value': ' foo' + } ) ) + + +def GetCursorIdentifier_FindsNothingForward_test(): + eq_( '', + identifier_completer._GetCursorIdentifier( + { + 'column_num': 4, + 'line_value': 'foo ()***()' + } ) ) + + +def GetCursorIdentifier_SingleCharIdentifier_test(): + eq_( 'f', + identifier_completer._GetCursorIdentifier( + { + 'column_num': 0, + 'line_value': ' f ' + } ) ) + + +def GetCursorIdentifier_StartsInMiddleOfIdentifier_test(): + eq_( 'foobar', + identifier_completer._GetCursorIdentifier( + { + 'column_num': 3, + 'line_value': 'foobar' + } ) ) + + +def GetCursorIdentifier_LineEmpty_test(): + eq_( '', + identifier_completer._GetCursorIdentifier( + { + 'column_num': 11, + 'line_value': '' + } ) ) diff --git a/python/ycm/completers/completer.py b/python/ycm/completers/completer.py index ec5732e5..321c8e1c 100644 --- a/python/ycm/completers/completer.py +++ b/python/ycm/completers/completer.py @@ -265,19 +265,19 @@ class Completer( object ): pass - def OnBufferVisit( self ): + def OnBufferVisit( self, request_data ): pass - def OnBufferUnload( self, deleted_buffer_file ): + def OnBufferUnload( self, request_data ): pass - def OnInsertLeave( self ): + def OnInsertLeave( self, request_data ): pass - def OnVimLeave( self ): + def OnVimLeave( self, request_data ): pass @@ -285,7 +285,7 @@ class Completer( object ): raise NotImplementedError( NO_USER_COMMANDS ) - def OnCurrentIdentifierFinished( self ): + def OnCurrentIdentifierFinished( self, request_data ): pass diff --git a/python/ycm/completers/cpp/clang_completer.py b/python/ycm/completers/cpp/clang_completer.py index 73e90eef..98d9f5a0 100644 --- a/python/ycm/completers/cpp/clang_completer.py +++ b/python/ycm/completers/cpp/clang_completer.py @@ -245,8 +245,8 @@ class ClangCompleter( Completer ): self.extra_parse_desired = False - def OnBufferUnload( self, deleted_buffer_file ): - self.completer.DeleteCachesForFileAsync( deleted_buffer_file ) + def OnBufferUnload( self, request_data ): + self.completer.DeleteCachesForFileAsync( request_data[ 'unloaded_buffer' ] ) def DiagnosticsForCurrentFileReady( self ): @@ -271,7 +271,7 @@ class ClangCompleter( Completer ): self.parse_future = None if self.extra_parse_desired: - self.OnFileReadyToParse() + self.OnFileReadyToParse( request_data ) return self.last_prepared_diagnostics diff --git a/python/ycm/completers/general/general_completer_store.py b/python/ycm/completers/general/general_completer_store.py index fcfe5f76..3269bc97 100644 --- a/python/ycm/completers/general/general_completer_store.py +++ b/python/ycm/completers/general/general_completer_store.py @@ -100,29 +100,29 @@ class GeneralCompleterStore( Completer ): completer.OnFileReadyToParse( request_data ) - def OnBufferVisit( self ): + def OnBufferVisit( self, request_data ): for completer in self._all_completers: - completer.OnBufferVisit() + completer.OnBufferVisit( request_data ) - def OnBufferUnload( self, deleted_buffer_file ): + def OnBufferUnload( self, request_data ): for completer in self._all_completers: - completer.OnBufferUnload( deleted_buffer_file ) + completer.OnBufferUnload( request_data ) - def OnInsertLeave( self ): + def OnInsertLeave( self, request_data ): for completer in self._all_completers: - completer.OnInsertLeave() + completer.OnInsertLeave( request_data ) - def OnVimLeave( self ): + def OnVimLeave( self, request_data ): for completer in self._all_completers: - completer.OnVimLeave() + completer.OnVimLeave( request_data ) - def OnCurrentIdentifierFinished( self ): + def OnCurrentIdentifierFinished( self, request_data ): for completer in self._all_completers: - completer.OnCurrentIdentifierFinished() + completer.OnCurrentIdentifierFinished( request_data ) def GettingCompletions( self ): diff --git a/python/ycm/completers/general/ultisnips_completer.py b/python/ycm/completers/general/ultisnips_completer.py index d16b174f..a66c6fdc 100644 --- a/python/ycm/completers/general/ultisnips_completer.py +++ b/python/ycm/completers/general/ultisnips_completer.py @@ -51,7 +51,9 @@ class UltiSnipsCompleter( GeneralCompleter ): return self._filtered_candidates if self._filtered_candidates else [] - def OnBufferVisit( self ): + def OnBufferVisit( self, request_data ): + # TODO: _GetCandidates should be called on the client and it should send + # the snippets to the server self._candidates = _GetCandidates() diff --git a/python/ycm/youcompleteme.py b/python/ycm/youcompleteme.py index 1e8d8efb..a7f8e276 100644 --- a/python/ycm/youcompleteme.py +++ b/python/ycm/youcompleteme.py @@ -31,8 +31,27 @@ from ycm.completers.general.general_completer_store import GeneralCompleterStore # TODO: Put the Request classes in separate files -class CompletionRequest( object ): +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 ) @@ -70,7 +89,7 @@ class CompletionRequest( object ): -class CommandRequest( object ): +class CommandRequest( BaseRequest ): class ServerResponse( object ): def __init__( self ): pass @@ -79,6 +98,8 @@ class CommandRequest( object ): return True def __init__( self, ycm_state, arguments, completer_target = None ): + super( CommandRequest, self ).__init__() + if not completer_target: completer_target = 'filetpe_default' @@ -95,9 +116,6 @@ class CommandRequest( object ): self._completer.OnUserCommand( self._arguments, _BuildRequestData() ) - def Done( self ): - return True - def Response( self ): # TODO: Call vimsupport.JumpToLocation if the user called a GoTo command... @@ -117,6 +135,31 @@ class CommandRequest( object ): 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 ): + getattr( self._ycm_state.GetGeneralCompleter(), + 'On' + self._event_name )( self._request_data ) + + if self._ycm_state.FiletypeCompletionUsable(): + getattr( self._ycm_state.GetFiletypeCompleter(), + 'On' + self._event_name )( self._request_data ) + + + +def SendEventNotificationAsync( event_name, ycm_state, extra_data = None ): + event = EventNotification( event_name, ycm_state, extra_data ) + event.Start() + class YouCompleteMe( object ): def __init__( self, user_options ): @@ -235,38 +278,36 @@ class YouCompleteMe( object ): def OnFileReadyToParse( self ): - self._gencomp.OnFileReadyToParse( _BuildRequestData() ) + extra_data = {} + if self._user_options[ 'collect_identifiers_from_tags_files' ]: + extra_data[ 'tag_files' ] = _GetTagFiles() - if self.FiletypeCompletionUsable(): - self.GetFiletypeCompleter().OnFileReadyToParse( _BuildRequestData() ) + # TODO: make this work again + # if self._user_options[ 'seed_identifiers_with_syntax' ]: + + SendEventNotificationAsync( 'FileReadyToParse', self, extra_data ) def OnBufferUnload( self, deleted_buffer_file ): - self._gencomp.OnBufferUnload( deleted_buffer_file ) - - if self.FiletypeCompletionUsable(): - self.GetFiletypeCompleter().OnBufferUnload( deleted_buffer_file ) + SendEventNotificationAsync( 'BufferUnload', + self, + { 'unloaded_buffer': deleted_buffer_file } ) def OnBufferVisit( self ): - self._gencomp.OnBufferVisit() - - if self.FiletypeCompletionUsable(): - self.GetFiletypeCompleter().OnBufferVisit() + SendEventNotificationAsync( 'BufferVisit', self ) def OnInsertLeave( self ): - self._gencomp.OnInsertLeave() - - if self.FiletypeCompletionUsable(): - self.GetFiletypeCompleter().OnInsertLeave() + SendEventNotificationAsync( 'InsertLeave', self ) def OnVimLeave( self ): - self._gencomp.OnVimLeave() + SendEventNotificationAsync( 'VimLeave', self ) - if self.FiletypeCompletionUsable(): - self.GetFiletypeCompleter().OnVimLeave() + + def OnCurrentIdentifierFinished( self ): + SendEventNotificationAsync( 'CurrentIdentifierFinished', self ) def DiagnosticsForCurrentFileReady( self ): @@ -292,13 +333,6 @@ class YouCompleteMe( object ): return False - def OnCurrentIdentifierFinished( self ): - self._gencomp.OnCurrentIdentifierFinished() - - if self.FiletypeCompletionUsable(): - self.GetFiletypeCompleter().OnCurrentIdentifierFinished() - - def DebugInfo( self ): completers = set( self._filetype_completers.values() ) completers.add( self._gencomp ) @@ -370,3 +404,8 @@ def _ConvertCompletionDataToVimData( 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 ]