term_rustdoc/tree/textline/
fold.rs

1use super::TreeLines;
2use crate::tree::{DModule, DocTree, IDMap, ID};
3use rustc_hash::FxHashSet as HashSet;
4use rustdoc_types::ItemEnum;
5
6/// how to fold the text tree
7#[derive(Default, PartialEq, Eq)]
8enum Kind {
9    /// Expand all public items in all modules.
10    #[default]
11    ExpandAll,
12    /// Only expand items under root module with sub modules folded.
13    ExpandZero,
14    /// Expand level zero and one items.
15    ///
16    /// Level zero refers to items directly under root module.
17    ///
18    /// Level one refers to items from level-zero modules.
19    ///
20    /// Items from these two outermost levels may be the most important APIs.
21    ExpandToFirstLevelModules,
22    /// Always expand and focus on current module.
23    ///
24    /// NOTE: this allows all (sub)modules to expand (but with non-module items hidden),
25    /// because it's helpful for users to not only know the current one, but also quickly
26    /// jump into any other one.
27    CurrentModule,
28}
29
30/// Fold based on module tree.
31#[derive(Default)]
32pub struct Fold {
33    kind: Kind,
34    /// module IDs that should be expanded
35    expand: HashSet<ID>,
36}
37
38// ─➤  ─⮞ ─▶ ▶
39
40/// Fold a tree.
41impl TreeLines {
42    /// Expand all module items including data structure's impl and trait's implementors.
43    pub fn expand_all_including_impls(&mut self) {
44        *self = Self::new_with(self.doc(), |doc| doc.dmodule_show_prettier()).0;
45    }
46
47    pub fn dmodule(&self) -> &DModule {
48        self.doc.dmodule()
49    }
50
51    pub fn idmap(&self) -> &IDMap {
52        &self.doc
53    }
54
55    pub fn expand_all(&mut self) {
56        self._expand_all();
57        self.lines = self.dmodule().item_tree(self.idmap()).cache_lines().0;
58    }
59
60    pub(super) fn _expand_all(&mut self) {
61        fn traversal_id(m: &DModule, mods: &mut HashSet<ID>) {
62            mods.insert(m.id.clone());
63            for submod in &m.modules {
64                traversal_id(submod, mods);
65            }
66        }
67        self.fold.kind = Kind::ExpandAll;
68        self.fold.expand.clear();
69        traversal_id(self.doc().dmodule(), &mut self.fold.expand);
70    }
71
72    pub fn expand_zero_level(&mut self) {
73        self.fold.kind = Kind::ExpandZero;
74        self.fold.expand.clear();
75        self.fold.expand.insert(self.dmodule().id.clone());
76        self.update_cached_lines(|dmod, map, _| {
77            let mut root = dmod.item_tree_only_in_one_specified_mod(map);
78            root.extend(
79                dmod.modules
80                    .iter()
81                    .map(|m| node!(ModuleFolded: map, Module, &m.id)),
82            );
83            root
84        });
85    }
86
87    pub fn expand_to_first_level_modules(&mut self) {
88        self.fold.kind = Kind::ExpandToFirstLevelModules;
89        let dmod = &self.dmodule().modules;
90        self.fold.expand = dmod.iter().map(|m| m.id.clone()).collect();
91        self.update_cached_lines(|dmod, map, mods| {
92            let mut root = dmod.item_tree_only_in_one_specified_mod(map);
93            for m in &dmod.modules {
94                let tree = if mods.contains(&m.id) {
95                    let mut tree = m.item_tree_only_in_one_specified_mod(map);
96                    for submod in &m.modules {
97                        let leaf = node!(ModuleFolded: map, Module, &submod.id);
98                        tree.push(leaf);
99                    }
100                    tree
101                } else {
102                    node!(ModuleFolded: map, Module, &m.id)
103                };
104                root.push(tree);
105            }
106            root
107        });
108    }
109}
110
111impl TreeLines {
112    /// Expand a folded module or fold an expanded one.
113    ///
114    /// This pushs a module ID to a without setting any fold kind.
115    pub fn expand_toggle(&mut self, id: ID) {
116        fn modules_traversal(
117            dmod: &DModule,
118            map: &IDMap,
119            parent: &mut DocTree,
120            should_stop: &mut impl FnMut(&DModule) -> bool,
121        ) {
122            for m in &dmod.modules {
123                if should_stop(m) {
124                    let node = node!(ModuleFolded: map, Module, &m.id);
125                    parent.push(node);
126                } else {
127                    let mut node = m.item_tree_only_in_one_specified_mod(map);
128                    modules_traversal(m, map, &mut node, should_stop);
129                    parent.push(node);
130                };
131            }
132        }
133
134        if self.fold.kind == Kind::CurrentModule {
135            // FIXME: poor interaction with CurrentModule bahavior
136            //
137            // To fix this, we have to remember all the modules' id and an extra
138            // expand-vs-fold state.
139            //
140            // This is a smallUX improvement, but for now, just forbid toggling
141            // when expanding CurrentModule.
142            return;
143        }
144
145        if !self.check_id(&id) {
146            return;
147        }
148        let mods = &mut self.fold.expand;
149        if mods.contains(&id) {
150            mods.remove(&id);
151        } else {
152            mods.insert(id);
153        }
154        self.update_cached_lines(|dmod, map, mods| {
155            let mut root = dmod.item_tree_only_in_one_specified_mod(map);
156            modules_traversal(dmod, map, &mut root, &mut |m| !mods.contains(&m.id));
157            root
158        });
159    }
160}
161
162impl TreeLines {
163    pub fn expand_current_module_only(&mut self, id: ID) {
164        self.fold.kind = Kind::CurrentModule;
165        if !self.check_id(&id) {
166            return;
167        }
168        self.fold.expand.clear();
169        self.fold.expand.insert(id);
170        self._expand_current_module_only();
171    }
172
173    fn _expand_current_module_only(&mut self) {
174        fn modules_traversal(
175            dmod: &DModule,
176            map: &IDMap,
177            parent: &mut DocTree,
178            should_stop: &mut impl FnMut(&DModule) -> bool,
179        ) {
180            for m in &dmod.modules {
181                if should_stop(m) {
182                    // use long path because it's helpful to instantly know where it is
183                    let mut node = m.item_tree_only_in_one_specified_mod(map);
184                    node.extend(
185                        m.modules
186                            .iter()
187                            .map(|m| node!(@name ModuleFolded: map, &m.id)),
188                    );
189                    parent.push(node);
190                    // NOTE: Stop traverlling down inside but still travel in other modules.
191                    // This is because it's not helpful to only show/know target modules.
192                } else {
193                    // use short name for non-target modules
194                    let mut node = node!(@name ModuleFolded: map, &m.id);
195                    modules_traversal(m, map, &mut node, should_stop);
196                    parent.push(node);
197                };
198            }
199        }
200        self.update_cached_lines(|dmod, map, mods| {
201            let mut root = node!(Module: map, &dmod.id);
202            if mods.contains(&dmod.id) {
203                // item tree already contains the root module, thus only mv the leaves
204                let iter = dmod.item_tree_only_in_one_specified_mod(map).tree.leaves;
205                root.tree.extend(iter);
206            }
207            modules_traversal(dmod, map, &mut root, &mut |m| mods.contains(&m.id));
208            root
209        });
210    }
211}
212
213impl TreeLines {
214    /// check if the id is a module (or reexported as module)
215    fn check_id(&self, id: &ID) -> bool {
216        if !self.dmodule().modules.iter().any(|_m| {
217            // only Module or reexported item as Module can be in list
218            self.idmap()
219                .get_item(id)
220                .map(|item| match &item.inner {
221                    ItemEnum::Module(_) => true,
222                    ItemEnum::Import(reepxort) => {
223                        if let Some(id) = &reepxort.id {
224                            if let Some(item) = self.idmap().get_item(&id.0) {
225                                return matches!(item.inner, ItemEnum::Module(_));
226                            }
227                        }
228                        false
229                    }
230                    _ => false,
231                })
232                .unwrap_or(false)
233        }) {
234            error!(
235                "ID({id}) is not a non-module item `{}` {:?}",
236                self.idmap().name(&id),
237                self.idmap().get_item(id)
238            );
239            return false;
240        }
241        true
242    }
243
244    fn update_cached_lines(&mut self, f: impl FnOnce(&DModule, &IDMap, &HashSet<ID>) -> DocTree) {
245        let map = self.idmap();
246        let dmod = &self.dmodule();
247        let mods = &self.fold.expand;
248        if mods.is_empty() {
249            // if no mods are sepecified, default to expand all
250            self.lines = self.dmodule().item_tree(map).cache_lines().0;
251            return;
252        }
253        let root = f(dmod, map, mods);
254        self.lines = root.cache_lines().0;
255    }
256}