RustDoc: Group methods for easy of navigation

I couldn't find an issue/topic about this, pardon me if I missed it.

This is a small feature request, to categorize a trait/impl methods by receiver type. The rationale is that when exploring the docs of a new crate, one usually has a goal in mind, for example "I want to find the constructor for this struct". Currently all Method are in the same list, so one has to read all method names. The search would be slightly easier if the methods would be grouped, for example, a constructor would usually have no self (Ignoring builders). It seems like a small change that could make learning new crates a tiny little bit easier.

Currently they are grouped by impl blocks (which normally implies they are grouped by generic constraints). There could be a button to change the way methods are grouped.

I'm not sure if changing the default grouping is the right choice. For non-generic structs maybe, but it's often useful to know that sometimes a certain set of methods can't be called at all (for example methods implemented for Struct<SomeType> and you have a Struct<OtherType>)

1 Like

Ordering between impl blocks is borderline arbitrary (it depends on what order rustdoc discovers the impls), but I'm fairly sure that ordering of items from a single file is always source order. This makes it a choice of the library what order to present functions in, and typically convenient source order corresponds to a reasonable doc discovery order.

If you collapse-all (the [-] button), you then get an okay outline of methods displaying name and signature in a presumably relation-clustered order.

I do wish it were more common for types with lots of methods to split (and document!) impl blocks based on conceptual purpose, though. Rustdoc always creates at least reasonable API reference documentation, but it takes specific intent to help it generate good documentation.

7 Likes

Consider different page layouts for rustdoc Β· Issue #74758 Β· rust-lang/rust Β· GitHub is a similar request, which suggests a dynamic way to select different layouts such as by receiver type.

@ehuss the issue is indeed very similar in spirit, thank you for the ref.

I was trying to propose something much simpler though. Instead of supporting many layouts, just changing the default from:

From having a single Methods subheader into collapsible categories, say Functions, Methods (&self), Methods (&mut self), Methods (self) would IMO already improve navigation without having to do all the work to support many different visualizations.

This would preserve the maintainer's control to define the order in the content section, as described by @CAD97 , while having extra structure, in addition to the lexical ordering, to easy navigation.

Xcode supports special comments:

// MARK: Section label goes here

which it inserts between methods in its navigation panel.

Rustdoc could have a similar feature for manual grouping of methods.

&self separate from &mut self may unnecessarily separate foo and foo_mut methods, which are often the same thing.

I think source code order is underappreciate by rustdoc. That's the order that must be already logical for the maintainer. Preserving it gives full control to the author.

That sounds at the same time really nice to have, and borderline impossible to implement correctly.

Simple example of what I mean: Constructors are by convention called new(), but not all types (can) adhere to that. For example a type that has multiple constructors, to initialize the type in different ways. One could do fuzzy matching there, and it might work reasonably, or it might not e.g. for types that ignore the convention altogether, perhaps because it's a wrapper for a C api where make_*()-like constructors are not unheard of.

Maybe some way to assign tags to methods for documentation purposes would be good? Some JS (with graceful fallbacks) to do filtering based on tags might be useful. For example "constructors", "helpers", "mutations", "queries", etc.

Also note that receivers can also be Arc<T> or Rc<T>.

I'm not saying for rustdoc to create this grouping, but for the source to the authored with this grouping. Example: my cstr8 crate could put all the methods into a single impl block, but instead does

/// Explicit conversions.
impl CStr8 {
    pub const fn as_str(&self) -> &str;
    pub const fn as_c_str(&self) -> &CStr;
    // …
}

/// Constructors.
impl CStr8 {
    pub fn from_utf8_with_nul(v: &[u8]) -> Result<&CStr8, CStr8Error>;
    pub fn from_utf8_until_nul(v: &[u8]) -> Result<&CStr8, CStr8Error>;
    // …
}

Also, FWIW, the compiler already uses a "constructorish" heuristic that it uses for error help (roughly that it returns Self or Result<Self, _> and doesn't have any parameter types that mention Self).

5 Likes

Just one thing that I'd like to point out.

Having the maintainers authoring the code such that the documentation is easy to use is great. This gives control to professionally developed and maintained libraries, which is great.

But focusing only on that type of maintenance excludes a lot of the code out there. The motivation for me to start this thread was exactly because I was using rust embedded libraries. A lot of these libraries are hacked together and maintained as a hobby project, and don't have that level of attention to detail.

I think it is great to have control over how things are displayed by rustdoc when I'm writing code for my full time job, and I appreciate having control over the docs. But at the same time, I don't think it should be assumed that every library out there will have that level of attention to detail, and "lack of attention" can exist for very fair reasons, like someone that reverse engineer some obscure protocol and wanted to share the results with the community.

Adding features to make navigation on these cases IMO benefits everyone.

1 Like

Wouldn't something like this be the simplest/most flexible solution (macro/attribute name chosen arbitrarily, doc probably can't be used), which doesn't require heuristics?

impl CStr8 {
    #[doc(group = "Constructors", priority = 2)] // Obvisouly not a constructor
    pub const fn a(&self) -> &str;
    #[doc(group = "SomeOtherGroupName", priority = 3)]
    pub const fn b(&self) -> &CStr;
    #[doc(group = "Conversion Methods", priority = 3)]
    pub const fn c(&self) -> &CStr;
    #[doc(group = "Constructors", priority = 6)]
    pub const fn d(&self) -> &CStr;

    pub const fn e(&self) -> &CStr;
    #[doc(priority = 4)]
    pub const fn f(&self) -> &CStr;
    pub const fn g(&self) -> &CStr;
}

This would let the author decide how it should be displayed, under which groups, how they should be named and in which order, regardless of the implementation order in the files or how they're put into impl blocks.

Since this allows setting multiple priorities for a group the following would have to be decided for group ordering:

  • Group Priority = max(priorities) // Probably best and easiest to understand
  • Group Priority = min(priorities) // Not sure why you'd want that
  • Group Priority = average(priorities)

In the following I'm going to assume max(priorities):

This would result in the following:

  • Constuctors Because it has the highest group priority: max(2, 6) = 8
    • d Because the priority (6) is higher than that of a (2)
    • a
  • SomeOtherGroupName They have the same priority, so it's up to the compiler which will probably choose based on the order in the file
    • b
  • Conversion Methods
    • c
  • Ungrouped
    • f Because it's the only one with a priority set
    • e Order between e and g is up to the compiler again
    • g

Basically if group is not set put it into ungrouped (which could be sorted by impl block) and if the priority is not set read it as 0.

If it can be set on an entire impl block (especially the group one would be useful) it works the same and applies to all below it that don't overwrite category or priority, thus giving you the per-impl-block sorting we have but with a way to give it a name and a priority to set which group shows up higher in the docs.

This is very important I believe. Sensible defaults for simple use-cases such that it "just works" for all the basic cases are so important in development of any new project.

I am so glad that this topic gets some attention. :heart:

1 Like

Maybe we could have a config file for which groups do exist such that linters can easily check for typos. Something along the lines of

# DocGroups.toml
[groups]
# Set inheritance of groups to false by default
inherit = false

[crate]
groups = [
    # This group will be available for every submodule of the crate
    { name = "config", inherit = true },
]

# These are module-specific groups
[crate.module1.submodule_alpha]
groups = [
    "Methods",
    { name = "methods-ref-self", display = "Methods `(&self)`" },
    { "methods-ref-mut-self", display = "Methods `(&mut self)`" },
]

This would avoid typos such as

impl MyConfig {
    #[doc_groups("Config")]
    fn new() -> Self {...}

    #[doc_groups("methods_ref_self")]
    fn uuid(&self) -> u64 {...}
}

and they can be caught directly inside an IDE since every valid group needs to be specified.

I am very open for feedback on this.

I always organize my crate into submodules which contain encapsulated functionality (at least aim to do so). This structure is more difficult when importing from this crate and I usually prefer a flat layout structure without any prelude where I either provide every public object or at least every commonly used one a bit like syn.

I would love to also have the option to group at this level of documentation.

I really want a new interface for rustdoc to have the feature in OP, and more opinionated functionalities.

So I'm working on GitHub - zjp-CN/term-rustdoc: [WIP] A TUI for Rust docs that aims to improve the UX on tree view and generic code.

Specifically for the topic here, I drew the desgin in repo before seeing OP.

                                                                                                     
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                                                                 Navigation Panel    
   β”‚Item Detailβ”‚                                                               β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜                                                               β”‚Module Tree         β”‚
         β”‚         β”Œβ”€β”€β”€β”€β”€β”€β”€β”                                                   β”‚Field/Variant Tree  β”‚
         β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€Modulesβ”‚                                                   β”‚ITAB Impl Tree      β”‚
         β”‚         β””β”€β”€β”€β”€β”€β”€β”€β”˜                                                   β”‚Args Type Tree      β”‚
         β”‚                                                                     β”‚Return Type Tree    β”‚
         β”‚         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”               β”‚Construct Fn Tree   β”‚
         β”‚         β”‚Data Structures    β”œβ”€β”¬β”€β”€β”€β”€β–Ίβ”‚Fields/Varaintsβ”‚               β”‚Non-receiver Fn Treeβ”‚
         β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€(struct/enum/union)β”‚ β”‚     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜               β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚                                                           
         β”‚                           β–²   β”‚                                                           
         β”‚                           β”‚   β”‚     β”Œβ”€β”€β”€β”€β”€β” For methods in all Impls                      
         β”‚                           β”‚   β”œβ”€β”€β”€β”€β–Ίβ”‚Implsβ”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                      
         β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”              β”‚   β”‚     β””β”€β”€β”€β”¬β”€β”˜                        β”‚                      
         β”œβ”€β”€β”€β”‚Funcionsβ”‚              β”‚   β”‚         β”œβ”€β”€β”€β”€β”€β–Ί Inherent methods   β”‚                      
         β”‚   β””β”€β”€β”¬β”€β”€β”€β”€β”€β”˜              β”‚   β”‚         β”‚                          β”‚                      
         β”‚      β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”‚   β”‚ Sort    β”œβ”€β”€β”€β”€β”€β–Ί Trait methods      β”‚                      
         β”‚      β”œβ”€β”€β–Ίβ”‚Args Typesβ”œβ”€β”€β”€β” β”‚   β”‚ By Name β”‚                          β”‚                      
         β”‚      β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”œβ”€β”€   β”‚ In kindsβ”œβ”€β”€β”€β”€β”€β–Ί Auto traits        β”‚                      
         β”‚      β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”œβ”€β”€   β”‚         β”‚                          β”‚                      
         β”‚      └──►│Return Typeβ”œβ”€β”€β”˜ β”‚   β”‚         └─────► Blanket traits     β”‚                      
         β”‚          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚   β”‚                                    β”‚                      
         β”‚                           β”‚   β”‚                            β”Œβ”€β”€β”€β”€β”€β”€β”€β”˜                      
         β”‚                           β”‚   β”‚                            β”‚    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      
         β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”                β”‚   β”‚                            β”œβ”€β”€β”€β–Ίβ”‚Sort By Args Typesβ”œβ”€β”€β”€β”  
         └────Traitsβ”‚                β”‚   β”‚                        No  β”‚    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚  
             β””β”€β”¬β”€β”€β”€β”€β”˜   TODO         β”‚   β”‚                        ITABβ”‚    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚  
               β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚   β”‚                            └───►│Sort By Return Typeβ”œβ”€β”€β”€  
               β”‚  β”‚Sub/Super traitsβ”‚ β”‚   β”‚                                 β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚  
               β”‚  β”‚Assoc Types     β”‚ β”‚   β”‚     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                              β”‚  
               └───Type Parameters β”‚ β”‚   β”œβ”€β”€β”€β”€β”€β”‚Construct Functionsβ”‚ fn(...) -> Self              β”‚  
                  β”‚Method Args     β”‚ β”‚   β”‚     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜             β”‚                β”‚  
                  β”‚Method Return   β”‚ β”‚   β”‚     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”             β”‚                β”‚  
                  β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚   └─────│Used as an argumentβ”‚             β”‚                β”‚  
                          β”‚          β”‚         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜             β”‚                β”‚  
                          β”‚          β”‚          fn(.., Self) -> ...              β”‚                β”‚  
                          β”‚          β”‚          i.e. non-receiver fn             β”‚                β”‚  
                          β–Ό          β–Ό                   β”‚                       β”‚                β”‚  
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”‚                       β”‚                β”‚  
  β”‚       β”Œβ”€β”€β–Ί Concrete Types                  β”‚         β”‚                       β”‚                β”‚  
  β”‚       β”‚                                    β”‚         β”‚                       β”‚                β”‚  
  β”‚Types───                                    β”‚         β”‚                       β”‚                β”‚  
  β”‚       β”‚                   β”Œβ”€β–Ί Semi Generic β”‚β—„β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  
  β”‚       └──► Generic Types───                β”‚                                                     
  β”‚                           └─► Fully Genericβ”‚                                                     
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                                                     

I like your project but I personally will probably not use it. I would much rather prefer a tool that generates a static website such as rustdoc such that I can upload the documentation where everyone can see it and still keep it customized.

I am not familiar with the structure of rustdoc but I would like to get started and just see how far I can come. Can anyone point me into the right direction where to look initially to possibly get started on this topic?

Long time ago, I had an idea to redesign the Rust doc. I made a prototype in full JavaScript (not for real use: incomplete and with a lot of bugs) to let me test quickly alternatives before submitting contribution, It helped me eliminate a lot of bad ideas.

If I talk about it there, it is because I added a summary section with different ways to group the methods (name, implementation, self type, return type). I had not enough spare time to turn that in actual contribution. I plan to do that soon, but there is still much to discuss.

1 Like