Skip to content

Commit

Permalink
Add extra conf support to Clangd completer
Browse files Browse the repository at this point in the history
  • Loading branch information
micbou committed May 16, 2019
1 parent 3b5d0bc commit 6e395b2
Show file tree
Hide file tree
Showing 15 changed files with 465 additions and 224 deletions.
59 changes: 30 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,14 +97,13 @@ non-semantic.

There are also several semantic engines in YCM. There's a libclang-based
completer and [clangd][clangd]-based completer that both provide semantic
completion for C-family languages. The [clangd][clangd]-based completer doesn't
support extra conf; you must have a compilation database. [clangd][clangd]
support is currently **experimental** and changes in the near future might break
backwards compatibility. There's also a Jedi-based completer for semantic
completion for Python, an OmniSharp-based completer for C#, a
[Gocode][gocode]-based completer for Go (using [Godef][godef] for jumping to
definitions), a TSServer-based completer for JavaScript and TypeScript, and a
[jdt.ls][jdtls]-based server for Java. More will be added with time.
completion for C-family languages. [clangd][clangd] support is currently
**experimental** and changes in the near future might break backwards
compatibility. There's also a Jedi-based completer for semantic completion for
Python, an OmniSharp-based completer for C#, a [Gocode][gocode]-based completer
for Go (using [Godef][godef] for jumping to definitions), a TSServer-based
completer for JavaScript and TypeScript, and a [jdt.ls][jdtls]-based server for
Java. More will be added with time.

There are also other completion engines, like the filepath completer (part of
the identifier completer).
Expand Down Expand Up @@ -217,8 +216,8 @@ The `.ycm_extra_conf.py` module may define the following functions:
#### `Settings( **kwargs )`

This function allows users to configure the language completers on a per project
basis or globally. Currently, it is required by the C-family completer and
optional for the Python completer. The following arguments can be retrieved from
basis or globally. Currently, it is required by the libclang-based completer and
optional for other completers. The following arguments can be retrieved from
the `kwargs` dictionary and are common to all completers:

- `language`: an identifier of the completer that called the function. Its value
Expand All @@ -231,7 +230,7 @@ the `kwargs` dictionary and are common to all completers:
language = kwargs[ 'language' ]
if language == 'cfamily':
return {
# Settings for the C-family completer.
# Settings for the libclang and clangd-based completer.
}
if language == 'python':
return {
Expand All @@ -240,6 +239,8 @@ the `kwargs` dictionary and are common to all completers:
return {}
```

- `filename`: absolute path of the file currently edited.

- `client_data`: any additional data supplied by the client application.
See the [YouCompleteMe documentation][extra-conf-vim-data-doc] for an
example.
Expand All @@ -248,32 +249,32 @@ The return value is a dictionary whose content depends on the completer.

##### C-family settings

The `Settings` function is called by the C-family completer to get the compiler
flags to use when compiling the current file. The absolute path of this file is
accessible under the `filename` key of the `kwargs` dictionary.
[clangd][clangd]-based completer doesn't support extra conf files. If you are
using [clangd][clangd]-based completer, you must have a compilation database in
your project's root or in one of the parent directories to provide compiler
flags.
The `Settings` function is called by the libclang and clangd-based completers to
get the compiler flags to use when compiling the current file. The absolute path
of this file is accessible under the `filename` key of the `kwargs` dictionary.

The return value expected by the completer is a dictionary containing the
The return value expected by both completers is a dictionary containing the
following items:

- `flags`: (mandatory) a list of compiler flags.
- `flags`: (mandatory for libclang, optional for clangd) a list of compiler
flags.

- `include_paths_relative_to_dir`: (optional) the directory to which the
include paths in the list of flags are relative. Defaults to ycmd working
directory.

- `override_filename`: (optional) a string indicating the name of the file to
parse as the translation unit for the supplied file name. This fairly
advanced feature allows for projects that use a 'unity'-style build, or
for header files which depend on other includes in other files.
- `include_paths_relative_to_dir`: (optional) the directory to which the include
paths in the list of flags are relative. Defaults to ycmd working directory
for the libclang completer and `.ycm_extra_conf.py`'s directory for the
clangd completer.

- `do_cache`: (optional) a boolean indicating whether or not the result of
this call (i.e. the list of flags) should be cached for this file name.
Defaults to `True`. If unsure, the default is almost always correct.

The libclang-based completer also supports the following items:

- `override_filename`: (optional) a string indicating the name of the file to
parse as the translation unit for the supplied file name. This fairly advanced
feature allows for projects that use a 'unity'-style build, or for header
files which depend on other includes in other files.

- `flags_ready`: (optional) a boolean indicating that the flags should be
used. Defaults to `True`. If unsure, the default is almost always correct.

Expand Down Expand Up @@ -393,7 +394,7 @@ License
-------

This software is licensed under the [GPL v3 license][gpl].
© 2015-2018 ycmd contributors
© 2015-2019 ycmd contributors

[ycmd-users]: https://groups.google.com/forum/?hl=en#!forum/ycmd-users
[ycm]: http://valloric.github.io/YouCompleteMe/
Expand Down
82 changes: 81 additions & 1 deletion ycmd/completers/cpp/clangd_completer.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@
import os
import subprocess

from ycmd import responses
from ycmd import extra_conf_store, responses
from ycmd.completers.completer_utils import GetFileLines
from ycmd.completers.cpp.flags import ( RemoveUnusedFlags,
ShouldAllowWinStyleFlags )
from ycmd.completers.language_server import simple_language_server_completer
from ycmd.completers.language_server import language_server_completer
from ycmd.completers.language_server import language_server_protocol as lsp
Expand Down Expand Up @@ -200,6 +202,27 @@ def ShouldEnableClangdCompleter( user_options ):
return True


def AddCompilerToFlags( flags, enable_windows_style_flags ):
"""Removes everything before the first flag and returns the remaining flags
prepended with clangd."""
for index, flag in enumerate( flags ):
if ( flag.startswith( '-' ) or
( enable_windows_style_flags and
flag.startswith( '/' ) and
not os.path.exists( flag ) ) ):
flags = flags[ index: ]
break
return [ 'clangd' ] + flags


def BuildCompilationCommand( flags, filepath ):
"""Returns a compilation command from a list of flags and a file."""
enable_windows_style_flags = ShouldAllowWinStyleFlags( flags )
flags = AddCompilerToFlags( flags, enable_windows_style_flags )
flags = RemoveUnusedFlags( flags, filepath, enable_windows_style_flags )
return flags + [ filepath ]


class ClangdCompleter( simple_language_server_completer.SimpleLSPCompleter ):
"""A LSP-based completer for C-family languages, powered by Clangd.
Expand All @@ -214,6 +237,17 @@ def __init__( self, user_options ):

self._clangd_command = GetClangdCommand( user_options )
self._use_ycmd_caching = user_options[ 'clangd_uses_ycmd_caching' ]
self._flags_for_file = {}


def _Reset( self ):
with self._server_state_mutex:
super( ClangdCompleter, self )._Reset()
self._flags_for_file = {}


def GetCompleterName( self ):
return 'C-family'


def GetServerName( self ):
Expand All @@ -224,6 +258,10 @@ def GetCommandLine( self ):
return self._clangd_command


def Language( self ):
return 'cfamily'


def SupportedFiletypes( self ):
return ( 'c', 'cpp', 'objc', 'objcpp', 'cuda' )

Expand Down Expand Up @@ -361,3 +399,45 @@ def GetDetailedDiagnostic( self, request_data ):
minimum_distance = distance

return responses.BuildDisplayMessageResponse( message )


def _SendFlagsFromExtraConf( self, request_data ):
"""Reads the flags from the extra conf of the given request and sends them
to Clangd as an entry of a compilation database using the
'compilationDatabaseChanges' configuration."""
filepath = request_data[ 'filepath' ]

with self._server_info_mutex:
module = extra_conf_store.ModuleForSourceFile( filepath )
if not module:
return

settings = self.GetSettings( module, request_data )
if settings.get( 'do_cache', True ) and filepath in self._flags_for_file:
return

flags = settings.get( 'flags', [] )

self.GetConnection().SendNotification( lsp.DidChangeConfiguration( {
'compilationDatabaseChanges': {
filepath: {
'compilationCommand': BuildCompilationCommand( flags, filepath ),
'workingDirectory': settings.get( 'include_paths_relative_to_dir',
self._project_directory )
}
}
} ) )

self._flags_for_file[ filepath ] = flags


def OnFileReadyToParse( self, request_data ):
return super( ClangdCompleter, self ).OnFileReadyToParse(
request_data,
handlers = [ lambda self: self._SendFlagsFromExtraConf( request_data ) ] )


def ExtraDebugItems( self, request_data ):
return [ responses.DebugInfoItem(
'using flags from extra configuration file',
self._flags_for_file.get( request_data[ 'filepath' ], False ) ) ]
20 changes: 10 additions & 10 deletions ycmd/completers/cpp/flags.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ def _ParseFlagsFromExtraConfOrDatabase( self,
sanitized_flags = PrepareFlagsForClang( flags,
filename,
add_extra_clang_flags,
_ShouldAllowWinStyleFlags( flags ) )
ShouldAllowWinStyleFlags( flags ) )

if results.get( 'do_cache', True ):
self.flags_for_file[ filename, client_data ] = sanitized_flags, filename
Expand Down Expand Up @@ -248,7 +248,7 @@ def _ExtractFlagsList( flags_for_file_output ):
return [ ToUnicode( x ) for x in flags_for_file_output[ 'flags' ] ]


def _ShouldAllowWinStyleFlags( flags ):
def ShouldAllowWinStyleFlags( flags ):
if OnWindows():
# Iterate in reverse because we only care
# about the last occurrence of --driver-mode flag.
Expand Down Expand Up @@ -302,7 +302,7 @@ def PrepareFlagsForClang( flags,
enable_windows_style_flags = False ):
flags = _AddLanguageFlagWhenAppropriate( flags, enable_windows_style_flags )
flags = _RemoveXclangFlags( flags )
flags = _RemoveUnusedFlags( flags, filename, enable_windows_style_flags )
flags = RemoveUnusedFlags( flags, filename, enable_windows_style_flags )
if add_extra_clang_flags:
# This flag tells libclang where to find the builtin includes.
flags.append( '-resource-dir=' + CLANG_RESOURCE_DIR )
Expand Down Expand Up @@ -407,7 +407,7 @@ def _AddLanguageFlagWhenAppropriate( flags, enable_windows_style_flags ):
return flags


def _RemoveUnusedFlags( flags, filename, enable_windows_style_flags ):
def RemoveUnusedFlags( flags, filename, enable_windows_style_flags ):
"""Given an iterable object that produces strings (flags for Clang), removes
the '-c' and '-o' options that Clang does not like to see when it's producing
completions for a file. Same for '-MD' etc.
Expand Down Expand Up @@ -455,8 +455,8 @@ def _RemoveUnusedFlags( flags, filename, enable_windows_style_flags ):
# flags for headers. The returned flags include "foo.cpp" and we need to
# remove that.
if _SkipStrayFilenameFlag( current_flag,
previous_flag,
enable_windows_style_flags ):
previous_flag,
enable_windows_style_flags ):
continue

new_flags.append( flag )
Expand All @@ -465,8 +465,8 @@ def _RemoveUnusedFlags( flags, filename, enable_windows_style_flags ):


def _SkipStrayFilenameFlag( current_flag,
previous_flag,
enable_windows_style_flags ):
previous_flag,
enable_windows_style_flags ):
current_flag_starts_with_slash = current_flag.startswith( '/' )
previous_flag_starts_with_slash = previous_flag.startswith( '/' )

Expand Down Expand Up @@ -622,7 +622,7 @@ def _MakeRelativePathsInFlagsAbsolute( flags, working_directory ):
new_flags = []
make_next_absolute = False
path_flags = ( PATH_FLAGS + INCLUDE_FLAGS_WIN_STYLE
if _ShouldAllowWinStyleFlags( flags )
if ShouldAllowWinStyleFlags( flags )
else PATH_FLAGS )
for flag in flags:
new_flag = flag
Expand Down Expand Up @@ -685,7 +685,7 @@ def UserIncludePaths( user_flags, filename ):
'-isystem': include_paths,
'-F': framework_paths,
'-iframework': framework_paths }
if _ShouldAllowWinStyleFlags( user_flags ):
if ShouldAllowWinStyleFlags( user_flags ):
include_flags[ '/I' ] = include_paths

try:
Expand Down
36 changes: 22 additions & 14 deletions ycmd/completers/language_server/language_server_completer.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright (C) 2017-2018 ycmd contributors
# Copyright (C) 2017-2019 ycmd contributors
#
# This file is part of ycmd.
#
Expand Down Expand Up @@ -1087,10 +1087,12 @@ def _DiscoverSubcommandSupport( self, commands ):
return subcommands_map


def _GetSettings( self, module, client_data ):
def GetSettings( self, module, request_data ):
if hasattr( module, 'Settings' ):
settings = module.Settings( language = self.Language(),
client_data = client_data )
settings = module.Settings(
language = self.Language(),
filename = request_data[ 'filepath' ],
client_data = request_data[ 'extra_conf_data' ] )
if settings is not None:
return settings

Expand All @@ -1102,7 +1104,7 @@ def _GetSettings( self, module, client_data ):
def _GetSettingsFromExtraConf( self, request_data ):
module = extra_conf_store.ModuleForSourceFile( request_data[ 'filepath' ] )
if module:
settings = self._GetSettings( module, request_data[ 'extra_conf_data' ] )
settings = self.GetSettings( module, request_data )
self._settings = settings.get( 'ls' ) or {}
# Only return the dir if it was found in the paths; we don't want to use
# the path of the global extra conf as a project root dir.
Expand Down Expand Up @@ -1130,7 +1132,7 @@ def _StartAndInitializeServer( self, request_data, *args, **kwargs ):
self._SendInitialize( request_data, extra_conf_dir )


def OnFileReadyToParse( self, request_data ):
def OnFileReadyToParse( self, request_data, handlers = None ):
if not self.ServerIsHealthy() and not self._server_started:
# We have to get the settings before starting the server, as this call
# might throw UnknownExtraConf.
Expand All @@ -1139,17 +1141,23 @@ def OnFileReadyToParse( self, request_data ):
if not self.ServerIsHealthy():
return

# If we haven't finished initializing yet, we need to queue up a call to
# _UpdateServerWithFileContents. This ensures that the server is up to date
# as soon as we are able to send more messages. This is important because
# server start up can be quite slow and we must not block the user, while we
# must keep the server synchronized.
# If we haven't finished initializing yet, we need to queue up
# all functions from |handlers| and _UpdateServerWithFileContents. This
# ensures that the server is up to date as soon as we are able to send more
# messages. This is important because server start up can be quite slow and
# we must not block the user, while we must keep the server synchronized.
if handlers is None:
handlers = []
handlers.append(
lambda self: self._UpdateServerWithFileContents( request_data ) )

if not self._initialize_event.is_set():
self._OnInitializeComplete(
lambda self: self._UpdateServerWithFileContents( request_data ) )
for handler in handlers:
self._OnInitializeComplete( handler )
return

self._UpdateServerWithFileContents( request_data )
for handler in handlers:
handler( self )

# Return the latest diagnostics that we have received.
#
Expand Down
Loading

0 comments on commit 6e395b2

Please sign in to comment.