From 786e6182ff6cca4df21583c61f1521072c1c9ecf Mon Sep 17 00:00:00 2001 From: Strahinja Val Markovic Date: Sun, 10 Feb 2013 19:55:05 -0800 Subject: [PATCH] Initial version of omnifunc-based omni_completer Still a work in progress (needs better triggering and bug fixes) --- autoload/youcompleteme.vim | 8 +- cpp/ycm/ClangCompleter/ClangCompleter.cpp | 3 +- cpp/ycm/PythonSupport.cpp | 96 +++++++++++++++++++ cpp/ycm/PythonSupport.h | 37 +++++++ cpp/ycm/Result.h | 13 +++ cpp/ycm/ycm_core.cpp | 2 + python/completers/all/identifier_completer.py | 1 + python/completers/all/omni_completer.py | 76 +++++++++++++++ python/completers/completer.py | 82 +++++++++++++++- python/completers/cpp/clang_completer.py | 2 + python/vimsupport.py | 6 +- python/ycm.py | 43 ++++++--- 12 files changed, 343 insertions(+), 26 deletions(-) create mode 100644 cpp/ycm/PythonSupport.cpp create mode 100644 cpp/ycm/PythonSupport.h create mode 100644 python/completers/all/omni_completer.py diff --git a/autoload/youcompleteme.vim b/autoload/youcompleteme.vim index 94b8f85c..2a7937ea 100644 --- a/autoload/youcompleteme.vim +++ b/autoload/youcompleteme.vim @@ -191,7 +191,7 @@ function! s:SetCompleteFunc() let &completefunc = 'youcompleteme#Complete' let &l:completefunc = 'youcompleteme#Complete' - if pyeval( 'ycm_state.FiletypeCompletionEnabled()' ) + if pyeval( 'ycm_state.NativeFiletypeCompletionUsable()' ) let &omnifunc = 'youcompleteme#OmniComplete' let &l:omnifunc = 'youcompleteme#OmniComplete' endif @@ -299,7 +299,7 @@ endfunction function! s:UpdateDiagnosticNotifications() if get( g:, 'loaded_syntastic_plugin', 0 ) && - \ pyeval( 'ycm_state.FiletypeCompletionEnabled()' ) && + \ pyeval( 'ycm_state.NativeFiletypeCompletionUsable()' ) && \ pyeval( 'ycm_state.DiagnosticsForCurrentFileReady()' ) SyntasticCheck endif @@ -408,6 +408,8 @@ function! youcompleteme#Complete( findstart, base ) return -2 endif + + " TODO: make this a function-local variable instead of a script-local one let s:completion_start_column = pyeval( 'ycm.CompletionStartColumn()' ) let s:should_use_filetype_completion = \ pyeval( 'ycm_state.ShouldUseFiletypeCompleter(' . @@ -465,7 +467,7 @@ command! YcmDebugInfo call s:DebugInfo() function! s:ForceCompile() - if !pyeval( 'ycm_state.FiletypeCompletionEnabled()' ) + if !pyeval( 'ycm_state.NativeFiletypeCompletionUsable()' ) echom "Filetype completion not supported for current file, " \ . "cannot force recompilation." endif diff --git a/cpp/ycm/ClangCompleter/ClangCompleter.cpp b/cpp/ycm/ClangCompleter/ClangCompleter.cpp index 7e90201e..a8a085fd 100644 --- a/cpp/ycm/ClangCompleter/ClangCompleter.cpp +++ b/cpp/ycm/ClangCompleter/ClangCompleter.cpp @@ -55,6 +55,7 @@ extern const unsigned int MIN_ASYNC_THREADS; namespace { +// TODO: replace this with ResultAnd from Result.h struct CompletionDataAndResult { CompletionDataAndResult( const CompletionData *completion_data, const Result &result ) @@ -419,7 +420,7 @@ std::vector< CompletionData > ClangCompleter::SortCandidatesForQuery( if ( result.IsSubsequence() ) { CompletionDataAndResult data_and_result( &completion_datas[ i ], result ); - data_and_results.push_back( data_and_result ); + data_and_results.push_back( boost::move( data_and_result ) ); } } diff --git a/cpp/ycm/PythonSupport.cpp b/cpp/ycm/PythonSupport.cpp new file mode 100644 index 00000000..148abb05 --- /dev/null +++ b/cpp/ycm/PythonSupport.cpp @@ -0,0 +1,96 @@ +// Copyright (C) 2011, 2012, 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 . + +#include "PythonSupport.h" +#include "standard.h" +#include "Result.h" +#include "Candidate.h" +#include "CandidateRepository.h" +#include + +using boost::python::len; +using boost::python::extract; +using boost::python::object; +typedef boost::python::list pylist; + +namespace YouCompleteMe { + +namespace { + +std::vector< const Candidate * > CandidatesFromObjectList( + const pylist &candidates, + const std::string &candidate_property ) { + int num_candidates = len( candidates ); + std::vector< std::string > candidate_strings; + candidate_strings.reserve( num_candidates ); + + for ( int i = 0; i < num_candidates; ++i ) { + if ( candidate_property.empty() ) { + candidate_strings.push_back( extract< std::string >( candidates[ i ] ) ); + } else { + object holder = extract< object >( candidates[ i ] ); + candidate_strings.push_back( extract< std::string >( + holder[ candidate_property.c_str() ] ) ); + } + } + + return CandidateRepository::Instance().GetCandidatesForStrings( + candidate_strings ); +} + +} // unnamed namespace + +boost::python::list FilterAndSortCandidates( + const boost::python::list &candidates, + const std::string &candidate_property, + const std::string &query ) { + pylist filtered_candidates; + if ( query.empty() ) + return filtered_candidates; + + std::vector< const Candidate * > repository_candidates = + CandidatesFromObjectList( candidates, candidate_property ); + + Bitset query_bitset = LetterBitsetFromString( query ); + int num_candidates = len( candidates ); + std::vector< ResultAnd< int > > object_and_results; + + for ( int i = 0; i < num_candidates; ++i ) { + const Candidate *candidate = repository_candidates[ i ]; + + if ( !candidate->MatchesQueryBitset( query_bitset ) ) + continue; + + Result result = candidate->QueryMatchResult( query ); + + if ( result.IsSubsequence() ) { + ResultAnd< int > object_and_result( i, result ); + object_and_results.push_back( boost::move( object_and_result ) ); + } + } + + std::sort( object_and_results.begin(), object_and_results.end() ); + + foreach ( const ResultAnd< int > &object_and_result, + object_and_results ) { + filtered_candidates.append( candidates[ object_and_result.extra_object_ ] ); + } + + return filtered_candidates; +} + +} // namespace YouCompleteMe diff --git a/cpp/ycm/PythonSupport.h b/cpp/ycm/PythonSupport.h new file mode 100644 index 00000000..4f2e3782 --- /dev/null +++ b/cpp/ycm/PythonSupport.h @@ -0,0 +1,37 @@ +// Copyright (C) 2011, 2012, 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 . + +#ifndef PYTHONSUPPORT_H_KWGFEX0V +#define PYTHONSUPPORT_H_KWGFEX0V + +#include + +namespace YouCompleteMe { + +// Given a list of python objects (that represent completion candidates) in a +// python list |candidates|, a |candidate_property| on which to filter and sort +// the candidates and a user query, returns a new sorted python list with the +// original objects that survived the filtering. +boost::python::list FilterAndSortCandidates( + const boost::python::list &candidates, + const std::string &candidate_property, + const std::string &query ); + +} // namespace YouCompleteMe + +#endif /* end of include guard: PYTHONSUPPORT_H_KWGFEX0V */ + diff --git a/cpp/ycm/Result.h b/cpp/ycm/Result.h index 1b3a7cfa..97b93ec8 100644 --- a/cpp/ycm/Result.h +++ b/cpp/ycm/Result.h @@ -86,6 +86,19 @@ private: }; +template< class T > +struct ResultAnd { + ResultAnd( T extra_object, const Result &result ) + : extra_object_( extra_object ), result_( result ) {} + + bool operator< ( const ResultAnd &other ) const { + return result_ < other.result_; + } + + T extra_object_; + Result result_; +}; + } // namespace YouCompleteMe #endif /* end of include guard: RESULT_H_CZYD2SGN */ diff --git a/cpp/ycm/ycm_core.cpp b/cpp/ycm/ycm_core.cpp index d55599b6..fc23837e 100644 --- a/cpp/ycm/ycm_core.cpp +++ b/cpp/ycm/ycm_core.cpp @@ -16,6 +16,7 @@ // along with YouCompleteMe. If not, see . #include "IdentifierCompleter.h" +#include "PythonSupport.h" #include "Future.h" #ifdef USE_CLANG_COMPLETER @@ -47,6 +48,7 @@ BOOST_PYTHON_MODULE(ycm_core) using namespace YouCompleteMe; def( "HasClangSupport", HasClangSupport ); + def( "FilterAndSortCandidates", FilterAndSortCandidates ); class_< IdentifierCompleter, boost::noncopyable >( "IdentifierCompleter" ) .def( "EnableThreading", &IdentifierCompleter::EnableThreading ) diff --git a/python/completers/all/identifier_completer.py b/python/completers/all/identifier_completer.py index dbf11eeb..2c3abeac 100644 --- a/python/completers/all/identifier_completer.py +++ b/python/completers/all/identifier_completer.py @@ -30,6 +30,7 @@ MIN_NUM_CHARS = int( vimsupport.GetVariableValue( class IdentifierCompleter( Completer ): def __init__( self ): + super( IdentifierCompleter, self ).__init__() self.completer = ycm_core.IdentifierCompleter() self.completer.EnableThreading() diff --git a/python/completers/all/omni_completer.py b/python/completers/all/omni_completer.py new file mode 100644 index 00000000..3fec79e8 --- /dev/null +++ b/python/completers/all/omni_completer.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python +# +# Copyright (C) 2011, 2012, 2013 Strahinja Val Markovic +# +# This file is part of YouCompleteMe. +# +# YouCompleteMe is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# YouCompleteMe is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with YouCompleteMe. If not, see . + +import vim +import vimsupport +from completers.completer import Completer + +class OmniCompleter( Completer ): + def __init__( self ): + super( OmniCompleter, self ).__init__() + self.omnifunc = None + self.stored_candidates = None + + + def SupportedFiletypes( self ): + return [] + + + def ShouldUseNowInner( self, 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 + return False + + + def CandidatesForQueryAsyncInner( self, query ): + if not self.omnifunc: + self.stored_candidates = None + return + + return_value = vim.eval( self.omnifunc + '(1,"")' ) + if return_value < 0: + self.stored_candidates = None + return + + omnifunc_call = [ self.omnifunc, + "(0,'", + vimsupport.EscapeForVim( query ), + "')" ] + + self.stored_candidates = vim.eval( ''.join( omnifunc_call ) ) + + + def AsyncCandidateRequestReadyInner( self ): + return True + + + def OnFileReadyToParse( self ): + self.omnifunc = vim.eval( '&omnifunc' ) + + + def CandidatesFromStoredRequestInner( self ): + return self.stored_candidates + diff --git a/python/completers/completer.py b/python/completers/completer.py index 0281a20e..b6fc4961 100644 --- a/python/completers/completer.py +++ b/python/completers/completer.py @@ -18,6 +18,23 @@ # along with YouCompleteMe. If not, see . import abc +import vim +import vimsupport +import ycm_core + + +class CompletionsCache( object ): + def __init__( self ): + self.line = -1 + self.column = -1 + self.raw_completions = [] + self.filtered_completions = [] + + + def CacheValid( self ): + completion_line, _ = vimsupport.CurrentLineAndColumn() + completion_column = int( vim.eval( "s:completion_start_column" ) ) + return completion_line == self.line and completion_column == self.column class Completer( object ): @@ -26,9 +43,57 @@ class Completer( object ): def __init__( self ): self.completions_future = None + self.completions_cache = None + + + def ShouldUseNow( self, start_column ): + inner_says_yes = self.ShouldUseNowInner( start_column ) + previous_results_were_empty = ( self.completions_cache and + not self.completions_cache.raw_completions ) + return inner_says_yes and not previous_results_were_empty + + + def ShouldUseNowInner( self, start_column ): + pass + + + def CandidatesForQueryAsync( self, query ): + if query and self.completions_cache and self.completions_cache.CacheValid(): + self.completions_cache.filtered_completions = ( + self.FilterAndSortCandidates( + self.completions_cache.raw_completions, + query ) ) + else: + self.completions_cache = None + self.CandidatesForQueryAsyncInner( query ) + + + def FilterAndSortCandidates( self, candidates, query ): + if not candidates: + return [] + + if hasattr( candidates, 'words' ): + candidates = candidates.words + items_are_objects = 'word' in candidates[ 0 ] + + return ycm_core.FilterAndSortCandidates( + candidates, + 'word' if items_are_objects else '', + query ) + + + def CandidatesForQueryAsyncInner( self, query ): + pass def AsyncCandidateRequestReady( self ): + if self.completions_cache: + return True + else: + return self.AsyncCandidateRequestReadyInner() + + + def AsyncCandidateRequestReadyInner( self ): if not self.completions_future: # We return True so that the caller can extract the default value from the # future @@ -37,6 +102,18 @@ class Completer( object ): def CandidatesFromStoredRequest( self ): + if self.completions_cache: + return self.completions_cache.filtered_completions + else: + self.completions_cache = CompletionsCache() + self.completions_cache.raw_completions = self.CandidatesFromStoredRequestInner() + self.completions_cache.line, _ = vimsupport.CurrentLineAndColumn() + self.completions_cache.column = int( + vim.eval( "s:completion_start_column" ) ) + return self.completions_cache.raw_completions + + + def CandidatesFromStoredRequestInner( self ): if not self.completions_future: return [] return self.completions_future.GetResults() @@ -90,10 +167,5 @@ class Completer( object ): pass - @abc.abstractmethod - def ShouldUseNow( self, start_column ): - pass - - def DebugInfo( self ): return '' diff --git a/python/completers/cpp/clang_completer.py b/python/completers/cpp/clang_completer.py index 6865d4af..bd8222fa 100644 --- a/python/completers/cpp/clang_completer.py +++ b/python/completers/cpp/clang_completer.py @@ -31,6 +31,7 @@ MAX_DIAGNOSTICS_TO_DISPLAY = int( vimsupport.GetVariableValue( class ClangCompleter( Completer ): def __init__( self ): + super( ClangCompleter, self ).__init__() self.completer = ycm_core.ClangCompleter() self.completer.EnableThreading() self.contents_holder = [] @@ -101,6 +102,7 @@ class ClangCompleter( Completer ): files = self.GetUnsavedFilesVector() line, _ = vim.current.window.cursor + # TODO: this should be a function parameter column = int( vim.eval( "s:completion_start_column" ) ) + 1 self.completions_future = ( self.completer.CandidatesForQueryAndLocationInFileAsync( diff --git a/python/vimsupport.py b/python/vimsupport.py index 725f0c10..4ee93a28 100644 --- a/python/vimsupport.py +++ b/python/vimsupport.py @@ -55,12 +55,12 @@ def NumLinesInBuffer( buffer ): def PostVimMessage( message ): - vim.command( 'echohl WarningMsg | echomsg "{0}" | echohl None' - .format( message.replace( '"', '\\"' ) ) ) + vim.command( "echohl WarningMsg | echomsg '{0}' | echohl None" + .format( EscapeForVim( message ) ) ) def EchoText( text ): - vim.command( "echom '{0}'".format( text.replace( "'", r"''") ) ) + vim.command( "echom '{0}'".format( EscapeForVim( text ) ) ) def EscapeForVim( text ): diff --git a/python/ycm.py b/python/ycm.py index 810bf13a..337793d5 100644 --- a/python/ycm.py +++ b/python/ycm.py @@ -34,6 +34,8 @@ except ImportError, e: os.path.dirname( os.path.abspath( __file__ ) ), str( e ) ) ) from completers.all.identifier_completer import IdentifierCompleter +from completers.all.omni_completer import OmniCompleter + FILETYPE_SPECIFIC_COMPLETION_TO_DISABLE = vim.eval( 'g:ycm_filetype_specific_completion_to_disable' ) @@ -42,6 +44,7 @@ FILETYPE_SPECIFIC_COMPLETION_TO_DISABLE = vim.eval( class YouCompleteMe( object ): def __init__( self ): self.identcomp = IdentifierCompleter() + self.omnicomp = OmniCompleter() self.filetype_completers = {} @@ -56,7 +59,6 @@ class YouCompleteMe( object ): completer = self.GetFiletypeCompleterForFiletype( filetype ) if completer: return completer - return None @@ -79,6 +81,8 @@ class YouCompleteMe( object ): completer = module.GetCompleter() if completer: supported_filetypes.extend( completer.SupportedFiletypes() ) + else: + completer = self.omnicomp for supported_filetype in supported_filetypes: self.filetype_completers[ supported_filetype ] = completer @@ -90,58 +94,64 @@ class YouCompleteMe( object ): def ShouldUseFiletypeCompleter( self, start_column ): - if self.FiletypeCompletionEnabled(): + if self.FiletypeCompletionUsable(): return self.GetFiletypeCompleter().ShouldUseNow( start_column ) return False + def NativeFiletypeCompletionAvailable( self ): + completer = self.GetFiletypeCompleter() + return bool( completer ) and completer is not self.omnicomp + + def FiletypeCompletionAvailable( self ): return bool( self.GetFiletypeCompleter() ) - def FiletypeCompletionEnabled( self ): - filetypes = vimsupport.CurrentFiletypes() - filetype_disabled = all([ x in FILETYPE_SPECIFIC_COMPLETION_TO_DISABLE - for x in filetypes ]) + def NativeFiletypeCompletionUsable( self ): + return ( _CurrentFiletypeCompletionEnabled() and + self.NativeFiletypeCompletionAvailable() ) - return ( not filetype_disabled and + + def FiletypeCompletionUsable( self ): + return ( _CurrentFiletypeCompletionEnabled() and self.FiletypeCompletionAvailable() ) def OnFileReadyToParse( self ): self.identcomp.OnFileReadyToParse() - if self.FiletypeCompletionEnabled(): + if self.FiletypeCompletionUsable(): self.GetFiletypeCompleter().OnFileReadyToParse() def OnInsertLeave( self ): self.identcomp.OnInsertLeave() - if self.FiletypeCompletionEnabled(): + if self.FiletypeCompletionUsable(): self.GetFiletypeCompleter().OnInsertLeave() def DiagnosticsForCurrentFileReady( self ): - if self.FiletypeCompletionEnabled(): + if self.FiletypeCompletionUsable(): return self.GetFiletypeCompleter().DiagnosticsForCurrentFileReady() return False def GetDiagnosticsForCurrentFile( self ): - if self.FiletypeCompletionEnabled(): + if self.FiletypeCompletionUsable(): return self.GetFiletypeCompleter().GetDiagnosticsForCurrentFile() return [] def ShowDetailedDiagnostic( self ): - if self.FiletypeCompletionEnabled(): + if self.FiletypeCompletionUsable(): return self.GetFiletypeCompleter().ShowDetailedDiagnostic() def GettingCompletions( self ): - if self.FiletypeCompletionEnabled(): + if self.FiletypeCompletionUsable(): return self.GetFiletypeCompleter().GettingCompletions() return False @@ -149,7 +159,7 @@ class YouCompleteMe( object ): def OnCurrentIdentifierFinished( self ): self.identcomp.OnCurrentIdentifierFinished() - if self.FiletypeCompletionEnabled(): + if self.FiletypeCompletionUsable(): self.GetFiletypeCompleter().OnCurrentIdentifierFinished() @@ -174,6 +184,11 @@ class YouCompleteMe( object ): return '\n'.join( output ) +def _CurrentFiletypeCompletionEnabled(): + filetypes = vimsupport.CurrentFiletypes() + return not all([ x in FILETYPE_SPECIFIC_COMPLETION_TO_DISABLE + for x in filetypes ]) + def _PathToCompletersFolder(): dir_of_current_script = os.path.dirname( os.path.abspath( __file__ ) )