Implementation of DidChangeConfiguration notification handling in clangd.
This currently only supports changing one setting: the path of the compilation database to be used for the current project
In other words, it is no longer necessary to restart clangd with a different command line argument in order to change
the compilation database.
Details
Diff Detail
- Repository
- rCTE Clang Tools Extra
- Build Status
Buildable 13001 Build 13001: arc lint + arc unit
Event Timeline
clangd/ClangdLSPServer.cpp | ||
---|---|---|
43 | Are you sure the existing tests don't fail? usually when we change this, some tests have to be adjusted. | |
246 | I'm thinking, maybe it's better not to have the map and just pass along the ClangdConfigurationParams to the server (instead of the map). I think ClangdConfigurationParams is more useful as a structure than a "flat" map with all the keys being at the same level. In ClangdConfigurationParams, we'll be able to add sub-configurations sections (index, code-completion, more?) which is well suited to reflect the JSON format. Unless perhaps you had another use case for the map that I'm not thinking about? | |
248 | Won't getValue crash if compilationDatabasePath was not set since it's optional? | |
250 | Why is this line needed? Maybe there should be a comment? | |
255 | Pass Params.settings? see previous comment. | |
clangd/ClangdServer.cpp | ||
516 | I think we need to discuss what should happen when the compilation database changes. Since this is mainly for switching "configuration", I think it should invalidate all previously parsed units. Otherwise, if the user has a file open in the editor, it will report diagnostics based on the old configuration and similarly code completion will be outdated (until Clangd is restarted?). Would it be unreasonable to reparse all units in memory? Perhaps ClangdServer::forceReparse would be useful for that? | |
clangd/GlobalCompilationDatabase.h | ||
57 | remove? | |
58 | call getCompilationDatabase from here? | |
60 | move back to private? | |
clangd/Protocol.cpp | ||
183 | I think this needs a test, perhaps create did-change-configuration.test? It can also test the ClangdConfigurationParams::parse code
There are similar tests in execute-command.test if you'd like an example. | |
210 | If "settings" was not set in the end, return llvm::None, because this is mandatory in the protocol. | |
227 | Here there should be a test when "compilationDatabasePath" is not a scalar node. You can test this with a scalar node like "compilationDatabasePath": {} | |
231 | else { logIgnoredField(KeyValue, Logger); } ? "customField": "" } | |
clangd/Protocol.h | ||
260 | I think there should to be comment to mention that this is a Clangd extension. | |
269 | I don't think you need this constructor? | |
271 | Maybe remove this to make it more consistent with the other interfaces/struct. We usually just assign to fields after the struct is instantiated, not in the constructor. | |
274 | I think there should to be comment to mention that this is a Clangd extension. |
I don't think we should pass the very general configuration map to ClangdServer. Especially given that we can easily update DirectoryBaseCompilationDatabase instance in ClangdLSPServer itself.
What are your thoughts on this?
clangd/ClangdLSPServer.cpp | ||
---|---|---|
246 | I don't think ClangdServer should handle changes to compilation database path at all. | |
clangd/ClangdServer.h | ||
295 | NIT: Add full stop at the end of comments. | |
296 | This function is way too general for ClangdServer's interface, can we have more fine-grained settings here (i.e. setCompletionParameters etc?) and handle the "general" case in ClangdLSPServer (i.e., unknown setting names, invalid setting parameters, json parsing, etc.)? I suggest we remove this function altogether. |
clangd/ClangdLSPServer.cpp | ||
---|---|---|
43 | I'll verify this when I will have the time. | |
clangd/ClangdServer.h | ||
296 | So if I understand correctly, we would have the most generic workspace/didChangeConfiguration handler located in ClangdLSPServer | |
clangd/Protocol.cpp | ||
183 | Yes, I was planning on adding a simple test in the coming days. I imagine this test will be greatly expanded on once more settings become available to change on the server side. |
clangd/ClangdServer.h | ||
---|---|---|
296 | Exactly! |
clangd/Protocol.h | ||
---|---|---|
269 | I do inside parse() for DidChangeConfigurationParams. |
Added test for didChangeConfiguration notification.
Compilation database and extra file flags are now properly reloaded when changing database path.
ClangdConfigurationParams now used instead of map.
changeConfiguration removed from ClangdServer. This class will now handle more specific settings.
Added more checks when parsing notification from client..
clangd/ClangdServer.cpp | ||
---|---|---|
584 | Could we have a method in DraftStore that returns all active files in a vector<Path> instead? | |
585 | NIT: single-statement blocks don't need {} | |
clangd/ClangdServer.h | ||
318 | NIT: description of what function does seems more useful than a particular use-case. Maybe swap the two sentences in the comments? Also, DraftMgr is an implementation detail. Let's simply mentioned "all currently opened files" instead of the DraftMgr. | |
320 | Maybe place declaration of reparseOpenedFiles right after the declaration of forceReparse? | |
320 | forceReparse() exposes a future to wait to its completion. We should probably do the same in reparseOpenedFiles. | |
clangd/DraftStore.h | ||
44 | We shouldn't expose this in the interface, as it breaks the thread-safety of DraftStore. | |
clangd/GlobalCompilationDatabase.cpp | ||
108–109 | Accidental change? | |
clangd/GlobalCompilationDatabase.h | ||
65 | NIT: We don't need to call StringRef explicitly constructor here. | |
65 | Why do we need to call getCompilationDatabase here? Why not simply set the CompileCommandsDir and clear the CompilationDatabases cache? | |
74 | Accidental change? Why do we need to swap the order of these functions? | |
clangd/Protocol.h | ||
263 | Maybe call it ClangdConfigurationParamsChange to make it clear those are diffs, not the actual params? | |
268 | Why do we overload operator!? Can't we use llvm::Optional<ClangdConfigurationParams> where appropriate instead? | |
test/clangd/initialize-params-invalid.test | ||
2 | Accidental newline? | |
21 | NIT: the indent seems off by one character | |
test/clangd/initialize-params.test | ||
2 | Accidental newline? | |
21 | NIT: the indent seems off by one character |
clangd/Protocol.h | ||
---|---|---|
263 | The idea was that we can reuse the same struct for InitializeParams.initializationOptions |
clangd/Protocol.h | ||
---|---|---|
263 | Since InitializeParams.initializationOptions may also have unset values (llvm::None), it also seems fine to treat those as a "diff" between the default parameters and the new ones. I don't have a strong opinion on this one, though. If you'd prefer to keep the current name, it's totally fine with me. |
clangd/Protocol.h | ||
---|---|---|
263 | That makes sense the way you explained it. I think ClangdConfigurationParamsChange is good. |
Fixed DraftStore thread-safe API being broken
Removed superfluous getCompilationDatabase call
Changed name of struct to ClangDConfigurationParamsChange
Removed operator ! overload for struct
Minor code cleanup
clangd/GlobalCompilationDatabase.cpp | ||
---|---|---|
108–109 | Twine(File) and "in overriden directory" did not have a space to separate otherwise. |
clangd/ClangdLSPServer.cpp | ||
---|---|---|
251 | Replace Settings instead of Params.settings here? | |
clangd/ClangdServer.cpp | ||
584 | NIT: remote empty line | |
586 | DonePromise and DoneFuture are never used. Remove them? | |
592 | Use "const auto&" or StringRef instead to avoid copies? | |
clangd/ClangdServer.h | ||
241 | NIT: remove this empty line | |
clangd/GlobalCompilationDatabase.cpp | ||
108–109 | Right. Sorry, my mistake. | |
clangd/Protocol.h | ||
262 | NIT: remote empty line | |
264 | NIT: remote empty line | |
271 | NIT: remove empty line | |
272 | NIT: Use = default instead | |
273 | We need to initialize settings field with settings parameter. Maybe even better to remove both constructors to align with other classes in this file? | |
test/clangd/did-change-configuration.test | ||
16 | Do we want to add a # CHECK: for that output? | |
34 | clangd won't see this file. didOpen only sets contents for diagnostics, not any other features. Writing it under root ('/') is obviously not an option. Lit tests allow you to use temporary paths, this is probably an approach you could take. See lit docs for more details. | |
test/clangd/initialize-params-invalid.test | ||
1 | I am still seeing this newline |
test/clangd/did-change-configuration.test | ||
---|---|---|
34 | Are there examples available on how to use this? I have to use a # RUN: to create a file and then use it's path in a workspace/didChangeConfiguration notification? |
It seems this patch is out of date, we need to merge it with the latests head.
clangd/DraftStore.cpp | ||
---|---|---|
45 | Why do we need this assert? I don't see how empty file is worse than any other invalid value. | |
55 | Why do we need this assert? I don't see how empty file is worse than any other invalid value. | |
clangd/GlobalCompilationDatabase.h | ||
65 | We need to lock the Mutex here! | |
clangd/Protocol.cpp | ||
368 | NIT: maybe remove this empty line? | |
test/clangd/did-change-configuration.test | ||
34 | After thinking about it, I really don't see an easy way to test it currently. We just don't have the infrastructure in place for that. |
I just got familiar with the patch, I cleaned up the bits that I thought were
unnecessary for this change. For example, we don't need a toJSON function for
DidChangeConfigurationParams, or explicit constructors to
DidChangeConfigurationParams, etc.
clangd/ClangdLSPServer.cpp | ||
---|---|---|
65 | I find configurationChangeProvider a bit weird. It makes it sound like clangd can provide configuration changes. In reality, it can accept configuration changes. So I think this should be named something else. |
clangd/ClangdLSPServer.cpp | ||
---|---|---|
65 | Agreed, perhaps configurationChangeManager would be more appropriate then? |
clangd/ClangdLSPServer.cpp | ||
---|---|---|
65 | I'm thinking of removing it for the time being. Since it's not defined in the protocol what types of configuration changes exist (it's specific to each language server), it not very useful to simply advertise that we support configuration changes. We would need to advertise that we support compilation database changes in particular. I think this can be done later. |
Fix formatting, remove capability
- Fix some formatting nits
- Remove the entry in the capabilities object
clangd/ClangdLSPServer.cpp | ||
---|---|---|
244 | Following what you said here: https://reviews.llvm.org/D39571?id=124024#inline-359345 I have not really looked into what was wrong with the test, and what is missing in the infrastructure to make it work. But I assumed that the situation did not change since then. Can you enlighten me on what the problem was, and what is missing? | |
clangd/GlobalCompilationDatabase.h | ||
64 | Will do. |
clangd/ClangdLSPServer.cpp | ||
---|---|---|
244 | We usually write unittests for that kind of thing, since they allow to plug an in-memory filesystem, but we only test ClangdServer (examples are in unittests/clangd/ClangdTests.cpp). ClangdLSPServer does not allow to plug in a virtual filesystem (vfs). Even if we add vfs, it's still hard to unit-test because we'll have to match the json input/output directly. This leaves us with an option of a lit test that runs clangd directly, similar to tests in test/clangd. It's not impossible to write that test, it's just a bit involved. Having a test would be nice, though, to ensure we don't break this method while doing other things. Especially given that this functionality is not used anywhere in clangd. |
clangd/ClangdLSPServer.cpp | ||
---|---|---|
244 |
What do you mean by "we'll have to match the json input/output directly"? That we'll have to match the complete JSON output textually? Couldn't the test parse the JSON into some data structures, then we could assert specific things, like that this particular field is present and contains a certain substring, for example?
Ok, I see the complication with the Content-Length. I am not familiar with lit yet, so I don't know what it is capable of. But being able to craft and send arbitrary LSP messages would certainly be helpful in the future for all kinds of black box test, so having a framework that allows to do this would be helpful, I think. I'm not familiar enough with the ecosystem to do this right now, but I'll keep it in mind. One question about this particular test. Would there be some race condition here? If we do:
Since clangd is multi-threaded and does work in the background, are we sure that we'll get the result we want in #4?
I agree. For the time being, is it fine to leave the FIXME there? We can work on improving the test frameworks to get rid of it. |
clangd/ClangdLSPServer.cpp | ||
---|---|---|
65 | "configurationChangeProvider" is not in the LSP, right? | |
244 |
The interface to interact with ClangdLSPServer is JSONOutput, which only allows you to pass the output of requests to the stream at the moment. That means not only parsing json, but also finding the individual responses in the combined output.
Technically clangd can do everything in parallel, but we have a flag -run-synchronously that will make it do all the work on the main thread and we use that in the tests. LLVM has lots of tests that do substring matches, there's a special tool, called FileCheck, to make writing them simpler. See the test from my previous message (specifically the # CHECK: lines), they should be enough to get started.
FIXME looks fine for now, but make sure to test this code does really work for your use-case. |
clangd/ClangdLSPServer.cpp | ||
---|---|---|
244 |
So far I have just tested with some small hello-world projects, but I'll take the time to test with some bigger project (gdb). |
clangd/ClangdLSPServer.cpp | ||
---|---|---|
65 | I've removed it for now, we can add it later. We should think about how to express what we support. It's not enough to say we support the didChangeConfiguration notification, we should also express what element of configuration we support (the location of the compile commands directory, in this case). | |
244 |
I've tested many scenarios with a project of good size. It works as expected, but I've hit the assert in ClangdServer::forceReparse maybe twice. I don't know how to reproduce it. I think we can go ahead with the patch and maybe the problem will become clearer with time. At least even if this feature is not completely stable, it won't affect you if you don't use it. |
clangd/ClangdLSPServer.cpp | ||
---|---|---|
244 |
Exactly. |
clangd/ClangdLSPServer.cpp | ||
---|---|---|
244 | I am not sure how it can happen. The main thread calls DraftMgr.getActiveFiles(), which returns the keys of the Drafts map. These keys are then passed to forceReparse, which asserts that passed key is in Drafts. The only way for it not to be in Drafts would be if it was removed in the mean time. But only the main thread can remove something from Drafts, if the didClose notification is received. And we know it didn't, because it's busy handling the didChangeConfiguration notification. So I'm confused. |
clangd/ClangdLSPServer.cpp | ||
---|---|---|
244 | I think I got it. Now we definitely need a fix and a test for that :-) | |
clangd/DraftStore.cpp | ||
29 | Only add those entries where value is not None! |
clangd/ClangdLSPServer.cpp | ||
---|---|---|
244 | Ah you are right! "didClosing" a document before doing the config change triggers the assert.
Are you saying that closing a document should remove the entry from the DraftMgr instead of just setting it to None? If so I can make a patch for that, it seems easy enough for a beginner. Or is the current behavior correct and we just need to make a test to verify it? |
clangd/ClangdLSPServer.cpp | ||
---|---|---|
244 | Unfortunately we can't move away from storing versions for removed files right now. We rely on them to report diagnostics in a consistent order (this is due to the weirdness of the threading model clangd currently uses, it's going away soon but we still need some time before that happens). |
Fix assertion about parsing a document that is not open
As found by Ilya, the getActiveFiles method would return the documents that were previously opened and then closed. This would cause the assertion in forceReparse:
assert(FileContents.Draft && "forceReparse() was called for non-added document");
to trigger. I changed getActiveFiles to return only the files/drafts that are open at the time of the call.
I haven't looked at the newest patch yet but I gave it a quick try and saw something odd. If I change the configuration to something invalid (say I specify the path to a CMakeLists.txt), then I get many errors/diagnostics, which is normal. But then when I change the config to something valid, the same diagnostics re-emitted, as if the configuration failed to change. I'll check the code tomorrow a bit but I thought I'd share this with you early.
Maybe you were still seeing diagnostics from the older versions of the files? clangd should eventually produce the right diagnostics, but if processing of some of the requests is in-flight at the time there results may still be reported.
BTW, I don't know if you've seen it before, but there's -input-mirror-file option to clangd that records the lsp input which can be used to rerun clangd later.
# This should be run by VSCode (or Theia) clangd -input-mirror-file=/tmp/clangd.input # Later we can rerun from command line $ clangd < /tmp/clangd.input # Pass the flag to execute everyhing on one thread $ clangd -run-synchronously < /tmp/clangd.input
This is useful to debug/rerun and see the logs, etc.
This patch is good to go after remove std::future<> from reparseOpenedFiles, unless @malaperle discovers there are other problems.
Ideally, it'd be also nice to have a test for it.
clangd/ClangdServer.cpp | ||
---|---|---|
583 | We're not returning futures from forceReparse anymore, this function has to be updated accordingly. |
I think I managed to make some tests by using the MockCompilationDatabase. Basically with some code like:
#ifndef MACRO static void func () {} // 1 #else static void func () {} // 2 #endif
and these steps:
- Server.addDocument(...)
- Server.findDefinitions (assert that it returns definition 1)
- CDB.ExtraClangFlags.push_back("-DMACRO=1")
- Server.reparseOpenedFiles()
- Server.findDefinitions (assert that it returns definition 2)
Right now that test fails, but it's not clear to me whether it's because the test is wrong or there's really a bug in there. I'll upload a work-in-progress version.
clangd/ClangdServer.cpp | ||
---|---|---|
583 | Indeed, I found this by rebasing. |
Add tests, work in progress
Can you take a look at the "TEST(DidChangeConfiguration, DifferentDeclaration)"
test, and tell me if you see anything wrong with it? It seems to me like
changes in command line arguments (adding -DMACRO=1) are not taken into account
when changing configuration.
The other test (also very much work-in-progress) would be to verify that
changing configuration gets us the right diagnostics.
It looks like a bug in the preamble handling. (It does not check if macros were redefined).
You can workaround that by making sure the preamble ends before your code starts (preamble only captures preprocessor directives, so any C++ decl will end it):
Annotations SourceAnnotations(R"cpp( int avoid_preamble; #ifndef MACRO $before[[static void bob() {}]] #else $after[[static void bob() {}]] #endif /// .... )cpp"
unittests/clangd/XRefsTests.cpp | ||
---|---|---|
260 ↗ | (On Diff #134097) | I'd move it to ClangdTests.cpp, generic ClangdServer tests usually go there. It's fine to #include "Annotations.h" there, too, even though it hasn't been used before. |
271 ↗ | (On Diff #134097) | Specifying /*UseRelPath=*/true is not necessary for this test, default value should do. |
Ah ok, indeed this test now works when I add this.
The other test I am working on (at the bottom of the file) acts weird, even if I add int avoid_preamble. The idea of the test is:
- Set -DWITH_ERROR=1 in the compile commands database
- Add the document, expect one error diagnostic
- Unset WITH_ERROR in the compile commands database
- Reparse the file, expect no error diagnostic
If I do it in this order, I get one diagnostic both times, when I don't expect one the second time the file is parsed. But if I do it the other way (first parse with no errors, second parse with an error), it works fine.
unittests/clangd/XRefsTests.cpp | ||
---|---|---|
260 ↗ | (On Diff #134097) | Ok. I put it there for rapid prototyping, but I also though it didn't really belong there. |
271 ↗ | (On Diff #134097) | Ok. |
That looks like another preamble-handling bug. It's much easier to fix and it's clangd-specific, so I'll make sure to fix that soon.
Thanks for bringing this up, we haven't been testing compile command changes thoroughly. I'll come up with a fix shortly.
Before this is fixed, you could try writing a similar test with other, non-preprocessor, flags. See ClangdVFSTest.ForceReparseCompileCommand that tests a similar thing by changing -xc to -xc++.
I'll make sure to test the -D case when fixing the bug.
New version
Added a test for reparseOpenedFiles. It includes testing adding a file,
removing it, and reparsing the opened files, since this was broken at some
point.
New version, take 2
The previous version contains the changes of already merged patches, I'm not
sure what I did wrong. This is another try.
unittests/clangd/ClangdTests.cpp | ||
---|---|---|
83 ↗ | (On Diff #135153) | NIT: misspelling: ErrorCHecking instead of ErrorChecking |
94 ↗ | (On Diff #135153) | This function should be const. |
99 ↗ | (On Diff #135153) | This function should be const and assert that P is in the map. |
111 ↗ | (On Diff #135153) | Maybe replace std::map<Path,bool> to llvm::StringMap<bool>? |
492 ↗ | (On Diff #135153) | Maybe expose a copy of the map from DiagConsumer and check for all files in a single line? class MultipleErrorCHeckingDiagConsumer { /// Exposes all files consumed by onDiagnosticsReady in an unspecified order. /// For each file, a bool value indicates whether the last diagnostics contained an error. std::vector<std::pair<Path, bool>> filesWithDiags() const { /* ... */ } }; /// .... EXPECT_THAT(DiagConsumer.filesWithDiags(), UnorderedElementsAre(Pair(FooCpp, false), Pair(BarCpp, true), Pair(BazCpp, false)); It would make the test more concise. |
unittests/clangd/ClangdTests.cpp | ||
---|---|---|
492 ↗ | (On Diff #135153) | Nice :) |
unittests/clangd/ClangdTests.cpp | ||
---|---|---|
492 ↗ | (On Diff #135153) | It's also better because we don't have to explicitly check that Baz is not there. So if some other file sneaks in the result for some reason and shouldn't be there, the test will now fail, where it wouldn't have previously. |
Are you sure the existing tests don't fail? usually when we change this, some tests have to be adjusted.