From 88742b564f52458a38972a53ab42d958c99897d6 Mon Sep 17 00:00:00 2001 From: deb0ch Date: Mon, 29 Aug 2016 16:23:34 +0200 Subject: [PATCH] ycmd layer: a better global_conf.py --- layers/+tools/ycmd/README.org | 36 +++- layers/+tools/ycmd/global_conf.py | 346 +++++++++++++++++++++++------- 2 files changed, 302 insertions(+), 80 deletions(-) diff --git a/layers/+tools/ycmd/README.org b/layers/+tools/ycmd/README.org index 43ad63f38..0b11329a6 100644 --- a/layers/+tools/ycmd/README.org +++ b/layers/+tools/ycmd/README.org @@ -7,6 +7,8 @@ - [[#ycmd][YCMD]] - [[#other-requirements][Other Requirements]] - [[#configuration][Configuration]] + - [[#activating-ycmd-in-a-major-mode][Activating ycmd in a major mode]] + - [[#getting-the-compilation-flags][Getting the compilation flags]] - [[#key-bindings][Key Bindings]] * Description @@ -25,7 +27,10 @@ file. #+BEGIN_SRC emacs-lisp (setq ycmd-server-command '("python" "/path/to/YouCompleteMe/third_party/ycmd/ycmd")) #+END_SRC -3) Instead of =.clang_complete= ycmd [[https://github.com/Valloric/YouCompleteMe/blob/master/README.md#c-family-semantic-completion][uses a .ycm_extra_conf.py file]]. +3) By default, spacemacs configures ycmd for getting the compilation flags from either + a compile_commands.json or a .clang_complete file and get additionnal flags from a + .ycm_extra_flags file. If you do not like this behaviour, you can write your own + [[https://github.com/Valloric/YouCompleteMe/blob/master/README.md#c-family-semantic-completion][.ycm_extra_conf.py file]]. See [[#configuration][Configuration]] for more details. 4) Whitelist the file by adding the following to =.spacemacs=: #+BEGIN_SRC emacs-lisp ;; In this example we whitelist everything in the Develop folder @@ -42,7 +47,9 @@ This package requires the =auto-completion= layer in order to get actual completion. The =syntax-checking= layer is required for flycheck support. * Configuration -By default this layer only activates ycmd for =c++-mode=. + +** Activating ycmd in a major mode +By default this layer only activates ycmd for =c++-mode= and =c-mode=. If you want ycmd support in other modes you might just want to add it for specific languages like: @@ -51,6 +58,31 @@ specific languages like: (add-hook 'c++-mode-hook 'ycmd-mode) #+END_SRC +** Getting the compilation flags + +Spacemacs uses its own ycmd global configuration file. If you prefer, you can +write your own [[https://github.com/Valloric/YouCompleteMe/blob/master/README.md#c-family-semantic-completion][.ycm_extra_conf.py]]. + +Spacemacs will search for a compile_command.json or fall back to a +.clang_complete file in all parent directories of the current translation unit. +Spacemacs will try to make up for missing files in the compile_commands.json +using heuristics described in global_conf.py. + +The user can provide additionnal flags by writing a .ycm_extra_flags in any +parent directory of the current translation unit. This is particularly useful +when cross-compiling. + +Example .ycm_extra_flags: + +#+BEGIN_SRC conf +# Additionnal flags for ycmd +--sysroot="/path/to/your/toolchain/libc" # if you are cross-compiling +#+END_SRC + +If your build system doesn't handle the creation of a compile_commands.json, +you can use tools such as [[https://github.com/rizsotto/Bear][Bear]] or [[https://pypi.python.org/pypi/scan-build][scan-build]] to generate it, which both work +with almost any build system. + * Key Bindings Adds ~SPC m g g~ go to definition binding to =c++-mode= as well as ~SPC m g G~ diff --git a/layers/+tools/ycmd/global_conf.py b/layers/+tools/ycmd/global_conf.py index 929cf9f5e..839ce8f05 100644 --- a/layers/+tools/ycmd/global_conf.py +++ b/layers/+tools/ycmd/global_conf.py @@ -1,84 +1,274 @@ +# global_conf.py --- ycmd global configuration file for Spacemacs +# +# Copyright (c) 2012-2016 Sylvain Benner & Contributors +# +# Author: Thomas de Beauchene +# URL: https://github.com/syl20bnr/spacemacs +# +# This file is not part of GNU Emacs. +# +# License: GPLv3 +# +# This script tries to get the compilation flags for a translation unit using +# the following logic: +# +# 1) If there is a compile_commands.json in a parent directory: +# a) If the file is a header file: +# - search for the header file itself in db +# - search for a sibling source file in the same directory (i.e. a source +# file with the same name but different extension) +# - search for a source file that includes our header's path +# - search for the nearest source file in db +# +# b) If the file is a source file: +# - search for the source file itself +# - search for the nearest source file in db +# +# 2) If no compile_commands.json, search for a .clang_complete: +# - get flags from .clang_complete +# +# 3) Always try to add extra flags from a .ycm_extra_flags file in a parent +# directory. (like --sysroot="/path/to/your/toolchain/libc" if you are cross-compiling) +# +# Thanks to Jonas Devlieghere and Gabor Marton for their work on which this code is based. +# https://jonasdevlieghere.com/a-better-youcompleteme-config/ +# https://github.com/martong/ycm_extra_conf.jsondb + +import itertools +import json +import logging import os +import os.path +import re import ycm_core -default_flags = [ -'-Wall', -'-Wextra', -'-Werror', -'-Wc++98-compat', -'-Wno-long-long', -'-Wno-variadic-macros', -'-fexceptions', -'-DNDEBUG', -'-std=c++11', -'-x', -'c++', -# This path will only work on OS X, but extra paths that don't exist are not harmful -'-isystem' -'/System/Library/Frameworks/Python.framework/Headers', -'-isystem', -'/usr/include', -'-isystem', -'/usr/local/include', -'-isystem', -'/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/../include/c++/v1', -'-isystem', -'/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include', -] +SOURCE_EXTENSIONS = ['.cpp', '.cxx', '.cc', '.c', '.m', '.mm'] +HEADER_EXTENSIONS = ['.h', '.hxx', '.hpp', '.hh'] -def DirectoryOfThisScript(): - return os.path.dirname( os.path.abspath( __file__ ) ) +# This function is called by ycmd. +def FlagsForFile(filename): + logging.info("%s: Getting flags for %s" % (__file__, filename)) + root = os.path.realpath(filename); + compilation_db_flags = FlagsFromCompilationDatabase(root, filename) + if compilation_db_flags: + flags = compilation_db_flags + else: + flags = FlagsFromClangComplete(root, filename) + extra_flags = GetUserExtraFlags(filename) + if extra_flags: + if flags: + flags += extra_flags + else: + flags = extra_flags + if flags: + flags = [ flag for flag in flags if not flag.startswith("-m") ] # strip -m flags + logging.info("%s: Flags = [\n\t\t%s\n]" + % (os.path.basename(filename), "\n\t\t".join(flags))) + else: + flags = [] + logging.error("%s: No flags were found !" % (os.path.basename(filename))) + return { 'flags': flags, 'do_cache': True } - -def MakeRelativePathsInFlagsAbsolute( flags, working_directory ): - if not working_directory: - return list( flags ) - new_flags = [] - make_next_absolute = False - path_flags = [ '-isystem', '-I', '-iquote', '--sysroot=' ] - for flag in flags: - new_flag = flag - - if make_next_absolute: - make_next_absolute = False - if not flag.startswith( '/' ): - new_flag = os.path.join( working_directory, flag ) - - for path_flag in path_flags: - if flag == path_flag: - make_next_absolute = True - break - - if flag.startswith( path_flag ): - path = flag[ len( path_flag ): ] - new_flag = path_flag + os.path.join( working_directory, path ) - break - - if new_flag: - new_flags.append( new_flag ) - return new_flags - -# Thanks to https://github.com/decrispell/vim-config for this code -def FlagsForFile( filename, **kwargs ): - """ given the source filename, return the compiler flags """ - opt_basename = '.clang_complete' - curr_dir = os.path.dirname(filename) - opt_fname = os.path.join(curr_dir, opt_basename) - # keep traversing up the tree until we find the file, or hit the root - while not os.path.exists(opt_fname): - new_dir = os.path.dirname(curr_dir) - if new_dir == curr_dir: - # we've reached the root of the tree - break - curr_dir = new_dir - opt_fname = os.path.join(curr_dir, opt_basename) +def FlagsFromClangComplete(root, filename): try: - fd = open(opt_fname, 'r') - except IOError: - return {'flags': default_flags, 'do_cache': True} - flags = [line.strip() for line in fd.readlines()] - relative_to = os.path.dirname(opt_fname) - flags = MakeRelativePathsInFlagsAbsolute(flags, relative_to) - return { - 'flags': flags, 'do_cache': True - } + clang_complete_path = FindNearest(root, '.clang_complete', filename) + clang_complete_flags = open(clang_complete_path, 'r').read().splitlines() + logging.info("%s: Using %s" % (os.path.basename(filename), clang_complete_path)) + return MakeRelativePathsInFlagsAbsolute(clang_complete_flags, + os.path.dirname(clang_complete_path)) + except: + return None + +def FlagsFromCompilationDatabase(root, filename): + try: + compilation_db_path = FindNearest(root, 'compile_commands.json', filename) + database = ycm_core.CompilationDatabase(os.path.dirname(compilation_db_path)) + if not database: + logging.info("%s: Compilation database file found but unable to load" + % os.path.basename(filename)) + return None + extension = os.path.splitext(filename)[1] + if extension in HEADER_EXTENSIONS: + compilation_info = GetFlagsForHeader(compilation_db_path, database, filename) + else: + compilation_info = GetFlagsForSourceFile(database, filename) + if not compilation_info: + logging.info("%s: No compilation info for %s in compilation database" + % (os.path.basename(filename), filename)) + return None + return MakeRelativePathsInFlagsAbsolute(compilation_info.compiler_flags_, + compilation_info.compiler_working_dir_) + except Exception, e: + logging.info("%s: Could not get compilation flags from db: %s" + % (os.path.basename(filename), e)) + return None + +def GetFlagsForHeader(database_path, database, filename): + flags = FindFileInDb(database, filename) + if flags: + return flags + flags = FindSiblingFileForHeader(database, filename) + if flags: + return flags + flags = SearchForTranslationUnitWhichIncludesPath(database_path, + database, + os.path.dirname(filename), + filename) + if flags: + return flags + return FindNearestSourceFileInDb(database, os.path.dirname(filename), filename) + +def GetFlagsForSourceFile (database, filename): + flags = FindFileInDb(database, filename) + if flags: + return flags + return FindNearestSourceFileInDb(database, os.path.dirname(filename), filename) + +def FindNearest(path, target, filename): + candidate = os.path.join(path, target) + if(os.path.isfile(candidate) or os.path.isdir(candidate)): + logging.info("%s: Found nearest %s at %s" + % (os.path.basename(filename), target, candidate)) + return candidate; + else: + parent = os.path.dirname(os.path.abspath(path)); + if(parent == path): + raise RuntimeError("could not find %s" % target); + return FindNearest(parent, target, filename) + +def FindFileInDb(database, filename): + logging.info("%s: Trying to find file in database..." + % (os.path.basename(filename))) + flags = database.GetCompilationInfoForFile(filename) + if flags.compiler_flags_: + logging.info("%s: Found file in database." + % (os.path.basename(filename))) + return flags + logging.info("%s: File not found in compilation db." + % (os.path.basename(filename))) + return None + +def FindSiblingFileForHeader(database, filename): + logging.info("%s: Trying to find a sibling source file for that header in database..." + % (os.path.basename(filename))) + basename = os.path.splitext(filename)[0] + for extension in SOURCE_EXTENSIONS: + replacement_file = basename + extension + if os.path.exists(replacement_file): + compilation_info = database.GetCompilationInfoForFile(replacement_file) + if compilation_info.compiler_flags_: + logging.info("%s: Found sibling source file: %s" + % (os.path.basename(filename), replacement_file)) + return compilation_info + logging.info("%s: Did not find sibling source file." + % (os.path.basename(filename))) + return None + +# Todo: search children directories AND parent directories +# Todo: we don't need dirname +def FindNearestSourceFileInDb(database, dirname, refFile): + logging.info("%s: Trying to find nearest source file in database..." + % (os.path.basename(refFile))) + refFile = os.path.split(refFile)[1] + for root, dirnames, filenames in os.walk(dirname): + for filename in filenames: + if filename.endswith(tuple(SOURCE_EXTENSIONS)): + if str(filename) != str(refFile): + compilation_info = database.GetCompilationInfoForFile(str(os.path.join(root, filename))) + if compilation_info.compiler_flags_: + logging.info("%s: Found nearest source file from %s: %s" + % (refFile, refFile, str(os.path.join(root, filename)))) + return compilation_info + logging.info("%s: Could not find nearest source file from %s in compilation db." % (refFile, refFile)) + return None + +def Pairwise(iterable): + "s -> (s0,s1), (s1,s2), (s2, s3), ..." + a, b = itertools.tee(iterable) + next(b, None) + return itertools.izip(a, b) + +def RemoveClosingSlash(path): + if path.endswith('/'): + path = path[:-1] + return path + +def SearchForTranslationUnitWhichIncludesPath(database_path, database, path, filename): + logging.info("%s: Trying to find a translation unit that includes our header's path..." + % (os.path.basename(filename))) + with open(database_path, 'r') as f: + jsonDb = json.load(f) + path = RemoveClosingSlash(os.path.abspath(path)) + found = [] + for translationUnit in jsonDb: + buildDir = translationUnit["directory"] + switches = translationUnit["command"].split() + for currentSwitch, nextSwitch in Pairwise(switches): + matchObj = re.match(r'(-I|-isystem)(.*)', currentSwitch) + includeDir = "" + isIncFlag = False + if currentSwitch == "-I" or currentSwitch == "-isystem": + includeDir = nextSwitch + isIncFlag = True + elif matchObj: + includeDir = matchObj.group(2) + isIncFlag = True + if not isIncFlag: + continue + includeDir = RemoveClosingSlash(os.path.abspath(os.path.join(buildDir, includeDir))) + # Check all the parent dirs in path + pathCopy = path + distance = 0 + while pathCopy != os.path.abspath(os.sep): + if includeDir == pathCopy: + found.append((distance, str(translationUnit["file"]))) + distance += 1 + pathCopy, tail = os.path.split(pathCopy) + found.sort() + if len(found) == 0: + logging.info("%s: Did not find translation unit which includes path %s" + % (os.path.basename(filename), path)) + return None + else: + result = found[0][1] + logging.info("%s: Found best source file which includes path: %s" + % (os.path.basename(filename), result)) + return database.GetCompilationInfoForFile(result) + +def GetUserExtraFlags(filename): + try: + extra_flags_file = FindNearest(os.path.dirname(filename), ".ycm_extra_flags", filename) + except: + logging.info("%s: No extra flags." + % (os.path.basename(filename))) + return None + with open(extra_flags_file, 'r') as f: + lines = f.readlines() + lines = [ line[0:line.find("#")].split() for line in lines ] + lines = list(itertools.chain.from_iterable(lines)) + logging.info("%s: Extra flags = [\n\t\t%s\n]" + % (os.path.basename(filename), "\n\t\t".join(lines))) + return lines + +def MakeRelativePathsInFlagsAbsolute(flags, working_directory): + if not working_directory: + return list(flags) + new_flags = [] + make_next_absolute = False + for flag in flags: + new_flag = flag + if make_next_absolute: + make_next_absolute = False + if not flag.startswith('/'): + new_flag = os.path.join(working_directory, flag) + for path_flag in [ '-isystem', '-I', '-iquote', '--sysroot=' ]: + if flag == path_flag: + make_next_absolute = True + break + if flag.startswith(path_flag): + path = flag[ len(path_flag): ] + new_flag = path_flag + os.path.join(working_directory, path) + break + if new_flag: + new_flags.append(new_flag) + return new_flags