Index: clangd/ClangdLSPServer.cpp =================================================================== --- clangd/ClangdLSPServer.cpp +++ clangd/ClangdLSPServer.cpp @@ -74,6 +74,7 @@ JSONOutput &Out) override; void onSwitchSourceHeader(TextDocumentIdentifier Params, StringRef ID, JSONOutput &Out) override; + void onFileEvent(const DidChangeWatchedFilesParams &Params) override; private: ClangdLSPServer &LangServer; @@ -112,6 +113,11 @@ Params.textDocument.text); } +void ClangdLSPServer::LSPProtocolCallbacks::onFileEvent( + const DidChangeWatchedFilesParams &Params) { + LangServer.Server.onFileEvent(Params); +} + void ClangdLSPServer::LSPProtocolCallbacks::onDocumentDidChange( DidChangeTextDocumentParams Params, JSONOutput &Out) { // We only support full syncing right now. Index: clangd/ClangdServer.h =================================================================== --- clangd/ClangdServer.h +++ clangd/ClangdServer.h @@ -268,6 +268,8 @@ /// Waits until all requests to worker thread are finished and dumps AST for /// \p File. \p File must be in the list of added documents. std::string dumpAST(PathRef File); + /// Called when an event occurs for a watched file in the workspace. + void onFileEvent(const DidChangeWatchedFilesParams &Params); private: std::future Index: clangd/ClangdServer.cpp =================================================================== --- clangd/ClangdServer.cpp +++ clangd/ClangdServer.cpp @@ -424,3 +424,8 @@ std::move(DeferredCancel)); return DoneFuture; } + +void ClangdServer::onFileEvent(const DidChangeWatchedFilesParams &Params) { + // TODO: Do nothing for now. This will be used for indexing and potentially + // invalidating other caches. +} Index: clangd/Protocol.h =================================================================== --- clangd/Protocol.h +++ clangd/Protocol.h @@ -237,6 +237,33 @@ parse(llvm::yaml::MappingNode *Params, clangd::Logger &Logger); }; +enum class FileChangeType { + /// The file got created. + Created = 1, + /// The file got changed. + Changed = 2, + /// The file got deleted. + Deleted = 3 +}; + +struct FileEvent { + /// The file's URI. + URI uri; + /// The change type. + FileChangeType type; + + static llvm::Optional parse(llvm::yaml::MappingNode *Params, + clangd::Logger &Logger); +}; + +struct DidChangeWatchedFilesParams { + /// The actual file events. + std::vector changes; + + static llvm::Optional + parse(llvm::yaml::MappingNode *Params, clangd::Logger &Logger); +}; + struct FormattingOptions { /// Size of a tab in spaces. int tabSize; Index: clangd/Protocol.cpp =================================================================== --- clangd/Protocol.cpp +++ clangd/Protocol.cpp @@ -447,6 +447,84 @@ return Result; } +llvm::Optional FileEvent::parse(llvm::yaml::MappingNode *Params, + clangd::Logger &Logger) { + llvm::Optional Result = FileEvent(); + for (auto &NextKeyValue : *Params) { + // We have to consume the whole MappingNode because it doesn't support + // skipping and we want to be able to parse further valid events. + if (!Result) + continue; + + auto *KeyString = dyn_cast(NextKeyValue.getKey()); + if (!KeyString) { + Result.reset(); + continue; + } + + llvm::SmallString<10> KeyStorage; + StringRef KeyValue = KeyString->getValue(KeyStorage); + auto *Value = + dyn_cast_or_null(NextKeyValue.getValue()); + if (!Value) { + Result.reset(); + continue; + } + llvm::SmallString<10> Storage; + if (KeyValue == "uri") { + Result->uri = URI::parse(Value); + } else if (KeyValue == "type") { + long long Val; + if (llvm::getAsSignedInteger(Value->getValue(Storage), 0, Val)) { + Result.reset(); + continue; + } + Result->type = static_cast(Val); + if (Result->type < FileChangeType::Created || + Result->type > FileChangeType::Deleted) + Result.reset(); + } else { + logIgnoredField(KeyValue, Logger); + } + } + return Result; +} + +llvm::Optional +DidChangeWatchedFilesParams::parse(llvm::yaml::MappingNode *Params, + clangd::Logger &Logger) { + DidChangeWatchedFilesParams Result; + for (auto &NextKeyValue : *Params) { + auto *KeyString = dyn_cast(NextKeyValue.getKey()); + if (!KeyString) + return llvm::None; + + llvm::SmallString<10> KeyStorage; + StringRef KeyValue = KeyString->getValue(KeyStorage); + auto *Value = NextKeyValue.getValue(); + + llvm::SmallString<10> Storage; + if (KeyValue == "changes") { + auto *Seq = dyn_cast(Value); + if (!Seq) + return llvm::None; + for (auto &Item : *Seq) { + auto *I = dyn_cast(&Item); + if (!I) + return llvm::None; + auto Parsed = FileEvent::parse(I, Logger); + if (Parsed) + Result.changes.push_back(std::move(*Parsed)); + else + Logger.log("Failed to decode a FileEvent.\n"); + } + } else { + logIgnoredField(KeyValue, Logger); + } + } + return Result; +} + llvm::Optional TextDocumentContentChangeEvent::parse(llvm::yaml::MappingNode *Params, clangd::Logger &Logger) { Index: clangd/ProtocolHandlers.h =================================================================== --- clangd/ProtocolHandlers.h +++ clangd/ProtocolHandlers.h @@ -50,7 +50,8 @@ virtual void onGoToDefinition(TextDocumentPositionParams Params, StringRef ID, JSONOutput &Out) = 0; virtual void onSwitchSourceHeader(TextDocumentIdentifier Params, StringRef ID, - JSONOutput &Out) = 0; + JSONOutput &Out) = 0; + virtual void onFileEvent(const DidChangeWatchedFilesParams &Params) = 0; }; void registerCallbackHandlers(JSONRPCDispatcher &Dispatcher, JSONOutput &Out, Index: clangd/ProtocolHandlers.cpp =================================================================== --- clangd/ProtocolHandlers.cpp +++ clangd/ProtocolHandlers.cpp @@ -226,6 +226,25 @@ ProtocolCallbacks &Callbacks; }; +struct WorkspaceDidChangeWatchedFilesHandler : Handler { + WorkspaceDidChangeWatchedFilesHandler(JSONOutput &Output, + ProtocolCallbacks &Callbacks) + : Handler(Output), Callbacks(Callbacks) {} + + void handleNotification(llvm::yaml::MappingNode *Params) { + auto DCWFP = DidChangeWatchedFilesParams::parse(Params, Output); + if (!DCWFP) { + Output.log("Failed to decode DidChangeWatchedFilesParams.\n"); + return; + } + + Callbacks.onFileEvent(*DCWFP); + } + +private: + ProtocolCallbacks &Callbacks; +}; + } // namespace void clangd::registerCallbackHandlers(JSONRPCDispatcher &Dispatcher, @@ -264,5 +283,8 @@ llvm::make_unique(Out, Callbacks)); Dispatcher.registerHandler( "textDocument/switchSourceHeader", - llvm::make_unique(Out, Callbacks)); + llvm::make_unique(Out, Callbacks)); + Dispatcher.registerHandler( + "workspace/didChangeWatchedFiles", + llvm::make_unique(Out, Callbacks)); } Index: clangd/clients/clangd-vscode/src/extension.ts =================================================================== --- clangd/clients/clangd-vscode/src/extension.ts +++ clangd/clients/clangd-vscode/src/extension.ts @@ -23,13 +23,16 @@ const clientOptions: vscodelc.LanguageClientOptions = { // Register the server for C/C++ files - documentSelector: ['c', 'cc', 'cpp', 'h', 'hh', 'hpp'], + documentSelector: ['cpp', 'c', 'cc', 'cxx', 'c++', 'm', 'mm','h', 'hh', 'hpp', 'hxx', 'inc'], uriConverters: { // FIXME: by default the URI sent over the protocol will be percent encoded (see rfc3986#section-2.1) // the "workaround" below disables temporarily the encoding until decoding // is implemented properly in clangd code2Protocol: (uri: vscode.Uri) : string => uri.toString(true), protocol2Code: (uri: string) : vscode.Uri => vscode.Uri.parse(uri) + }, + synchronize: { + fileEvents: vscode.workspace.createFileSystemWatcher('**/*.{cpp,c,cc,cxx,c++,m,mm,h,hh,hpp,hxx,inc}') } }; Index: test/clangd/did-change-watch-files.test =================================================================== --- /dev/null +++ test/clangd/did-change-watch-files.test @@ -0,0 +1,61 @@ +# RUN: clangd -run-synchronously < %s 2>&1 | FileCheck -check-prefix=STDERR %s +# It is absolutely vital that this file has CRLF line endings. +# +# Test initialize request parameters with rootUri +Content-Length: 143 + +{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootUri":"file:///path/to/workspace","capabilities":{},"trace":"off"}} +# CHECK: Content-Length: 466 +# CHECK: {"jsonrpc":"2.0","id":0,"result":{"capabilities":{ +# CHECK: "textDocumentSync": 1, +# CHECK: "documentFormattingProvider": true, +# CHECK: "documentRangeFormattingProvider": true, +# CHECK: "documentOnTypeFormattingProvider": {"firstTriggerCharacter":"}","moreTriggerCharacter":[]}, +# CHECK: "codeActionProvider": true, +# CHECK: "completionProvider": {"resolveProvider": false, "triggerCharacters": [".",">",":"]}, +# CHECK: "definitionProvider": true +# CHECK: }}} +# +#Normal case +Content-Length: 217 + +{"jsonrpc":"2.0","method":"workspace/didChangeWatchedFiles","params":{"changes":[{"uri":"file:///path/to/file.cpp","type":1},{"uri":"file:///path/to/file2.cpp","type":2},{"uri":"file:///path/to/file3.cpp","type":3}]}} + +# Wrong event type, integer +Content-Length: 173 + +{"jsonrpc":"2.0","method":"workspace/didChangeWatchedFiles","params":{"changes":[{"uri":"file:///path/to/file2.cpp","type":0},{"uri":"file:///path/to/file3.cpp","type":4}]}} +# STDERR: Failed to decode a FileEvent. +# Wrong event type, string +Content-Length: 132 + +{"jsonrpc":"2.0","method":"workspace/didChangeWatchedFiles","params":{"changes":[{"uri":"file:///path/to/file2.cpp","type":"foo"}]}} +# STDERR: Failed to decode a FileEvent. +#Custom event field +Content-Length: 143 + +{"jsonrpc":"2.0","method":"workspace/didChangeWatchedFiles","params":{"changes":[{"uri":"file:///path/to/file2.cpp","type":1,"custom":"foo"}]}} +# STDERR: Failed to decode a FileEvent. +#Event field with object +Content-Length: 140 + +{"jsonrpc":"2.0","method":"workspace/didChangeWatchedFiles","params":{"changes":[{"uri":"file:///path/to/file2.cpp","type":{"foo":"bar"}}]}} +# STDERR: Failed to decode a FileEvent. +# Changes field with sequence but no object +Content-Length: 86 + +{"jsonrpc":"2.0","method":"workspace/didChangeWatchedFiles","params":{"changes":[""]}} +# STDERR: Failed to decode DidChangeWatchedFilesParams. +# Changes field with no sequence +Content-Length: 84 + +{"jsonrpc":"2.0","method":"workspace/didChangeWatchedFiles","params":{"changes":""}} +# STDERR: Failed to decode DidChangeWatchedFilesParams. +# Custom field +Content-Length: 86 + +{"jsonrpc":"2.0","method":"workspace/didChangeWatchedFiles","params":{"custom":"foo"}} +# STDERR: Ignored unknown field "custom" +Content-Length: 44 + +{"jsonrpc":"2.0","id":3,"method":"shutdown"}