diff --git a/python/completers/cpp/clang_completer.py b/python/completers/cpp/clang_completer.py index dad9792d..11f3db51 100644 --- a/python/completers/cpp/clang_completer.py +++ b/python/completers/cpp/clang_completer.py @@ -22,6 +22,7 @@ from collections import defaultdict import vim import vimsupport import ycm_core +import extra_conf_store from flags import Flags CLANG_FILETYPES = set( [ 'c', 'cpp', 'objc', 'objcpp' ] ) @@ -287,7 +288,7 @@ class ClangCompleter( Completer ): def DebugInfo( self ): filename = vim.current.buffer.name flags = self.flags.FlagsForFile( filename ) or [] - source = self.flags.ModuleForFile( filename ) + source = extra_conf_store.ModuleFileForSourceFile( filename ) return 'Flags for {0} loaded from {1}:\n{2}'.format( filename, source, list( flags ) ) diff --git a/python/completers/cpp/flags.py b/python/completers/cpp/flags.py index 4d5e8d46..15aae8d5 100644 --- a/python/completers/cpp/flags.py +++ b/python/completers/cpp/flags.py @@ -17,24 +17,14 @@ # You should have received a copy of the GNU General Public License # along with YouCompleteMe. If not, see . -import imp -import os import ycm_core -import random -import string -import sys +import os import vimsupport -from fnmatch import fnmatch +import extra_conf_store -YCM_EXTRA_CONF_FILENAME = '.ycm_extra_conf.py' NO_EXTRA_CONF_FILENAME_MESSAGE = ('No {0} file detected, so no compile flags ' 'are available. Thus no semantic support for C/C++/ObjC/ObjC++. See the ' - 'docs for details.').format( YCM_EXTRA_CONF_FILENAME ) -CONFIRM_CONF_FILE_MESSAGE = ('Found {0}. Load? \n\n(Question can be turned ' - 'off with options, see YCM docs)') -GLOBAL_YCM_EXTRA_CONF_FILE = os.path.expanduser( - vimsupport.GetVariableValue( "g:ycm_global_ycm_extra_conf" ) -) + 'docs for details.').format( extra_conf_store.YCM_EXTRA_CONF_FILENAME ) class Flags( object ): """Keeps track of the flags necessary to compile a file. @@ -44,36 +34,22 @@ class Flags( object ): def __init__( self ): # It's caches all the way down... self.flags_for_file = {} - self.module_for_file = {} - self.modules = FlagsModules() self.special_clang_flags = _SpecialClangIncludes() self.no_extra_conf_file_warning_posted = False - def ModuleForFile( self, filename ): - """This will try all files returned by _FlagsModuleSourceFilesForFile in - order and return the filename of the first module that was allowed to load. - If no module was found or allowed to load, None is returned.""" - - if not self.module_for_file.has_key( filename ): - for flags_module_file in _FlagsModuleSourceFilesForFile( filename ): - if self.modules.Load( flags_module_file ): - self.module_for_file[ filename ] = flags_module_file - break - - return self.module_for_file.setdefault( filename ) def FlagsForFile( self, filename ): try: return self.flags_for_file[ filename ] except KeyError: - module_file = self.ModuleForFile( filename ) - if not module_file: + module = extra_conf_store.ModuleForSourceFile( filename ) + if not module: if not self.no_extra_conf_file_warning_posted: vimsupport.PostVimMessage( NO_EXTRA_CONF_FILENAME_MESSAGE ) self.no_extra_conf_file_warning_posted = True return None - results = self.modules[ module_file ].FlagsForFile( filename ) + results = module.FlagsForFile( filename ) if not results.get( 'flags_ready', True ): return None @@ -85,113 +61,6 @@ class Flags( object ): self.flags_for_file[ filename ] = sanitized_flags return sanitized_flags - def ReloadModule( self, module_file ): - """Reloads a module file cleaning the flags cache for all files - associated with that module. Returns False if reloading failed - (for example due to the model not being loaded in the first place).""" - - module_file = os.path.abspath(module_file) - if self.modules.Reload( module_file ): - for filename, module in self.module_for_file.iteritems(): - if module == module_file: - del self.flags_for_file[ filename ] - return True - return False - - -class FlagsModules( object ): - """Keeps track of modules. - Modules are loaded on-demand and cached in self.modules for quick access.""" - - def __init__( self ): - self.modules = {} - - def Disable( self, module_file ): - """Disables the loading of a module for the current session.""" - self.modules[ module_file ] = None - - @staticmethod - def ShouldLoad( module_file ): - """Checks if a module is safe to be loaded. By default this will try to - decide using a white-/blacklist and ask the user for confirmation as a - fallback.""" - - if ( module_file == GLOBAL_YCM_EXTRA_CONF_FILE or - not vimsupport.GetBoolValue( 'g:ycm_confirm_extra_conf' ) ): - return True - - globlist = vimsupport.GetVariableValue( 'g:ycm_extra_conf_globlist' ) - for glob in globlist: - is_blacklisted = glob[0] == '!' - if MatchesGlobPattern( module_file, glob.lstrip('!') ): - return not is_blacklisted - - return vimsupport.Confirm( CONFIRM_CONF_FILE_MESSAGE.format( module_file ) ) - - def Load( self, module_file, force = False ): - """Load and return the module contained in a file. - Using force = True the module will be loaded regardless - of the criteria in ShouldLoad. - This will return None if the module was not allowed to be loaded.""" - - if not force: - if self.modules.has_key( module_file ): - return self.modules[ module_file ] - - if not self.ShouldLoad( module_file ): - return self.Disable( module_file ) - - sys.path.insert( 0, _DirectoryOfThisScript() ) - module = imp.load_source( _RandomName(), module_file ) - del sys.path[ 0 ] - - self.modules[ module_file ] = module - return module - - def Reload( self, module_file ): - """Reloads the given module. If it has not been loaded yet does nothing. - Note that the module will not be subject to the loading criteria again.""" - - if self.modules.get( module_file ): - return self.Load( module_file, force = True ) - - def __getitem__( self, key ): - return self.Load( key ) - - -def _FlagsModuleSourceFilesForFile( filename ): - """For a given filename, search all parent folders for YCM_EXTRA_CONF_FILENAME - files that will compute the flags necessary to compile the file. - If GLOBAL_YCM_EXTRA_CONF_FILE exists it is returned as a fallback.""" - - for folder in _PathsToAllParentFolders( filename ): - candidate = os.path.join( folder, YCM_EXTRA_CONF_FILENAME ) - if os.path.exists( candidate ): - yield candidate - if ( GLOBAL_YCM_EXTRA_CONF_FILE - and os.path.exists( GLOBAL_YCM_EXTRA_CONF_FILE ) ): - yield GLOBAL_YCM_EXTRA_CONF_FILE - - -def _PathsToAllParentFolders( filename ): - """Build a list of all parent folders of a file. - The neares files will be returned first. - Example: _PathsToAllParentFolders( '/home/user/projects/test.c' ) - [ '/home/user/projects', '/home/user', '/home', '/' ]""" - - parent_folders = os.path.abspath( - os.path.dirname( filename ) ).split( os.path.sep ) - if not parent_folders[0]: - parent_folders[0] = os.path.sep - parent_folders = [ os.path.join( *parent_folders[:i + 1] ) - for i in xrange( len( parent_folders ) ) ] - return reversed( parent_folders ) - - -def _RandomName(): - """Generates a random module name.""" - return ''.join( random.choice( string.ascii_lowercase ) for x in range( 15 ) ) - def _SanitizeFlags( flags ): """Drops unsafe flags. Currently these are only -arch flags; they tend to @@ -223,14 +92,3 @@ def _SpecialClangIncludes(): return [ '-I', path_to_includes ] -def _DirectoryOfThisScript(): - return os.path.dirname( os.path.abspath( __file__ ) ) - - -def MatchesGlobPattern( filename, glob ): - """Returns true if a filename matches a given pattern. A '~' in glob will be - expanded to the home directory and checking will be performed using absolute - paths. See the documentation of fnmatch for the supported patterns.""" - - abspath = os.path.abspath( filename ) - return fnmatch( abspath, os.path.abspath( os.path.expanduser( glob ) ) ) diff --git a/python/extra_conf_store.py b/python/extra_conf_store.py new file mode 100644 index 00000000..3eb7aac5 --- /dev/null +++ b/python/extra_conf_store.py @@ -0,0 +1,158 @@ +#!/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 . + +# NOTE: This module is used as a Singleton + +import os +import imp +import random +import string +import sys +import vimsupport +from fnmatch import fnmatch + +# Constants +YCM_EXTRA_CONF_FILENAME = '.ycm_extra_conf.py' +CONFIRM_CONF_FILE_MESSAGE = ('Found {0}. Load? \n\n(Question can be turned ' + 'off with options, see YCM docs)') +GLOBAL_YCM_EXTRA_CONF_FILE = os.path.expanduser( + vimsupport.GetVariableValue( "g:ycm_global_ycm_extra_conf" ) +) + +# Singleton variables +_module_for_module_file = {} +_module_file_for_source_file = {} + + +def ModuleForSourceFile( filename ): + return _Load( ModuleFileForSourceFile( filename ) ) + + +def ModuleFileForSourceFile( filename ): + """This will try all files returned by _ExtraConfModuleSourceFilesForFile in + order and return the filename of the first module that was allowed to load. + If no module was found or allowed to load, None is returned.""" + + if not _module_file_for_source_file.has_key( filename ): + for module_file in _ExtraConfModuleSourceFilesForFile( filename ): + if _Load( module_file ): + _module_file_for_source_file[ filename ] = module_file + break + + return _module_file_for_source_file.setdefault( filename ) + + +def _Disable( module_file ): + """Disables the loading of a module for the current session.""" + _module_for_module_file[ module_file ] = None + + +def _ShouldLoad( module_file ): + """Checks if a module is safe to be loaded. By default this will try to + decide using a white-/blacklist and ask the user for confirmation as a + fallback.""" + + if ( module_file == GLOBAL_YCM_EXTRA_CONF_FILE or + not vimsupport.GetBoolValue( 'g:ycm_confirm_extra_conf' ) ): + return True + + globlist = vimsupport.GetVariableValue( 'g:ycm_extra_conf_globlist' ) + for glob in globlist: + is_blacklisted = glob[0] == '!' + if _MatchesGlobPattern( module_file, glob.lstrip('!') ): + return not is_blacklisted + + return vimsupport.Confirm( CONFIRM_CONF_FILE_MESSAGE.format( module_file ) ) + + +def _Load( module_file, force = False ): + """Load and return the module contained in a file. + Using force = True the module will be loaded regardless + of the criteria in _ShouldLoad. + This will return None if the module was not allowed to be loaded.""" + + if not module_file: + return None + + if not force: + if _module_for_module_file.has_key( module_file ): + return _module_for_module_file[ module_file ] + + if not _ShouldLoad( module_file ): + return _Disable( module_file ) + + sys.path.insert( 0, _PathToCppCompleterFolder() ) + module = imp.load_source( _RandomName(), module_file ) + del sys.path[ 0 ] + + _module_for_module_file[ module_file ] = module + return module + + +def _MatchesGlobPattern( filename, glob ): + """Returns true if a filename matches a given pattern. A '~' in glob will be + expanded to the home directory and checking will be performed using absolute + paths. See the documentation of fnmatch for the supported patterns.""" + + abspath = os.path.abspath( filename ) + return fnmatch( abspath, os.path.abspath( os.path.expanduser( glob ) ) ) + + +def _ExtraConfModuleSourceFilesForFile( filename ): + """For a given filename, search all parent folders for YCM_EXTRA_CONF_FILENAME + files that will compute the flags necessary to compile the file. + If GLOBAL_YCM_EXTRA_CONF_FILE exists it is returned as a fallback.""" + + for folder in _PathsToAllParentFolders( filename ): + candidate = os.path.join( folder, YCM_EXTRA_CONF_FILENAME ) + if os.path.exists( candidate ): + yield candidate + if ( GLOBAL_YCM_EXTRA_CONF_FILE + and os.path.exists( GLOBAL_YCM_EXTRA_CONF_FILE ) ): + yield GLOBAL_YCM_EXTRA_CONF_FILE + + +def _PathsToAllParentFolders( filename ): + """Build a list of all parent folders of a file. + The neares files will be returned first. + Example: _PathsToAllParentFolders( '/home/user/projects/test.c' ) + [ '/home/user/projects', '/home/user', '/home', '/' ]""" + + parent_folders = os.path.abspath( + os.path.dirname( filename ) ).split( os.path.sep ) + if not parent_folders[0]: + parent_folders[0] = os.path.sep + parent_folders = [ os.path.join( *parent_folders[:i + 1] ) + for i in xrange( len( parent_folders ) ) ] + return reversed( parent_folders ) + + +def _PathToCppCompleterFolder(): + """Returns the path to the 'cpp' completer folder. This is necessary + because ycm_extra_conf files need it on the path.""" + return os.path.join( _DirectoryOfThisScript(), 'completers', 'cpp' ) + + +def _DirectoryOfThisScript(): + return os.path.dirname( os.path.abspath( __file__ ) ) + + +def _RandomName(): + """Generates a random module name.""" + return ''.join( random.choice( string.ascii_lowercase ) for x in range( 15 ) )