Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[clangd] Add inlay hints for default function arguments #95712

Merged
merged 7 commits into from
Oct 19, 2024

Conversation

torshepherd
Copy link
Contributor

@torshepherd torshepherd commented Jun 16, 2024

Title. This PR adds support for defaulted function arguments:

image

as inlay hints.

If the list of default args is too long (based on TypeNameLimit), shows "..." instead:

image

Note: I wrote this before #85497 landed. With this we should probably also follow up with go-to-definition on the lambda captures at least; I could see that being helpful

Edit (9/23): Removed ' and implicit lambda captures'

Copy link

Thank you for submitting a Pull Request (PR) to the LLVM Project!

This PR will be automatically labeled and the relevant teams will be
notified.

If you wish to, you can add reviewers by using the "Reviewers" section on this page.

If this is not working for you, it is probably because you do not have write
permissions for the repository. In which case you can instead tag reviewers by
name in a comment by using @ followed by their GitHub username.

If you have received no comments on your PR for a week, you can request a review
by "ping"ing the PR by adding a comment “Ping”. The common courtesy "ping" rate
is once a week. Please remember that you are asking for valuable time from other developers.

If you have further questions, they may be answered by the LLVM GitHub User Guide.

You can also ask questions in a comment on this PR, on the LLVM Discord or on the forums.

@llvmbot
Copy link
Collaborator

llvmbot commented Jun 16, 2024

@llvm/pr-subscribers-clangd

@llvm/pr-subscribers-clang-tools-extra

Author: Tor Shepherd (torshepherd)

Changes

Title. This PR adds support for showing implicit lambda captures:

image

and defaulted function arguments:

image

as inlay hints.

If the list of captures (or default args) is too long (based on TypeNameLimit), shows "..." instead:

image

image

Note: I wrote this before #85497 landed. With this we should probably also follow up with go-to-definition on the lambda captures at least; I could see that being helpful


Full diff: https://github.com/llvm/llvm-project/pull/95712.diff

8 Files Affected:

  • (modified) clang-tools-extra/clangd/Config.h (+2)
  • (modified) clang-tools-extra/clangd/ConfigCompile.cpp (+14-5)
  • (modified) clang-tools-extra/clangd/ConfigFragment.h (+5)
  • (modified) clang-tools-extra/clangd/ConfigYAML.cpp (+8-1)
  • (modified) clang-tools-extra/clangd/InlayHints.cpp (+84-3)
  • (modified) clang-tools-extra/clangd/Protocol.cpp (+7-1)
  • (modified) clang-tools-extra/clangd/Protocol.h (+24-9)
  • (modified) clang-tools-extra/clangd/unittests/InlayHintTests.cpp (+62-4)
diff --git a/clang-tools-extra/clangd/Config.h b/clang-tools-extra/clangd/Config.h
index 41143b9ebc8d2..5100214244454 100644
--- a/clang-tools-extra/clangd/Config.h
+++ b/clang-tools-extra/clangd/Config.h
@@ -148,6 +148,8 @@ struct Config {
     bool DeducedTypes = true;
     bool Designators = true;
     bool BlockEnd = false;
+    bool LambdaCaptures = false;
+    bool DefaultArguments = false;
     // Limit the length of type names in inlay hints. (0 means no limit)
     uint32_t TypeNameLimit = 32;
   } InlayHints;
diff --git a/clang-tools-extra/clangd/ConfigCompile.cpp b/clang-tools-extra/clangd/ConfigCompile.cpp
index f32f674443ffe..dcdb71ea01bb2 100644
--- a/clang-tools-extra/clangd/ConfigCompile.cpp
+++ b/clang-tools-extra/clangd/ConfigCompile.cpp
@@ -43,7 +43,6 @@
 #include "llvm/Support/Regex.h"
 #include "llvm/Support/SMLoc.h"
 #include "llvm/Support/SourceMgr.h"
-#include <algorithm>
 #include <memory>
 #include <optional>
 #include <string>
@@ -504,10 +503,10 @@ struct FragmentCompiler {
       auto Fast = isFastTidyCheck(Str);
       if (!Fast.has_value()) {
         diag(Warning,
-             llvm::formatv(
-                 "Latency of clang-tidy check '{0}' is not known. "
-                 "It will only run if ClangTidy.FastCheckFilter is Loose or None",
-                 Str)
+             llvm::formatv("Latency of clang-tidy check '{0}' is not known. "
+                           "It will only run if ClangTidy.FastCheckFilter is "
+                           "Loose or None",
+                           Str)
                  .str(),
              Arg.Range);
       } else if (!*Fast) {
@@ -654,6 +653,16 @@ struct FragmentCompiler {
       Out.Apply.push_back([Value(**F.BlockEnd)](const Params &, Config &C) {
         C.InlayHints.BlockEnd = Value;
       });
+    if (F.LambdaCaptures)
+      Out.Apply.push_back(
+          [Value(**F.LambdaCaptures)](const Params &, Config &C) {
+            C.InlayHints.LambdaCaptures = Value;
+          });
+    if (F.DefaultArguments)
+      Out.Apply.push_back(
+          [Value(**F.DefaultArguments)](const Params &, Config &C) {
+            C.InlayHints.DefaultArguments = Value;
+          });
     if (F.TypeNameLimit)
       Out.Apply.push_back(
           [Value(**F.TypeNameLimit)](const Params &, Config &C) {
diff --git a/clang-tools-extra/clangd/ConfigFragment.h b/clang-tools-extra/clangd/ConfigFragment.h
index f3e51a9b6dbc4..ad30b43f34874 100644
--- a/clang-tools-extra/clangd/ConfigFragment.h
+++ b/clang-tools-extra/clangd/ConfigFragment.h
@@ -331,6 +331,11 @@ struct Fragment {
     std::optional<Located<bool>> Designators;
     /// Show defined symbol names at the end of a definition block.
     std::optional<Located<bool>> BlockEnd;
+    /// Show names of captured variables by default capture groups in lambdas.
+    std::optional<Located<bool>> LambdaCaptures;
+    /// Show parameter names and default values of default arguments after all
+    /// of the explicit arguments.
+    std::optional<Located<bool>> DefaultArguments;
     /// Limit the length of type name hints. (0 means no limit)
     std::optional<Located<uint32_t>> TypeNameLimit;
   };
diff --git a/clang-tools-extra/clangd/ConfigYAML.cpp b/clang-tools-extra/clangd/ConfigYAML.cpp
index 3e9b6a07d3b32..58e5f8bead101 100644
--- a/clang-tools-extra/clangd/ConfigYAML.cpp
+++ b/clang-tools-extra/clangd/ConfigYAML.cpp
@@ -14,7 +14,6 @@
 #include "llvm/Support/YAMLParser.h"
 #include <optional>
 #include <string>
-#include <system_error>
 
 namespace clang {
 namespace clangd {
@@ -264,6 +263,14 @@ class Parser {
       if (auto Value = boolValue(N, "BlockEnd"))
         F.BlockEnd = *Value;
     });
+    Dict.handle("LambdaCaptures", [&](Node &N) {
+      if (auto Value = boolValue(N, "LambdaCaptures"))
+        F.LambdaCaptures = *Value;
+    });
+    Dict.handle("DefaultArguments", [&](Node &N) {
+      if (auto Value = boolValue(N, "DefaultArguments"))
+        F.DefaultArguments = *Value;
+    });
     Dict.handle("TypeNameLimit", [&](Node &N) {
       if (auto Value = uint32Value(N, "TypeNameLimit"))
         F.TypeNameLimit = *Value;
diff --git a/clang-tools-extra/clangd/InlayHints.cpp b/clang-tools-extra/clangd/InlayHints.cpp
index cd4f1931b3ce1..2631c17409eab 100644
--- a/clang-tools-extra/clangd/InlayHints.cpp
+++ b/clang-tools-extra/clangd/InlayHints.cpp
@@ -11,27 +11,37 @@
 #include "Config.h"
 #include "HeuristicResolver.h"
 #include "ParsedAST.h"
+#include "Protocol.h"
 #include "SourceCode.h"
 #include "clang/AST/ASTDiagnostic.h"
 #include "clang/AST/Decl.h"
+#include "clang/AST/DeclBase.h"
 #include "clang/AST/DeclarationName.h"
 #include "clang/AST/Expr.h"
 #include "clang/AST/ExprCXX.h"
+#include "clang/AST/LambdaCapture.h"
 #include "clang/AST/RecursiveASTVisitor.h"
 #include "clang/AST/Stmt.h"
 #include "clang/AST/StmtVisitor.h"
 #include "clang/AST/Type.h"
 #include "clang/Basic/Builtins.h"
+#include "clang/Basic/Lambda.h"
 #include "clang/Basic/OperatorKinds.h"
+#include "clang/Basic/SourceLocation.h"
 #include "clang/Basic/SourceManager.h"
 #include "llvm/ADT/DenseSet.h"
+#include "llvm/ADT/STLExtras.h"
+#include "llvm/ADT/SmallVector.h"
 #include "llvm/ADT/StringExtras.h"
 #include "llvm/ADT/StringRef.h"
 #include "llvm/ADT/Twine.h"
 #include "llvm/Support/Casting.h"
+#include "llvm/Support/FormatVariadic.h"
 #include "llvm/Support/SaveAndRestore.h"
 #include "llvm/Support/ScopedPrinter.h"
 #include "llvm/Support/raw_ostream.h"
+#include <algorithm>
+#include <iterator>
 #include <optional>
 #include <string>
 
@@ -372,6 +382,34 @@ maybeDropCxxExplicitObjectParameters(ArrayRef<const ParmVarDecl *> Params) {
   return Params;
 }
 
+llvm::StringRef getLambdaCaptureName(const LambdaCapture &Capture) {
+  if (Capture.capturesVariable())
+    return Capture.getCapturedVar()->getName();
+  if (Capture.capturesThis())
+    return llvm::StringRef{"this"};
+  return llvm::StringRef{"unknown"};
+}
+
+template <typename R, typename P>
+std::string joinAndTruncate(R &&Range, size_t MaxLength,
+                            P &&GetAsStringFunction) {
+  std::string Out;
+  bool IsFirst = true;
+  for (auto &&Element : Range) {
+    if (!IsFirst)
+      Out.append(", ");
+    else
+      IsFirst = false;
+    auto AsString = GetAsStringFunction(Element);
+    if (Out.size() + AsString.size() >= MaxLength) {
+      Out.append("...");
+      break;
+    }
+    Out.append(AsString);
+  }
+  return Out;
+}
+
 struct Callee {
   // Only one of Decl or Loc is set.
   // Loc is for calls through function pointers.
@@ -422,7 +460,8 @@ class InlayHintVisitor : public RecursiveASTVisitor<InlayHintVisitor> {
     Callee.Decl = E->getConstructor();
     if (!Callee.Decl)
       return true;
-    processCall(Callee, {E->getArgs(), E->getNumArgs()});
+    processCall(Callee, E->getParenOrBraceRange().getEnd(),
+                {E->getArgs(), E->getNumArgs()});
     return true;
   }
 
@@ -495,7 +534,7 @@ class InlayHintVisitor : public RecursiveASTVisitor<InlayHintVisitor> {
             dyn_cast_or_null<CXXMethodDecl>(Callee.Decl))
       if (IsFunctor || Method->hasCXXExplicitFunctionObjectParameter())
         Args = Args.drop_front(1);
-    processCall(Callee, Args);
+    processCall(Callee, E->getRParenLoc(), Args);
     return true;
   }
 
@@ -598,6 +637,15 @@ class InlayHintVisitor : public RecursiveASTVisitor<InlayHintVisitor> {
   }
 
   bool VisitLambdaExpr(LambdaExpr *E) {
+    if (Cfg.InlayHints.LambdaCaptures && E->getCaptureDefault() != LCD_None &&
+        !E->implicit_captures().empty()) {
+      std::string FormattedCaptureList =
+          joinAndTruncate(E->implicit_captures(), Cfg.InlayHints.TypeNameLimit,
+                          [](const LambdaCapture &ImplicitCapture) {
+                            return getLambdaCaptureName(ImplicitCapture);
+                          });
+      addImplicitCaptureHint(E->getCaptureDefaultLoc(), FormattedCaptureList);
+    }
     FunctionDecl *D = E->getCallOperator();
     if (!E->hasExplicitResultType())
       addReturnTypeHint(D, E->hasExplicitParameters()
@@ -709,7 +757,8 @@ class InlayHintVisitor : public RecursiveASTVisitor<InlayHintVisitor> {
 private:
   using NameVec = SmallVector<StringRef, 8>;
 
-  void processCall(Callee Callee, llvm::ArrayRef<const Expr *> Args) {
+  void processCall(Callee Callee, SourceRange LParenOrBraceRange,
+                   llvm::ArrayRef<const Expr *> Args) {
     assert(Callee.Decl || Callee.Loc);
 
     if (!Cfg.InlayHints.Parameters || Args.size() == 0)
@@ -721,6 +770,9 @@ class InlayHintVisitor : public RecursiveASTVisitor<InlayHintVisitor> {
         if (Ctor->isCopyOrMoveConstructor())
           return;
 
+    SmallVector<std::string> FormattedDefaultArgs;
+    bool HasNonDefaultArgs = false;
+
     ArrayRef<const ParmVarDecl *> Params, ForwardedParams;
     // Resolve parameter packs to their forwarded parameter
     SmallVector<const ParmVarDecl *> ForwardedParamsStorage;
@@ -755,12 +807,34 @@ class InlayHintVisitor : public RecursiveASTVisitor<InlayHintVisitor> {
       bool NameHint = shouldHintName(Args[I], Name);
       bool ReferenceHint = shouldHintReference(Params[I], ForwardedParams[I]);
 
+      bool IsDefault = isa<CXXDefaultArgExpr>(Args[I]);
+      HasNonDefaultArgs |= !IsDefault;
+      if (Cfg.InlayHints.DefaultArguments && IsDefault) {
+        auto SourceText = Lexer::getSourceText(
+            CharSourceRange::getTokenRange(Params[I]->getDefaultArgRange()),
+            Callee.Decl->getASTContext().getSourceManager(),
+            Callee.Decl->getASTContext().getLangOpts());
+        FormattedDefaultArgs.emplace_back(llvm::formatv(
+            "{0} = {1}", Name,
+            SourceText.size() > Cfg.InlayHints.TypeNameLimit ? "..."
+                                                             : SourceText));
+      }
+
       if (NameHint || ReferenceHint) {
         addInlayHint(Args[I]->getSourceRange(), HintSide::Left,
                      InlayHintKind::Parameter, ReferenceHint ? "&" : "",
                      NameHint ? Name : "", ": ");
       }
     }
+
+    if (!FormattedDefaultArgs.empty()) {
+      std::string Hint =
+          joinAndTruncate(FormattedDefaultArgs, Cfg.InlayHints.TypeNameLimit,
+                          [](const auto &E) { return E; });
+      addInlayHint(LParenOrBraceRange, HintSide::Left,
+                   InlayHintKind::DefaultArgument,
+                   HasNonDefaultArgs ? ", " : "", Hint, "");
+    }
   }
 
   static bool isSetter(const FunctionDecl *Callee, const NameVec &ParamNames) {
@@ -968,6 +1042,8 @@ class InlayHintVisitor : public RecursiveASTVisitor<InlayHintVisitor> {
       CHECK_KIND(Type, DeducedTypes);
       CHECK_KIND(Designator, Designators);
       CHECK_KIND(BlockEnd, BlockEnd);
+      CHECK_KIND(LambdaCapture, LambdaCaptures);
+      CHECK_KIND(DefaultArgument, DefaultArguments);
 #undef CHECK_KIND
     }
 
@@ -1021,6 +1097,11 @@ class InlayHintVisitor : public RecursiveASTVisitor<InlayHintVisitor> {
                  /*Prefix=*/"", Text, /*Suffix=*/"=");
   }
 
+  void addImplicitCaptureHint(SourceRange R, llvm::StringRef Text) {
+    addInlayHint(R, HintSide::Right, InlayHintKind::LambdaCapture,
+                 /*Prefix=*/": ", Text, /*Suffix=*/"");
+  }
+
   bool shouldPrintTypeHint(llvm::StringRef TypeName) const noexcept {
     return Cfg.InlayHints.TypeNameLimit == 0 ||
            TypeName.size() < Cfg.InlayHints.TypeNameLimit;
diff --git a/clang-tools-extra/clangd/Protocol.cpp b/clang-tools-extra/clangd/Protocol.cpp
index c08f80442eaa0..dafc3f4eab567 100644
--- a/clang-tools-extra/clangd/Protocol.cpp
+++ b/clang-tools-extra/clangd/Protocol.cpp
@@ -209,7 +209,7 @@ bool fromJSON(const llvm::json::Value &Params, ChangeAnnotation &R,
          O.map("needsConfirmation", R.needsConfirmation) &&
          O.mapOptional("description", R.description);
 }
-llvm::json::Value toJSON(const ChangeAnnotation & CA) {
+llvm::json::Value toJSON(const ChangeAnnotation &CA) {
   llvm::json::Object Result{{"label", CA.label}};
   if (CA.needsConfirmation)
     Result["needsConfirmation"] = *CA.needsConfirmation;
@@ -1477,6 +1477,8 @@ llvm::json::Value toJSON(const InlayHintKind &Kind) {
     return 2;
   case InlayHintKind::Designator:
   case InlayHintKind::BlockEnd:
+  case InlayHintKind::LambdaCapture:
+  case InlayHintKind::DefaultArgument:
     // This is an extension, don't serialize.
     return nullptr;
   }
@@ -1517,6 +1519,10 @@ llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, InlayHintKind Kind) {
       return "designator";
     case InlayHintKind::BlockEnd:
       return "block-end";
+    case InlayHintKind::LambdaCapture:
+      return "lambda-capture";
+    case InlayHintKind::DefaultArgument:
+      return "default-argument";
     }
     llvm_unreachable("Unknown clang.clangd.InlayHintKind");
   };
diff --git a/clang-tools-extra/clangd/Protocol.h b/clang-tools-extra/clangd/Protocol.h
index a0f8b04bc4ffd..793eebf4d51df 100644
--- a/clang-tools-extra/clangd/Protocol.h
+++ b/clang-tools-extra/clangd/Protocol.h
@@ -281,7 +281,7 @@ struct TextDocumentEdit {
   /// The text document to change.
   VersionedTextDocumentIdentifier textDocument;
 
-	/// The edits to be applied.
+  /// The edits to be applied.
   /// FIXME: support the AnnotatedTextEdit variant.
   std::vector<TextEdit> edits;
 };
@@ -557,7 +557,7 @@ struct ClientCapabilities {
 
   /// The client supports versioned document changes for WorkspaceEdit.
   bool DocumentChanges = false;
-  
+
   /// The client supports change annotations on text edits,
   bool ChangeAnnotation = false;
 
@@ -1013,12 +1013,12 @@ struct WorkspaceEdit {
   /// Versioned document edits.
   ///
   /// If a client neither supports `documentChanges` nor
-	/// `workspace.workspaceEdit.resourceOperations` then only plain `TextEdit`s
-	/// using the `changes` property are supported.
+  /// `workspace.workspaceEdit.resourceOperations` then only plain `TextEdit`s
+  /// using the `changes` property are supported.
   std::optional<std::vector<TextDocumentEdit>> documentChanges;
-  
+
   /// A map of change annotations that can be referenced in
-	/// AnnotatedTextEdit.
+  /// AnnotatedTextEdit.
   std::map<std::string, ChangeAnnotation> changeAnnotations;
 };
 bool fromJSON(const llvm::json::Value &, WorkspaceEdit &, llvm::json::Path);
@@ -1274,13 +1274,13 @@ enum class InsertTextFormat {
 /// Additional details for a completion item label.
 struct CompletionItemLabelDetails {
   /// An optional string which is rendered less prominently directly after label
-	/// without any spacing. Should be used for function signatures or type
+  /// without any spacing. Should be used for function signatures or type
   /// annotations.
   std::string detail;
 
   /// An optional string which is rendered less prominently after
-	/// CompletionItemLabelDetails.detail. Should be used for fully qualified
-	/// names or file path.
+  /// CompletionItemLabelDetails.detail. Should be used for fully qualified
+  /// names or file path.
   std::string description;
 };
 llvm::json::Value toJSON(const CompletionItemLabelDetails &);
@@ -1681,6 +1681,21 @@ enum class InlayHintKind {
   /// This is a clangd extension.
   BlockEnd = 4,
 
+  /// An inlay hint that is for a variable captured implicitly in a lambda.
+  ///
+  /// An example of parameter hint for implicit lambda captures:
+  ///    [&^] { return A; };
+  /// Adds an inlay hint ": A".
+  LambdaCapture = 5,
+
+  /// An inlay hint that is for a default argument.
+  ///
+  /// An example of a parameter hint for a default argument:
+  ///    void foo(bool A = true);
+  ///    foo(^);
+  /// Adds an inlay hint "A = true".
+  DefaultArgument = 6,
+
   /// Other ideas for hints that are not currently implemented:
   ///
   /// * Chaining hints, showing the types of intermediate expressions
diff --git a/clang-tools-extra/clangd/unittests/InlayHintTests.cpp b/clang-tools-extra/clangd/unittests/InlayHintTests.cpp
index 5b1531eb2fa60..65ab743fb3e38 100644
--- a/clang-tools-extra/clangd/unittests/InlayHintTests.cpp
+++ b/clang-tools-extra/clangd/unittests/InlayHintTests.cpp
@@ -15,9 +15,12 @@
 #include "support/Context.h"
 #include "llvm/ADT/StringRef.h"
 #include "llvm/Support/ScopedPrinter.h"
+#include "llvm/Support/raw_ostream.h"
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
+#include <optional>
 #include <string>
+#include <utility>
 #include <vector>
 
 namespace clang {
@@ -81,6 +84,8 @@ Config noHintsConfig() {
   C.InlayHints.DeducedTypes = false;
   C.InlayHints.Designators = false;
   C.InlayHints.BlockEnd = false;
+  C.InlayHints.LambdaCaptures = false;
+  C.InlayHints.DefaultArguments = false;
   return C;
 }
 
@@ -1458,13 +1463,66 @@ TEST(TypeHints, DefaultTemplateArgs) {
     struct A {};
     A<float> foo();
     auto $var[[var]] = foo();
-    A<float> bar[1];
+    A<float> baz;
+    A<float> bar[1]{};
     auto [$binding[[value]]] = bar;
   )cpp",
                   ExpectedHint{": A<float>", "var"},
                   ExpectedHint{": A<float>", "binding"});
 }
 
+TEST(LambdaCaptures, Smoke) {
+  Config Cfg;
+  Cfg.InlayHints.Parameters = false;
+  Cfg.InlayHints.DeducedTypes = false;
+  Cfg.InlayHints.Designators = false;
+  Cfg.InlayHints.BlockEnd = false;
+  Cfg.InlayHints.DefaultArguments = false;
+
+  Cfg.InlayHints.LambdaCaptures = true;
+  Cfg.InlayHints.TypeNameLimit = 10;
+  WithContextValue WithCfg(Config::Key, std::move(Cfg));
+
+  assertHints(InlayHintKind::LambdaCapture, R"cpp(
+    void foo() {
+      int A = 1;
+      int ReallyLongName = 1;
+      auto B = [$byvalue[[=]]] { return A; };
+      auto C = [$byref[[&]]] { return A; };
+      auto D = [$trunc[[=]], &A] {  B(); (void)ReallyLongName; return A; };
+    }
+  )cpp",
+              ExpectedHint{": A", "byvalue", Right},
+              ExpectedHint{": A", "byref", Right},
+              ExpectedHint{": B, ...", "trunc", Right});
+}
+
+TEST(DefaultArguments, Smoke) {
+  Config Cfg;
+  Cfg.InlayHints.Parameters =
+      true; // To test interplay of parameters and default parameters
+  Cfg.InlayHints.DeducedTypes = false;
+  Cfg.InlayHints.Designators = false;
+  Cfg.InlayHints.BlockEnd = false;
+  Cfg.InlayHints.LambdaCaptures = false;
+
+  Cfg.InlayHints.DefaultArguments = true;
+  WithContextValue WithCfg(Config::Key, std::move(Cfg));
+
+  const auto *Code = R"cpp(
+    int foo(int A = 4) { return A; }
+    int bar(int A, int B = 1, bool C = foo($default1[[)]]) { return A; }
+    int A = bar($explicit[[2]]$default2[[)]];
+  )cpp";
+
+  assertHints(InlayHintKind::DefaultArgument, Code,
+              ExpectedHint{"A = 4", "default1", Left},
+              ExpectedHint{", B = 1, C = foo()", "default2", Left});
+
+  assertHints(InlayHintKind::Parameter, Code,
+              ExpectedHint{"A: ", "explicit", Left});
+}
+
 TEST(TypeHints, Deduplication) {
   assertTypeHints(R"cpp(
     template <typename T>
@@ -1568,7 +1626,7 @@ TEST(TypeHints, SubstTemplateParameterAliases) {
   )cpp";
 
   llvm::StringRef VectorIntPtr = R"cpp(
-    vector<int *> array;
+    vector<int *> $init[[array]];
     auto $no_modifier[[x]] = array[3];
     auto* $ptr_modifier[[ptr]] = &array[3];
     auto& $ref_modifier[[ref]] = array[3];
@@ -1592,7 +1650,7 @@ TEST(TypeHints, SubstTemplateParameterAliases) {
       ExpectedHint{": non_template_iterator", "end"});
 
   llvm::StringRef VectorInt = R"cpp(
-  vector<int> array;
+  vector<int> $init[[array]];
   auto $no_modifier[[by_value]] = array[3];
   auto* $ptr_modifier[[ptr]] = &array[3];
   auto& $ref_modifier[[ref]] = array[3];
@@ -1741,7 +1799,7 @@ TEST(ParameterHints, PseudoObjectExpr) {
     int printf(const char *Format, ...);
 
     int main() {
-      S s;
+      S $init[[s]];
       __builtin_dump_struct(&s, printf); // Not `Format: __builtin_dump_struct()`
       printf($Param[["Hello, %d"]], 42); // Normal calls are not affected.
       // This builds a PseudoObjectExpr, but here it's useful for showing the

Copy link
Contributor

@zyn0217 zyn0217 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My two cents:

While I appreciate the value of inspecting more semantic information about lambdas, I still have mixed feelings about user experience.

Admittedly, presenting implicitly captured variables provides users with insight into ODR-used variables, but I'm unsure about our strategy: We would probably end up showing too many hints inside the lambda capture, which would visually elongate the lambda header and distract users from reading.

Indeed, we have a configuration entry to restrict the length, but I feel that's insufficient (Although we're baking it with TypeNameLimit currently, which is also inappropriate). For example, a user might have a lambda capturing several variables named in single letters e.g. a-z. As a result, we probably eventually display [&: a, b, c, d, e, f, g, ...](...), which is IMO too spammy.

That said, I wonder if we can implement the capture stuff in hovers instead. From what I can think of, we present something e.g.

// implicitly captured by reference/copy:
int a;
std::string s;
...

when user hovers over the capture defaults.

(I don't have a strong opinion on the hints for default arguments. It would also be great to hear @HighCommander4 and @kadircet's input.)

Comment on lines 396 to 410
std::string Out;
bool IsFirst = true;
for (auto &&Element : Range) {
if (!IsFirst)
Out.append(", ");
else
IsFirst = false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: 1. We don't usually append to a string; instead, we use llvm::raw_string_ostream. 2. This whole thing can be simplified by llvm::ListSeparator

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed this to use raw_string_ostream, lmk how it looks

clang-tools-extra/clangd/InlayHints.cpp Outdated Show resolved Hide resolved
clang-tools-extra/clangd/InlayHints.cpp Outdated Show resolved Hide resolved
clang-tools-extra/clangd/unittests/InlayHintTests.cpp Outdated Show resolved Hide resolved
clang-tools-extra/clangd/unittests/InlayHintTests.cpp Outdated Show resolved Hide resolved
clang-tools-extra/clangd/ConfigCompile.cpp Outdated Show resolved Hide resolved
clang-tools-extra/clangd/Protocol.h Outdated Show resolved Hide resolved
@torshepherd
Copy link
Contributor Author

Aha, that's a great suggestion. I agree that beyond the trivial case of 2 captured variables it gets a bit noisy. On-hover would be quite nice though!

To clarify, you mean hovering over the default capture '=' or '&' right?

I'm happy to remove the lambdas from this PR in favor of the hover approach. Thoughts on the default arguments?

@zyn0217
Copy link
Contributor

zyn0217 commented Jun 17, 2024

To clarify, you mean hovering over the default capture '=' or '&' right?

Yep, that's what I thought.

I'm happy to remove the lambdas from this PR in favor of the hover approach.

No worries, but let's wait for other reviewers' opinions before we move forward. :)

@torshepherd torshepherd force-pushed the clangd-subset-of-inlay-hints branch 4 times, most recently from c856574 to bd294be Compare June 19, 2024 16:24
@torshepherd
Copy link
Contributor Author

Ok I took a stab at doing lambda captures as hover instead.

  • Targeting the exact '=' or '&' is nontrivial since the capture list isn't in the AST, the lambda is just LambdaExpr with children for the parameters and the CompoundStmt body.
  • auto MyLambda = [... already produces a nice function-like hover over the variable. I tested adding a "Captures:" bullet-list, it works well enough but isn't very noticeable.
  • We could take the above HoverInfo over the variable containing the lambda and apply it to hovering the lambda itself as well?

It strikes me, if we could either add HoverInfo or styling to the variables within the body of the lambda themselves, that would be the most readable representation probably.

For instance:

  • Can we add an inlay hint like [&] before actual variables in the body of the lambda?
  • Can we apply a semantic token to style lambda captures differently? Maybe as property since they are similar conceptually to struct members?
  • Can we use the mutable-ness of the lambda to color tokens as const if they are captured by copy and the lambda is not mutable?

Overall I think capture hints should be removed from this PR so we can get default args in in the meantime.

@torshepherd torshepherd force-pushed the clangd-subset-of-inlay-hints branch from bd294be to f7a243b Compare July 2, 2024 03:13
Copy link
Collaborator

@HighCommander4 HighCommander4 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apologies for the slow response time, it's been quite a busy summer.

High-level feedback:

  • Let's definitely split the default arguments part from the lambda captures part. Even if we decide we want both, it helps to have separate features in separate patches, so that e.g. if one causes a problem and needs to be reverted, it's easy to do that without affecting the other.
  • I tend to agree with @zyn0217 that lambda captures might be a better fit for hovers than inlay hints. I think we could solve the problem targeting the capture-default token: if SelectionTree selects the LambdaExpr, massage the input position into a SourceLocation, compare it to getCaptureDefaultLoc(), and if so then take a different branch (in Hover.cpp code). (This doesn't prevent us from potentially also pursuing the idea of adding a "captures" section to the hover for the lambda variable.)
  • For the default argument hints, what do you think about using name: value syntax instead of name = value syntax, for consistency with parameter hints?

clang-tools-extra/clangd/Protocol.h Outdated Show resolved Hide resolved
clang-tools-extra/clangd/InlayHints.cpp Outdated Show resolved Hide resolved
clang-tools-extra/clangd/InlayHints.cpp Outdated Show resolved Hide resolved
clang-tools-extra/clangd/InlayHints.cpp Outdated Show resolved Hide resolved
@torshepherd torshepherd changed the title [clangd] Add inlay hints for default function arguments and implicit lambda captures [clangd] Add inlay hints for default function arguments Sep 24, 2024
@torshepherd torshepherd force-pushed the clangd-subset-of-inlay-hints branch 3 times, most recently from 41f0a7f to 2c9e7c1 Compare September 24, 2024 03:12
@torshepherd
Copy link
Contributor Author

Hmm another case to consider:

image

In this case the argument is unnamed. I think for this I should just insert /* unused */ or /* unnamed */ ?

@torshepherd
Copy link
Contributor Author

Hmm another case to consider:

image

In this case the argument is unnamed. I think for this I should just insert /* unused */ or /* unnamed */ ?

Fixed this and added a test case

image

@torshepherd
Copy link
Contributor Author

Ping

Copy link
Collaborator

@HighCommander4 HighCommander4 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the update!

The patch is looking pretty good, just some minor comments remaining and then it should be good to go.

clang-tools-extra/clangd/Protocol.h Outdated Show resolved Hide resolved
clang-tools-extra/clangd/InlayHints.cpp Outdated Show resolved Hide resolved
clang-tools-extra/clangd/InlayHints.cpp Outdated Show resolved Hide resolved
@torshepherd
Copy link
Contributor Author

Ok I found a couple of nontrivial issues that I took care of in the latest commit.

  1. Using DefaultArguments required having parameter names set to true as well. I separated them so that both have a modality that works with or without the other. See screenshot for defaults w/out parameter names:

image

  1. Having newlines in the default definition caused LF to be displayed in the hints. I fixed this by just displaying ... any time a default definition contains \n.
  2. I was getting Property DefaultArguments is not allowed in config.yaml. Is this a schema issue? I wasn't able to find where to update this

Also, a couple of follow-up things I should do:

  • Add support for default Designators. For instance,
struct Foo { int a; int b = 2 };
Foo f{1}; // Currently shows 'Foo f{.a=1}', should show 'Foo f{.a=1, .b=2}'
  • Add support for template args? such as std::vector<int, std::allocator<int>> when user types std::vector<int>. This could also fall under "add InlayHints for FTAD and CTAD", which may be a nongoal, idk
  • Add a link to the source location of the definition (leveraging [clangd] Support go-to-definition on type hints. The protocol part #85497)
  • Do the formatting better. Basically, in some cases, collapsing newlines preserves the meaning of the code and therefore we should do that. However in other cases (comments for example), collapsing newlines makes the code gibberish (3 // + 2 from the test case for example), and we should just show ... instead.

@HighCommander4
Copy link
Collaborator

  1. I was getting Property DefaultArguments is not allowed in config.yaml. Is this a schema issue? I wasn't able to find where to update this

That sort of diagnostic is likely produced by a YAML plugin, which uses a schema from https://github.com/SchemaStore/schemastore.

I do file a SchemaStore issue once per clangd release about updating the clangd schema, here's the one for clangd 19: SchemaStore/schemastore#4022.

If you'd like to file one about DefaultArguments sooner, or even submit a PR, please feel free.

Copy link
Collaborator

@HighCommander4 HighCommander4 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the update and for spotting and fixing the added issues. I agree that keeping DefaultArguments orthogonal to Parameters is a good choice.

clang-tools-extra/clangd/unittests/InlayHintTests.cpp Outdated Show resolved Hide resolved
@torshepherd
Copy link
Contributor Author

Accepted the comment and added relnotes. Lmk if you need anything else prior to merge

@HighCommander4
Copy link
Collaborator

Thanks. The buildkite failure looks unrelated (it's a clang-move test, and this patch only touches clangd code), so I will go ahead and merge this.

@HighCommander4 HighCommander4 merged commit 2eb1699 into llvm:main Oct 19, 2024
7 of 9 checks passed
Copy link

@torshepherd Congratulations on having your first Pull Request (PR) merged into the LLVM Project!

Your changes will be combined with recent changes from other authors, then tested by our build bots. If there is a problem with a build, you may receive a report in an email or a comment on this PR.

Please check whether problems have been caused by your change specifically, as the builds can include changes from many authors. It is not uncommon for your change to be included in a build that fails due to someone else's changes, or infrastructure issues.

How to do this, and the rest of the post-merge process, is covered in detail here.

If your change does cause a problem, it may be reverted, or you can revert it yourself. This is a normal part of LLVM development. You can fix your changes and open a new PR to merge them again.

If you don't get any reports, no action is required from you. Your changes are working as expected, well done!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants