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 @@ -144,7 +144,9 @@ void onCallHierarchyOutgoingCalls( const CallHierarchyOutgoingCallsParams &, Callback>); - void onInlayHints(const InlayHintsParams &, Callback>); + void onClangdInlayHints(const InlayHintsParams &, + Callback); + void onInlayHint(const InlayHintsParams &, Callback>); void onChangeConfiguration(const DidChangeConfigurationParams &); void onSymbolInfo(const TextDocumentPositionParams &, Callback>); 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 @@ -29,6 +29,7 @@ #include "llvm/ADT/Optional.h" #include "llvm/ADT/ScopeExit.h" #include "llvm/ADT/StringRef.h" +#include "llvm/ADT/Twine.h" #include "llvm/Support/Allocator.h" #include "llvm/Support/Error.h" #include "llvm/Support/FormatVariadic.h" @@ -574,6 +575,7 @@ llvm::json::Object{{"automaticReload", true}}}, {"callHierarchyProvider", true}, {"clangdInlayHintsProvider", true}, + {"inlayHintProvider", true}, }; { @@ -1207,8 +1209,41 @@ Server->incomingCalls(Params.item, std::move(Reply)); } -void ClangdLSPServer::onInlayHints(const InlayHintsParams &Params, - Callback> Reply) { +void ClangdLSPServer::onClangdInlayHints(const InlayHintsParams &Params, + Callback Reply) { + // Our extension has a different representation on the wire than the standard. + // We have a "range" property and "kind" is represented as a string, not as an + // enum value. + // https://clangd.llvm.org/extensions#inlay-hints + auto Serialize = [Reply = std::move(Reply)]( + llvm::Expected> Hints) mutable { + if (!Hints) { + Reply(Hints.takeError()); + return; + } + llvm::json::Array Result; + Result.reserve(Hints->size()); + for (auto &Hint : *Hints) { + Result.emplace_back(llvm::json::Object{ + {"kind", llvm::to_string(Hint.kind)}, + {"range", Hint.range}, + {"position", Hint.position}, + // Extension doesn't have paddingLeft/Right so adjust the label + // accordingly. + {"label", + ((Hint.paddingLeft ? " " : "") + llvm::StringRef(Hint.label) + + (Hint.paddingRight ? " " : "")) + .str()}, + }); + } + Reply(std::move(Result)); + }; + Server->inlayHints(Params.textDocument.uri.file(), Params.range, + std::move(Serialize)); +} + +void ClangdLSPServer::onInlayHint(const InlayHintsParams &Params, + Callback> Reply) { Server->inlayHints(Params.textDocument.uri.file(), Params.range, std::move(Reply)); } @@ -1491,7 +1526,8 @@ Bind.method("textDocument/documentLink", this, &ClangdLSPServer::onDocumentLink); Bind.method("textDocument/semanticTokens/full", this, &ClangdLSPServer::onSemanticTokens); Bind.method("textDocument/semanticTokens/full/delta", this, &ClangdLSPServer::onSemanticTokensDelta); - Bind.method("clangd/inlayHints", this, &ClangdLSPServer::onInlayHints); + Bind.method("clangd/inlayHints", this, &ClangdLSPServer::onClangdInlayHints); + Bind.method("textDocument/inlayHint", this, &ClangdLSPServer::onInlayHint); Bind.method("$/memoryUsage", this, &ClangdLSPServer::onMemoryUsage); if (Opts.FoldingRanges) Bind.method("textDocument/foldingRange", this, &ClangdLSPServer::onFoldingRange); diff --git a/clang-tools-extra/clangd/InlayHints.cpp b/clang-tools-extra/clangd/InlayHints.cpp --- a/clang-tools-extra/clangd/InlayHints.cpp +++ b/clang-tools-extra/clangd/InlayHints.cpp @@ -411,7 +411,7 @@ if (NameHint || ReferenceHint) { addInlayHint(Args[I]->getSourceRange(), HintSide::Left, - InlayHintKind::ParameterHint, ReferenceHint ? "&" : "", + InlayHintKind::Parameter, ReferenceHint ? "&" : "", NameHint ? Name : "", ": "); } } @@ -593,9 +593,9 @@ if (!Cfg.InlayHints.ConfigProperty) \ return; \ break - CHECK_KIND(ParameterHint, Parameters); - CHECK_KIND(TypeHint, DeducedTypes); - CHECK_KIND(DesignatorHint, Designators); + CHECK_KIND(Parameter, Parameters); + CHECK_KIND(Type, DeducedTypes); + CHECK_KIND(Designator, Designators); #undef CHECK_KIND } @@ -614,8 +614,10 @@ // file that was included after the preamble), do not show in that case. if (!AST.getSourceManager().isWrittenInMainFile(FileRange->getBegin())) return; - Results.push_back( - InlayHint{LSPPos, LSPRange, Kind, (Prefix + Label + Suffix).str()}); + bool PadLeft = Prefix.consume_front(" "); + bool PadRight = Suffix.consume_back(" "); + Results.push_back(InlayHint{LSPPos, (Prefix + Label + Suffix).str(), Kind, + PadLeft, PadRight, LSPRange}); } void addTypeHint(SourceRange R, QualType T, llvm::StringRef Prefix) { @@ -629,12 +631,12 @@ std::string TypeName = T.getAsString(Policy); if (TypeName.length() < TypeNameLimit) - addInlayHint(R, HintSide::Right, InlayHintKind::TypeHint, Prefix, - TypeName, /*Suffix=*/""); + addInlayHint(R, HintSide::Right, InlayHintKind::Type, Prefix, TypeName, + /*Suffix=*/""); } void addDesignatorHint(SourceRange R, llvm::StringRef Text) { - addInlayHint(R, HintSide::Left, InlayHintKind::DesignatorHint, + addInlayHint(R, HintSide::Left, InlayHintKind::Designator, /*Prefix=*/"", Text, /*Suffix=*/"="); } 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 @@ -1512,37 +1512,41 @@ }; llvm::json::Value toJSON(const CallHierarchyOutgoingCall &); -/// The parameter of a `clangd/inlayHints` request. -/// -/// This is a clangd extension. +/// A parameter literal used in inlay hint requests. struct InlayHintsParams { - /// The text document for which inlay hints are requested. + /// The text document. TextDocumentIdentifier textDocument; - /// If set, requests inlay hints for only part of the document. + /// The visible document range for which inlay hints should be computed. + /// + /// None is a clangd extension, which hints for computing hints on the whole + /// file. llvm::Optional range; }; bool fromJSON(const llvm::json::Value &, InlayHintsParams &, llvm::json::Path); -/// A set of predefined hint kinds. +/// Inlay hint kinds. enum class InlayHintKind { - /// The hint corresponds to parameter information. - /// An example of a parameter hint is a hint in this position: - /// func(^arg); - /// which shows the name of the corresponding parameter. - ParameterHint, - - /// The hint corresponds to information about a deduced type. + /// An inlay hint that for a type annotation. + /// /// An example of a type hint is a hint in this position: /// auto var ^ = expr; /// which shows the deduced type of the variable. - TypeHint, + Type = 1, + + /// An inlay hint that is for a parameter. + /// + /// An example of a parameter hint is a hint in this position: + /// func(^arg); + /// which shows the name of the corresponding parameter. + Parameter = 2, /// A hint before an element of an aggregate braced initializer list, /// indicating what it is initializing. /// Pair{^1, ^2}; /// Uses designator syntax, e.g. `.first:`. - DesignatorHint, + /// This is a clangd extension. + Designator = 3, /// Other ideas for hints that are not currently implemented: /// @@ -1550,31 +1554,43 @@ /// in a chain of function calls. /// * Hints indicating implicit conversions or implicit constructor calls. }; -llvm::json::Value toJSON(InlayHintKind); +llvm::json::Value toJSON(const InlayHintKind &); -/// An annotation to be displayed inline next to a range of source code. -/// -/// This is a clangd extension. +/// Inlay hint information. struct InlayHint { - /// The position between two characters where the hint should be displayed. - /// - /// For example, n parameter hint may be positioned before an argument. + /// The position of this hint. Position position; - /// The range of source code to which the hint applies. + /// The label of this hint. A human readable string or an array of + /// InlayHintLabelPart label parts. /// - /// For example, a parameter hint may have the argument as its range. - /// The range allows clients more flexibility of when/how to display the hint. - Range range; + /// *Note* that neither the string nor the label part can be empty. + std::string label; - /// The type of hint, such as a parameter hint. + /// The kind of this hint. Can be omitted in which case the client should fall + /// back to a reasonable default. InlayHintKind kind; - /// The label that is displayed in the editor, such as a parameter name. + /// Render padding before the hint. /// - /// The label may contain punctuation and/or whitespace to allow it to read - /// naturally when placed inline with the code. - std::string label; + /// Note: Padding should use the editor's background color, not the + /// background color of the hint itself. That means padding can be used + /// to visually align/separate an inlay hint. + bool paddingLeft = false; + + /// Render padding after the hint. + /// + /// Note: Padding should use the editor's background color, not the + /// background color of the hint itself. That means padding can be used + /// to visually align/separate an inlay hint. + bool paddingRight = false; + + /// The range of source code to which the hint applies. + /// + /// For example, a parameter hint may have the argument as its range. + /// The range allows clients more flexibility of when/how to display the hint. + /// This is an (unserialized) clangd extension. + Range range; }; llvm::json::Value toJSON(const InlayHint &); bool operator==(const InlayHint &, const InlayHint &); 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 @@ -1319,25 +1319,27 @@ return O && O.map("textDocument", R.textDocument) && O.map("range", R.range); } -static llvm::StringLiteral toString(InlayHintKind K) { - switch (K) { - case InlayHintKind::ParameterHint: - return "parameter"; - case InlayHintKind::TypeHint: - return "type"; - case InlayHintKind::DesignatorHint: - return "designator"; +llvm::json::Value toJSON(const InlayHintKind &Kind) { + switch (Kind) { + case InlayHintKind::Type: + return 1; + case InlayHintKind::Parameter: + return 2; + case InlayHintKind::Designator: // This is an extension, don't serialize. + return nullptr; } llvm_unreachable("Unknown clang.clangd.InlayHintKind"); } -llvm::json::Value toJSON(InlayHintKind K) { return toString(K); } - llvm::json::Value toJSON(const InlayHint &H) { - return llvm::json::Object{{"position", H.position}, - {"range", H.range}, - {"kind", H.kind}, - {"label", H.label}}; + llvm::json::Object Result{{"position", H.position}, + {"label", H.label}, + {"paddingLeft", H.paddingLeft}, + {"paddingRight", H.paddingRight}}; + auto K = toJSON(H.kind); + if (!K.getAsNull()) + Result["kind"] = std::move(K); + return std::move(Result); } bool operator==(const InlayHint &A, const InlayHint &B) { return std::tie(A.position, A.range, A.kind, A.label) == @@ -1349,7 +1351,18 @@ } llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, InlayHintKind Kind) { - return OS << toString(Kind); + auto ToString = [](InlayHintKind K) { + switch (K) { + case InlayHintKind::Parameter: + return "parameter"; + case InlayHintKind::Type: + return "type"; + case InlayHintKind::Designator: + return "designator"; + } + llvm_unreachable("Unknown clang.clangd.InlayHintKind"); + }; + return OS << ToString(Kind); } static const char *toString(OffsetEncoding OE) { diff --git a/clang-tools-extra/clangd/test/initialize-params.test b/clang-tools-extra/clangd/test/initialize-params.test --- a/clang-tools-extra/clangd/test/initialize-params.test +++ b/clang-tools-extra/clangd/test/initialize-params.test @@ -74,6 +74,7 @@ # CHECK-NEXT: }, # CHECK-NEXT: "hoverProvider": true, # CHECK-NEXT: "implementationProvider": true, +# CHECK-NEXT: "inlayHintProvider": true, # CHECK-NEXT: "memoryUsageProvider": true, # CHECK-NEXT: "referencesProvider": true, # CHECK-NEXT: "renameProvider": true, diff --git a/clang-tools-extra/clangd/test/inlayHints.test b/clang-tools-extra/clangd/test/inlayHints.test --- a/clang-tools-extra/clangd/test/inlayHints.test +++ b/clang-tools-extra/clangd/test/inlayHints.test @@ -39,6 +39,29 @@ # CHECK-NEXT: ] # CHECK-NEXT:} --- +{"jsonrpc":"2.0","id":2,"method":"textDocument/inlayHint","params":{ + "textDocument":{"uri":"test:///main.cpp"}, + "range":{ + "start": {"line":1,"character":0}, + "end": {"line":2,"character":0} + } +}} +# CHECK: "id": 2, +# CHECK-NEXT: "jsonrpc": "2.0", +# CHECK-NEXT: "result": [ +# CHECK-NEXT: { +# CHECK-NEXT: "kind": 2, +# CHECK-NEXT: "label": "bar:", +# CHECK-NEXT: "paddingLeft": false, +# CHECK-NEXT: "paddingRight": true, +# CHECK-NEXT: "position": { +# CHECK-NEXT: "character": 12, +# CHECK-NEXT: "line": 1 +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: ] +# CHECK-NEXT:} +--- {"jsonrpc":"2.0","id":100,"method":"shutdown"} --- {"jsonrpc":"2.0","method":"exit"} diff --git a/clang-tools-extra/clangd/unittests/InlayHintTests.cpp b/clang-tools-extra/clangd/unittests/InlayHintTests.cpp --- a/clang-tools-extra/clangd/unittests/InlayHintTests.cpp +++ b/clang-tools-extra/clangd/unittests/InlayHintTests.cpp @@ -13,6 +13,7 @@ #include "TestWorkspace.h" #include "XRefs.h" #include "support/Context.h" +#include "llvm/ADT/StringRef.h" #include "llvm/Support/ScopedPrinter.h" #include "gmock/gmock.h" #include "gtest/gtest.h" @@ -53,8 +54,11 @@ }; MATCHER_P2(HintMatcher, Expected, Code, llvm::to_string(Expected)) { - if (arg.label != Expected.Label) { - *result_listener << "label is " << arg.label; + llvm::StringRef ExpectedView(Expected.Label); + if (arg.label != ExpectedView.trim(" ") || + arg.paddingLeft != ExpectedView.startswith(" ") || + arg.paddingRight != ExpectedView.endswith(" ")) { + *result_listener << "label is '" << arg.label << "'"; return false; } if (arg.range != Code.range(Expected.RangeName)) { @@ -99,14 +103,14 @@ void assertParameterHints(llvm::StringRef AnnotatedSource, ExpectedHints... Expected) { ignore(Expected.Side = Left...); - assertHints(InlayHintKind::ParameterHint, AnnotatedSource, Expected...); + assertHints(InlayHintKind::Parameter, AnnotatedSource, Expected...); } template void assertTypeHints(llvm::StringRef AnnotatedSource, ExpectedHints... Expected) { ignore(Expected.Side = Right...); - assertHints(InlayHintKind::TypeHint, AnnotatedSource, Expected...); + assertHints(InlayHintKind::Type, AnnotatedSource, Expected...); } template @@ -115,7 +119,7 @@ Config Cfg; Cfg.InlayHints.Designators = true; WithContextValue WithCfg(Config::Key, std::move(Cfg)); - assertHints(InlayHintKind::DesignatorHint, AnnotatedSource, Expected...); + assertHints(InlayHintKind::Designator, AnnotatedSource, Expected...); } TEST(ParameterHints, Smoke) { @@ -570,7 +574,7 @@ ASSERT_TRUE(bool(AST)); // Ensure the hint for the call in foo.inc is NOT materialized in foo.cc. - EXPECT_EQ(hintsOfKind(*AST, InlayHintKind::ParameterHint).size(), 0u); + EXPECT_EQ(hintsOfKind(*AST, InlayHintKind::Parameter).size(), 0u); } TEST(TypeHints, Smoke) { @@ -818,7 +822,7 @@ TU.ExtraArgs.push_back("-xc"); auto AST = TU.build(); - EXPECT_THAT(hintsOfKind(AST, InlayHintKind::TypeHint), IsEmpty()); + EXPECT_THAT(hintsOfKind(AST, InlayHintKind::Type), IsEmpty()); } TEST(DesignatorHints, Basic) {