ycmd layer: a better global_conf.py

This commit is contained in:
deb0ch 2016-08-29 16:23:34 +02:00 committed by Eivind Fonn
parent 64a1b85523
commit 88742b564f
2 changed files with 302 additions and 80 deletions

View File

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

View File

@ -1,84 +1,274 @@
# global_conf.py --- ycmd global configuration file for Spacemacs
#
# Copyright (c) 2012-2016 Sylvain Benner & Contributors
#
# Author: Thomas de Beauchene <thomas.de.beauchene@gmail.com>
# 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