diff --git a/clang-tools-extra/clangd/CMakeLists.txt b/clang-tools-extra/clangd/CMakeLists.txt --- a/clang-tools-extra/clangd/CMakeLists.txt +++ b/clang-tools-extra/clangd/CMakeLists.txt @@ -74,6 +74,7 @@ Hover.cpp IncludeFixer.cpp JSONTransport.cpp + ModuleDefaults.cpp PathMapping.cpp Protocol.cpp Quality.cpp diff --git a/clang-tools-extra/clangd/ClangdLSPServer.h b/clang-tools-extra/clangd/ClangdLSPServer.h --- a/clang-tools-extra/clangd/ClangdLSPServer.h +++ b/clang-tools-extra/clangd/ClangdLSPServer.h @@ -100,12 +100,6 @@ void onDocumentDidClose(const DidCloseTextDocumentParams &); void onDocumentDidSave(const DidSaveTextDocumentParams &); void onAST(const ASTParams &, Callback>); - void onDocumentOnTypeFormatting(const DocumentOnTypeFormattingParams &, - Callback>); - void onDocumentRangeFormatting(const DocumentRangeFormattingParams &, - Callback>); - void onDocumentFormatting(const DocumentFormattingParams &, - Callback>); // The results are serialized 'vector' if // SupportsHierarchicalDocumentSymbol is true and 'vector' // otherwise. diff --git a/clang-tools-extra/clangd/ClangdLSPServer.cpp b/clang-tools-extra/clangd/ClangdLSPServer.cpp --- a/clang-tools-extra/clangd/ClangdLSPServer.cpp +++ b/clang-tools-extra/clangd/ClangdLSPServer.cpp @@ -267,6 +267,16 @@ }; } + void bindAll(ModuleSet &Mods) { + Module::MethodBinder Binder; + for (auto &Mod : Mods) + Mod.initializeLSP(Binder); + for (auto &M : Binder.Methods) + Calls.try_emplace(M.first(), std::move(M.second)); + for (auto &M : Binder.Notifications) + Notifications.try_emplace(M.first(), std::move(M.second)); + } + // Bind a reply callback to a request. The callback will be invoked when // clangd receives the reply from the LSP client. // Return a call id of the request. @@ -547,7 +557,7 @@ llvm::Optional WithOffsetEncoding; if (Opts.Encoding) WithOffsetEncoding.emplace(kCurrentOffsetEncoding, *Opts.Encoding); - Server.emplace(*CDB, TFS, Opts, + Server.emplace(*CDB, TFS, DraftMgr, Opts, static_cast(this)); } applyConfiguration(Params.initializationOptions.ConfigSettings); @@ -605,6 +615,7 @@ {"change", (int)TextDocumentSyncKind::Incremental}, {"save", true}, }}, + // XXX move into format module {"documentFormattingProvider", true}, {"documentRangeFormattingProvider", true}, {"documentOnTypeFormattingProvider", @@ -906,61 +917,6 @@ publishDiagnostics(Notification); } -void ClangdLSPServer::onDocumentOnTypeFormatting( - const DocumentOnTypeFormattingParams &Params, - Callback> Reply) { - auto File = Params.textDocument.uri.file(); - auto Code = DraftMgr.getDraft(File); - if (!Code) - return Reply(llvm::make_error( - "onDocumentOnTypeFormatting called for non-added file", - ErrorCode::InvalidParams)); - - Server->formatOnType(File, Code->Contents, Params.position, Params.ch, - std::move(Reply)); -} - -void ClangdLSPServer::onDocumentRangeFormatting( - const DocumentRangeFormattingParams &Params, - Callback> Reply) { - auto File = Params.textDocument.uri.file(); - auto Code = DraftMgr.getDraft(File); - if (!Code) - return Reply(llvm::make_error( - "onDocumentRangeFormatting called for non-added file", - ErrorCode::InvalidParams)); - - Server->formatRange( - File, Code->Contents, Params.range, - [Code = Code->Contents, Reply = std::move(Reply)]( - llvm::Expected Result) mutable { - if (Result) - Reply(replacementsToEdits(Code, Result.get())); - else - Reply(Result.takeError()); - }); -} - -void ClangdLSPServer::onDocumentFormatting( - const DocumentFormattingParams &Params, - Callback> Reply) { - auto File = Params.textDocument.uri.file(); - auto Code = DraftMgr.getDraft(File); - if (!Code) - return Reply(llvm::make_error( - "onDocumentFormatting called for non-added file", - ErrorCode::InvalidParams)); - - Server->formatFile(File, Code->Contents, - [Code = Code->Contents, Reply = std::move(Reply)]( - llvm::Expected Result) mutable { - if (Result) - Reply(replacementsToEdits(Code, Result.get())); - else - Reply(Result.takeError()); - }); -} - /// The functions constructs a flattened view of the DocumentSymbol hierarchy. /// Used by the clients that do not support the hierarchical view. static std::vector @@ -1516,9 +1472,6 @@ MsgHandler->bind("initialized", &ClangdLSPServer::onInitialized); MsgHandler->bind("shutdown", &ClangdLSPServer::onShutdown); MsgHandler->bind("sync", &ClangdLSPServer::onSync); - MsgHandler->bind("textDocument/rangeFormatting", &ClangdLSPServer::onDocumentRangeFormatting); - MsgHandler->bind("textDocument/onTypeFormatting", &ClangdLSPServer::onDocumentOnTypeFormatting); - MsgHandler->bind("textDocument/formatting", &ClangdLSPServer::onDocumentFormatting); MsgHandler->bind("textDocument/codeAction", &ClangdLSPServer::onCodeAction); MsgHandler->bind("textDocument/completion", &ClangdLSPServer::onCompletion); MsgHandler->bind("textDocument/signatureHelp", &ClangdLSPServer::onSignatureHelp); @@ -1555,6 +1508,8 @@ if (Opts.FoldingRanges) MsgHandler->bind("textDocument/foldingRange", &ClangdLSPServer::onFoldingRange); // clang-format on + if (Opts.Modules) + MsgHandler->bindAll(*Opts.Modules); } ClangdLSPServer::~ClangdLSPServer() { diff --git a/clang-tools-extra/clangd/ClangdServer.h b/clang-tools-extra/clangd/ClangdServer.h --- a/clang-tools-extra/clangd/ClangdServer.h +++ b/clang-tools-extra/clangd/ClangdServer.h @@ -14,6 +14,7 @@ #include "ConfigProvider.h" #include "GlobalCompilationDatabase.h" #include "Hover.h" +#include "Module.h" #include "Protocol.h" #include "SemanticHighlighting.h" #include "TUScheduler.h" @@ -95,6 +96,8 @@ /// AST caching policy. The default is to keep up to 3 ASTs in memory. ASTRetentionPolicy RetentionPolicy; + ModuleSet *Modules = nullptr; + /// Cached preambles are potentially large. If false, store them on disk. bool StorePreamblesInMemory = true; @@ -165,6 +168,7 @@ /// those arguments for subsequent reparses. However, ClangdServer will check /// if compilation arguments changed on calls to forceReparse(). ClangdServer(const GlobalCompilationDatabase &CDB, const ThreadsafeFS &TFS, + const DraftStore &Drafts, // XXX const Options &Opts, Callbacks *Callbacks = nullptr); /// Add a \p File to the list of tracked C++ files or update the contents if @@ -249,19 +253,6 @@ void findReferences(PathRef File, Position Pos, uint32_t Limit, Callback CB); - /// Run formatting for \p Rng inside \p File with content \p Code. - void formatRange(PathRef File, StringRef Code, Range Rng, - Callback CB); - - /// Run formatting for the whole \p File with content \p Code. - void formatFile(PathRef File, StringRef Code, - Callback CB); - - /// Run formatting after \p TriggerText was typed at \p Pos in \p File with - /// content \p Code. - void formatOnType(PathRef File, StringRef Code, Position Pos, - StringRef TriggerText, Callback> CB); - /// Test the validity of a rename operation. /// /// If NewName is provided, it performs a name validation. @@ -345,6 +336,7 @@ const GlobalCompilationDatabase &CDB; const ThreadsafeFS &TFS; + ModuleSet *Modules; Path ResourceDir; // The index used to look up symbols. This could be: diff --git a/clang-tools-extra/clangd/ClangdServer.cpp b/clang-tools-extra/clangd/ClangdServer.cpp --- a/clang-tools-extra/clangd/ClangdServer.cpp +++ b/clang-tools-extra/clangd/ClangdServer.cpp @@ -9,6 +9,7 @@ #include "ClangdServer.h" #include "CodeComplete.h" #include "Config.h" +#include "DraftStore.h" #include "DumpAST.h" #include "FindSymbols.h" #include "Format.h" @@ -32,7 +33,6 @@ #include "support/MemoryTree.h" #include "support/ThreadsafeFS.h" #include "support/Trace.h" -#include "clang/Format/Format.h" #include "clang/Frontend/CompilerInstance.h" #include "clang/Frontend/CompilerInvocation.h" #include "clang/Lex/Preprocessor.h" @@ -136,9 +136,10 @@ } ClangdServer::ClangdServer(const GlobalCompilationDatabase &CDB, - const ThreadsafeFS &TFS, const Options &Opts, - Callbacks *Callbacks) - : CDB(CDB), TFS(TFS), + const ThreadsafeFS &TFS, + const DraftStore &Drafts, // XXX + const Options &Opts, Callbacks *Callbacks) + : CDB(CDB), TFS(TFS), Modules(Opts.Modules), DynamicIdx(Opts.BuildDynamicSymbolIndex ? new FileIndex() : nullptr), ClangTidyProvider(Opts.ClangTidyProvider), WorkspaceRoot(Opts.WorkspaceRoot), @@ -179,6 +180,16 @@ } if (DynamicIdx) AddIndex(DynamicIdx.get()); + + if (Modules) { + Module::ServerFacilities Facilities{ + Drafts, + WorkScheduler, + TFS, + }; + for (auto &Mod : *Modules) + Mod.Facilities.emplace(Facilities); + } } void ClangdServer::addDocument(PathRef File, llvm::StringRef Contents, @@ -362,48 +373,6 @@ std::move(Action)); } -void ClangdServer::formatRange(PathRef File, llvm::StringRef Code, Range Rng, - Callback CB) { - llvm::Expected Begin = positionToOffset(Code, Rng.start); - if (!Begin) - return CB(Begin.takeError()); - llvm::Expected End = positionToOffset(Code, Rng.end); - if (!End) - return CB(End.takeError()); - formatCode(File, Code, {tooling::Range(*Begin, *End - *Begin)}, - std::move(CB)); -} - -void ClangdServer::formatFile(PathRef File, llvm::StringRef Code, - Callback CB) { - // Format everything. - formatCode(File, Code, {tooling::Range(0, Code.size())}, std::move(CB)); -} - -void ClangdServer::formatOnType(PathRef File, llvm::StringRef Code, - Position Pos, StringRef TriggerText, - Callback> CB) { - llvm::Expected CursorPos = positionToOffset(Code, Pos); - if (!CursorPos) - return CB(CursorPos.takeError()); - auto Action = [File = File.str(), Code = Code.str(), - TriggerText = TriggerText.str(), CursorPos = *CursorPos, - CB = std::move(CB), this]() mutable { - auto Style = format::getStyle(format::DefaultFormatStyle, File, - format::DefaultFallbackStyle, Code, - TFS.view(/*CWD=*/llvm::None).get()); - if (!Style) - return CB(Style.takeError()); - - std::vector Result; - for (const tooling::Replacement &R : - formatIncremental(Code, CursorPos, TriggerText, *Style)) - Result.push_back(replacementToEdit(Code, R)); - return CB(Result); - }; - WorkScheduler.runQuick("FormatOnType", File, std::move(Action)); -} - void ClangdServer::prepareRename(PathRef File, Position Pos, llvm::Optional NewName, const RenameOptions &RenameOpts, @@ -566,8 +535,7 @@ // Tweaks don't apply clang-format, do that centrally here. for (auto &It : (*Effect)->ApplyEdits) { Edit &E = It.second; - format::FormatStyle Style = - getFormatStyleForFile(File, E.InitialCode, TFS); + auto Style = getFormatStyleForFile(File, E.InitialCode, TFS); if (llvm::Error Err = reformatEdit(E, Style)) elog("Failed to format {0}: {1}", It.first(), std::move(Err)); } @@ -611,27 +579,6 @@ WorkScheduler.runWithAST("SwitchHeaderSource", Path, std::move(Action)); } -void ClangdServer::formatCode(PathRef File, llvm::StringRef Code, - llvm::ArrayRef Ranges, - Callback CB) { - // Call clang-format. - auto Action = [File = File.str(), Code = Code.str(), Ranges = Ranges.vec(), - CB = std::move(CB), this]() mutable { - format::FormatStyle Style = getFormatStyleForFile(File, Code, TFS); - tooling::Replacements IncludeReplaces = - format::sortIncludes(Style, Code, Ranges, File); - auto Changed = tooling::applyAllReplacements(Code, IncludeReplaces); - if (!Changed) - return CB(Changed.takeError()); - - CB(IncludeReplaces.merge(format::reformat( - Style, *Changed, - tooling::calculateRangesAfterReplacements(IncludeReplaces, Ranges), - File))); - }; - WorkScheduler.runQuick("Format", File, std::move(Action)); -} - void ClangdServer::findDocumentHighlights( PathRef File, Position Pos, Callback> CB) { auto Action = @@ -651,8 +598,8 @@ this](llvm::Expected InpAST) mutable { if (!InpAST) return CB(InpAST.takeError()); - format::FormatStyle Style = getFormatStyleForFile( - File, InpAST->Inputs.Contents, *InpAST->Inputs.TFS); + auto Style = getFormatStyleForFile(File, InpAST->Inputs.Contents, + *InpAST->Inputs.TFS); CB(clangd::getHover(InpAST->AST, Pos, std::move(Style), Index)); }; diff --git a/clang-tools-extra/clangd/Format.h b/clang-tools-extra/clangd/Format.h --- a/clang-tools-extra/clangd/Format.h +++ b/clang-tools-extra/clangd/Format.h @@ -13,13 +13,14 @@ #ifndef LLVM_CLANG_TOOLS_EXTRA_CLANGD_FORMAT_H #define LLVM_CLANG_TOOLS_EXTRA_CLANGD_FORMAT_H +#include "Module.h" #include "Protocol.h" #include "clang/Format/Format.h" -#include "clang/Tooling/Core/Replacement.h" #include "llvm/ADT/StringRef.h" namespace clang { namespace clangd { +namespace format { /// Applies limited formatting around new \p InsertedText. /// The \p Code already contains the updated text before \p Cursor, and may have @@ -41,7 +42,8 @@ /// would merge these, and thus lose information about cursor position. std::vector formatIncremental(llvm::StringRef Code, unsigned Cursor, - llvm::StringRef InsertedText, format::FormatStyle Style); + llvm::StringRef InsertedText, + clang::format::FormatStyle Style); /// Determine the new cursor position after applying \p Replacements. /// Analogue of tooling::Replacements::getShiftedCodePosition(). @@ -49,6 +51,68 @@ transformCursorPosition(unsigned Offset, const std::vector &Replacements); +// Note: we do not parse FormattingOptions for *FormattingParams. +// In general, we use a clang-format style detected from common mechanisms +// (.clang-format files and the -fallback-style flag). +// It would be possible to override these with FormatOptions, but: +// - the protocol makes FormatOptions mandatory, so many clients set them to +// useless values, and we can't tell when to respect them +// - we also format in other places, where FormatOptions aren't available. + +struct DocumentRangeFormattingParams { + /// The document to format. + TextDocumentIdentifier textDocument; + + /// The range to format + Range range; +}; +bool fromJSON(const llvm::json::Value &, DocumentRangeFormattingParams &, + llvm::json::Path); + +struct DocumentOnTypeFormattingParams { + /// The document to format. + TextDocumentIdentifier textDocument; + + /// The position at which this request was sent. + Position position; + + /// The character that has been typed. + std::string ch; +}; +bool fromJSON(const llvm::json::Value &, DocumentOnTypeFormattingParams &, + llvm::json::Path); + +struct DocumentFormattingParams { + /// The document to format. + TextDocumentIdentifier textDocument; +}; +bool fromJSON(const llvm::json::Value &, DocumentFormattingParams &, + llvm::json::Path); + +} // namespace format + +class FormatModule final : public Module { +public: + /// The document range formatting request is sent from the client to the + /// server to format a given range in a document. + void rangeFormatting(const format::DocumentRangeFormattingParams &, + Callback>); + /// The document formatting request is sent from the client to the server to + /// format a whole document. + void formatting(const format::DocumentFormattingParams &, + Callback>); + /// The document on type formatting request is sent from the client to the + /// server to format parts of the document during typing. + void onTypeFormatting(const format::DocumentOnTypeFormattingParams &, + Callback>); + +private: + void formatImpl(llvm::StringRef Name, llvm::StringRef Code, + tooling::Replacements Range, Callback>); + + void initializeLSP(MethodBinder &MB) override; +}; + } // namespace clangd } // namespace clang diff --git a/clang-tools-extra/clangd/Format.cpp b/clang-tools-extra/clangd/Format.cpp --- a/clang-tools-extra/clangd/Format.cpp +++ b/clang-tools-extra/clangd/Format.cpp @@ -5,8 +5,14 @@ // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception // //===----------------------------------------------------------------------===// + +// I wonder whether we should split this file into FormatModule.cpp and +// Format.cpp, as they have pretty different sets of implementation deps... + #include "Format.h" +#include "SourceCode.h" #include "support/Logger.h" +#include "support/ThreadsafeFS.h" #include "clang/Basic/FileManager.h" #include "clang/Basic/SourceManager.h" #include "clang/Format/Format.h" @@ -16,18 +22,110 @@ namespace clang { namespace clangd { + +void FormatModule::initializeLSP(MethodBinder &Bind) { + Bind.method("textDocument/formatting", this, &FormatModule::formatting); + Bind.method("textDocument/rangeFormatting", this, + &FormatModule::rangeFormatting); + Bind.method("textDocument/onTypeFormatting", this, + &FormatModule::onTypeFormatting); +} + +static llvm::Expected> +formatCode(PathRef File, llvm::StringRef Code, + llvm::ArrayRef Ranges, const ThreadsafeFS &FS) { + auto Style = getFormatStyleForFile(File, Code, FS); + tooling::Replacements IncludeReplaces = + clang::format::sortIncludes(Style, Code, Ranges, File); + auto Changed = tooling::applyAllReplacements(Code, IncludeReplaces); + if (!Changed) + return Changed.takeError(); + auto Replacements = IncludeReplaces.merge(clang::format::reformat( + Style, *Changed, + tooling::calculateRangesAfterReplacements(IncludeReplaces, Ranges), + File)); + return replacementsToEdits(Code, Replacements); +}; + +void FormatModule::rangeFormatting( + const format::DocumentRangeFormattingParams &Params, + Callback> Reply) { + auto Draft = drafts().getDraft(Params.textDocument.uri.file()); + if (!Draft) + return Reply(llvm::make_error("formatting non-added file", + ErrorCode::InvalidParams)); + llvm::StringRef Code = Draft->Contents; + llvm::Expected Begin = positionToOffset(Code, Params.range.start); + if (!Begin) + return Reply(Begin.takeError()); + llvm::Expected End = positionToOffset(Code, Params.range.end); + if (!End) + return Reply(End.takeError()); + + scheduler().runQuick("rangeFormatting", Params.textDocument.uri.file(), + [File(std::string(Params.textDocument.uri.file())), + Code(std::string(Code)), Begin(*Begin), End(*End), + Reply(std::move(Reply)), FS(&fs())]() mutable { + Reply(formatCode(File, Code, + {tooling::Range(Begin, End - Begin)}, + *FS)); + }); +} + +void FormatModule::formatting(const format::DocumentFormattingParams &Params, + Callback> Reply) { + auto Draft = drafts().getDraft(Params.textDocument.uri.file()); + if (!Draft) + return Reply(llvm::make_error("formatting non-added file", + ErrorCode::InvalidParams)); + scheduler().runQuick( + "formatting", Params.textDocument.uri.file(), + [File(std::string(Params.textDocument.uri.file())), + Code(std::string(Draft->Contents)), Reply(std::move(Reply)), + FS(&fs())]() mutable { + Reply(formatCode(File, Code, {tooling::Range(0, Code.size())}, *FS)); + }); +} + +void FormatModule::onTypeFormatting( + const format::DocumentOnTypeFormattingParams &Params, + Callback> Reply) { + auto Draft = drafts().getDraft(Params.textDocument.uri.file()); + if (!Draft) + return Reply(llvm::make_error("formatting non-added file", + ErrorCode::InvalidParams)); + llvm::StringRef Code = Draft->Contents; + llvm::Expected CursorPos = positionToOffset(Code, Params.position); + if (!CursorPos) + return Reply(CursorPos.takeError()); + + scheduler().runQuick( + "onTypeFormatting", Params.textDocument.uri.file(), + [File(std::string(Params.textDocument.uri.file())), CursorPos(*CursorPos), + TriggerText(Params.ch), Code(std::string(Code)), FS(&fs()), + Reply(std::move(Reply))]() mutable { + auto Style = getFormatStyleForFile(File, Code, *FS); + std::vector Result; + for (const tooling::Replacement &R : + format::formatIncremental(Code, CursorPos, TriggerText, Style)) + Result.push_back(replacementToEdit(Code, R)); + return Reply(Result); + }); +} + +namespace format { namespace { /// Append closing brackets )]} to \p Code to make it well-formed. /// Clang-format conservatively refuses to format files with unmatched brackets /// as it isn't sure where the errors are and so can't correct. /// When editing, it's reasonable to assume code before the cursor is complete. -void closeBrackets(std::string &Code, const format::FormatStyle &Style) { +void closeBrackets(std::string &Code, const clang::format::FormatStyle &Style) { SourceManagerForFile FileSM("dummy.cpp", Code); auto &SM = FileSM.get(); FileID FID = SM.getMainFileID(); Lexer Lex(FID, SM.getBufferOrFake(FID), SM, - format::getFormattingLangOpts(Style)); + clang::format::getFormattingLangOpts(Style)); Token Tok; std::vector Brackets; while (!Lex.LexFromRawLexer(Tok)) { @@ -246,7 +344,8 @@ // all the regions we want to format, and discard changes in them. std::vector formatIncremental(llvm::StringRef OriginalCode, unsigned OriginalCursor, - llvm::StringRef InsertedText, format::FormatStyle Style) { + llvm::StringRef InsertedText, + clang::format::FormatStyle Style) { IncrementalChanges Incremental = getIncrementalChanges(OriginalCode, OriginalCursor, InsertedText); // Never *remove* lines in response to pressing enter! This annoys users. @@ -286,8 +385,8 @@ // Run clang-format, and truncate changes at FormatLimit. tooling::Replacements FormattingChanges; - format::FormattingAttemptStatus Status; - for (const tooling::Replacement &R : format::reformat( + clang::format::FormattingAttemptStatus Status; + for (const tooling::Replacement &R : clang::format::reformat( Style, CodeToFormat, RangesToFormat, Filename, &Status)) { if (R.getOffset() + R.getLength() <= FormatLimit) // Before limit. cantFail(FormattingChanges.add(R)); @@ -375,5 +474,25 @@ return Offset; } +bool fromJSON(const llvm::json::Value &Params, DocumentRangeFormattingParams &R, + llvm::json::Path P) { + llvm::json::ObjectMapper O(Params, P); + return O && O.map("textDocument", R.textDocument) && O.map("range", R.range); +} + +bool fromJSON(const llvm::json::Value &Params, + DocumentOnTypeFormattingParams &R, llvm::json::Path P) { + llvm::json::ObjectMapper O(Params, P); + return O && O.map("textDocument", R.textDocument) && + O.map("position", R.position) && O.map("ch", R.ch); +} + +bool fromJSON(const llvm::json::Value &Params, DocumentFormattingParams &R, + llvm::json::Path P) { + llvm::json::ObjectMapper O(Params, P); + return O && O.map("textDocument", R.textDocument); +} + +} // namespace format } // namespace clangd } // namespace clang diff --git a/clang-tools-extra/clangd/Module.h b/clang-tools-extra/clangd/Module.h new file mode 100644 --- /dev/null +++ b/clang-tools-extra/clangd/Module.h @@ -0,0 +1,196 @@ +#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANGD_MODULE_H +#define LLVM_CLANG_TOOLS_EXTRA_CLANGD_MODULE_H + +#include "DraftStore.h" +#include "Protocol.h" +#include "TUScheduler.h" +#include "support/Function.h" +#include "support/Logger.h" +#include "support/ThreadsafeFS.h" +#include "llvm/ADT/Sequence.h" +#include "llvm/ADT/StringMap.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/ADT/iterator.h" +#include "llvm/Support/JSON.h" +#include + +namespace clang { +namespace clangd { + +/// A Module contributes a vertical feature to clangd. This consists of: +/// +/// - LSP bindings (e.g. setting capabilities, implementing methods) +/// - a public interface to access the functionality from C++ embedders +/// ClangdServer::getModule()->foo(...) +/// +/// The lifetime of a module is roughly: +/// - modules are created before the LSP server, in ClangdMain.cpp +/// - when ClangdLSPServer is created, initializeLSP is called to bind methods +/// FIXME: this should be later, during the initialize LSP call, and have +/// access to capabilities. +/// - when ClangdServer is created, it fills in the ServerFacilities +/// - module hooks and LSP method handlers may be called at this point +/// - ClangdServer will not be destroyed until all requests are done +/// FIXME: I guess we need a way to to block server shutdown in case modules +/// have background work. +/// - ServerFacilities are removed before ClangdServer is destroyed. +/// There are no more calls after this point. +/// - The module is destroyed sometime after ClangdLSPServer +/// +/// Conventionally, standard modules live in the `clangd` namespace, and other +/// exposed details live in a sub-namespace. +/// clang::clangd:: +/// FormatModule +/// format:: +/// FormattingRequest +/// formatIncremental() // exposed for testing +class Module { +public: + virtual ~Module() = default; + +protected: // hooks for subclasses to implement (called by module container) + class MethodBinder; + // Register handlers for LSP methods and notifications. + virtual void initializeLSP(MethodBinder &){}; + +protected: // API for subclasses to use + const DraftStore &drafts() { return Facilities->Drafts; } + TUScheduler &scheduler() { return Facilities->Scheduler; } + const ThreadsafeFS &fs() { return Facilities->FS; } + +private: // State is set up by module container, which must be a friend. + struct ServerFacilities { + const DraftStore &Drafts; + TUScheduler &Scheduler; + const ThreadsafeFS &FS; + }; + llvm::Optional Facilities; + friend class ClangdServer; + friend class ClangdLSPServer; + friend class ModuleTesting; +}; + +class Module::MethodBinder { +public: + // Binds an LSP method to a module method which handles it. e.g: + // void FooModule::foo(const FooParams &, Callback); + // Bind.method("textDocument/foo", this, &FooModule::foo) + template + void method(const char *Method, Subclass *This, + void (Subclass::*Handler)(const Param &, Callback)); + // Binds an LSP notification to a module method which handles it. e.g: + // void FooModule::ping(const PingParams &); + // Bind.method("textDocument/onPing", this, &FooModule::ping) + template + void notification(const char *Method, Subclass *This, + void (Subclass::*Handler)(const Param &)); + +private: + template using HandlerMap = llvm::StringMap>; + using Method = void(llvm::json::Value, Callback); + using Notification = void(llvm::json::Value); + HandlerMap Methods; + HandlerMap Notifications; + + template + static llvm::Expected parse(const llvm::json::Value &Raw, + llvm::StringRef PayloadName, + llvm::StringRef PayloadKind); + + friend class ClangdLSPServer; // pulls out the results. +}; + +/// A collection of Modules, with type-based lookup. +/// This allows modules to be used generically through the Module/LSP interface, +/// but also to expose a public API to C++ callers. +class ModuleSet { + std::vector Vec; + +public: + /// The standard set of built-in modules. + static ModuleSet defaults(); + + using iterator = llvm::pointee_iterator; + using const_iterator = llvm::pointee_iterator; + + iterator begin() { return iterator(Vec.begin()); } + iterator end() { return iterator(Vec.end()); } + const_iterator begin() const { return const_iterator(Vec.begin()); } + const_iterator end() const { return const_iterator(Vec.end()); } + + template const T *get() const { + static_assert(std::is_base_of(), "Can only get Modules!"); + return static_cast(Map.lookup(&Key)); + } + + template bool add(std::unique_ptr M) { + static_assert(std::is_base_of(), "Can only add Modules!"); + static_assert(std::is_final(), "Modules must be final!"); + auto R = Map.try_emplace(&Key, std::unique_ptr(std::move(M))); + if (R.second) + Vec.push_back(R.first->second.get()); + return R.second; + } + +private: + template static int Key; + llvm::DenseMap> Map; +}; + +/// Tedious template implementation stuff lives below. + +template int ModuleSet::Key; + +template +void Module::MethodBinder::method(const char *Method, Subclass *This, + void (Subclass::*Handler)(const Param &, + Callback)) { + Methods[Method] = [Method, Handler, This](llvm::json::Value RawParams, + Callback Reply) { + llvm::Expected P = parse(RawParams, Method, "request"); + if (!P) + return Reply(P.takeError()); + (This->*Handler)(*P, std::move(Reply)); + }; +} + +template +void Module::MethodBinder::notification( + const char *Method, Subclass *This, + void (Subclass::*Handler)(const Param &)) { + Notifications[Method] = [Method, Handler, This](llvm::json::Value RawParams) { + llvm::Expected P = parse(RawParams, Method, "request"); + if (!P) + return llvm::consumeError(P.takeError()); + // XXX trace latency + (This->*Handler)(*P); + }; +} + +template +llvm::Expected Module::MethodBinder::parse(const llvm::json::Value &Raw, + llvm::StringRef PayloadName, + llvm::StringRef PayloadKind) { + T Result; + llvm::json::Path::Root Root; + if (!fromJSON(Raw, Result, Root)) { + elog("Failed to decode {0} {1}: {2}", PayloadName, PayloadKind, + Root.getError()); + // Dump the relevant parts of the broken message. + std::string Context; + llvm::raw_string_ostream OS(Context); + Root.printErrorContext(Raw, OS); + vlog("{0}", OS.str()); + // Report the error (e.g. to the client). + return llvm::make_error( + llvm::formatv("failed to decode {0} {1}: {2}", PayloadName, PayloadKind, + fmt_consume(Root.getError())), + ErrorCode::InvalidParams); + } + return std::move(Result); +} + +} // namespace clangd +} // namespace clang + +#endif diff --git a/clang-tools-extra/clangd/ModuleDefaults.cpp b/clang-tools-extra/clangd/ModuleDefaults.cpp new file mode 100644 --- /dev/null +++ b/clang-tools-extra/clangd/ModuleDefaults.cpp @@ -0,0 +1,16 @@ +#include "Format.h" +#include "Module.h" + +namespace clang { +namespace clangd { + +// This could use registration magic if we wanted linked-in modules to +// automatically be included without source changes. +ModuleSet ModuleSet::defaults() { + ModuleSet All; + All.add(std::make_unique()); + return All; +} + +} // namespace clangd +} // namespace clang diff --git a/clang-tools-extra/clangd/Protocol.h b/clang-tools-extra/clangd/Protocol.h --- a/clang-tools-extra/clangd/Protocol.h +++ b/clang-tools-extra/clangd/Protocol.h @@ -745,44 +745,6 @@ bool fromJSON(const llvm::json::Value &, DidChangeConfigurationParams &, llvm::json::Path); -// Note: we do not parse FormattingOptions for *FormattingParams. -// In general, we use a clang-format style detected from common mechanisms -// (.clang-format files and the -fallback-style flag). -// It would be possible to override these with FormatOptions, but: -// - the protocol makes FormatOptions mandatory, so many clients set them to -// useless values, and we can't tell when to respect them -// - we also format in other places, where FormatOptions aren't available. - -struct DocumentRangeFormattingParams { - /// The document to format. - TextDocumentIdentifier textDocument; - - /// The range to format - Range range; -}; -bool fromJSON(const llvm::json::Value &, DocumentRangeFormattingParams &, - llvm::json::Path); - -struct DocumentOnTypeFormattingParams { - /// The document to format. - TextDocumentIdentifier textDocument; - - /// The position at which this request was sent. - Position position; - - /// The character that has been typed. - std::string ch; -}; -bool fromJSON(const llvm::json::Value &, DocumentOnTypeFormattingParams &, - llvm::json::Path); - -struct DocumentFormattingParams { - /// The document to format. - TextDocumentIdentifier textDocument; -}; -bool fromJSON(const llvm::json::Value &, DocumentFormattingParams &, - llvm::json::Path); - struct DocumentSymbolParams { // The text document to find symbols in. TextDocumentIdentifier textDocument; diff --git a/clang-tools-extra/clangd/Protocol.cpp b/clang-tools-extra/clangd/Protocol.cpp --- a/clang-tools-extra/clangd/Protocol.cpp +++ b/clang-tools-extra/clangd/Protocol.cpp @@ -539,25 +539,6 @@ O.map("text", R.text); } -bool fromJSON(const llvm::json::Value &Params, DocumentRangeFormattingParams &R, - llvm::json::Path P) { - llvm::json::ObjectMapper O(Params, P); - return O && O.map("textDocument", R.textDocument) && O.map("range", R.range); -} - -bool fromJSON(const llvm::json::Value &Params, - DocumentOnTypeFormattingParams &R, llvm::json::Path P) { - llvm::json::ObjectMapper O(Params, P); - return O && O.map("textDocument", R.textDocument) && - O.map("position", R.position) && O.map("ch", R.ch); -} - -bool fromJSON(const llvm::json::Value &Params, DocumentFormattingParams &R, - llvm::json::Path P) { - llvm::json::ObjectMapper O(Params, P); - return O && O.map("textDocument", R.textDocument); -} - bool fromJSON(const llvm::json::Value &Params, DocumentSymbolParams &R, llvm::json::Path P) { llvm::json::ObjectMapper O(Params, P); diff --git a/clang-tools-extra/clangd/test/crash-non-added-files.test b/clang-tools-extra/clangd/test/crash-non-added-files.test --- a/clang-tools-extra/clangd/test/crash-non-added-files.test +++ b/clang-tools-extra/clangd/test/crash-non-added-files.test @@ -11,21 +11,21 @@ {"jsonrpc":"2.0","id":3,"method":"textDocument/rangeFormatting","params":{"textDocument":{"uri":"test:///foo.c"},"range":{"start":{"line":1,"character":4},"end":{"line":1,"character":12}},"options":{"tabSize":4,"insertSpaces":true}}} # CHECK: "error": { # CHECK-NEXT: "code": -32602 -# CHECK-NEXT: "message": "onDocumentRangeFormatting called for non-added file" +# CHECK-NEXT: "message": "formatting non-added file" # CHECK-NEXT: }, # CHECK-NEXT: "id": 3, --- {"jsonrpc":"2.0","id":4,"method":"textDocument/formatting","params":{"textDocument":{"uri":"test:///foo.c"},"options":{"tabSize":4,"insertSpaces":true}}} # CHECK: "error": { # CHECK-NEXT: "code": -32602 -# CHECK-NEXT: "message": "onDocumentFormatting called for non-added file" +# CHECK-NEXT: "message": "formatting non-added file" # CHECK-NEXT: }, # CHECK-NEXT: "id": 4, --- {"jsonrpc":"2.0","id":5,"method":"textDocument/onTypeFormatting","params":{"textDocument":{"uri":"test:///foo.c"},"position":{"line":3,"character":1},"ch":"}","options":{"tabSize":4,"insertSpaces":true}}} # CHECK: "error": { # CHECK-NEXT: "code": -32602 -# CHECK-NEXT: "message": "onDocumentOnTypeFormatting called for non-added file" +# CHECK-NEXT: "message": "formatting non-added file" # CHECK-NEXT: }, # CHECK-NEXT: "id": 5, --- diff --git a/clang-tools-extra/clangd/tool/ClangdMain.cpp b/clang-tools-extra/clangd/tool/ClangdMain.cpp --- a/clang-tools-extra/clangd/tool/ClangdMain.cpp +++ b/clang-tools-extra/clangd/tool/ClangdMain.cpp @@ -706,6 +706,8 @@ log("{0}: {1}", FlagsEnvVar, *EnvFlags); ClangdLSPServer::Options Opts; + auto Modules = ModuleSet::defaults(); + Opts.Modules = &Modules; Opts.UseDirBasedCDB = (CompileArgsFrom == FilesystemCompileArgs); // If --compile-commands-dir arg was invoked, check value and override default