diff --git a/autoload/youcompleteme.vim b/autoload/youcompleteme.vim index d881a0a7..843ba37b 100644 --- a/autoload/youcompleteme.vim +++ b/autoload/youcompleteme.vim @@ -22,7 +22,7 @@ set cpo&vim " This needs to be called outside of a function let s:script_folder_path = escape( expand( ':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 " trigger omni completion, deselects the first completion " candidate that vim selects by default inoremap @@ -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 diff --git a/plugin/youcompleteme.vim b/plugin/youcompleteme.vim index 90a8de41..b73dd316 100644 --- a/plugin/youcompleteme.vim +++ b/plugin/youcompleteme.vim @@ -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 diff --git a/python/completer.py b/python/completer.py new file mode 100644 index 00000000..2db18fff --- /dev/null +++ b/python/completer.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python +# +# Copyright (C) 2011, 2012 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 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 diff --git a/python/completers/__init__.py b/python/completers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/python/completers/all.py b/python/completers/all.py new file mode 100644 index 00000000..546114b7 --- /dev/null +++ b/python/completers/all.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python +# +# Copyright (C) 2011, 2012 Strahinja Val Markovic +# +# This file is part of YouCompleteMe. +# +# YouCompleteMe is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# YouCompleteMe is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with YouCompleteMe. If not, see . + +from 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("")' ) + 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 ] + diff --git a/python/completers/cpp.py b/python/completers/cpp.py new file mode 100644 index 00000000..96d800b7 --- /dev/null +++ b/python/completers/cpp.py @@ -0,0 +1,281 @@ +#!/usr/bin/env python +# +# Copyright (C) 2011, 2012 Strahinja Val Markovic +# +# This file is part of YouCompleteMe. +# +# YouCompleteMe is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# YouCompleteMe is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with YouCompleteMe. If not, see . + +from 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 diff --git a/python/utils.py b/python/utils.py new file mode 100644 index 00000000..68744e50 --- /dev/null +++ b/python/utils.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python +# +# Copyright (C) 2011, 2012 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 . + +def IsIdentifierChar( char ): + return char.isalnum() or char == '_' + + +def SanitizeQuery( query ): + return query.strip() diff --git a/python/vimsupport.py b/python/vimsupport.py new file mode 100644 index 00000000..e576636e --- /dev/null +++ b/python/vimsupport.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python +# +# Copyright (C) 2011, 2012 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 + +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" ) diff --git a/python/ycm.py b/python/ycm.py index 03d011f5..1c61e789 100644 --- a/python/ycm.py +++ b/python/ycm.py @@ -17,408 +17,105 @@ # You should have received a copy of the GNU General Public License # along with YouCompleteMe. If not, see . -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("")' ) - 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()