Mercurial > vim-crosoft
changeset 10:f444739dd8af
Improvements to YCM dynamic flags.
- Fallback to a "companion" item (e.g. header/source) or a nearby item
when no flags are found for an item.
- Finding a "companion" is also exposed as a standalone script.
- Ability to pass extra clang flags, including some from a special
file found in the .vimcrosoft directory.
- Add support for PCH and other forced-include files.
- Add options for short/long args, or forcing forward slashes.
- Debugging/troubleshooting options, including dumping a batch file and
response file to run clang directly, and the ability to auto-load a
solution's last known environment when running in command line.
author | Ludovic Chabant <ludovic@chabant.com> |
---|---|
date | Thu, 24 Sep 2020 23:02:16 -0700 |
parents | 4ba6df1b2f97 |
children | 096e80c13781 |
files | .hgignore autoload/vimcrosoft/youcompleteme.vim scripts/find_companion.py scripts/vshelpers.py scripts/ycm_extra_conf.py |
diffstat | 5 files changed, 402 insertions(+), 41 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.hgignore Thu Sep 24 23:02:16 2020 -0700 @@ -0,0 +1,3 @@ +scripts/clang_ycm_args.rsp +scripts/clang_ycm_invoke.cmd +scripts/clang_ycm_invoke.log
--- a/autoload/vimcrosoft/youcompleteme.vim Thu Sep 24 22:57:50 2020 -0700 +++ b/autoload/vimcrosoft/youcompleteme.vim Thu Sep 24 23:02:16 2020 -0700 @@ -1,3 +1,5 @@ + +let g:vimcrosoft_extra_clang_args = get(g:, 'vimcrosoft_extra_clang_args', []) function! vimcrosoft#youcompleteme#init() abort endfunction @@ -8,7 +10,8 @@ \'g:vimcrosoft_current_sln', \'g:vimcrosoft_current_sln_cache', \'g:vimcrosoft_current_config', - \'g:vimcrosoft_current_platform' + \'g:vimcrosoft_current_platform', + \'g:vimcrosoft_extra_clang_args' \] endfunction
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/scripts/find_companion.py Thu Sep 24 23:02:16 2020 -0700 @@ -0,0 +1,88 @@ +import argparse +import logging +import os.path +import sys + + +if True: # 'vim' in sys.modules: + sys.path.append(os.path.dirname(__file__)) + + +from logutil import setup_logging +from vshelpers import load_vimcrosoft_auto_env, find_vimcrosoft_slncache, find_item_project +from vsutil import SolutionCache, ITEM_TYPE_CPP_SRC, ITEM_TYPE_CPP_HDR + + +logger = logging.getLogger(__name__) + + +def _find_companion_item(solution, item_path, companion_name=None, companion_type=None, slncache=None): + # Try to guess the default companion file if needed. + if companion_name == None or companion_type == None: + primary_name, primary_ext = os.path.splitext(item_path) + primary_name = os.path.basename(primary_name) + if primary_ext == '.cpp': + companion_name = primary_name + '.h' + companion_type = ITEM_TYPE_CPP_HDR + elif primary_ext == '.h': + companion_name = primary_name + '.cpp' + companion_type = ITEM_TYPE_CPP_SRC + else: + raise Exception("Can't guess the companion file for: %s" % item_path) + + # Find the primary file in the solution. + cache, proj = find_item_project(item_path, solution, slncache) + logger.debug("Found project %s: %s" % (proj.name, proj.abspath)) + + # Look for the companion file in that project: + candidates = [] + dfgroup = proj.defaultitemgroup() + for cur_item in dfgroup.get_items_of_type(companion_type): + cur_item_name = os.path.basename(cur_item.include) + if cur_item_name == companion_name: + cur_item_path = proj.get_abs_item_include(cur_item) + candidates.append((cur_item, _get_companion_score(cur_item_path, item_path))) + candidates = sorted(candidates, key=lambda i: i[1], reverse=True) + logger.debug("Found candidates: %s" % [(c[0].include, c[1]) for c in candidates]) + if candidates: + return proj.get_abs_item_include(candidates[0][0]) + return None + + +def _get_companion_score(item_path, ref_path): + for i, c in enumerate(zip(item_path, ref_path)): + if c[0] != c[1]: + return i + return min(len(item_path, ref_path)) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('solution', + help="The solution file") + parser.add_argument('filename', + help="The filename for which to get the companion") + parser.add_argument('--no-auto-env', + action='store_true', + help="Don't read configuration information from Vimcrosoft cache") + parser.add_argument('-c', '--cache', + help="The solution cache to use") + parser.add_argument('-v', '--verbose', + action='store_true', + help="Show debugging information") + args = parser.parse_args() + setup_logging(args.verbose) + + build_env = {} + slncache = args.cache + if not args.no_auto_env: + load_vimcrosoft_auto_env(args.solution, build_env) + if not slncache: + slncache = find_vimcrosoft_slncache(args.solution) + + companion = _find_companion_item(args.solution, args.filename, + slncache=slncache) + print(companion) + +if __name__ == '__main__': + main()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/scripts/vshelpers.py Thu Sep 24 23:02:16 2020 -0700 @@ -0,0 +1,81 @@ +import os.path +import logging +from vsutil import SolutionCache + + +logger = logging.getLogger(__name__) + +config_format = 2 # Keep in sync with vimcrosoft.vim + + +def load_vimcrosoft_auto_env(sln_file, build_env): + cache_file = os.path.join(os.path.dirname(sln_file), '.vimcrosoft', 'config.txt') + if not os.path.isfile(cache_file): + logger.warn("No Vimcrosoft cache file found, you will probably have to specify " + "all configuration values in the command line!") + return + + with open(cache_file, 'r', encoding='utf8') as fp: + lines = fp.readlines() + + tokens = [ + '_VimcrosoftConfigFormat', + '_VimcrosoftCurrentSolution', + 'Configuration', + 'Platform', + '_VimcrosoftActiveProject'] + for i, line in enumerate(lines): + token = tokens[i] + build_env[token] = line.strip() + + try: + found_conffmt = int(build_env['_VimcrosoftConfigFormat'].split('=')[-1]) + except: + raise Exception("Invalid Vimcrosoft cache file found.") + if found_conffmt != config_format: + raise Exception("Incompatible Vimcrosoft cache file found. " + "Expected format %d but got %d" % (config_format, found_conffmt)) + + logger.info("Loaded cached configuration|platform: %s|%s" % + (build_env['Configuration'], build_env['Platform'])) + + +def find_vimcrosoft_slncache(sln_file): + return os.path.join(os.path.dirname(sln_file), '.vimcrosoft', 'slncache.bin') + + +def get_solution_cache(solution, slncache=None): + if not solution: + raise Exception( + "No solution path was provided!") + + cache, loaded = SolutionCache.load_or_rebuild(solution, slncache) + if not loaded: + cache.build_cache() + if slncache: + logger.debug(f"Saving solution cache: {slncache}") + cache.save(slncache) + + return cache + + +def find_item_project(item_path, solution, slncache=None): + # Load the solution + cache = get_solution_cache(solution, slncache) + + # Find the primary file in the solution. + item_path_lower = item_path.lower() + projpath = None + for pp, pi in cache.index.items(): + if item_path_lower in pi: + projpath = pp + break + else: + raise Exception("File doesn't belong to the solution: %s" % item_path) + + # Find the project that our file belongs to. + proj = cache.slnobj.find_project_by_path(projpath) + if not proj: + raise Exception("Can't find project in solution: %s" % projpath) + + return cache, proj
--- a/scripts/ycm_extra_conf.py Thu Sep 24 22:57:50 2020 -0700 +++ b/scripts/ycm_extra_conf.py Thu Sep 24 23:02:16 2020 -0700 @@ -1,6 +1,7 @@ import argparse import logging import os.path +import shutil import sys @@ -9,36 +10,88 @@ from logutil import setup_logging -from vsutil import SolutionCache +from vshelpers import load_vimcrosoft_auto_env, find_vimcrosoft_slncache, find_item_project +from vsutil import SolutionCache, ITEM_TYPE_CPP_SRC, ITEM_TYPE_CPP_HDR logger = logging.getLogger(__name__) -def _build_cflags(filename, solution, buildenv=None, slncache=None): - # Load the solution. - if not solution: - raise Exception( - "No solution path was provided in the client data!") +def _split_paths_property(val): + if val: + return val.strip(';').split(';') + return [] + - cache, loaded = SolutionCache.load_or_rebuild(solution, slncache) - if not loaded: - cache.build_cache() +def _split_paths_property_and_make_absolute(basedir, val): + return [os.path.abspath(os.path.join(basedir, p)) + for p in _split_paths_property(val)] - # Find the current file in the solution. + +def _get_item_specific_flags(projdir, clcompileitems, filename): + logger.debug("Looking through %d items to find: %s" % (len(clcompileitems), filename)) filename_lower = filename.lower() - projpath = None - for pp, pi in cache.index.items(): - if filename_lower in pi: - projpath = pp - break - else: - raise Exception("File doesn't belong to the solution: %s" % filename) + for item in clcompileitems: + absiteminclude = os.path.normpath(os.path.join(projdir, item.include)) + if absiteminclude.lower() != filename_lower: + continue + logger.debug("Found file-specific flags for: %s" % filename) + incpaths = _split_paths_property_and_make_absolute( + projdir, item.metadata.get('AdditionalIncludeDirectories')) + incfiles = _split_paths_property_and_make_absolute( + projdir, item.metadata.get('ForcedIncludeFiles')) + return (incpaths, incfiles) + return ([], []) + + +def _find_any_possible_item_specific_flags( + solution, slncache, projdir, clcompileitems, filename, incpaths, incfiles, *, + search_neighbours=True): + # First, find any actual flags for this item. + item_incpaths, item_incfiles = _get_item_specific_flags(projdir, clcompileitems, filename) + if item_incpaths or item_incfiles: + incpaths += item_incpaths + incfiles += item_incfiles + return True - # Find the project that our file belongs to. - proj = cache.slnobj.find_project_by_path(projpath) - if not proj: - raise Exception("Can't find project in solution: %s" % projpath) + logger.debug("Requested item didn't have any flags, looking for companion item") + from find_companion import _find_companion_item + companion_item = _find_companion_item(solution, filename, slncache=slncache) + if companion_item: + logger.debug("Found companion item: %s" % companion_item) + item_incpaths, item_incfiles = _get_item_specific_flags(projdir, clcompileitems, companion_item) + incpaths += item_incpaths + incfiles += item_incfiles + return True + + #logger.debug("No companion item found, see if we can find flags for a neighbour") + #os.path.dirname(filename) + return False + + +def _expand_extra_flags_with_solution_extra_flags(solution, extraflags): + argfilename = os.path.join( + os.path.dirname(solution), + '.vimcrosoft', + (os.path.basename(solution) + '.flags')) + try: + with open(argfilename, 'r', encoding='utf8') as fp: + lines = fp.readlines() + logger.debug("Read extra flags from: %s (%d lines)" % (argfilename, len(lines))) + except OSError: + return extraflags + + extraflags = extraflags or [] + for line in lines: + if not line.startswith('#'): + extraflags.append(line.strip()) + return extraflags + + +def _build_cflags(filename, solution, buildenv=None, slncache=None, extraflags=None, + force_fwd_slashes=True, short_flags=True): + # Find the current file in the solution. + cache, proj = find_item_project(filename, solution, slncache) logger.debug("Found project %s: %s" % (proj.name, proj.abspath)) # Get the provided config/platform combo, which represent a solution @@ -73,6 +126,7 @@ # Let's prepare a list of standard stuff for C++. preproc = [] incpaths = [] + incfiles = [] projdir = os.path.dirname(proj.abspath) if cfgtype == 'Makefile': @@ -83,11 +137,28 @@ defaultpropgroup = proj.defaultpropertygroup(proj_buildenv) nmake_preproc = defaultpropgroup.get('NMakePreprocessorDefinitions') - preproc += nmake_preproc.strip(';').split(';') + preproc += _split_paths_property(nmake_preproc) + + vs_incpaths = defaultpropgroup.get('IncludePath') + if vs_incpaths: + incpaths += _split_paths_property_and_make_absolute( + projdir, vs_incpaths) nmake_incpaths = defaultpropgroup.get('NMakeIncludeSearchPath') - incpaths += [os.path.abspath(os.path.join(projdir, p)) - for p in nmake_incpaths.strip(';').split(';')] + if nmake_incpaths: + incpaths += _split_paths_property_and_make_absolute( + projdir, nmake_incpaths) + + nmake_forcedincs = defaultpropgroup.get('NMakeForcedIncludes') + if nmake_forcedincs: + incfiles += _split_paths_property_and_make_absolute( + projdir, nmake_forcedincs) + + # Find stuff specific to the file we are working on. + defaultitemgroup = proj.defaultitemgroup(proj_buildenv) + clcompileitems = list(defaultitemgroup.get_items_of_types([ITEM_TYPE_CPP_SRC, ITEM_TYPE_CPP_HDR])) + _find_any_possible_item_specific_flags( + solution, slncache, projdir, clcompileitems, filename, incpaths, incfiles) else: # We should definitely support standard VC++ projects here but @@ -95,21 +166,66 @@ raise Exception("Don't know how to handle configuration type: %s" % cfgtype) + # We need to duplicate all the forced-included files because they could + # have a VS-generated PCH file next to them. Clang then tries to pick it + # up and complains that it doesn't use a valid format... :( + incfiles = _cache_pch_files(incfiles) + # Build the clang/YCM flags with what we found. flags = ['-x', 'c++'] # TODO: check language type from project file. for symbol in preproc: flags.append('-D%s' % symbol) for path in incpaths: + flagname = '-I' if path.startswith("C:\\Program Files"): - flags.append('-isystem') + flagname = '-isystem' + flagval = path.replace('\\', '/') if force_fwd_slashes else path + if short_flags: + flags.append('%s%s' % (flagname, flagval)) else: - flags.append('-I') - flags.append(path) + flags.append(flagname) + flags.append(flagval) + # For some reason it seems VS applies those in last-to-first order. + incfiles = list(reversed(incfiles)) + for path in incfiles: + if force_fwd_slashes: + flags.append('--include=%s' % path.replace('\\', '/')) + else: + flags.append('--include=%s' % path) + + if extraflags: + flags += extraflags return {'flags': flags} +_clang_shadow_pch_suffix = '-for-clang' + + +def _cache_pch_files(paths): + outpaths = [] + for path in paths: + name, ext = os.path.splitext(path) + outpath = "%s%s%s" % (name, _clang_shadow_pch_suffix, ext) + + do_cache = False + orig_mtime = os.path.getmtime(path) + try: + out_mtime = os.path.getmtime(outpath) + if orig_mtime >= out_mtime: + do_cache = True + except OSError: + do_cache = True + + if do_cache: + logger.debug("Creating shadow PCH file: %s" % path) + shutil.copy2(path, outpath) + + outpaths.append(outpath) + return outpaths + + def _build_env_from_vim(client_data): buildenv = {} buildenv['Configuration'] = client_data.get('g:vimcrosoft_current_config', '') @@ -117,6 +233,35 @@ return buildenv +_dump_debug_file = True + + +def _do_dump_debug_file(args, flags, debug_filename): + import pprint + + with open(debug_filename, 'w') as fp: + fp.write(" args: \n") + fp.write("=========\n") + pp = pprint.PrettyPrinter(indent=2, stream=fp) + pp.pprint(args) + fp.write("\n\n") + + fp.write(" flags: \n") + fp.write("==========\n") + flags = flags.get('flags') + if flags: + fp.write(flags[0]) + for flag in flags[1:]: + if flag[0] == '-': + fp.write("\n") + else: + fp.write(" ") + fp.write(flag) + else: + fp.write("<no flags found>\n") + fp.write("\n\n") + + def Settings(**kwargs): language = kwargs.get('language') filename = kwargs.get('filename') @@ -127,17 +272,24 @@ solution = client_data.get('solution') slncache = client_data.get('slncache') buildenv = client_data.get('env', {}) + extraflags = client_data.get('extra_flags') else: + if not client_data: + raise Exception("No client data provided by Vim host!") solution = client_data.get('g:vimcrosoft_current_sln') slncache = client_data.get('g:vimcrosoft_current_sln_cache') buildenv = _build_env_from_vim(client_data) + extraflags = client_data.get('g:vimcrosoft_extra_clang_args') + + extraflags = _expand_extra_flags_with_solution_extra_flags(solution, extraflags) flags = None if language == 'cfamily': try: flags = _build_cflags(filename, solution, - buildenv=buildenv, slncache=slncache) + buildenv=buildenv, slncache=slncache, + extraflags=extraflags) except Exception as exc: if from_cli: raise @@ -145,13 +297,11 @@ else: flags = {'error': f"Unknown language: {language}"} - with open("D:\\P4\\DevEditor\\debug.txt", 'w') as fp: - fp.write("kwargs:") - fp.write(str(kwargs)) - fp.write("client_data:") - fp.write(str(list(kwargs['client_data'].items()))) - fp.write("flags:") - fp.write(str(flags)) + if _dump_debug_file: + debug_filename = os.path.join( + os.path.dirname(solution), '.vimcrosoft', 'debug_flags.txt') + _do_dump_debug_file(kwargs, flags, debug_filename) + return flags @@ -175,11 +325,17 @@ help="The solution file") parser.add_argument('filename', help="The filename for which to get flags") + parser.add_argument('--no-auto-env', + action='store_true', + help="Don't read configuration information from Vimcrosoft cache") parser.add_argument('-p', '--property', action="append", help="Specifies a build property") parser.add_argument('-c', '--cache', help="The solution cache to use") + parser.add_argument('--cmdline', + action='store_true', + help="Output flags in a command-line form") parser.add_argument('-v', '--verbose', action='store_true', help="Show debugging information") @@ -190,13 +346,18 @@ logger.debug(f"Got language {lang} for {args.filename}") build_env = {} + slncache = args.cache + if not args.no_auto_env: + load_vimcrosoft_auto_env(args.solution, build_env) + if not slncache: + slncache = find_vimcrosoft_slncache(args.solution) if args.property: for p in args.property: pname, pval = p.split('=', 1) build_env[pname] = pval logger.debug(f"Got build environment: {build_env}") client_data = {'solution': args.solution, - 'slncache': args.cache, + 'slncache': slncache, 'env': build_env} params = {'from_cli': True, @@ -205,10 +366,35 @@ 'client_data': client_data } flags = Settings(**params) - logger.info("Flags:") - import pprint - pp = pprint.PrettyPrinter(indent=2) - pp.pprint(flags) + if args.cmdline: + import shlex + if hasattr(shlex, 'join'): + joinargs = shlex.join + else: + joinargs = lambda a: ' '.join(a) + + with open('clang_ycm_args.rsp', 'w', encoding='utf8') as fp: + fp.write("%s -fsyntax-only \"%s\"" % ( + joinargs(sanitizeargs(flags['flags'])), + args.filename.replace('\\', '/') + )) + with open('clang_ycm_invoke.cmd', 'w', encoding='utf8') as fp: + fp.write("\"c:\\Program Files\\LLVM\\bin\\clang++.exe\" @clang_ycm_args.rsp > clang_ycm_invoke.log 2>&1") + + logger.info("Command line written to: clang_ycm_invoke.cmd") + else: + logger.info("Flags:") + import pprint + pp = pprint.PrettyPrinter(indent=2) + pp.pprint(flags) + + +def sanitizeargs(args): + for arg in args: + if ' ' in arg: + yield '"%s"' % arg + else: + yield arg if __name__ == '__main__':