Initial, rough version of completer separation

We are trying to build a nice plugin-oriented architechure for loading varios
completer objects.
This commit is contained in:
Strahinja Val Markovic 2012-08-04 17:46:54 -07:00
parent 1fd2515617
commit d0f62d1205
9 changed files with 729 additions and 476 deletions

View File

@ -22,7 +22,7 @@ set cpo&vim
" This needs to be called outside of a function
let s:script_folder_path = escape( expand( '<sfile>:p:h' ), '\' )
let s:searched_and_no_results_found = 0
let s:should_use_clang = 0
let s:should_use_filetype_completion = 0
let s:completion_start_column = 0
let s:omnifunc_mode = 0
@ -79,10 +79,9 @@ function! youcompleteme#Enable()
py import vim
exe 'python sys.path = sys.path + ["' . s:script_folder_path . '/../python"]'
py import ycm
py identcomp = ycm.IdentifierCompleter()
py ycm_state = ycm.YouCompleteMe()
if g:ycm_clang_completion_enabled
py clangcomp = ycm.ClangCompleter()
if g:ycm_filetype_completion_enabled
" <c-x><c-o> trigger omni completion, <c-p> deselects the first completion
" candidate that vim selects by default
inoremap <unique> <C-Space> <C-X><C-O><C-P>
@ -97,27 +96,18 @@ endfunction
function! s:OnBufferVisit()
call s:SetCompleteFunc()
call s:ParseFile()
call s:OnFileReadyToParse()
endfunction
function! s:OnCursorHold()
call s:ParseFile()
call s:OnFileReadyToParse()
call s:UpdateDiagnosticNotifications()
endfunction
function! s:ClangEnabledForCurrentFile()
return g:ycm_clang_completion_enabled && pyeval('ycm.ClangAvailableForFile()')
endfunction
function! s:ParseFile()
py identcomp.OnFileReadyToParse()
if s:ClangEnabledForCurrentFile()
py clangcomp.OnFileReadyToParse()
endif
function! s:OnFileReadyToParse()
py ycm_state.OnFileReadyToParse()
endfunction
@ -125,9 +115,9 @@ function! s:SetCompleteFunc()
let &completefunc = 'youcompleteme#Complete'
let &l:completefunc = 'youcompleteme#Complete'
if s:ClangEnabledForCurrentFile()
let &omnifunc = 'youcompleteme#ClangOmniComplete'
let &l:omnifunc = 'youcompleteme#ClangOmniComplete'
if pyeval( 'ycm_state.FiletypeCompletionEnabledForCurrentFile()' )
let &omnifunc = 'youcompleteme#OmniComplete'
let &l:omnifunc = 'youcompleteme#OmniComplete'
endif
endfunction
@ -146,13 +136,14 @@ endfunction
function! s:OnInsertLeave()
let s:omnifunc_mode = 0
call s:UpdateDiagnosticNotifications()
py identcomp.AddIdentifierUnderCursor()
py ycm_state.OnInsertLeave()
endfunction
function! s:UpdateDiagnosticNotifications()
if get( g:, 'loaded_syntastic_plugin', 0 ) && s:ClangEnabledForCurrentFile()
\ && pyeval( 'clangcomp.DiagnosticsForCurrentFileReady()' )
if get( g:, 'loaded_syntastic_plugin', 0 ) &&
\ pyeval( 'ycm_state.FiletypeCompletionEnabledForCurrentFile()' ) &&
\ pyeval( 'ycm_state.DiagnosticsForCurrentFileReady()' )
SyntasticCheck
endif
endfunction
@ -162,7 +153,7 @@ function! s:IdentifierFinishedOperations()
if !pyeval( 'ycm.CurrentIdentifierFinished()' )
return
endif
py identcomp.AddPreviousIdentifier()
py ycm_state.OnCurrentIdentifierFinished()
let s:omnifunc_mode = 0
endfunction
@ -213,40 +204,31 @@ function! s:InvokeCompletion()
endfunction
function! s:IdentifierCompletion(query)
if strlen( a:query ) < g:ycm_min_num_of_chars_for_completion
function! s:CompletionsForQuery( query, use_filetype_completer )
" TODO: needed?
if !a:use_filetype_completer &&
\ strlen( a:query ) < g:ycm_min_num_of_chars_for_completion
return []
endif
py identcomp.CandidatesForQueryAsync( vim.eval( 'a:query' ) )
if a:use_filetype_completer
py completer = ycm_state.GetFiletypeCompleterForCurrentFile()
else
py completer = ycm_state.GetIdentifierCompleter()
endif
let l:results_ready = 0
while !l:results_ready
let l:results_ready = pyeval( 'identcomp.AsyncCandidateRequestReady()' )
if complete_check()
return { 'words' : [], 'refresh' : 'always'}
endif
endwhile
let l:results = pyeval( 'identcomp.CandidatesFromStoredRequest()' )
let s:searched_and_no_results_found = len( l:results ) == 0
return { 'words' : l:results, 'refresh' : 'always' }
endfunction
function! s:ClangCompletion( query )
" TODO: don't trigger on a dot inside a string constant
py clangcomp.CandidatesForQueryAsync( vim.eval( 'a:query' ) )
py completer.CandidatesForQueryAsync( vim.eval( 'a:query' ) )
let l:results_ready = 0
while !l:results_ready
let l:results_ready = pyeval( 'clangcomp.AsyncCandidateRequestReady()' )
let l:results_ready = pyeval( 'completer.AsyncCandidateRequestReady()' )
if complete_check()
return { 'words' : [], 'refresh' : 'always'}
endif
endwhile
let l:results = pyeval( 'clangcomp.CandidatesFromStoredRequest()' )
let l:results = pyeval( 'completer.CandidatesFromStoredRequest()' )
let s:searched_and_no_results_found = len( l:results ) == 0
return { 'words' : l:results, 'refresh' : 'always' }
endfunction
@ -254,24 +236,25 @@ endfunction
" This is our main entry point. This is what vim calls to get completions.
function! youcompleteme#Complete( findstart, base )
" Aften the user types one character afte the call to the omnifunc, the
" After the user types one character after the call to the omnifunc, the
" completefunc will be called because of our mapping that calls the
" completefunc on every keystroke. Therefore we need to delegate the call we
" 'stole' back to the omnifunc
if s:omnifunc_mode
return youcompleteme#ClangOmniComplete( a:findstart, a:base )
return youcompleteme#OmniComplete( a:findstart, a:base )
endif
if a:findstart
let s:completion_start_column = pyeval( 'ycm.CompletionStartColumn()' )
let s:should_use_clang =
\ pyeval( 'ycm.ShouldUseClang(' . s:completion_start_column . ')' )
let s:should_use_filetype_completion =
\ pyeval( 'ycm_state.ShouldUseFiletypeCompleter(' .
\ s:completion_start_column . ')' )
if ( !s:should_use_clang )
" TODO: use ShouldUseIdentifierCompleter() which checks query length
if ( !s:should_use_filetype_completion )
let l:current_column = col('.') - 1
let l:query_length = current_column - s:completion_start_column
if ( query_length < g:ycm_min_num_of_chars_for_completion )
" for vim, -2 means not found but don't trigger an error message
" see :h complete-functions
@ -280,22 +263,18 @@ function! youcompleteme#Complete( findstart, base )
endif
return s:completion_start_column
else
if ( s:should_use_clang )
return s:ClangCompletion( a:base )
else
return s:IdentifierCompletion( a:base )
endif
return s:CompletionsForQuery( a:base, s:should_use_filetype_completion )
endif
endfunction
function! youcompleteme#ClangOmniComplete( findstart, base )
function! youcompleteme#OmniComplete( findstart, base )
if a:findstart
let s:omnifunc_mode = 1
let s:completion_start_column = pyeval( 'ycm.CompletionStartColumn()' )
return s:completion_start_column
else
return s:ClangCompletion( a:base )
return s:CompletionsForQuery( a:base, 1 )
endif
endfunction
@ -304,10 +283,7 @@ endfunction
" required (currently that's on buffer save) OR when the SyntasticCheck command
" is invoked
function! youcompleteme#CurrentFileDiagnostics()
if s:ClangEnabledForCurrentFile()
return pyeval( 'clangcomp.GetDiagnosticsForCurrentFile()' )
endif
return []
return pyeval( 'ycm_state.GetDiagnosticsForCurrentFile()' )
endfunction
" This is basic vim plugin boilerplate

View File

@ -37,11 +37,11 @@ let g:ycm_min_num_of_chars_for_completion =
let g:ycm_filetypes_to_ignore =
\ get( g:, 'ycm_filetypes_to_ignore', { 'notes' : 1 } )
let g:ycm_clang_completion_enabled =
\ get( g:, 'ycm_clang_completion_enabled', 1 )
let g:ycm_filetype_completion_enabled =
\ get( g:, 'ycm_filetype_completion_enabled', 1 )
let g:ycm_allow_changing_updatetime =
\ get( g:, 'ycm_clang_completion_enabled', 1 )
\ get( g:, 'ycm_allow_changing_updatetime', 1 )
" This is basic vim plugin boilerplate

88
python/completer.py Normal file
View File

@ -0,0 +1,88 @@
#!/usr/bin/env python
#
# Copyright (C) 2011, 2012 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/>.
import abc
class Completer( object ):
__metaclass__ = abc.ABCMeta
def __init__( self ):
self.future = None
def AsyncCandidateRequestReady( self ):
if not self.future:
# We return True so that the caller can extract the default value from the
# future
return True
return self.future.ResultsReady()
def CandidatesFromStoredRequest( self ):
if not self.future:
return []
return self.future.GetResults()
def OnFileReadyToParse( self ):
pass
def OnCursorMovedInsertMode( self ):
pass
def OnCursorMovedNormalMode( self ):
pass
def OnBufferVisit( self ):
pass
def OnCursorHold( self ):
pass
def OnInsertLeave( self ):
pass
def OnCurrentIdentifierFinished( self ):
pass
def DiagnosticsForCurrentFileReady( self ):
return False
def GetDiagnosticsForCurrentFile( self ):
return []
@abc.abstractmethod
def SupportedFiletypes( self ):
pass
@abc.abstractmethod
def ShouldUseNow( self, start_column ):
pass

View File

157
python/completers/all.py Normal file
View File

@ -0,0 +1,157 @@
#!/usr/bin/env python
#
# Copyright (C) 2011, 2012 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 completer import Completer
import vim
import vimsupport
import indexer
import utils
MAX_IDENTIFIER_COMPLETIONS_RETURNED = 10
MIN_NUM_CHARS = int( vim.eval( "g:ycm_min_num_of_chars_for_completion" ) )
def GetCompleter():
return IdentifierCompleter()
class IdentifierCompleter( Completer ):
def __init__( self ):
self.completer = indexer.IdentifierCompleter()
self.completer.EnableThreading()
def SupportedFiletypes( self ):
# magic token meaning all filetypes
return set( [ 'ycm_all' ] )
# TODO: implement this
def ShouldUseNow( self, start_column ):
return True
def CandidatesForQueryAsync( self, query ):
filetype = vim.eval( "&filetype" )
self.future = self.completer.CandidatesForQueryAndTypeAsync(
utils.SanitizeQuery( query ),
filetype )
def AddIdentifier( self, identifier ):
filetype = vim.eval( "&filetype" )
filepath = vim.eval( "expand('%:p')" )
if not filetype or not filepath or not identifier:
return
vector = indexer.StringVec()
vector.append( identifier )
self.completer.AddCandidatesToDatabase( vector,
filetype,
filepath )
def AddPreviousIdentifier( self ):
self.AddIdentifier( PreviousIdentifier() )
def AddIdentifierUnderCursor( self ):
cursor_identifier = vim.eval( 'expand("<cword>")' )
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 )
def AddBufferIdentifiers( self ):
filetype = vim.eval( "&filetype" )
filepath = vim.eval( "expand('%:p')" )
if not filetype or not filepath:
return
text = "\n".join( vim.current.buffer )
self.completer.AddCandidatesToDatabaseFromBufferAsync( text,
filetype,
filepath )
def OnFileReadyToParse( self ):
self.AddBufferIdentifiers()
def OnInsertLeave( self ):
self.AddIdentifierUnderCursor()
def OnCurrentIdentifierFinished( self ):
self.AddPreviousIdentifier()
def CandidatesFromStoredRequest( self ):
if not self.future:
return []
completions = self.future.GetResults()[
: MAX_IDENTIFIER_COMPLETIONS_RETURNED ]
# We will never have duplicates in completions so with 'dup':1 we tell Vim
# to add this candidate even if it's a duplicate of an existing one (which
# will never happen). This saves us some expensive string matching
# operations in Vim.
return [ { 'word': x, 'dup': 1 } for x in completions ]
def PreviousIdentifier():
line_num, column_num = vimsupport.CurrentLineAndColumn()
buffer = vim.current.buffer
line = buffer[ line_num ]
end_column = column_num
while end_column > 0 and not utils.IsIdentifierChar( line[ end_column - 1 ] ):
end_column -= 1
# Look at the previous line if we reached the end of the current one
if end_column == 0:
try:
line = buffer[ line_num - 1]
except:
return ""
end_column = len( line )
while end_column > 0 and not utils.IsIdentifierChar(
line[ end_column - 1 ] ):
end_column -= 1
print end_column, line
start_column = end_column
while start_column > 0 and utils.IsIdentifierChar( line[ start_column - 1 ] ):
start_column -= 1
if end_column - start_column < MIN_NUM_CHARS:
return ""
return line[ start_column : end_column ]

281
python/completers/cpp.py Normal file
View File

@ -0,0 +1,281 @@
#!/usr/bin/env python
#
# Copyright (C) 2011, 2012 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 completer import Completer
import vim
import vimsupport
import indexer
import random
import imp
import os
import string
CLANG_FILETYPES = set( [ 'c', 'cpp', 'objc', 'objcpp' ] )
CLANG_OPTIONS_FILENAME = '.ycm_clang_options.py'
def GetCompleter():
return ClangCompleter()
class ClangCompleter( Completer ):
def __init__( self ):
self.completer = indexer.ClangCompleter()
self.completer.EnableThreading()
self.contents_holder = []
self.filename_holder = []
self.last_diagnostics = []
self.possibly_new_diagnostics = False
self.flags = Flags()
def SupportedFiletypes( self ):
return CLANG_FILETYPES
def GetUnsavedFilesVector( self ):
files = indexer.UnsavedFileVec()
self.contents_holder = []
self.filename_holder = []
for buffer in vimsupport.GetUnsavedBuffers():
if not ClangAvailableForBuffer( buffer ):
continue
contents = '\n'.join( buffer )
name = buffer.name
if not contents or not name:
continue
self.contents_holder.append( contents )
self.filename_holder.append( name )
unsaved_file = indexer.UnsavedFile()
unsaved_file.contents_ = self.contents_holder[ -1 ]
unsaved_file.length_ = len( self.contents_holder[ -1 ] )
unsaved_file.filename_ = self.filename_holder[ -1 ]
files.append( unsaved_file )
return files
def CandidatesForQueryAsync( self, query ):
if self.completer.UpdatingTranslationUnit():
vimsupport.PostVimMessage( 'Still parsing file, no completions yet.' )
self.future = None
return
# TODO: sanitize query
# CAREFUL HERE! For UnsavedFile filename and contents we are referring
# directly to Python-allocated and -managed memory since we are accepting
# pointers to data members of python objects. We need to ensure that those
# objects outlive our UnsavedFile objects. This is why we need the
# contents_holder and filename_holder lists, to make sure the string objects
# are still around when we call CandidatesForQueryAndLocationInFile. We do
# this to avoid an extra copy of the entire file contents.
files = indexer.UnsavedFileVec()
if not query:
files = self.GetUnsavedFilesVector()
line, _ = vim.current.window.cursor
column = int( vim.eval( "s:completion_start_column" ) ) + 1
current_buffer = vim.current.buffer
self.future = self.completer.CandidatesForQueryAndLocationInFileAsync(
query,
current_buffer.name,
line,
column,
files,
self.flags.FlagsForFile( current_buffer.name ) )
def CandidatesFromStoredRequest( self ):
if not self.future:
return []
results = [ CompletionDataToDict( x ) for x in self.future.GetResults() ]
if not results:
vimsupport.PostVimMessage( 'No completions found; errors in the file?' )
return results
def OnFileReadyToParse( self ):
if vimsupport.NumLinesInBuffer( vim.current.buffer ) < 5:
return
self.possibly_new_diagnostics = True
filename = vim.current.buffer.name
self.completer.UpdateTranslationUnitAsync(
filename,
self.GetUnsavedFilesVector(),
self.flags.FlagsForFile( filename ) )
def DiagnosticsForCurrentFileReady( self ):
return ( self.possibly_new_diagnostics and not
self.completer.UpdatingTranslationUnit() )
def GetDiagnosticsForCurrentFile( self ):
if self.DiagnosticsForCurrentFileReady():
self.last_diagnostics = [ DiagnosticToDict( x ) for x in
self.completer.DiagnosticsForFile(
vim.current.buffer.name ) ]
self.possibly_new_diagnostics = False
return self.last_diagnostics
def ShouldUseNow( self, start_column ):
return ShouldUseClang( start_column )
class Flags( object ):
def __init__( self ):
# It's caches all the way down...
self.flags_for_file = {}
self.flags_module_for_file = {}
self.flags_module_for_flags_module_file = {}
def FlagsForFile( self, filename ):
try:
return self.flags_for_file[ filename ]
except KeyError:
flags_module = self.FlagsModuleForFile( filename )
if not flags_module:
return indexer.StringVec()
results = flags_module.FlagsForFile( filename )
sanitized_flags = SanitizeFlags( results[ 'flags' ] )
if results[ 'do_cache' ]:
self.flags_for_file[ filename ] = sanitized_flags
return sanitized_flags
def FlagsModuleForFile( self, filename ):
try:
return self.flags_module_for_file[ filename ]
except KeyError:
flags_module_file = FlagsModuleSourceFileForFile( filename )
if not flags_module_file:
return None
try:
flags_module = self.flags_module_for_flags_module_file[
flags_module_file ]
except KeyError:
flags_module = imp.load_source( RandomName(), flags_module_file )
self.flags_module_for_flags_module_file[
flags_module_file ] = flags_module
self.flags_module_for_file[ filename ] = flags_module
return flags_module
def FlagsModuleSourceFileForFile( filename ):
parent_folder = os.path.dirname( filename )
old_parent_folder = ''
while True:
current_file = os.path.join( parent_folder, CLANG_OPTIONS_FILENAME )
if os.path.exists( current_file ):
return current_file
old_parent_folder = parent_folder
parent_folder = os.path.dirname( parent_folder )
if parent_folder == old_parent_folder:
return None
def RandomName():
return ''.join( random.choice( string.ascii_lowercase ) for x in range( 15 ) )
def SanitizeFlags( flags ):
sanitized_flags = []
saw_arch = False
for i, flag in enumerate( flags ):
if flag == '-arch':
saw_arch = True
continue
elif flag.startswith( '-arch' ):
continue
elif saw_arch:
saw_arch = False
continue
sanitized_flags.append( flag )
vector = indexer.StringVec()
for flag in sanitized_flags:
vector.append( flag )
return vector
def CompletionDataToDict( completion_data ):
# see :h complete-items for a description of the dictionary fields
return {
'word' : completion_data.TextToInsertInBuffer(),
'abbr' : completion_data.MainCompletionText(),
'menu' : completion_data.ExtraMenuInfo(),
'kind' : completion_data.kind_,
'dup' : 1,
# TODO: add detailed_info_ as 'info'
}
def DiagnosticToDict( diagnostic ):
# see :h getqflist for a description of the dictionary fields
return {
'bufnr' : int( vim.eval( "bufnr('{0}', 1)".format(
diagnostic.filename_ ) ) ),
'lnum' : diagnostic.line_number_,
'col' : diagnostic.column_number_,
'text' : diagnostic.text_,
'type' : diagnostic.kind_,
'valid' : 1
}
def ClangAvailableForBuffer( buffer_object ):
filetype = vim.eval( 'getbufvar({0}, "&ft")'.format( buffer_object.number ) )
return filetype in CLANG_FILETYPES
def ShouldUseClang( start_column ):
line = vim.current.line
previous_char_index = start_column - 1
if ( not len( line ) or
previous_char_index < 0 or
previous_char_index >= len( line ) ):
return False
if line[ previous_char_index ] == '.':
return True
if previous_char_index - 1 < 0:
return False
two_previous_chars = line[ previous_char_index - 1 : start_column ]
if ( two_previous_chars == '->' or two_previous_chars == '::' ):
return True
return False

25
python/utils.py Normal file
View File

@ -0,0 +1,25 @@
#!/usr/bin/env python
#
# Copyright (C) 2011, 2012 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/>.
def IsIdentifierChar( char ):
return char.isalnum() or char == '_'
def SanitizeQuery( query ):
return query.strip()

66
python/vimsupport.py Normal file
View File

@ -0,0 +1,66 @@
#!/usr/bin/env python
#
# Copyright (C) 2011, 2012 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/>.
import vim
def CurrentLineAndColumn():
# See the comment in CurrentColumn about the calculation for the line and
# column number
line, column = vim.current.window.cursor
line -= 1
return line, column
def CurrentColumn():
"""Do NOT access the CurrentColumn in vim.current.line. It doesn't exist yet.
Only the chars before the current column exist in vim.current.line."""
# vim's columns are 1-based while vim.current.line columns are 0-based
# ... but vim.current.window.cursor (which returns a (line, column) tuple)
# columns are 0-based, while the line from that same tuple is 1-based.
# vim.buffers buffer objects OTOH have 0-based lines and columns.
# Pigs have wings and I'm a loopy purple duck. Everything makes sense now.
return vim.current.window.cursor[ 1 ]
def GetUnsavedBuffers():
def BufferModified( buffer_number ):
to_eval = 'getbufvar({0}, "&mod")'.format( buffer_number )
return bool( int( vim.eval( to_eval ) ) )
return ( x for x in vim.buffers if BufferModified( x.number ) )
def NumLinesInBuffer( buffer ):
# This is actually less than obvious, that's why it's wrapped in a function
return len( buffer )
def PostVimMessage( message ):
# TODO: escape the message string before formating it
vim.command( 'echohl WarningMsg | echomsg "{0}" | echohl None'
.format( message ) )
def EscapeForVim( text ):
return text.replace( "'", "''" )
def CurrentFiletype():
return vim.eval( "&filetype" )

View File

@ -17,408 +17,105 @@
# You should have received a copy of the GNU General Public License
# along with YouCompleteMe. If not, see <http://www.gnu.org/licenses/>.
import vim
import indexer
import abc
import vimsupport
import imp
import string
import random
import vim
import utils
import os
MIN_NUM_CHARS = int( vim.eval( "g:ycm_min_num_of_chars_for_completion" ) )
CLANG_COMPLETION_ENABLED = int( vim.eval( "g:ycm_clang_completion_enabled" ) )
CLANG_FILETYPES = set( [ 'c', 'cpp', 'objc', 'objcpp' ] )
MAX_IDENTIFIER_COMPLETIONS_RETURNED = 10
CLANG_OPTIONS_FILENAME = '.ycm_clang_options.py'
from completers.all import IdentifierCompleter
class Completer( object ):
__metaclass__ = abc.ABCMeta
class YouCompleteMe( object ):
def __init__( self ):
self.future = None
def AsyncCandidateRequestReady( self ):
if not self.future:
# We return True so that the caller can extract the default value from the
# future
return True
return self.future.ResultsReady()
self.identcomp = IdentifierCompleter()
self.filetype_completers = {}
def CandidatesFromStoredRequest( self ):
if not self.future:
return []
return self.future.GetResults()
@abc.abstractmethod
def OnFileReadyToParse( self ):
pass
def GetIdentifierCompleter( self ):
return self.identcomp
class IdentifierCompleter( Completer ):
def __init__( self ):
self.completer = indexer.IdentifierCompleter()
self.completer.EnableThreading()
def GetFiletypeCompleterForCurrentFile( self ):
filetype = vimsupport.CurrentFiletype()
try:
return self.filetype_completers[ filetype ]
except KeyError:
pass
module_path = _PathToFiletypeCompleterPlugin( filetype )
completer = None
if os.path.exists( module_path ):
module = imp.load_source( filetype, module_path )
completer = module.GetCompleter()
for supported_filetype in completer.SupportedFiletypes():
self.filetype_completers[ supported_filetype ] = completer
else:
self.filetype_completers[ filetype ] = None
return completer
def CandidatesForQueryAsync( self, query ):
filetype = vim.eval( "&filetype" )
self.future = self.completer.CandidatesForQueryAndTypeAsync(
SanitizeQuery( query ),
filetype )
def ShouldUseIdentifierCompleter( self, start_column ):
return self.identcomp.ShouldUseNow( start_column )
def AddIdentifier( self, identifier ):
filetype = vim.eval( "&filetype" )
filepath = vim.eval( "expand('%:p')" )
if not filetype or not filepath or not identifier:
return
vector = indexer.StringVec()
vector.append( identifier )
self.completer.AddCandidatesToDatabase( vector,
filetype,
filepath )
def ShouldUseFiletypeCompleter( self, start_column ):
if self.FiletypeCompletionEnabledForCurrentFile():
return self.GetFiletypeCompleterForCurrentFile().ShouldUseNow(
start_column )
return False
def AddPreviousIdentifier( self ):
self.AddIdentifier( PreviousIdentifier() )
def FiletypeCompletionAvailableForFile( self ):
return bool( self.GetFiletypeCompleterForCurrentFile() )
def AddIdentifierUnderCursor( self ):
cursor_identifier = vim.eval( 'expand("<cword>")' )
if not cursor_identifier:
return
stripped_cursor_identifier = ''.join( ( x for x in
cursor_identifier if
IsIdentifierChar( x ) ) )
if not stripped_cursor_identifier:
return
self.AddIdentifier( stripped_cursor_identifier )
def AddBufferIdentifiers( self ):
filetype = vim.eval( "&filetype" )
filepath = vim.eval( "expand('%:p')" )
if not filetype or not filepath:
return
text = "\n".join( vim.current.buffer )
self.completer.AddCandidatesToDatabaseFromBufferAsync( text,
filetype,
filepath )
def FiletypeCompletionEnabledForCurrentFile( self ):
return ( bool( int( vim.eval( 'g:ycm_filetype_completion_enabled' ) ) ) and
self.FiletypeCompletionAvailableForFile() )
def OnFileReadyToParse( self ):
self.AddBufferIdentifiers()
self.identcomp.OnFileReadyToParse()
if self.FiletypeCompletionEnabledForCurrentFile():
self.GetFiletypeCompleterForCurrentFile().OnFileReadyToParse()
def CandidatesFromStoredRequest( self ):
if not self.future:
return []
completions = self.future.GetResults()[
: MAX_IDENTIFIER_COMPLETIONS_RETURNED ]
def OnInsertLeave( self ):
self.identcomp.OnInsertLeave()
# We will never have duplicates in completions so with 'dup':1 we tell Vim
# to add this candidate even if it's a duplicate of an existing one (which
# will never happen). This saves us some expensive string matching
# operations in Vim.
return [ { 'word': x, 'dup': 1 } for x in completions ]
class ClangCompleter( Completer ):
def __init__( self ):
self.completer = indexer.ClangCompleter()
self.completer.EnableThreading()
self.contents_holder = []
self.filename_holder = []
self.last_diagnostics = []
self.possibly_new_diagnostics = False
self.flags = Flags()
def GetUnsavedFilesVector( self ):
files = indexer.UnsavedFileVec()
self.contents_holder = []
self.filename_holder = []
for buffer in GetUnsavedBuffers():
if not ClangAvailableForBuffer( buffer ):
continue
contents = '\n'.join( buffer )
name = buffer.name
if not contents or not name:
continue
self.contents_holder.append( contents )
self.filename_holder.append( name )
unsaved_file = indexer.UnsavedFile()
unsaved_file.contents_ = self.contents_holder[ -1 ]
unsaved_file.length_ = len( self.contents_holder[ -1 ] )
unsaved_file.filename_ = self.filename_holder[ -1 ]
files.append( unsaved_file )
return files
def CandidatesForQueryAsync( self, query ):
if self.completer.UpdatingTranslationUnit():
PostVimMessage( 'Still parsing file, no completions yet.' )
self.future = None
return
# TODO: sanitize query
# CAREFUL HERE! For UnsavedFile filename and contents we are referring
# directly to Python-allocated and -managed memory since we are accepting
# pointers to data members of python objects. We need to ensure that those
# objects outlive our UnsavedFile objects. This is why we need the
# contents_holder and filename_holder lists, to make sure the string objects
# are still around when we call CandidatesForQueryAndLocationInFile. We do
# this to avoid an extra copy of the entire file contents.
files = indexer.UnsavedFileVec()
if not query:
files = self.GetUnsavedFilesVector()
line, _ = vim.current.window.cursor
column = int( vim.eval( "s:completion_start_column" ) ) + 1
current_buffer = vim.current.buffer
self.future = self.completer.CandidatesForQueryAndLocationInFileAsync(
query,
current_buffer.name,
line,
column,
files,
self.flags.FlagsForFile( current_buffer.name ) )
def CandidatesFromStoredRequest( self ):
if not self.future:
return []
results = [ CompletionDataToDict( x ) for x in self.future.GetResults() ]
if not results:
PostVimMessage( 'No completions found; errors in the file?' )
return results
def OnFileReadyToParse( self ):
if NumLinesInBuffer( vim.current.buffer ) < 5:
return
self.possibly_new_diagnostics = True
filename = vim.current.buffer.name
self.completer.UpdateTranslationUnitAsync(
filename,
self.GetUnsavedFilesVector(),
self.flags.FlagsForFile( filename ) )
if self.FiletypeCompletionEnabledForCurrentFile():
self.GetFiletypeCompleterForCurrentFile().OnInsertLeave()
def DiagnosticsForCurrentFileReady( self ):
return ( self.possibly_new_diagnostics and not
self.completer.UpdatingTranslationUnit() )
if self.FiletypeCompletionEnabledForCurrentFile():
return self.GetFiletypeCompleterForCurrentFile().DiagnosticsForCurrentFileReady()
return False
def OnCurrentIdentifierFinished( self ):
self.identcomp.OnCurrentIdentifierFinished()
if self.FiletypeCompletionEnabledForCurrentFile():
self.GetFiletypeCompleterForCurrentFile().OnCurrentIdentifierFinished()
def GetDiagnosticsForCurrentFile( self ):
if self.DiagnosticsForCurrentFileReady():
self.last_diagnostics = [ DiagnosticToDict( x ) for x in
self.completer.DiagnosticsForFile(
vim.current.buffer.name ) ]
self.possibly_new_diagnostics = False
return self.last_diagnostics
if self.FiletypeCompletionEnabledForCurrentFile():
return self.GetFiletypeCompleterForCurrentFile().GetDiagnosticsForCurrentFile()
return []
class Flags( object ):
def __init__( self ):
# It's caches all the way down...
self.flags_for_file = {}
self.flags_module_for_file = {}
self.flags_module_for_flags_module_file = {}
def _PathToCompletersFolder():
dir_of_current_script = os.path.dirname( os.path.abspath( __file__ ) )
return os.path.join( dir_of_current_script, 'completers' )
def FlagsForFile( self, filename ):
try:
return self.flags_for_file[ filename ]
except KeyError:
flags_module = self.FlagsModuleForFile( filename )
if not flags_module:
return indexer.StringVec()
def _PathToFiletypeCompleterPlugin( filetype ):
return os.path.join( _PathToCompletersFolder(), filetype + '.py' )
results = flags_module.FlagsForFile( filename )
sanitized_flags = SanitizeFlags( results[ 'flags' ] )
if results[ 'do_cache' ]:
self.flags_for_file[ filename ] = sanitized_flags
return sanitized_flags
def FlagsModuleForFile( self, filename ):
try:
return self.flags_module_for_file[ filename ]
except KeyError:
flags_module_file = FlagsModuleSourceFileForFile( filename )
if not flags_module_file:
return None
try:
flags_module = self.flags_module_for_flags_module_file[
flags_module_file ]
except KeyError:
flags_module = imp.load_source( RandomName(), flags_module_file )
self.flags_module_for_flags_module_file[
flags_module_file ] = flags_module
self.flags_module_for_file[ filename ] = flags_module
return flags_module
def FlagsModuleSourceFileForFile( filename ):
parent_folder = os.path.dirname( filename )
old_parent_folder = ''
while True:
current_file = os.path.join( parent_folder, CLANG_OPTIONS_FILENAME )
if os.path.exists( current_file ):
return current_file
old_parent_folder = parent_folder
parent_folder = os.path.dirname( parent_folder )
if parent_folder == old_parent_folder:
return None
def RandomName():
return ''.join( random.choice( string.ascii_lowercase ) for x in range( 15 ) )
def SanitizeFlags( flags ):
sanitized_flags = []
saw_arch = False
for i, flag in enumerate( flags ):
if flag == '-arch':
saw_arch = True
continue
elif flag.startswith( '-arch' ):
continue
elif saw_arch:
saw_arch = False
continue
sanitized_flags.append( flag )
vector = indexer.StringVec()
for flag in sanitized_flags:
vector.append( flag )
return vector
def NumLinesInBuffer( buffer ):
# This is actually less than obvious, that's why it's wrapped in a function
return len( buffer )
def PostVimMessage( message ):
# TODO: escape the message string before formating it
vim.command( 'echohl WarningMsg | echomsg "{0}" | echohl None'
.format( message ) )
def GetUnsavedBuffers():
def BufferModified( buffer_number ):
to_eval = 'getbufvar({0}, "&mod")'.format( buffer_number )
return bool( int( vim.eval( to_eval ) ) )
return ( x for x in vim.buffers if BufferModified( x.number ) )
def CompletionDataToDict( completion_data ):
# see :h complete-items for a description of the dictionary fields
return {
'word' : completion_data.TextToInsertInBuffer(),
'abbr' : completion_data.MainCompletionText(),
'menu' : completion_data.ExtraMenuInfo(),
'kind' : completion_data.kind_,
'dup' : 1,
# TODO: add detailed_info_ as 'info'
}
def DiagnosticToDict( diagnostic ):
# see :h getqflist for a description of the dictionary fields
return {
'bufnr' : int( vim.eval( "bufnr('{0}', 1)".format(
diagnostic.filename_ ) ) ),
'lnum' : diagnostic.line_number_,
'col' : diagnostic.column_number_,
'text' : diagnostic.text_,
'type' : diagnostic.kind_,
'valid' : 1
}
def CurrentColumn():
"""Do NOT access the CurrentColumn in vim.current.line. It doesn't exist yet.
Only the chars before the current column exist in vim.current.line."""
# vim's columns are 1-based while vim.current.line columns are 0-based
# ... but vim.current.window.cursor (which returns a (line, column) tuple)
# columns are 0-based, while the line from that same tuple is 1-based.
# vim.buffers buffer objects OTOH have 0-based lines and columns.
# Pigs have wings and I'm a loopy purple duck. Everything makes sense now.
return vim.current.window.cursor[ 1 ]
def CurrentLineAndColumn():
# See the comment in CurrentColumn about the calculation for the line and
# column number
line, column = vim.current.window.cursor
line -= 1
return line, column
def ClangAvailableForBuffer( buffer_object ):
filetype = vim.eval( 'getbufvar({0}, "&ft")'.format( buffer_object.number ) )
return filetype in CLANG_FILETYPES
def ClangAvailableForFile():
filetype = vim.eval( "&filetype" )
return filetype in CLANG_FILETYPES
def ShouldUseClang( start_column ):
if not CLANG_COMPLETION_ENABLED or not ClangAvailableForFile():
return False
line = vim.current.line
previous_char_index = start_column - 1
if ( not len( line ) or
previous_char_index < 0 or
previous_char_index >= len( line ) ):
return False
if line[ previous_char_index ] == '.':
return True
if previous_char_index - 1 < 0:
return False
two_previous_chars = line[ previous_char_index - 1 : start_column ]
if ( two_previous_chars == '->' or two_previous_chars == '::' ):
return True
return False
def IsIdentifierChar( char ):
return char.isalnum() or char == '_'
def CompletionStartColumn():
@ -430,50 +127,15 @@ def CompletionStartColumn():
"""
line = vim.current.line
start_column = CurrentColumn()
start_column = vimsupport.CurrentColumn()
while start_column > 0 and IsIdentifierChar( line[ start_column - 1 ] ):
while start_column > 0 and utils.IsIdentifierChar( line[ start_column - 1 ] ):
start_column -= 1
return start_column
def EscapeForVim( text ):
return text.replace( "'", "''" )
def PreviousIdentifier():
line_num, column_num = CurrentLineAndColumn()
buffer = vim.current.buffer
line = buffer[ line_num ]
end_column = column_num
while end_column > 0 and not IsIdentifierChar( line[ end_column - 1 ] ):
end_column -= 1
# Look at the previous line if we reached the end of the current one
if end_column == 0:
try:
line = buffer[ line_num - 1]
except:
return ""
end_column = len( line )
while end_column > 0 and not IsIdentifierChar( line[ end_column - 1 ] ):
end_column -= 1
print end_column, line
start_column = end_column
while start_column > 0 and IsIdentifierChar( line[ start_column - 1 ] ):
start_column -= 1
if end_column - start_column < MIN_NUM_CHARS:
return ""
return line[ start_column : end_column ]
def CurrentIdentifierFinished():
current_column = CurrentColumn()
current_column = vimsupport.CurrentColumn()
previous_char_index = current_column - 1
if previous_char_index < 0:
return True
@ -483,16 +145,14 @@ def CurrentIdentifierFinished():
except IndexError:
return False
if IsIdentifierChar( previous_char ):
if utils.IsIdentifierChar( previous_char ):
return False
if ( not IsIdentifierChar( previous_char ) and
if ( not utils.IsIdentifierChar( previous_char ) and
previous_char_index > 0 and
IsIdentifierChar( line[ previous_char_index - 1 ] ) ):
utils.IsIdentifierChar( line[ previous_char_index - 1 ] ) ):
return True
else:
return line[ : current_column ].isspace()
def SanitizeQuery( query ):
return query.strip()