diff --git a/autoload/youcompleteme.vim b/autoload/youcompleteme.vim index 8d612be2..0a9ae787 100644 --- a/autoload/youcompleteme.vim +++ b/autoload/youcompleteme.vim @@ -21,16 +21,19 @@ set cpo&vim " This needs to be called outside of a function let s:script_folder_path = escape( expand( ':p:h' ), '\' ) -let s:old_cursor_text = '' +let s:searched_and_no_results_found = 0 +let s:should_use_clang = 0 let g:ycm_min_num_of_chars_for_completion = 2 -" Set up the plugin, load all our modules, bind our keys etc. +" TODO: check for a non-broken version of Vim and stop executing (with an error +" message) if the detected version is too old + function! youcompleteme#Enable() augroup youcompleteme autocmd! autocmd CursorMovedI * call s:OnMovedI() - " BufWinEnter/Leave? + " TODO: BufWinEnter/Leave? autocmd BufRead,BufEnter * call s:OnBufferVisit() autocmd CursorHold,CursorHoldI * call s:OnCursorHold() augroup END @@ -62,6 +65,7 @@ function! youcompleteme#Enable() exe 'python sys.path = sys.path + ["' . s:script_folder_path . '/../python"]' py import ycm py csystem = ycm.CompletionSystem() + py clangcomp = ycm.ClangComplete() endfunction @@ -96,8 +100,8 @@ endfunction function! s:AddIdentifierIfNeeded() - py vim.command( "let should_add_identifier = '" + - \ str( int( ycm.ShouldAddIdentifier() ) ) + "'" ) + py vim.command( "let should_add_identifier = " + + \ str( int( ycm.ShouldAddIdentifier() ) ) ) if should_add_identifier != 1 return endif @@ -125,63 +129,108 @@ function! s:InvokeCompletion() return endif - py vim.command( "let cursor_text = '" + ycm.CurrentCursorTextVim() + "'" ) - - " infinite loops are bad, mkay? - if cursor_text == '' || cursor_text == s:old_cursor_text + " This is tricky. First, having 'refresh' set to 'always' in the dictionary + " that our completion function returns makes sure that our completion function + " is called on every keystroke when the completion menu is showing + " (pumvisible() == true). So there's no point in invoking the completion menu + " with our feedkeys call then. + " Secondly, when the sequence of characters the user typed produces no + " results in our search an infinite loop can occur. The problem is that our + " feedkeys call triggers the OnCursorMovedI event which we are tied to. + " So we solve this with the searched_and_no_results_found script-scope + " variable that prevents this infinite loop from starting. + if pumvisible() || s:searched_and_no_results_found + let s:searched_and_no_results_found = 0 return endif " invokes the user's completion function (which we have set to - " youcompleteme#Complete), and tells vim to select the previous - " completion candidate. This is necessary because by default, vim selects the + " youcompleteme#Complete), and tells Vim to select the previous + " completion candidate. This is necessary because by default, Vim selects the " first candidate when completion is invoked, and selecting a candidate - " automatically replaces the current text with it. Calling forces vim to + " automatically replaces the current text with it. Calling forces Vim to " deselect the first candidate and in turn preserve the user's current text " until he explicitly chooses to replace it with a completion. call feedkeys( "\\\", 'n' ) endfunction -" This is our main entry point. This is what vim calls to get completions. -function! youcompleteme#Complete(findstart, base) - if a:findstart - py vim.command( 'let start_column = ' + str( - \ ycm.CompletionStartColumn() ) ) - return start_column - else - let s:old_cursor_text = a:base - if strlen( a:base ) < g:ycm_min_num_of_chars_for_completion - return [] - endif +function! s:IdentifierCompletion(query) + if strlen( a:query ) < g:ycm_min_num_of_chars_for_completion + return [] + endif - py csystem.CandidatesForQueryAsync( vim.eval('a:base') ) + py csystem.CandidatesForQueryAsync( vim.eval('a:query') ) - let l:results_ready = 0 - while !l:results_ready + let l:results_ready = 0 + while !l:results_ready py << EOF results_ready = csystem.AsyncCandidateRequestReady() if results_ready: vim.command( 'let l:results_ready = 1' ) EOF - if complete_check() - return { 'words' : [], 'refresh' : 'always'} - endif - endwhile + if complete_check() + return { 'words' : [], 'refresh' : 'always'} + endif + endwhile - let l:results = [] + let l:results = [] py << EOF results = csystem.CandidatesFromStoredRequest() if results: vim.command( 'let l:results = ' + str( results ) ) EOF - " We need a very recent version of vim for this to work; otherwise, even - " when we set refresh = always, vim won't call our completefunc on every - " keystroke. The problem is still present in vim 7.3.390 but is fixed in - " 7.3.475. It's possible that patch 404 was the one that fixed this issue, - " but I haven't tested this assumption. - " A bug in vim causes the '.' register to break when we use set this... sigh - return { 'words' : l:results, 'refresh' : 'always'} + + let s:searched_and_no_results_found = len( l:results ) == 0 + + " We need a very recent version of vim for this to work; otherwise, even + " when we set refresh = always, vim won't call our completefunc on every + " keystroke. The problem is still present in vim 7.3.390 but is fixed in + " 7.3.475. It's possible that patch 404 was the one that fixed this issue, + " but I haven't tested this assumption. + " A bug in vim causes the '.' register to break when we use set this... sigh + return { 'words' : l:results, 'refresh' : 'always' } +endfunction + + +function! s:ClangCompletion( query ) + py vim.command( 'let l:results = ' + + \ str( clangcomp.CandidatesForQuery( vim.eval( 'a:query' ) ) ) ) + + let s:searched_and_no_results_found = len( l:results ) == 0 + return { 'words' : l:results, 'refresh' : 'always' } +endfunction + + +" This is our main entry point. This is what vim calls to get completions. +function! youcompleteme#Complete( findstart, base ) + if a:findstart + py << EOF +start_column = ycm.CompletionStartColumn() +vim.command( 'let l:start_column = ' + str( start_column ) ) +vim.command( 'let s:should_use_clang = ' + + str( int( ycm.ShouldUseClang( start_column ) ) ) ) +EOF + + if ( s:should_use_clang ) + return start_column + else + let l:current_column = col('.') - 1 + let l:query_length = current_column - 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 + return -2 + endif + return start_column + endif + else + if ( s:should_use_clang ) + return s:ClangCompletion( a:base ) + else + return s:IdentifierCompletion( a:base ) + endif endif endfunction diff --git a/cpp/ycm/ClangComplete.cpp b/cpp/ycm/ClangComplete.cpp index 1724ca55..0bbfdca9 100644 --- a/cpp/ycm/ClangComplete.cpp +++ b/cpp/ycm/ClangComplete.cpp @@ -31,9 +31,10 @@ std::vector< CXUnsavedFile > ToCXUnsavedFiles( std::vector< CXUnsavedFile > clang_unsaved_files( unsaved_files.size() ); for ( uint i = 0; i < unsaved_files.size(); ++i ) { - clang_unsaved_files[ i ].Filename = unsaved_files[ i ].filename_->c_str(); - clang_unsaved_files[ i ].Contents = unsaved_files[ i ].contents_->c_str(); - clang_unsaved_files[ i ].Length = unsaved_files[ i ].contents_->length(); + // TODO: assert non-null + clang_unsaved_files[ i ].Filename = unsaved_files[ i ].filename_; + clang_unsaved_files[ i ].Contents = unsaved_files[ i ].contents_; + clang_unsaved_files[ i ].Length = unsaved_files[ i ].length_; } return clang_unsaved_files; @@ -155,9 +156,15 @@ std::vector< std::string > ClangComplete::CandidatesForLocationInFile( std::vector< CXUnsavedFile > cxunsaved_files = ToCXUnsavedFiles( unsaved_files ); - // TODO: figure out does codeCompleteAt reparse the TU if the underlying - // source file has changed on disk since the last time the TU was updated and - // there are no unsaved files. + // codeCompleteAt reparses the TU if the underlying source file has changed on + // disk since the last time the TU was updated and there are no unsaved files. + // If there are unsaved files, then codeCompleteAt will parse the in-memory + // file contents we are giving it. In short, it is NEVER a good idea to call + // clang_reparseTranslationUnit right before a call to clang_codeCompleteAt. + // The only makes clang reparse the whole file TWICE, which has a huge impact + // on latency. At the time of writing, it seems that most users of libclang + // in the open-source world don't realize this (I checked). Some don't even + // call reparse*, but parse* which is even less efficient. CXCodeCompleteResults *results = clang_codeCompleteAt( GetTranslationUnitForFile( filename, unsaved_files ), @@ -189,7 +196,7 @@ CXTranslationUnit ClangComplete::CreateTranslationUnit( flags.size(), &cxunsaved_files[ 0 ], cxunsaved_files.size(), - clang_defaultEditingTranslationUnitOptions()); + clang_defaultEditingTranslationUnitOptions() ); } diff --git a/cpp/ycm/ClangComplete.h b/cpp/ycm/ClangComplete.h index fec31eee..4696837a 100644 --- a/cpp/ycm/ClangComplete.h +++ b/cpp/ycm/ClangComplete.h @@ -31,8 +31,20 @@ namespace YouCompleteMe struct UnsavedFile { - std::string *filename_; - std::string *contents_; + UnsavedFile() : filename_( NULL ), contents_( NULL ), length_( 0 ) {} + + const char *filename_; + const char *contents_; + unsigned long length_; + + // We need this to be able to export this struct to Python via Boost.Python's + // methods. I have no clue why, but it won't compile without it. + // TODO: report this problem on the Boost bug tracker, the default equality + // operator should be more than adequate here + bool operator==( const UnsavedFile &other) const + { + return filename_ == other.filename_ && contents_ == other.contents_; + } }; diff --git a/cpp/ycm/indexer.cpp b/cpp/ycm/indexer.cpp index d045173c..06be3b33 100644 --- a/cpp/ycm/indexer.cpp +++ b/cpp/ycm/indexer.cpp @@ -16,6 +16,7 @@ // along with YouCompleteMe. If not, see . #include "Completer.h" +#include "ClangComplete.h" #include "Future.h" #include @@ -27,17 +28,41 @@ BOOST_PYTHON_MODULE(indexer) using namespace boost::python; using namespace YouCompleteMe; + class_< std::vector< std::string > >( "StringVec" ) + .def( vector_indexing_suite< std::vector< std::string > >() ); + class_< Future >( "Future" ) .def( "ResultsReady", &Future::ResultsReady ) .def( "GetResults", &Future::GetResults ); class_< Completer, boost::noncopyable >( "Completer" ) .def( "EnableThreading", &Completer::EnableThreading ) - // .def( "AddCandidatesToDatabase", actd ) .def( "AddCandidatesToDatabase", &Completer::AddCandidatesToDatabase ) .def( "CandidatesForQueryAndTypeAsync", &Completer::CandidatesForQueryAndTypeAsync ); - class_< std::vector< std::string > >( "StringVec" ) - .def( vector_indexing_suite< std::vector< std::string > >() ); + // CAREFUL HERE! For 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. + class_< UnsavedFile >( "UnsavedFile" ) + .add_property( "filename_", + make_getter( &UnsavedFile::filename_ ), + make_setter( &UnsavedFile::filename_, + return_value_policy< reference_existing_object >() ) ) + .add_property( "contents_", + make_getter( &UnsavedFile::contents_ ), + make_setter( &UnsavedFile::contents_, + return_value_policy< reference_existing_object >() ) ) + .def_readwrite( "length_", &UnsavedFile::length_ ); + + class_< std::vector< UnsavedFile > >( "UnsavedFileVec" ) + .def( vector_indexing_suite< std::vector< UnsavedFile > >() ); + + class_< ClangComplete, boost::noncopyable >( "ClangComplete" ) + .def( "SetGlobalCompileFlags", &ClangComplete::SetGlobalCompileFlags ) + .def( "SetFileCompileFlags", &ClangComplete::SetFileCompileFlags ) + .def( "UpdateTranslationUnit", &ClangComplete::UpdateTranslationUnit ) + .def( "CandidatesForLocationInFile", + &ClangComplete::CandidatesForLocationInFile ); } diff --git a/python/ycm.py b/python/ycm.py index 364dc12c..41cd1ba0 100644 --- a/python/ycm.py +++ b/python/ycm.py @@ -23,6 +23,7 @@ import indexer min_num_chars = int( vim.eval( "g:ycm_min_num_of_chars_for_completion" ) ) +clang_filetypes = set( [ 'c', 'cpp', 'objc', 'objcpp' ] ) class CompletionSystem( object ): def __init__( self ): @@ -87,6 +88,34 @@ class CompletionSystem( object ): filepath, True ) +class ClangComplete( object ): + def __init__( self ): + self.completer = indexer.ClangComplete() + + def CandidatesForQuery( self, query ): + buffer = vim.current.buffer + + # 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. + # We do this to avoid an extra copy of the entire file contents. + contents = '\n'.join( buffer ) + unsaved_file = indexer.UnsavedFile() + unsaved_file.contents_ = contents + unsaved_file.length_ = len( contents ) + unsaved_file.filename_ = buffer.name + + files = indexer.UnsavedFileVec() + files.append( unsaved_file ) + + line = int( vim.eval( "line('.')" ) ) + column = CompletionStartColumn() + 1 + results = self.completer.CandidatesForLocationInFile( buffer.name, + line, + column, + files ) + return list( results ) def CurrentColumn(): """Do NOT access the CurrentColumn in vim.current.line. It doesn't exist yet. @@ -103,23 +132,48 @@ def CurrentLineAndColumn(): return line_num, column_num +def ShouldUseClang( start_column ): + filetype = vim.eval( "&filetype" ) + if filetype not in clang_filetypes: + 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(): + """Returns the 0-based index where the completion string should start. So if + the user enters: + foo.bar^ + with the cursor being at the location of the caret, then the starting column + would be the index of the letter 'b'. + """ + line = vim.current.line - current_column = CurrentColumn() - start_column = current_column + start_column = CurrentColumn() while start_column > 0 and IsIdentifierChar( line[ start_column - 1 ] ): start_column -= 1 - - if current_column - start_column < min_num_chars: - # for vim, -2 means not found but don't trigger an error message - # see :h complete-functions - return -2 - return start_column @@ -158,20 +212,6 @@ def PreviousIdentifier(): return line[ start_column : end_column ] -def CurrentCursorText(): - start_column = CompletionStartColumn() - current_column = CurrentColumn() - - if current_column - start_column < min_num_chars: - return "" - - return vim.current.line[ start_column : current_column ] - - -def CurrentCursorTextVim(): - return EscapeForVim( CurrentCursorText() ) - - def ShouldAddIdentifier(): current_column = CurrentColumn() previous_char_index = current_column - 1