Identifier completer now fully decoupled from Vim

This commit is contained in:
Strahinja Val Markovic 2013-09-07 17:39:52 -07:00
parent a7c609efd7
commit 6acc381262
7 changed files with 300 additions and 105 deletions

View File

@ -18,12 +18,10 @@
# along with YouCompleteMe. If not, see <http://www.gnu.org/licenses/>. # along with YouCompleteMe. If not, see <http://www.gnu.org/licenses/>.
import os import os
import vim
import ycm_core import ycm_core
from collections import defaultdict from collections import defaultdict
from ycm.completers.general_completer import GeneralCompleter from ycm.completers.general_completer import GeneralCompleter
from ycm.completers.general import syntax_parse # from ycm.completers.general import syntax_parse
from ycm import vimsupport
from ycm import utils from ycm import utils
from ycm import server_responses from ycm import server_responses
@ -50,9 +48,9 @@ class IdentifierCompleter( GeneralCompleter ):
request_data[ 'filetypes' ][ 0 ] ) request_data[ 'filetypes' ][ 0 ] )
def AddIdentifier( self, identifier ): def AddIdentifier( self, identifier, request_data ):
filetype = vim.eval( "&filetype" ) filetype = request_data[ 'filetypes' ][ 0 ]
filepath = vim.eval( "expand('%:p')" ) filepath = request_data[ 'filepath' ]
if not filetype or not filepath or not identifier: if not filetype or not filepath or not identifier:
return return
@ -64,23 +62,20 @@ class IdentifierCompleter( GeneralCompleter ):
filepath ) filepath )
def AddPreviousIdentifier( self ): def AddPreviousIdentifier( self, request_data ):
self.AddIdentifier( _PreviousIdentifier( self.user_options[ self.AddIdentifier(
'min_num_of_chars_for_completion' ] ) ) _PreviousIdentifier(
self.user_options[ 'min_num_of_chars_for_completion' ],
request_data ),
request_data )
def AddIdentifierUnderCursor( self ): def AddIdentifierUnderCursor( self, request_data ):
cursor_identifier = vim.eval( 'expand("<cword>")' ) cursor_identifier = _GetCursorIdentifier( request_data )
if not cursor_identifier: if not cursor_identifier:
return return
stripped_cursor_identifier = ''.join( ( x for x in self.AddIdentifier( cursor_identifier, request_data )
cursor_identifier if
utils.IsIdentifierChar( x ) ) )
if not stripped_cursor_identifier:
return
self.AddIdentifier( stripped_cursor_identifier )
def AddBufferIdentifiers( self, request_data ): def AddBufferIdentifiers( self, request_data ):
@ -100,26 +95,22 @@ class IdentifierCompleter( GeneralCompleter ):
collect_from_comments_and_strings ) collect_from_comments_and_strings )
def AddIdentifiersFromTagFiles( self ): def AddIdentifiersFromTagFiles( self, tag_files ):
tag_files = vim.eval( 'tagfiles()' )
current_working_directory = os.getcwd()
absolute_paths_to_tag_files = ycm_core.StringVec() absolute_paths_to_tag_files = ycm_core.StringVec()
for tag_file in tag_files: for tag_file in tag_files:
absolute_tag_file = os.path.join( current_working_directory,
tag_file )
try: try:
current_mtime = os.path.getmtime( absolute_tag_file ) current_mtime = os.path.getmtime( tag_file )
except: except:
continue 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 # 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 # process if it's changed since the last time we looked at it
if current_mtime <= last_mtime: if current_mtime <= last_mtime:
continue continue
self.tags_file_last_mtime[ absolute_tag_file ] = current_mtime self.tags_file_last_mtime[ tag_file ] = current_mtime
absolute_paths_to_tag_files.append( absolute_tag_file ) absolute_paths_to_tag_files.append( tag_file )
if not absolute_paths_to_tag_files: if not absolute_paths_to_tag_files:
return return
@ -128,41 +119,37 @@ class IdentifierCompleter( GeneralCompleter ):
absolute_paths_to_tag_files ) absolute_paths_to_tag_files )
def AddIdentifiersFromSyntax( self ): # def AddIdentifiersFromSyntax( self ):
filetype = vim.eval( "&filetype" ) # filetype = vim.eval( "&filetype" )
if filetype in self.filetypes_with_keywords_loaded: # if filetype in self.filetypes_with_keywords_loaded:
return # return
self.filetypes_with_keywords_loaded.add( filetype ) # self.filetypes_with_keywords_loaded.add( filetype )
keyword_set = syntax_parse.SyntaxKeywordsForCurrentBuffer() # keyword_set = syntax_parse.SyntaxKeywordsForCurrentBuffer()
keywords = ycm_core.StringVec() # keywords = ycm_core.StringVec()
for keyword in keyword_set: # for keyword in keyword_set:
keywords.append( keyword ) # keywords.append( keyword )
filepath = SYNTAX_FILENAME + filetype # filepath = SYNTAX_FILENAME + filetype
self.completer.AddIdentifiersToDatabase( keywords, # self.completer.AddIdentifiersToDatabase( keywords,
filetype, # filetype,
filepath ) # filepath )
def OnFileReadyToParse( self, request_data ): def OnFileReadyToParse( self, request_data ):
self.AddBufferIdentifiers( request_data ) self.AddBufferIdentifiers( request_data )
if 'tag_files' in request_data:
# TODO: make these work again self.AddIdentifiersFromTagFiles( request_data[ 'tag_files' ] )
# if self.user_options[ 'collect_identifiers_from_tags_files' ]: #self.AddIdentifiersFromSyntax()
# self.AddIdentifiersFromTagFiles()
# if self.user_options[ 'seed_identifiers_with_syntax' ]:
# self.AddIdentifiersFromSyntax()
def OnInsertLeave( self ): def OnInsertLeave( self, request_data ):
self.AddIdentifierUnderCursor() self.AddIdentifierUnderCursor( request_data )
def OnCurrentIdentifierFinished( self ): def OnCurrentIdentifierFinished( self, request_data ):
self.AddPreviousIdentifier() self.AddPreviousIdentifier( request_data )
def CandidatesFromStoredRequest( self ): def CandidatesFromStoredRequest( self ):
@ -177,10 +164,13 @@ class IdentifierCompleter( GeneralCompleter ):
return [ server_responses.BuildCompletionData( x ) for x in completions ] return [ server_responses.BuildCompletionData( x ) for x in completions ]
def _PreviousIdentifier( min_num_completion_start_chars ): def _PreviousIdentifier( min_num_completion_start_chars, request_data ):
line_num, column_num = vimsupport.CurrentLineAndColumn() line_num = request_data[ 'line_num' ]
buffer = vim.current.buffer column_num = request_data[ 'column_num' ]
line = buffer[ line_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 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 # Look at the previous line if we reached the end of the current one
if end_column == 0: if end_column == 0:
try: try:
line = buffer[ line_num - 1] line = contents_per_line[ line_num - 1 ]
except: except:
return "" return ""
end_column = len( line ) 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 ] return [ x for x in candidates if len( x ) >= min_num_candidate_size_chars ]
# This is meant to behave like 'expand("<cword")' in Vim, thus starting at the
# cursor column and returning the "cursor word". If the cursor is not on a valid
# character, it searches forward until a valid identifier is found.
def _GetCursorIdentifier( request_data ):
def FindFirstValidChar( line, column ):
current_column = column
while not utils.IsIdentifierChar( line[ current_column ] ):
current_column += 1
return current_column
def FindIdentifierStart( line, valid_char_column ):
identifier_start = valid_char_column
while identifier_start > 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 ''

View File

@ -0,0 +1,127 @@
#!/usr/bin/env python
#
# Copyright (C) 2013 Strahinja Val Markovic <val@markovic.io>
#
# 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 <http://www.gnu.org/licenses/>.
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': ''
} ) )

View File

@ -265,19 +265,19 @@ class Completer( object ):
pass pass
def OnBufferVisit( self ): def OnBufferVisit( self, request_data ):
pass pass
def OnBufferUnload( self, deleted_buffer_file ): def OnBufferUnload( self, request_data ):
pass pass
def OnInsertLeave( self ): def OnInsertLeave( self, request_data ):
pass pass
def OnVimLeave( self ): def OnVimLeave( self, request_data ):
pass pass
@ -285,7 +285,7 @@ class Completer( object ):
raise NotImplementedError( NO_USER_COMMANDS ) raise NotImplementedError( NO_USER_COMMANDS )
def OnCurrentIdentifierFinished( self ): def OnCurrentIdentifierFinished( self, request_data ):
pass pass

View File

@ -245,8 +245,8 @@ class ClangCompleter( Completer ):
self.extra_parse_desired = False self.extra_parse_desired = False
def OnBufferUnload( self, deleted_buffer_file ): def OnBufferUnload( self, request_data ):
self.completer.DeleteCachesForFileAsync( deleted_buffer_file ) self.completer.DeleteCachesForFileAsync( request_data[ 'unloaded_buffer' ] )
def DiagnosticsForCurrentFileReady( self ): def DiagnosticsForCurrentFileReady( self ):
@ -271,7 +271,7 @@ class ClangCompleter( Completer ):
self.parse_future = None self.parse_future = None
if self.extra_parse_desired: if self.extra_parse_desired:
self.OnFileReadyToParse() self.OnFileReadyToParse( request_data )
return self.last_prepared_diagnostics return self.last_prepared_diagnostics

View File

@ -100,29 +100,29 @@ class GeneralCompleterStore( Completer ):
completer.OnFileReadyToParse( request_data ) completer.OnFileReadyToParse( request_data )
def OnBufferVisit( self ): def OnBufferVisit( self, request_data ):
for completer in self._all_completers: 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: 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: 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: 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: for completer in self._all_completers:
completer.OnCurrentIdentifierFinished() completer.OnCurrentIdentifierFinished( request_data )
def GettingCompletions( self ): def GettingCompletions( self ):

View File

@ -51,7 +51,9 @@ class UltiSnipsCompleter( GeneralCompleter ):
return self._filtered_candidates if self._filtered_candidates else [] 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() self._candidates = _GetCandidates()

View File

@ -31,8 +31,27 @@ from ycm.completers.general.general_completer_store import GeneralCompleterStore
# TODO: Put the Request classes in separate files # 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 ): def __init__( self, ycm_state ):
super( CompletionRequest, self ).__init__()
self._completion_start_column = base.CompletionStartColumn() self._completion_start_column = base.CompletionStartColumn()
self._ycm_state = ycm_state self._ycm_state = ycm_state
self._request_data = _BuildRequestData( self._completion_start_column ) self._request_data = _BuildRequestData( self._completion_start_column )
@ -70,7 +89,7 @@ class CompletionRequest( object ):
class CommandRequest( object ): class CommandRequest( BaseRequest ):
class ServerResponse( object ): class ServerResponse( object ):
def __init__( self ): def __init__( self ):
pass pass
@ -79,6 +98,8 @@ class CommandRequest( object ):
return True return True
def __init__( self, ycm_state, arguments, completer_target = None ): def __init__( self, ycm_state, arguments, completer_target = None ):
super( CommandRequest, self ).__init__()
if not completer_target: if not completer_target:
completer_target = 'filetpe_default' completer_target = 'filetpe_default'
@ -95,9 +116,6 @@ class CommandRequest( object ):
self._completer.OnUserCommand( self._arguments, self._completer.OnUserCommand( self._arguments,
_BuildRequestData() ) _BuildRequestData() )
def Done( self ):
return True
def Response( self ): def Response( self ):
# TODO: Call vimsupport.JumpToLocation if the user called a GoTo command... # TODO: Call vimsupport.JumpToLocation if the user called a GoTo command...
@ -117,6 +135,31 @@ class CommandRequest( object ):
return self.ServerResponse() 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 ): class YouCompleteMe( object ):
def __init__( self, user_options ): def __init__( self, user_options ):
@ -235,38 +278,36 @@ class YouCompleteMe( object ):
def OnFileReadyToParse( self ): 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(): # TODO: make this work again
self.GetFiletypeCompleter().OnFileReadyToParse( _BuildRequestData() ) # if self._user_options[ 'seed_identifiers_with_syntax' ]:
SendEventNotificationAsync( 'FileReadyToParse', self, extra_data )
def OnBufferUnload( self, deleted_buffer_file ): def OnBufferUnload( self, deleted_buffer_file ):
self._gencomp.OnBufferUnload( deleted_buffer_file ) SendEventNotificationAsync( 'BufferUnload',
self,
if self.FiletypeCompletionUsable(): { 'unloaded_buffer': deleted_buffer_file } )
self.GetFiletypeCompleter().OnBufferUnload( deleted_buffer_file )
def OnBufferVisit( self ): def OnBufferVisit( self ):
self._gencomp.OnBufferVisit() SendEventNotificationAsync( 'BufferVisit', self )
if self.FiletypeCompletionUsable():
self.GetFiletypeCompleter().OnBufferVisit()
def OnInsertLeave( self ): def OnInsertLeave( self ):
self._gencomp.OnInsertLeave() SendEventNotificationAsync( 'InsertLeave', self )
if self.FiletypeCompletionUsable():
self.GetFiletypeCompleter().OnInsertLeave()
def OnVimLeave( 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 ): def DiagnosticsForCurrentFileReady( self ):
@ -292,13 +333,6 @@ class YouCompleteMe( object ):
return False return False
def OnCurrentIdentifierFinished( self ):
self._gencomp.OnCurrentIdentifierFinished()
if self.FiletypeCompletionUsable():
self.GetFiletypeCompleter().OnCurrentIdentifierFinished()
def DebugInfo( self ): def DebugInfo( self ):
completers = set( self._filetype_completers.values() ) completers = set( self._filetype_completers.values() )
completers.add( self._gencomp ) completers.add( self._gencomp )
@ -370,3 +404,8 @@ def _ConvertCompletionDataToVimData( completion_data ):
vim_data[ 'info' ] = completion_data[ 'detailed_info' ] vim_data[ 'info' ] = completion_data[ 'detailed_info' ]
return vim_data 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 ]