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] Show struct fields and enum members in hovers #89557

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

HighCommander4
Copy link
Collaborator

@HighCommander4 HighCommander4 marked this pull request as ready for review April 22, 2024 05:14
@llvmbot llvmbot added clang Clang issues not falling into any other category clang-tools-extra clangd clang:frontend Language frontend issues, e.g. anything involving "Sema" labels Apr 22, 2024
@llvmbot
Copy link
Collaborator

llvmbot commented Apr 22, 2024

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

@llvm/pr-subscribers-clangd

Author: Nathan Ridge (HighCommander4)

Changes

Fixes clangd/clangd#959


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

4 Files Affected:

  • (modified) clang-tools-extra/clangd/Hover.cpp (+2)
  • (modified) clang-tools-extra/clangd/unittests/HoverTests.cpp (+13-4)
  • (modified) clang/include/clang/AST/PrettyPrinter.h (+9-1)
  • (modified) clang/lib/AST/DeclPrinter.cpp (+17-3)
diff --git a/clang-tools-extra/clangd/Hover.cpp b/clang-tools-extra/clangd/Hover.cpp
index 06b949bc4a2b55..d7c433876b08bc 100644
--- a/clang-tools-extra/clangd/Hover.cpp
+++ b/clang-tools-extra/clangd/Hover.cpp
@@ -69,6 +69,8 @@ namespace {
 PrintingPolicy getPrintingPolicy(PrintingPolicy Base) {
   Base.AnonymousTagLocations = false;
   Base.TerseOutput = true;
+  // Show struct fields and enum members.
+  Base.PrintTagTypeContents = true;
   Base.PolishForDeclaration = true;
   Base.ConstantsAsWritten = true;
   Base.SuppressTemplateArgsInCXXConstructors = true;
diff --git a/clang-tools-extra/clangd/unittests/HoverTests.cpp b/clang-tools-extra/clangd/unittests/HoverTests.cpp
index 35db757b9c15b5..8c19f5fb3b0ad0 100644
--- a/clang-tools-extra/clangd/unittests/HoverTests.cpp
+++ b/clang-tools-extra/clangd/unittests/HoverTests.cpp
@@ -1645,7 +1645,16 @@ TEST(Hover, All) {
       {
           R"cpp(// Struct
             namespace ns1 {
-              struct MyClass {};
+              struct MyClass {
+                // Public fields shown in hover
+                int field1;
+                int field2;
+
+                // Methods and private fields not shown
+                void method();
+              private:
+                bool private_field;
+              };
             } // namespace ns1
             int main() {
               ns1::[[My^Class]]* Params;
@@ -1655,7 +1664,7 @@ TEST(Hover, All) {
             HI.Name = "MyClass";
             HI.Kind = index::SymbolKind::Struct;
             HI.NamespaceScope = "ns1::";
-            HI.Definition = "struct MyClass {}";
+            HI.Definition = "struct MyClass {\n  int field1;\n  int field2;\n}";
           }},
       {
           R"cpp(// Class
@@ -1685,7 +1694,7 @@ TEST(Hover, All) {
             HI.Name = "MyUnion";
             HI.Kind = index::SymbolKind::Union;
             HI.NamespaceScope = "ns1::";
-            HI.Definition = "union MyUnion {}";
+            HI.Definition = "union MyUnion {\n  int x;\n  int y;\n}";
           }},
       {
           R"cpp(// Function definition via pointer
@@ -2030,7 +2039,7 @@ TEST(Hover, All) {
             HI.Name = "Hello";
             HI.Kind = index::SymbolKind::Enum;
             HI.NamespaceScope = "";
-            HI.Definition = "enum Hello {}";
+            HI.Definition = "enum Hello { ONE, TWO, THREE }";
             HI.Documentation = "Enum declaration";
           }},
       {
diff --git a/clang/include/clang/AST/PrettyPrinter.h b/clang/include/clang/AST/PrettyPrinter.h
index da276e26049b00..6b8f1ae9143df3 100644
--- a/clang/include/clang/AST/PrettyPrinter.h
+++ b/clang/include/clang/AST/PrettyPrinter.h
@@ -70,7 +70,7 @@ struct PrintingPolicy {
         Restrict(LO.C99), Alignof(LO.CPlusPlus11), UnderscoreAlignof(LO.C11),
         UseVoidForZeroParams(!LO.CPlusPlus),
         SplitTemplateClosers(!LO.CPlusPlus11), TerseOutput(false),
-        PolishForDeclaration(false), Half(LO.Half),
+        PrintTagTypeContents(false), PolishForDeclaration(false), Half(LO.Half),
         MSWChar(LO.MicrosoftExt && !LO.WChar), IncludeNewlines(true),
         MSVCFormatting(false), ConstantsAsWritten(false),
         SuppressImplicitBase(false), FullyQualifiedName(false),
@@ -252,6 +252,14 @@ struct PrintingPolicy {
   LLVM_PREFERRED_TYPE(bool)
   unsigned TerseOutput : 1;
 
+  /// Print the contents of tag (i.e. record and enum) types, even with
+  /// TerseOutput=true.
+  ///
+  /// For record types (structs/classes/unions), this only prints public
+  /// data members. For enum types, this prints enumerators.
+  /// Has no effect if TerseOutput=false (which prints all members).
+  unsigned PrintTagTypeContents : 1;
+
   /// When true, do certain refinement needed for producing proper declaration
   /// tag; such as, do not print attributes attached to the declaration.
   ///
diff --git a/clang/lib/AST/DeclPrinter.cpp b/clang/lib/AST/DeclPrinter.cpp
index 43d221968ea3fb..3fe02fff687fe1 100644
--- a/clang/lib/AST/DeclPrinter.cpp
+++ b/clang/lib/AST/DeclPrinter.cpp
@@ -21,6 +21,7 @@
 #include "clang/AST/ExprCXX.h"
 #include "clang/AST/PrettyPrinter.h"
 #include "clang/Basic/Module.h"
+#include "clang/Basic/Specifiers.h"
 #include "llvm/Support/raw_ostream.h"
 using namespace clang;
 
@@ -464,8 +465,10 @@ void DeclPrinter::PrintConstructorInitializers(CXXConstructorDecl *CDecl,
 //----------------------------------------------------------------------------
 
 void DeclPrinter::VisitDeclContext(DeclContext *DC, bool Indent) {
-  if (Policy.TerseOutput)
-    return;
+  if (Policy.TerseOutput) {
+    if (!Policy.PrintTagTypeContents || !isa<TagDecl>(DC))
+      return;
+  }
 
   if (Indent)
     Indentation += Policy.Indentation;
@@ -474,6 +477,17 @@ void DeclPrinter::VisitDeclContext(DeclContext *DC, bool Indent) {
   for (DeclContext::decl_iterator D = DC->decls_begin(), DEnd = DC->decls_end();
        D != DEnd; ++D) {
 
+    // Print enum members and public struct fields when
+    // PrintTagTypeContents=true. Only applicable when TerseOutput=true since
+    // otherwise all members are printed.
+    if (Policy.TerseOutput) {
+      assert(Policy.PrintTagTypeContents);
+      if (!(isa<EnumConstantDecl>(*D) ||
+            (isa<FieldDecl>(*D) &&
+             dyn_cast<FieldDecl>(*D)->getAccess() == AS_public)))
+        continue;
+    }
+
     // Don't print ObjCIvarDecls, as they are printed when visiting the
     // containing ObjCInterfaceDecl.
     if (isa<ObjCIvarDecl>(*D))
@@ -1182,7 +1196,7 @@ void DeclPrinter::VisitCXXRecordDecl(CXXRecordDecl *D) {
 
     // Print the class definition
     // FIXME: Doesn't print access specifiers, e.g., "public:"
-    if (Policy.TerseOutput) {
+    if (Policy.TerseOutput && !Policy.PrintTagTypeContents) {
       Out << " {}";
     } else {
       Out << " {\n";

@HighCommander4
Copy link
Collaborator Author

A few things I would appreciate feedback on:

  1. I know a previous comment on the bug stated "We don't show bodies of classes/enums/functions etc by policy", but can we consider changing this policy in the more limited case of public struct fields and enum members? Like I mentioned in this comment, the usefulness/verbosity tradeoff might be different in this subset of cases. Also cc @colin-grant-work in case you'd like to weigh in on this further.
  2. If we're willing to make this change, do we want it unconditionally, or behind a new config option in the Hover section? My personal opinion is that even in the case of a struct/enum with many members this is not particularly bothersome and would lean towards making it unconditional, but I am of course happy to put it behind a config option if that helps build a consensus for this.
  3. Regarding the implementation approach, is it fine to add a flag to PrintingPolicy (which is a clang utility class used in a variety of places) for a clangd-specific use case like this? I did it this way because the alternative seemed to involve duplicating a bunch of code related to decl-printing, but I'm happy to explore alternatives if this is an issue.

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.

Thank you! I have a few questions:

  1. Do we want an RFC in discourse for the changes in DeclPrinter?
  2. Do you think we should also add some test cases to DeclPrinterTest.cpp?

// PrintTagTypeContents=true. Only applicable when TerseOutput=true since
// otherwise all members are printed.
if (Policy.TerseOutput) {
assert(Policy.PrintTagTypeContents);
Copy link
Contributor

Choose a reason for hiding this comment

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

This assertion is doing nothing because we have exited early at the function beginning...?

assert(Policy.PrintTagTypeContents);
if (!(isa<EnumConstantDecl>(*D) ||
(isa<FieldDecl>(*D) &&
dyn_cast<FieldDecl>(*D)->getAccess() == AS_public)))
Copy link
Contributor

Choose a reason for hiding this comment

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

Do you have any tests on C codes? Looking around the codebase, I think AS_none might be used in some cases wrt non-C++ codes?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The patch does seem to work for me on C files, but adding a test to exercise the code in C mode is definitely a good idea. Thanks!

@kadircet
Copy link
Member

So my 2cents;

  1. I know a previous comment on the bug stated "We don't show bodies of classes/enums/functions etc by policy", but can we consider changing this policy in the more limited case of public struct fields and enum members? Like I mentioned in this comment, the usefulness/verbosity tradeoff might be different in this subset of cases. Also cc @colin-grant-work in case you'd like to weigh in on this further.

I feel like a more focused approach could still be preferred. E.g. what about showing this kind of "summary" only when hovering over the declaration of the symbol, rather than references? (similar to the way we show size/alignment info). As I believe showing struct Foo {} in hover response, when the cursor is already at a struct's definition, isn't really useful.

For enabling it more generally, I still have some hesitations, as I am not sure how useful that interaction is in general. As not all editors let you interact with hover cards, and just seeing a bunch of field names, without any extra info, sounds suboptimal to me. I'd still prefer going over those fields via Foo{}.^ code completion, which shows all the "public" info with extra context like "documentation".

  1. If we're willing to make this change, do we want it unconditionally, or behind a new config option in the Hover section? My personal opinion is that even in the case of a struct/enum with many members this is not particularly bothersome and would lean towards making it unconditional, but I am of course happy to put it behind a config option if that helps build a consensus for this.

As you might've sensed from the previous response, i'd definitely prefer a config option. As most terminal-based editors don't really have "rich" hovercard support, and even if we set the default to "verbose", i'd like to have a way for such people to keep using hover cards without definition eating up the whole screen estate.

  1. Regarding the implementation approach, is it fine to add a flag to PrintingPolicy (which is a clang utility class used in a variety of places) for a clangd-specific use case like this? I did it this way because the alternative seemed to involve duplicating a bunch of code related to decl-printing, but I'm happy to explore alternatives if this is an issue.

I feel like "print only public fields" is too specific for clangd use case, and probably won't generalize to other callers at all. But I definitely agree with all the concerns around duplicating code. Looks like we have some PrintingCallbacks, maybe we can have something like SummarizeTagDecl, which enables customizing what to put into the body, when printing a TagDecl in terse mode?

@HighCommander4
Copy link
Collaborator Author

what about showing this kind of "summary" only when hovering over the declaration of the symbol, rather than references?

Did you mean the other way around (references and not declaration, since when you're at the declaration you're likely already looking at the fields in the code)? Otherwise I'm missing the motivation for not showing it on references, which I think is the main part of the use case (being able to see this information at a glance when you're in a different file).

@colin-grant-work
Copy link

As not all editors let you interact with hover cards, and just seeing a bunch of field names, without any extra info, sounds suboptimal to me... I'd like to have a way for such people to keep using hover cards without definition eating up the whole screen estate.

This basically raises the question of what the function of a hover is, and I don't think I'd like to limit its scope to just type annotation, with all other information available either only by triggering autocompletion (a bit of a workaround that may require extra typing, even if all you want to do is get an idea about how a struct is structured) or by jumping to the definition. ClangD's behavior is also a bit inconsistent. In a hover, it will show documentation comments around the declaration, but not any of the content of the declaration. That means that it may eat up a lot of screen real estate if the documentation is extensive, but in the absence of documentation, it won't show much at all. Given that most terminal editors have either a gesture to show a hover or a gesture to dismiss one, or both, I'm not sure that avoiding using space for their sake really helps anybody.

Given those premises, I'd like to suggest aiming to figure out whether there is some useful data about structs / enums / (maybe even) classes / functions that ClangD can straightforwardly derive and (compactly) display, and if so, what that is. Allowing the behavior to be configured makes a lot of sense, perhaps both booleanly and scalarly, i.e. whether to display such data at all, and if so, the maximum amount that should be displayed, and in my view should address most concerns about its impact on particular editors.

@HighCommander4
Copy link
Collaborator Author

  1. Regarding the implementation approach, is it fine to add a flag to PrintingPolicy (which is a clang utility class used in a variety of places) for a clangd-specific use case like this? I did it this way because the alternative seemed to involve duplicating a bunch of code related to decl-printing, but I'm happy to explore alternatives if this is an issue.

I feel like "print only public fields" is too specific for clangd use case, and probably won't generalize to other callers at all. But I definitely agree with all the concerns around duplicating code. Looks like we have some PrintingCallbacks, maybe we can have something like SummarizeTagDecl, which enables customizing what to put into the body, when printing a TagDecl in terse mode?

Thanks for the suggestion! Using PrintingCallbacks here sounds like a promising idea.

@HighCommander4
Copy link
Collaborator Author

I have a few questions:

  1. Do we want an RFC in discourse for the changes in DeclPrinter?

An RFC might be overkill, but asking for review from a clang code owner for changes to an interface such as PrintingPolicy or PrintingCallbacks is probably a good idea. (If the clang reviewer feels an RFC is warranted, we can do it then.)

  1. Do you think we should also add some test cases to DeclPrinterTest.cpp?

Yep, good call!

@HighCommander4
Copy link
Collaborator Author

I'm planning to revise the patch to make the following changes:

  1. Put the new behaviour behind a config option (I'm thinking Hover --> ShowFields)
  2. Add C language mode tests
  3. Use PrintingCallbacks instead of a PrintingPolicy flag

@HighCommander4 HighCommander4 marked this pull request as draft April 24, 2024 06:52
@HighCommander4
Copy link
Collaborator Author

I'm planning to revise the patch to make the following changes:

  1. Put the new behaviour behind a config option (I'm thinking Hover --> ShowFields)
  2. Add C language mode tests
  3. Use PrintingCallbacks instead of a PrintingPolicy flag

These changes have been made in the latest version of the patch.

@HighCommander4 HighCommander4 marked this pull request as ready for review April 26, 2024 02:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
clang:frontend Language frontend issues, e.g. anything involving "Sema" clang Clang issues not falling into any other category clang-tools-extra clangd
Projects
None yet
Development

Successfully merging this pull request may close these issues.

C struct members and enum values are not shown on type hover
5 participants