Refactoring ycm_extra_conf handling into module

This commit is contained in:
Strahinja Val Markovic 2013-04-12 12:06:52 -07:00
parent c3b7e55762
commit aea154e829
3 changed files with 166 additions and 149 deletions

View File

@ -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 ) )

View File

@ -17,24 +17,14 @@
# You should have received a copy of the GNU General Public License
# along with YouCompleteMe. If not, see <http://www.gnu.org/licenses/>.
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 ) ) )

158
python/extra_conf_store.py Normal file
View File

@ -0,0 +1,158 @@
#!/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/>.
# 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 ) )