From ddecdca736c53b8ae6dbcb379a6257f04cf5efda Mon Sep 17 00:00:00 2001 From: micbou Date: Mon, 3 Sep 2018 00:34:31 +0200 Subject: [PATCH] Improve filename completer Add the following improvements to the filename completer: - allow completion of paths containing spaces; - allow completion of multiple paths on the same line; - allow completion of relative paths not starting with ".", "..", or "./"; - allow completion of Windows environment variables (e.g. %USERPROFILE%). --- ycmd/completers/cpp/flags.py | 30 +- ycmd/completers/cpp/include_cache.py | 24 +- ycmd/completers/general/filename_completer.py | 295 +++++---- .../general/general_completer_store.py | 36 +- ycmd/tests/clang/flags_test.py | 23 +- ycmd/tests/filename_completer_test.py | 591 +++++++++--------- ycmd/tests/get_completions_test.py | 21 + .../Qt/QtGui | 0 .../QtGui/QDialog | 0 .../QtGui/QWidget | 0 ycmd/utils.py | 21 +- 11 files changed, 540 insertions(+), 501 deletions(-) rename ycmd/tests/testdata/filename_completer/inner_dir/{include => dir with spaces (x64)}/Qt/QtGui (100%) rename ycmd/tests/testdata/filename_completer/inner_dir/{include => dir with spaces (x64)}/QtGui/QDialog (100%) rename ycmd/tests/testdata/filename_completer/inner_dir/{include => dir with spaces (x64)}/QtGui/QWidget (100%) diff --git a/ycmd/completers/cpp/flags.py b/ycmd/completers/cpp/flags.py index 7f8838c35d..09179fa357 100644 --- a/ycmd/completers/cpp/flags.py +++ b/ycmd/completers/cpp/flags.py @@ -27,8 +27,14 @@ import inspect from future.utils import PY2, native from ycmd import extra_conf_store -from ycmd.utils import ( re, ToCppStringCompatible, OnMac, OnWindows, ToUnicode, - ToBytes, PathsToAllParentFolders ) +from ycmd.utils import ( ListDirectory, + OnMac, + OnWindows, + PathsToAllParentFolders, + re, + ToCppStringCompatible, + ToBytes, + ToUnicode ) from ycmd.responses import NoExtraConfDetected # -include-pch and --sysroot= must be listed before -include and --sysroot @@ -515,26 +521,12 @@ def _SelectMacToolchain(): ] for toolchain in MAC_CLANG_TOOLCHAIN_DIRS: - if _MacClangIncludeDirExists( toolchain ): + if os.path.exists( toolchain ): return toolchain return None -# Ultimately, this method exists only for testability -def _GetMacClangVersionList( candidates_dir ): - try: - return os.listdir( candidates_dir ) - except OSError: - # Path might not exist, so just ignore - return [] - - -# Ultimately, this method exists only for testability -def _MacClangIncludeDirExists( candidate_include ): - return os.path.exists( candidate_include ) - - # Return the list of flags including any Clang headers found in the supplied # toolchain. These are required for the same reasons as described below, but # unfortunately, these are in versioned directories and there is no easy way to @@ -547,11 +539,11 @@ def _LatestMacClangIncludes( toolchain ): # extract this information from xcode-select, though xcode-select -p does not # point at the toolchain directly. candidates_dir = os.path.join( toolchain, 'usr', 'lib', 'clang' ) - versions = _GetMacClangVersionList( candidates_dir ) + versions = ListDirectory( candidates_dir ) for version in reversed( sorted( versions ) ): candidate_include = os.path.join( candidates_dir, version, 'include' ) - if _MacClangIncludeDirExists( candidate_include ): + if os.path.exists( candidate_include ): return [ '-isystem', candidate_include ] return [] diff --git a/ycmd/completers/cpp/include_cache.py b/ycmd/completers/cpp/include_cache.py index 64425e5b1a..65c031adb4 100644 --- a/ycmd/completers/cpp/include_cache.py +++ b/ycmd/completers/cpp/include_cache.py @@ -33,9 +33,8 @@ from ycmd import responses from ycmd.completers.general.filename_completer import ( GetPathType, GetPathTypeName ) +from ycmd.utils import GetModificationTime, ListDirectory -import logging -_logger = logging.getLogger( __name__ ) """ Represents single include completion candidate. name is the name/string of the completion candidate, @@ -95,7 +94,7 @@ def GetIncludes( self, path, is_framework = False ): def _AddToCache( self, path, includes, mtime = None ): if not mtime: - mtime = _GetModificationTime( path ) + mtime = GetModificationTime( path ) # mtime of 0 is "a magic value" to represent inaccessible directory mtime. if mtime: with self._cache_lock: @@ -107,7 +106,7 @@ def _GetCached( self, path, is_framework ): with self._cache_lock: cache_entry = self._cache.get( path ) if cache_entry: - mtime = _GetModificationTime( path ) + mtime = GetModificationTime( path ) if mtime > cache_entry[ 'mtime' ]: includes = self._ListIncludes( path, is_framework ) self._AddToCache( path, includes, mtime ) @@ -118,14 +117,8 @@ def _GetCached( self, path, is_framework ): def _ListIncludes( self, path, is_framework ): - try: - names = os.listdir( path ) - except OSError: - _logger.exception( 'Can not list entries for include path %s.', path ) - return [] - includes = [] - for name in names: + for name in ListDirectory( path ): if is_framework: if not name.endswith( '.framework' ): continue @@ -136,12 +129,3 @@ def _ListIncludes( self, path, is_framework ): includes.append( IncludeEntry( name, entry_type ) ) return includes - - -def _GetModificationTime( path ): - try: - return os.path.getmtime( path ) - except OSError: - _logger.exception( 'Can not get modification time for include path %s.', - path ) - return 0 diff --git a/ycmd/completers/general/filename_completer.py b/ycmd/completers/general/filename_completer.py index 51d64e5c93..0b4fb655dd 100644 --- a/ycmd/completers/general/filename_completer.py +++ b/ycmd/completers/general/filename_completer.py @@ -22,12 +22,16 @@ # Not installing aliases from python-future; it's unreliable and slow. from builtins import * # noqa -import logging import os from ycmd.completers.completer import Completer -from ycmd.utils import ( ExpandVariablesInPath, GetCurrentDirectory, OnWindows, - re, ToUnicode ) +from ycmd.utils import ( ExpandVariablesInPath, + GetCurrentDirectory, + GetModificationTime, + ListDirectory, + OnWindows, + re, + ToUnicode ) from ycmd import responses FILE = 1 @@ -45,7 +49,22 @@ 7: '[File&Dir&Framework]' } -_logger = logging.getLogger( __name__ ) +PATH_SEPARATORS_PATTERN = '([{seps}][^{seps}]*|[{seps}]$)' + +HEAD_PATH_PATTERN_UNIX = """ + # Current and previous directories + \.{1,2}| + # Home directory + ~| + # UNIX environment variables + \$[^$]+ +""" +HEAD_PATH_PATTERN_WINDOWS = HEAD_PATH_PATTERN_UNIX + """| + # Drive letters + [A-Za-z]:| + # Windows environment variables + %[^%]+% +""" class FilenameCompleter( Completer ): @@ -56,34 +75,16 @@ class FilenameCompleter( Completer ): def __init__( self, user_options ): super( FilenameCompleter, self ).__init__( user_options ) - # On Windows, backslashes are also valid path separators. - self._triggers = [ '/', '\\' ] if OnWindows() else [ '/' ] - - self._path_regex = re.compile( """ - # Head part - (?: - # 'D:/'-like token - [A-z]+:[%(sep)s]| - - # '/', './', '../', or '~' - \.{0,2}[%(sep)s]|~| - - # '$var/' - \$[A-Za-z0-9{}_]+[%(sep)s] - )+ - - # Tail part - (?: - # any alphanumeric, symbol or space literal - [ %(sep)sa-zA-Z0-9(){}$+_~.\x80-\xff-\[\]]| - - # skip any special symbols - [^\x20-\x7E]| - - # backslash and 1 char after it - \\. - )*$ - """ % { 'sep': '/\\\\' if OnWindows() else '/' }, re.X ) + if OnWindows(): + self._path_separators = r'/\\' + self._head_path_pattern = HEAD_PATH_PATTERN_WINDOWS + else: + self._path_separators = '/' + self._head_path_pattern = HEAD_PATH_PATTERN_UNIX + self._path_separators_regex = re.compile( + PATH_SEPARATORS_PATTERN.format( seps = self._path_separators ) ) + self._head_path_for_directory = {} + self._candidates_for_directory = {} def CurrentFiletypeCompletionDisabled( self, request_data ): @@ -93,94 +94,172 @@ def CurrentFiletypeCompletionDisabled( self, request_data ): any( x in disabled_filetypes for x in filetypes ) ) - def ShouldUseNowInner( self, request_data ): + def GetWorkingDirectory( self, request_data ): + if self.user_options[ 'filepath_completion_use_working_dir' ]: + # Return paths relative to the working directory of the client, if + # supplied, otherwise relative to the current working directory of this + # process. + return request_data.get( 'working_dir' ) or GetCurrentDirectory() + # Return paths relative to the file. + return os.path.dirname( request_data[ 'filepath' ] ) + + + def GetCompiledHeadRegexForDirectory( self, directory ): + mtime = GetModificationTime( directory ) + + try: + head_regex = self._head_path_for_directory[ directory ] + if mtime and mtime <= head_regex[ 'mtime' ]: + return head_regex[ 'regex' ] + except KeyError: + pass + + current_paths = ListDirectory( directory ) + current_paths_pattern = '|'.join( + [ re.escape( path ) for path in current_paths ] ) + head_pattern = ( '(' + self._head_path_pattern + '|' + + current_paths_pattern + ')$' ) + head_regex = re.compile( head_pattern, re.VERBOSE ) + if mtime: + self._head_path_for_directory[ directory ] = { + 'regex': head_regex, + 'mtime': mtime + } + return head_regex + + + def SearchPath( self, request_data ): + """Return the tuple (|path|, |start_column|) where |path| is a path that + could be completed on the current line before the cursor and |start_column| + is the column where the completion should start. (None, None) is returned if + no suitable path is found.""" + + # Find all path separators on the current line before the cursor. Return + # early if no separators are found. + current_line = request_data[ 'prefix' ] + matches = list( self._path_separators_regex.finditer( current_line ) ) + if not matches: + return None, None + + working_dir = self.GetWorkingDirectory( request_data ) + + head_regex = self.GetCompiledHeadRegexForDirectory( working_dir ) + + last_match = matches[ -1 ] + last_match_start = last_match.start( 1 ) + + # Go through all path separators from left to right. + for match in matches: + # Check if ".", "..", "~", an environment variable, one of the current + # directories, or a drive letter on Windows match just before the + # separator. If so, extract the path from the start of the match to the + # latest path separator. Expand "~" and the environment variables in the + # path. If the path is relative, convert it to an absolute path relative + # to the working directory. If the resulting path exists, return it and + # the column just after the latest path separator as the starting column. + head_match = head_regex.search( current_line[ : match.start() ] ) + if head_match: + path = current_line[ head_match.start( 1 ) : last_match_start ] + path = ExpandVariablesInPath( path + os.path.sep ) + if not os.path.isabs( path ): + path = os.path.join( working_dir, path ) + if os.path.exists( path ): + # +2 because last_match_start is the 0-indexed position just before + # the latest path separator whose length is 1 on all platforms we + # support. + return path, last_match_start + 2 + + # Otherwise, the path may start with "/" (or "\" on Windows). Extract the + # path from the current path separator to the latest one. If the path is + # not empty and does not only consist of path separators, expand "~" and + # the environment variables in the path. If the resulting path exists, + # return it and the column just after the latest path separator as the + # starting column. + path = current_line[ match.start() : last_match_start ] + if path.strip( self._path_separators ): + path = ExpandVariablesInPath( path + os.path.sep ) + if os.path.exists( path ): + return path, last_match_start + 2 + + # No suitable paths have been found after going through all separators. The + # path could be exactly "/" (or "\" on Windows). Only return the path if + # there are no other path separators on the line. This prevents always + # completing the root directory if nothing is matched. + # TODO: completion on a single "/" or "\" is not really desirable in + # languages where such characters are part of special constructs like + # comments in C/C++ or closing tags in HTML. This behavior could be improved + # by using rules that depend on the filetype. + if len( matches ) == 1: + return os.path.sep, last_match_start + 2 + + return None, None + + + def ShouldUseNow( self, request_data ): if self.CurrentFiletypeCompletionDisabled( request_data ): return False - current_line = request_data[ 'line_value' ] - start_codepoint = request_data[ 'start_codepoint' ] - - # inspect the previous 'character' from the start column to find the trigger - # note: 1-based still. we subtract 1 when indexing into current_line - trigger_codepoint = start_codepoint - 1 - - return ( trigger_codepoint > 0 and - current_line[ trigger_codepoint - 1 ] in self._triggers ) + return bool( self.SearchPath( request_data )[ 0 ] ) def SupportedFiletypes( self ): return [] - def ComputeCandidatesInner( self, request_data ): - current_line = request_data[ 'line_value' ] - start_codepoint = request_data[ 'start_codepoint' ] - 1 - filepath = request_data[ 'filepath' ] - line = current_line[ : start_codepoint ] + def GetCandidatesForDirectory( self, directory ): + mtime = GetModificationTime( directory ) - path_match = self._path_regex.search( line ) - path_dir = ExpandVariablesInPath( path_match.group() ) if path_match else '' + try: + candidates = self._candidates_for_directory[ directory ] + if mtime and mtime <= candidates[ 'mtime' ]: + return candidates[ 'candidates' ] + except KeyError: + pass - # If the client supplied its working directory, use that instead of the - # working directory of ycmd - working_dir = request_data.get( 'working_dir' ) + candidates = _GeneratePathCompletionCandidates( directory ) + if mtime: + self._candidates_for_directory[ directory ] = { + 'candidates': candidates, + 'mtime': mtime + } + return candidates - return GeneratePathCompletionData( - _GetPathCompletionCandidates( - path_dir, - self.user_options[ 'filepath_completion_use_working_dir' ], - filepath, - working_dir ) ) + def ComputeCandidates( self, request_data ): + if not self.ShouldUseNow( request_data ): + return [] -def _GetAbsolutePathForCompletions( path_dir, - use_working_dir, - filepath, - working_dir ): - """ - Returns the absolute path for which completion suggestions should be returned - (in the standard case). - """ + # Calling this function seems inefficient when it's already been called in + # ShouldUseNow for that request but its execution time is so low once the + # head regex is cached that it doesn't matter. + directory, start_codepoint = self.SearchPath( request_data ) - if os.path.isabs( path_dir ): - # This is already an absolute path, return it - return path_dir - elif use_working_dir: - # Return paths relative to the working directory of the client, if - # supplied, otherwise relative to the current working directory of this - # process - if working_dir: - return os.path.join( working_dir, path_dir ) - else: - return os.path.join( GetCurrentDirectory(), path_dir ) - else: - # Return paths relative to the file - return os.path.join( os.path.join( os.path.dirname( filepath ) ), - path_dir ) - - -def _GetPathCompletionCandidates( path_dir, use_working_dir, - filepath, working_dir ): - - absolute_path_dir = _GetAbsolutePathForCompletions( path_dir, - use_working_dir, - filepath, - working_dir ) - entries = [] - unicode_path = ToUnicode( absolute_path_dir ) - try: - # We need to pass a unicode string to get unicode strings out of - # listdir. - relative_paths = os.listdir( unicode_path ) - except Exception: - _logger.exception( 'Error while listing %s folder.', absolute_path_dir ) - relative_paths = [] - - for rel_path in relative_paths: + old_start_codepoint = request_data[ 'start_codepoint' ] + request_data[ 'start_codepoint' ] = start_codepoint + + candidates = self.GetCandidatesForDirectory( directory ) + candidates = self.FilterAndSortCandidates( candidates, + request_data[ 'query' ] ) + if not candidates: + # No candidates were matched. Reset the start column for the identifier + # completer. + request_data[ 'start_codepoint' ] = old_start_codepoint + + return candidates + + +def _GeneratePathCompletionCandidates( path_dir ): + completions = [] + + unicode_path = ToUnicode( path_dir ) + + for rel_path in ListDirectory( unicode_path ): absolute_path = os.path.join( unicode_path, rel_path ) - entries.append( ( rel_path, GetPathType( absolute_path ) ) ) + path_type = GetPathTypeName( GetPathType( absolute_path ) ) + completions.append( + responses.BuildCompletionData( rel_path, path_type ) ) - return entries + return completions def GetPathType( path, is_framework = False ): @@ -193,13 +272,3 @@ def GetPathType( path, is_framework = False ): def GetPathTypeName( path_type ): return EXTRA_INFO_MAP[ path_type ] - - -def GeneratePathCompletionData( entries ): - completion_dicts = [] - for entry in entries: - completion_dicts.append( - responses.BuildCompletionData( entry[ 0 ], - GetPathTypeName( entry[ 1 ] ) ) ) - - return completion_dicts diff --git a/ycmd/completers/general/general_completer_store.py b/ycmd/completers/general/general_completer_store.py index 7b6c4dd558..fe9e80f7d1 100644 --- a/ycmd/completers/general/general_completer_store.py +++ b/ycmd/completers/general/general_completer_store.py @@ -1,5 +1,4 @@ -# Copyright (C) 2013 Stanislav Golovanov -# Google Inc. +# Copyright (C) 2013-2018 ycmd contributors # # This file is part of ycmd. # @@ -33,7 +32,7 @@ class GeneralCompleterStore( Completer ): """ Holds a list of completers that can be used in all filetypes. - It overrides all Competer API methods so that specific calls to + It overrides all Completer API methods so that specific calls to GeneralCompleterStore are passed to all general completers. """ @@ -45,11 +44,9 @@ def __init__( self, user_options ): self._non_filename_completers = [ self._identifier_completer ] if user_options.get( 'use_ultisnips_completer', True ): self._non_filename_completers.append( self._ultisnips_completer ) - self._all_completers = [ self._identifier_completer, self._filename_completer, self._ultisnips_completer ] - self._current_query_completers = [] def SupportedFiletypes( self ): @@ -60,33 +57,12 @@ def GetIdentifierCompleter( self ): return self._identifier_completer - def ShouldUseNow( self, request_data ): - self._current_query_completers = [] - - if self._filename_completer.ShouldUseNow( request_data ): - self._current_query_completers = [ self._filename_completer ] - return True - - should_use_now = False - - for completer in self._non_filename_completers: - should_use_this_completer = completer.ShouldUseNow( request_data ) - should_use_now = should_use_now or should_use_this_completer - - if should_use_this_completer: - self._current_query_completers.append( completer ) - - return should_use_now - - def ComputeCandidates( self, request_data ): - if not self.ShouldUseNow( request_data ): - return [] - - candidates = [] - for completer in self._current_query_completers: + candidates = self._filename_completer.ComputeCandidates( request_data ) + if candidates: + return candidates + for completer in self._non_filename_completers: candidates += completer.ComputeCandidates( request_data ) - return candidates diff --git a/ycmd/tests/clang/flags_test.py b/ycmd/tests/clang/flags_test.py index ee7f4f8262..56943b515c 100644 --- a/ycmd/tests/clang/flags_test.py +++ b/ycmd/tests/clang/flags_test.py @@ -810,34 +810,28 @@ def ExtraClangFlags_test(): @MacOnly -@patch( 'ycmd.completers.cpp.flags._GetMacClangVersionList', +@patch( 'os.listdir', return_value = [ '1.0.0', '7.0.1', '7.0.2', '___garbage__' ] ) -@patch( 'ycmd.completers.cpp.flags._MacClangIncludeDirExists', - side_effect = [ False, True, True, True ] ) +@patch( 'os.path.exists', side_effect = [ False, True, True, True ] ) def Mac_LatestMacClangIncludes_test( *args ): eq_( flags._LatestMacClangIncludes( '/tmp' ), [ '-isystem', '/tmp/usr/lib/clang/7.0.2/include' ] ) @MacOnly -def Mac_LatestMacClangIncludes_NoSuchDirectory_test(): - def RaiseOSError( x ): - raise OSError( x ) - - with patch( 'os.listdir', side_effect = RaiseOSError ): - eq_( flags._LatestMacClangIncludes( '/tmp' ), [] ) +@patch( 'os.listdir', side_effect = OSError ) +def Mac_LatestMacClangIncludes_NoSuchDirectory_test( *args ): + eq_( flags._LatestMacClangIncludes( '/tmp' ), [] ) @MacOnly -@patch( 'ycmd.completers.cpp.flags._MacClangIncludeDirExists', - side_effect = [ False, False ] ) +@patch( 'os.path.exists', side_effect = [ False, False ] ) def Mac_SelectMacToolchain_None_test( *args ): eq_( flags._SelectMacToolchain(), None ) @MacOnly -@patch( 'ycmd.completers.cpp.flags._MacClangIncludeDirExists', - side_effect = [ True, False ] ) +@patch( 'os.path.exists', side_effect = [ True, False ] ) def Mac_SelectMacToolchain_XCode_test( *args ): eq_( flags._SelectMacToolchain(), '/Applications/Xcode.app/Contents/Developer/Toolchains/' @@ -845,8 +839,7 @@ def Mac_SelectMacToolchain_XCode_test( *args ): @MacOnly -@patch( 'ycmd.completers.cpp.flags._MacClangIncludeDirExists', - side_effect = [ False, True ] ) +@patch( 'os.path.exists', side_effect = [ False, True ] ) def Mac_SelectMacToolchain_CommandLineTools_test( *args ): eq_( flags._SelectMacToolchain(), '/Library/Developer/CommandLineTools' ) diff --git a/ycmd/tests/filename_completer_test.py b/ycmd/tests/filename_completer_test.py index b4dd7f4c3b..7f52cb9d92 100644 --- a/ycmd/tests/filename_completer_test.py +++ b/ycmd/tests/filename_completer_test.py @@ -1,6 +1,6 @@ # coding: utf-8 # -# Copyright (C) 2014 Davit Samvelyan +# Copyright (C) 2014-2018 ycmd contributors # # This file is part of ycmd. # @@ -26,12 +26,14 @@ import os from hamcrest import assert_that, contains_inanyorder, empty, is_not -from nose.tools import eq_, ok_ -from ycmd.completers.general.filename_completer import FilenameCompleter -from ycmd.request_wrap import RequestWrap -from ycmd import user_options_store +from mock import patch +from nose.tools import ok_ + from ycmd.tests import IsolatedYcmd -from ycmd.tests.test_utils import BuildRequest, CurrentWorkingDirectory +from ycmd.tests.test_utils import ( BuildRequest, + CurrentWorkingDirectory, + CompletionEntryMatcher, + WindowsOnly ) from ycmd.utils import GetCurrentDirectory, ToBytes TEST_DIR = os.path.dirname( os.path.abspath( __file__ ) ) @@ -39,300 +41,280 @@ 'testdata', 'filename_completer', 'inner_dir' ) -PATH_TO_TEST_FILE = os.path.join( DATA_DIR, "test.cpp" ) - - -def _CompletionResultsForLine( filename_completer, - contents, - extra_data = None, - column_num = None ): - - # Strictly, column numbers are *byte* offsets, not character offsets. If - # the contents of the file contain unicode characters, then we should manually - # supply the correct byte offset. - column_num = len( contents ) + 1 if not column_num else column_num - - request = BuildRequest( column_num = column_num, - filepath = PATH_TO_TEST_FILE, - contents = contents ) - if extra_data: - request.update( extra_data ) - - request_data = RequestWrap( request ) - candidates = filename_completer.ComputeCandidatesInner( request_data ) - return [ ( c[ 'insertion_text' ], c[ 'extra_menu_info' ] ) - for c in candidates ] - - -def _ShouldUseNowForLine( filename_completer, - contents, - extra_data = None, - column_num = None ): - - # Strictly, column numbers are *byte* offsets, not character offsets. If - # the contents of the file contain unicode characters, then we should manually - # supply the correct byte offset. - column_num = len( contents ) + 1 if not column_num else column_num - - request = BuildRequest( column_num = column_num, - filepath = PATH_TO_TEST_FILE, - contents = contents ) - if extra_data: - request.update( extra_data ) - - request_data = RequestWrap( request ) - return filename_completer.ShouldUseNow( request_data ) - - -class FilenameCompleter_test( object ): - def setUp( self ): - self._filename_completer = FilenameCompleter( - user_options_store.DefaultOptions() ) - - - def _CompletionResultsForLine( self, contents, column_num=None ): - return _CompletionResultsForLine( self._filename_completer, - contents, - column_num = column_num ) - - - def _ShouldUseNowForLine( self, contents, column_num=None ): - return _ShouldUseNowForLine( self._filename_completer, - contents, - column_num = column_num ) - - - def SystemPathCompletion_test( self ): - # Order of system path completion entries may differ - # on different systems - data = self._CompletionResultsForLine( 'const char* c = "./' ) - assert_that( data, contains_inanyorder( - ( 'foo漢字.txt', '[File]' ), - ( 'include', '[Dir]' ), - ( 'test.cpp', '[File]' ), - ( 'test.hpp', '[File]' ) - ) ) - - data = self._CompletionResultsForLine( 'const char* c = "./include/' ) - assert_that( data, contains_inanyorder( - ( 'Qt', '[Dir]' ), - ( 'QtGui', '[Dir]' ) - ) ) - - - def EnvVar_AtStart_File_test( self ): - os.environ[ 'YCM_TEST_DATA_DIR' ] = DATA_DIR - data = self._CompletionResultsForLine( - 'set x = $YCM_TEST_DATA_DIR/include/QtGui/' ) - - os.environ.pop( 'YCM_TEST_DATA_DIR' ) - assert_that( data, contains_inanyorder( - ( 'QDialog', '[File]' ), - ( 'QWidget', '[File]' ) - ) ) - - - def EnvVar_AtStart_File_Partial_test( self ): - # The reason all entries in the directory are returned is that the - # RequestWrapper tells the completer to effectively return results for - # $YCM_TEST_DIR/testdata/filename_completer/inner_dir/ and the client - # filters based on the additional characters. - os.environ[ 'YCM_TEST_DIR' ] = TEST_DIR - data = self._CompletionResultsForLine( - 'set x = $YCM_TEST_DIR/testdata/filename_completer/inner_dir/te' ) - os.environ.pop( 'YCM_TEST_DIR' ) - - assert_that( data, contains_inanyorder( - ( 'foo漢字.txt', '[File]' ), - ( 'include', '[Dir]' ), - ( 'test.cpp', '[File]' ), - ( 'test.hpp', '[File]' ) - ) ) - - - def EnvVar_AtStart_Dir_test( self ): - os.environ[ 'YCMTESTDIR' ] = TEST_DIR - - data = self._CompletionResultsForLine( - 'set x = $YCMTESTDIR/testdata/filename_completer/' ) - - os.environ.pop( 'YCMTESTDIR' ) - - assert_that( data, contains_inanyorder( - ( 'inner_dir', '[Dir]' ), - ( '∂†∫', '[Dir]' ) - ) ) - - - def EnvVar_AtStart_Dir_Partial_test( self ): - os.environ[ 'ycm_test_dir' ] = TEST_DIR - data = self._CompletionResultsForLine( - 'set x = $ycm_test_dir/testdata/filename_completer/inn' ) - - os.environ.pop( 'ycm_test_dir' ) - assert_that( data, contains_inanyorder( - ( 'inner_dir', '[Dir]' ), - ( '∂†∫', '[Dir]' ) - ) ) - - - def EnvVar_InMiddle_File_test( self ): - os.environ[ 'YCM_TEST_filename_completer' ] = 'inner_dir' - data = self._CompletionResultsForLine( - 'set x = ' - + TEST_DIR - + '/testdata/filename_completer/$YCM_TEST_filename_completer/' ) - os.environ.pop( 'YCM_TEST_filename_completer' ) - assert_that( data, contains_inanyorder( - ( 'foo漢字.txt', '[File]' ), - ( 'include', '[Dir]' ), - ( 'test.cpp', '[File]' ), - ( 'test.hpp', '[File]' ) - ) ) - - - def EnvVar_InMiddle_File_Partial_test( self ): - os.environ[ 'YCM_TEST_filename_c0mpleter' ] = 'inner_dir' - data = self._CompletionResultsForLine( - 'set x = ' - + TEST_DIR - + '/testdata/filename_completer/$YCM_TEST_filename_c0mpleter/te' ) - os.environ.pop( 'YCM_TEST_filename_c0mpleter' ) - assert_that( data, contains_inanyorder( - ( 'foo漢字.txt', '[File]' ), - ( 'include', '[Dir]' ), - ( 'test.cpp', '[File]' ), - ( 'test.hpp', '[File]' ) - ) ) - - - def EnvVar_InMiddle_Dir_test( self ): - os.environ[ 'YCM_TEST_td' ] = 'testd' - data = self._CompletionResultsForLine( - 'set x = ' + TEST_DIR + '/${YCM_TEST_td}ata/filename_completer/' ) - - os.environ.pop( 'YCM_TEST_td' ) - assert_that( data, contains_inanyorder( - ( 'inner_dir', '[Dir]' ), - ( '∂†∫', '[Dir]' ) - ) ) - - - def EnvVar_InMiddle_Dir_Partial_test( self ): - os.environ[ 'YCM_TEST_td' ] = 'tdata' - data = self._CompletionResultsForLine( - 'set x = ' + TEST_DIR + '/tes${YCM_TEST_td}/filename_completer/' ) - os.environ.pop( 'YCM_TEST_td' ) - - assert_that( data, contains_inanyorder( - ( 'inner_dir', '[Dir]' ), - ( '∂†∫', '[Dir]' ) - ) ) - - - def EnvVar_Undefined_test( self ): - data = self._CompletionResultsForLine( - 'set x = ' + TEST_DIR + '/testdata/filename_completer${YCM_TEST_td}/' ) - - assert_that( data, empty() ) - - - def EnvVar_Empty_Matches_test( self ): - os.environ[ 'YCM_empty_var' ] = '' - data = self._CompletionResultsForLine( - 'set x = ' - + TEST_DIR - + '/testdata/filename_completer${YCM_empty_var}/' ) - os.environ.pop( 'YCM_empty_var' ) - - assert_that( data, contains_inanyorder( - ( 'inner_dir', '[Dir]' ), - ( '∂†∫', '[Dir]' ) - ) ) - - - def EnvVar_Undefined_Garbage_test( self ): - os.environ[ 'YCM_TEST_td' ] = 'testdata/filename_completer' - data = self._CompletionResultsForLine( - 'set x = ' + TEST_DIR + '/$YCM_TEST_td}/' ) - - os.environ.pop( 'YCM_TEST_td' ) - assert_that( data, empty() ) - - - def EnvVar_Undefined_Garbage_2_test( self ): - os.environ[ 'YCM_TEST_td' ] = 'testdata/filename_completer' - data = self._CompletionResultsForLine( - 'set x = ' + TEST_DIR + '/${YCM_TEST_td/' ) - - os.environ.pop( 'YCM_TEST_td' ) - assert_that( data, empty() ) - - - def EnvVar_Undefined_Garbage_3_test( self ): - os.environ[ 'YCM_TEST_td' ] = 'testdata/filename_completer' - data = self._CompletionResultsForLine( - 'set x = ' + TEST_DIR + '/$ YCM_TEST_td/' ) - - os.environ.pop( 'YCM_TEST_td' ) - assert_that( data, empty() ) - - - def Unicode_In_Line_Works_test( self ): - eq_( True, self._ShouldUseNowForLine( - contents = "var x = /†/testing", - # The † character is 3 bytes in UTF-8 - column_num = 15 ) ) - assert_that( self._CompletionResultsForLine( - contents = "var x = /†/testing", - # The † character is 3 bytes in UTF-8 - column_num = 15 ), empty() ) - - - def Unicode_Paths_test( self ): - contents = "test " + DATA_DIR + "/../∂" - # The column number is the first byte of the ∂ character (1-based ) - column_num = ( len( ToBytes( "test" ) ) + - len( ToBytes( DATA_DIR ) ) + - len( ToBytes( '/../' ) ) + - 1 + # 0-based offset of ∂ - 1 ) # Make it 1-based - eq_( True, self._ShouldUseNowForLine( contents, column_num = column_num ) ) - assert_that( self._CompletionResultsForLine( contents, - column_num = column_num ), - contains_inanyorder( ( 'inner_dir', '[Dir]' ), - ( '∂†∫', '[Dir]' ) ) ) +ROOT_FOLDER_COMPLETIONS = tuple( + ( path, '[Dir]' if os.path.isdir( os.path.sep + path ) else '[File]' ) + for path in os.listdir( os.path.sep ) ) +DRIVE = os.path.splitdrive( TEST_DIR )[ 0 ] +PATH_TO_TEST_FILE = os.path.join( DATA_DIR, 'test.cpp' ) + + +@IsolatedYcmd( { 'max_num_candidates': 0 } ) +def FilenameCompleter_Completion( app, + contents, + environ, + filetype, + completions ): + completion_data = BuildRequest( contents = contents, + filepath = PATH_TO_TEST_FILE, + filetype = filetype, + column_num = len( ToBytes( contents ) ) + 1 ) + if completions: + completion_matchers = [ CompletionEntryMatcher( *completion ) + for completion in completions ] + expected_results = contains_inanyorder( *completion_matchers ) + else: + expected_results = empty() + + with patch.dict( 'os.environ', environ ): + results = app.post_json( '/completions', + completion_data ).json[ 'completions' ] + + assert_that( results, expected_results ) + + +def FilenameCompleter_Completion_test(): + # A series of tests represented by tuples whose elements are: + # - the line to complete; + # - the environment variables; + # - the expected completions. + tests = ( + ( '/', + {}, + ROOT_FOLDER_COMPLETIONS ), + ( '//', + {}, + () ), + ( 'const char* c = "/', + {}, + ROOT_FOLDER_COMPLETIONS ), + ( 'const char* c = "./', + {}, + ( ( 'dir with spaces (x64)', '[Dir]' ), + ( 'foo漢字.txt', '[File]' ), + ( 'test.cpp', '[File]' ), + ( 'test.hpp', '[File]' ) ) ), + ( 'const char* c = "./漢', + {}, + ( ( 'foo漢字.txt', '[File]' ), ) ), + ( 'const char* c = "./dir with spaces (x64)/', + {}, + ( ( 'Qt', '[Dir]' ), + ( 'QtGui', '[Dir]' ) ) ), + ( 'const char* c = "./dir with spaces (x64)//', + {}, + ( ( 'Qt', '[Dir]' ), + ( 'QtGui', '[Dir]' ) ) ), + ( 'const char* c = "../', + {}, + ( ( 'inner_dir', '[Dir]' ), + ( '∂†∫', '[Dir]' ) ) ), + ( 'const char* c = "../inner_dir/', + {}, + ( ( 'dir with spaces (x64)', '[Dir]' ), + ( 'foo漢字.txt', '[File]' ), + ( 'test.cpp', '[File]' ), + ( 'test.hpp', '[File]' ) ) ), + ( 'const char* c = "../inner_dir/dir with spaces (x64)/', + {}, + ( ( 'Qt', '[Dir]' ), + ( 'QtGui', '[Dir]' ) ) ), + ( 'const char* c = "~/', + { 'HOME': DATA_DIR }, + ( ( 'dir with spaces (x64)', '[Dir]' ), + ( 'foo漢字.txt', '[File]' ), + ( 'test.cpp', '[File]' ), + ( 'test.hpp', '[File]' ) ) ), + ( 'const char* c = "~/dir with spaces (x64)/', + { 'HOME': DATA_DIR }, + ( ( 'Qt', '[Dir]' ), + ( 'QtGui', '[Dir]' ) ) ), + ( 'const char* c = "~/dir with spaces (x64)/Qt/', + { 'HOME': DATA_DIR }, + ( ( 'QtGui', '[File]' ), ) ), + ( 'const char* c = "dir with spaces (x64)/', + {}, + ( ( 'Qt', '[Dir]' ), + ( 'QtGui', '[Dir]' ) ) ), + ( 'const char* c = "dir with spaces (x64)/Qt/', + {}, + ( ( 'QtGui', '[File]' ), ) ), + ( 'const char* c = "dir with spaces (x64)/Qt/QtGui dir with spaces (x64)/', + {}, + ( ( 'Qt', '[Dir]' ), + ( 'QtGui', '[Dir]' ) ) ), + ( 'const char* c = "dir with spaces (x64)/Qt/QtGui/', + {}, + () ), + ( 'const char* c = "dir with spaces (x64)/Qt/QtGui /', + {}, + () ), + ( 'set x = $YCM_TEST_DATA_DIR/dir with spaces (x64)/QtGui/', + { 'YCM_TEST_DATA_DIR': DATA_DIR }, + ( ( 'QDialog', '[File]' ), + ( 'QWidget', '[File]' ) ) ), + ( 'set x = $YCM_TEST_DIR/testdata/filename_completer/inner_dir/test', + { 'YCM_TEST_DIR': TEST_DIR }, + ( ( 'test.cpp', '[File]' ), + ( 'test.hpp', '[File]' ) ) ), + ( 'set x = $YCMTESTDIR/testdata/filename_completer/', + { 'YCMTESTDIR': TEST_DIR }, + ( ( 'inner_dir', '[Dir]' ), + ( '∂†∫', '[Dir]' ) ) ), + ( 'set x = $ycm_test_dir/testdata/filename_completer/inn', + { 'ycm_test_dir': TEST_DIR }, + ( ( 'inner_dir', '[Dir]' ), ) ), + ( 'set x = ' + TEST_DIR + + '/testdata/filename_completer/$YCM_TEST_filename_completer/', + { 'YCM_TEST_filename_completer': 'inner_dir' }, + ( ( 'dir with spaces (x64)', '[Dir]' ), + ( 'foo漢字.txt', '[File]' ), + ( 'test.cpp', '[File]' ), + ( 'test.hpp', '[File]' ) ) ), + ( 'set x = ' + TEST_DIR + + '/testdata/filename_completer/$YCM_TEST_filename_c0mpleter/test', + { 'YCM_TEST_filename_c0mpleter': 'inner_dir' }, + ( ( 'test.cpp', '[File]' ), + ( 'test.hpp', '[File]' ) ) ), + ( 'set x = ' + TEST_DIR + '/${YCM_TEST_td}ata/filename_completer/', + { 'YCM_TEST_td': 'testd' }, + ( ( 'inner_dir', '[Dir]' ), + ( '∂†∫', '[Dir]' ) ) ), + ( 'set x = ' + TEST_DIR + '/tes${YCM_TEST_td}/filename_completer/', + { 'YCM_TEST_td': 'tdata' }, + ( ( 'inner_dir', '[Dir]' ), + ( '∂†∫', '[Dir]' ) ) ), + ( 'set x = ' + TEST_DIR + '/testdata/filename_completer${YCM_TEST_td}/', + {}, + () ), + ( 'set x = ' + TEST_DIR + '/testdata/filename_completer${YCM_empty_var}/', + { 'YCM_empty_var': '' }, + ( ( 'inner_dir', '[Dir]' ), + ( '∂†∫', '[Dir]' ) ) ), + ( 'set x = ' + TEST_DIR + '/$YCM_TEST_td}/', + { 'YCM_TEST_td': 'testdata/filename_completer' }, + () ), + ( 'set x = ' + TEST_DIR + '/${YCM_TEST_td/', + { 'YCM_TEST_td': 'testdata/filename_completer' }, + () ), + ( 'set x = ' + TEST_DIR + '/$ YCM_TEST_td/', + { 'YCM_TEST_td': 'testdata/filename_completer' }, + () ), + ( 'test ' + DATA_DIR + '/../∂', + {}, + ( ( '∂†∫', '[Dir]' ), ) ), + ) + + for test in tests: + yield FilenameCompleter_Completion, test[ 0 ], test[ 1 ], 'foo', test[ 2 ] + + +@WindowsOnly +def FilenameCompleter_Completion_Windows_test(): + # A series of tests represented by tuples whose elements are: + # - the line to complete; + # - the environment variables; + # - the expected completions. + tests = ( + ( '\\', + {}, + ROOT_FOLDER_COMPLETIONS ), + ( '/\\', + {}, + () ), + ( '\\\\', + {}, + () ), + ( '\\/', + {}, + () ), + ( 'const char* c = "\\', + {}, + ROOT_FOLDER_COMPLETIONS ), + ( 'const char* c = "' + DRIVE + '/', + {}, + ROOT_FOLDER_COMPLETIONS ), + ( 'const char* c = "' + DRIVE + '\\', + {}, + ROOT_FOLDER_COMPLETIONS ), + ( 'const char* c = ".\\', + {}, + ( ( 'dir with spaces (x64)', '[Dir]' ), + ( 'foo漢字.txt', '[File]' ), + ( 'test.cpp', '[File]' ), + ( 'test.hpp', '[File]' ) ) ), + ( 'const char* c = ".\\dir with spaces (x64)\\', + {}, + ( ( 'Qt', '[Dir]' ), + ( 'QtGui', '[Dir]' ) ) ), + ( 'const char* c = ".\\dir with spaces (x64)\\\\', + {}, + ( ( 'Qt', '[Dir]' ), + ( 'QtGui', '[Dir]' ) ) ), + ( 'const char* c = ".\\dir with spaces (x64)/\\', + {}, + ( ( 'Qt', '[Dir]' ), + ( 'QtGui', '[Dir]' ) ) ), + ( 'const char* c = ".\\dir with spaces (x64)\\/', + {}, + ( ( 'Qt', '[Dir]' ), + ( 'QtGui', '[Dir]' ) ) ), + ( 'const char* c = ".\\dir with spaces (x64)/Qt\\', + {}, + ( ( 'QtGui', '[File]' ), ) ), + ( 'const char* c = "dir with spaces (x64)\\Qt\\', + {}, + ( ( 'QtGui', '[File]' ), ) ), + ( 'dir with spaces (x64)\\Qt/QtGui dir with spaces (x64)\\', + {}, + ( ( 'Qt', '[Dir]' ), + ( 'QtGui', '[Dir]' ) ) ), + ( 'set x = %YCM_TEST_DIR%\\testdata/filename_completer\\inner_dir/test', + { 'YCM_TEST_DIR': TEST_DIR }, + ( ( 'test.cpp', '[File]' ), + ( 'test.hpp', '[File]' ) ) ), + ( 'set x = YCM_TEST_DIR%\\testdata/filename_completer\\inner_dir/test', + { 'YCM_TEST_DIR': TEST_DIR }, + () ), + ( 'set x = %YCM_TEST_DIR\\testdata/filename_completer\\inner_dir/test', + { 'YCM_TEST_DIR': TEST_DIR }, + () ), + ) + + for test in tests: + yield FilenameCompleter_Completion, test[ 0 ], test[ 1 ], 'foo', test[ 2 ] @IsolatedYcmd( { 'filepath_completion_use_working_dir': 0 } ) def WorkingDir_UseFilePath_test( app ): - ok_( GetCurrentDirectory() != DATA_DIR, ( 'Please run this test from a ' - 'different directory' ) ) - - completer = FilenameCompleter( user_options_store.GetAll() ) + ok_( GetCurrentDirectory() != DATA_DIR, 'Please run this test from a ' + 'different directory' ) - data = _CompletionResultsForLine( completer, 'ls ./include/' ) - assert_that( data, contains_inanyorder( - ( 'Qt', '[Dir]' ), - ( 'QtGui', '[Dir]' ) + completion_data = BuildRequest( contents = 'ls ./dir with spaces (x64)/', + filepath = PATH_TO_TEST_FILE, + column_num = 28 ) + results = app.post_json( '/completions', + completion_data ).json[ 'completions' ] + assert_that( results, contains_inanyorder( + CompletionEntryMatcher( 'Qt', '[Dir]' ), + CompletionEntryMatcher( 'QtGui', '[Dir]' ) ) ) @IsolatedYcmd( { 'filepath_completion_use_working_dir': 1 } ) def WorkingDir_UseServerWorkingDirectory_test( app ): - test_dir = os.path.join( DATA_DIR, 'include' ) + test_dir = os.path.join( DATA_DIR, 'dir with spaces (x64)' ) with CurrentWorkingDirectory( test_dir ) as old_current_dir: - ok_( old_current_dir != test_dir, ( 'Please run this test from a different ' - 'directory' ) ) - - completer = FilenameCompleter( user_options_store.GetAll() ) - - # We don't supply working_dir in the request, so the current working - # directory is used. - data = _CompletionResultsForLine( completer, 'ls ./' ) - assert_that( data, contains_inanyorder( - ( 'Qt', '[Dir]' ), - ( 'QtGui', '[Dir]' ) + ok_( old_current_dir != test_dir, 'Please run this test from a different ' + 'directory' ) + + completion_data = BuildRequest( contents = 'ls ./', + filepath = PATH_TO_TEST_FILE, + column_num = 6 ) + results = app.post_json( '/completions', + completion_data ).json[ 'completions' ] + assert_that( results, contains_inanyorder( + CompletionEntryMatcher( 'Qt', '[Dir]' ), + CompletionEntryMatcher( 'QtGui', '[Dir]' ) ) ) @@ -343,32 +325,35 @@ def WorkingDir_UseServerWorkingDirectory_Unicode_test( app ): ok_( old_current_dir != test_dir, ( 'Please run this test from a different ' 'directory' ) ) - completer = FilenameCompleter( user_options_store.GetAll() ) - # We don't supply working_dir in the request, so the current working # directory is used. - data = _CompletionResultsForLine( completer, 'ls ./' ) - assert_that( data, contains_inanyorder( - ( '†es†.txt', '[File]' ) + completion_data = BuildRequest( contents = 'ls ./', + filepath = PATH_TO_TEST_FILE, + column_num = 6 ) + results = app.post_json( '/completions', + completion_data ).json[ 'completions' ] + assert_that( results, contains_inanyorder( + CompletionEntryMatcher( '†es†.txt', '[File]' ) ) ) @IsolatedYcmd( { 'filepath_completion_use_working_dir': 1 } ) def WorkingDir_UseClientWorkingDirectory_test( app ): - test_dir = os.path.join( DATA_DIR, 'include' ) + test_dir = os.path.join( DATA_DIR, 'dir with spaces (x64)' ) ok_( GetCurrentDirectory() != test_dir, ( 'Please run this test from a ' 'different directory' ) ) - completer = FilenameCompleter( user_options_store.GetAll() ) - # We supply working_dir in the request, so we expect results to be # relative to the supplied path. - data = _CompletionResultsForLine( completer, 'ls ./', { - 'working_dir': test_dir - } ) - assert_that( data, contains_inanyorder( - ( 'Qt', '[Dir]' ), - ( 'QtGui', '[Dir]' ) + completion_data = BuildRequest( contents = 'ls ./', + filepath = PATH_TO_TEST_FILE, + column_num = 6, + working_dir = test_dir ) + results = app.post_json( '/completions', + completion_data ).json[ 'completions' ] + assert_that( results, contains_inanyorder( + CompletionEntryMatcher( 'Qt', '[Dir]' ), + CompletionEntryMatcher( 'QtGui', '[Dir]' ) ) ) diff --git a/ycmd/tests/get_completions_test.py b/ycmd/tests/get_completions_test.py index bb6454a7ec..00cd54162d 100644 --- a/ycmd/tests/get_completions_test.py +++ b/ycmd/tests/get_completions_test.py @@ -350,6 +350,27 @@ def GetCompletions_FilenameCompleter_Works_test( app ): has_items( CompletionEntryMatcher( 'inner_dir', '[Dir]' ) ) ) +@SharedYcmd +def GetCompletions_FilenameCompleter_FallBackToIdentifierCompleter_test( app ): + filepath = PathToTestFile( 'filename_completer', 'test.foo' ) + event_data = BuildRequest( filepath = filepath, + contents = './nonexisting_dir', + filetype = 'foo', + event_name = 'FileReadyToParse' ) + + app.post_json( '/event_notification', event_data ) + + completion_data = BuildRequest( filepath = filepath, + contents = './nonexisting_dir nd', + filetype = 'foo', + column_num = 21 ) + + assert_that( + app.post_json( '/completions', completion_data ).json[ 'completions' ], + has_items( CompletionEntryMatcher( 'nonexisting_dir', '[ID]' ) ) + ) + + @SharedYcmd def GetCompletions_UltiSnipsCompleter_Works_test( app ): event_data = BuildRequest( diff --git a/ycmd/tests/testdata/filename_completer/inner_dir/include/Qt/QtGui b/ycmd/tests/testdata/filename_completer/inner_dir/dir with spaces (x64)/Qt/QtGui similarity index 100% rename from ycmd/tests/testdata/filename_completer/inner_dir/include/Qt/QtGui rename to ycmd/tests/testdata/filename_completer/inner_dir/dir with spaces (x64)/Qt/QtGui diff --git a/ycmd/tests/testdata/filename_completer/inner_dir/include/QtGui/QDialog b/ycmd/tests/testdata/filename_completer/inner_dir/dir with spaces (x64)/QtGui/QDialog similarity index 100% rename from ycmd/tests/testdata/filename_completer/inner_dir/include/QtGui/QDialog rename to ycmd/tests/testdata/filename_completer/inner_dir/dir with spaces (x64)/QtGui/QDialog diff --git a/ycmd/tests/testdata/filename_completer/inner_dir/include/QtGui/QWidget b/ycmd/tests/testdata/filename_completer/inner_dir/dir with spaces (x64)/QtGui/QWidget similarity index 100% rename from ycmd/tests/testdata/filename_completer/inner_dir/include/QtGui/QWidget rename to ycmd/tests/testdata/filename_completer/inner_dir/dir with spaces (x64)/QtGui/QWidget diff --git a/ycmd/utils.py b/ycmd/utils.py index 81e3c5549a..adfb83ed69 100644 --- a/ycmd/utils.py +++ b/ycmd/utils.py @@ -1,6 +1,6 @@ # encoding: utf-8 # -# Copyright (C) 2011, 2012 Google Inc. +# Copyright (C) 2011-2018 ycmd contributors # # This file is part of ycmd. # @@ -28,6 +28,7 @@ import collections import copy import json +import logging import os import socket import subprocess @@ -36,6 +37,7 @@ import time import threading +_logger = logging.getLogger( __name__ ) # Idiom to import pathname2url, url2pathname, urljoin, and urlparse on Python 2 # and 3. By exposing these functions here, we can import them directly from this @@ -532,3 +534,20 @@ def __eq__( self, other ): def __ne__( self, other ): return not self == other + + +def ListDirectory( path ): + try: + # Path must be a Unicode string to get Unicode strings out of listdir. + return os.listdir( ToUnicode( path ) ) + except Exception: + _logger.exception( 'Error while listing %s folder.', path ) + return [] + + +def GetModificationTime( path ): + try: + return os.path.getmtime( path ) + except OSError: + _logger.exception( 'Cannot get modification time for path %s.', path ) + return 0