@@ -11,7 +11,7 @@ use emmylua_code_analysis::{DbIndex, LuaDeclId, LuaTypeDeclId};
1111use tera:: Tera ;
1212
1313use crate :: OutputDestination ;
14- use types:: { HtmlDoc , NavGroup , NavItem , NavModel } ;
14+ use types:: { HtmlDoc , NavGroup , NavItem , NavModel , NavTreeNode } ;
1515
1616use self :: generators:: { GenContext , build_global_doc, build_module_doc, build_type_doc} ;
1717
@@ -227,11 +227,14 @@ fn mark_active(nav: &mut NavModel, kind: &str, name: &str) {
227227 }
228228}
229229
230- /// Splits each nav list into letter groups (used by the sidebar).
230+ /// Prepares the nav for rendering: letter groups (index page), the module
231+ /// hierarchy tree and the pre-rendered sidebar HTML.
231232fn finalize_nav ( nav : & mut NavModel ) {
232233 nav. type_groups = build_groups ( & nav. types ) ;
233234 nav. module_groups = build_groups ( & nav. modules ) ;
234235 nav. global_groups = build_groups ( & nav. globals ) ;
236+ nav. type_tree = build_type_tree ( & nav. types ) ;
237+ nav. sidebar_html = build_sidebar_html ( nav) ;
235238}
236239
237240fn build_groups ( items : & [ NavItem ] ) -> Vec < NavGroup > {
@@ -276,6 +279,160 @@ fn escape(name: String) -> String {
276279 super :: markdown_generator:: escape_type_name ( & name)
277280}
278281
282+ // ─── Module hierarchy tree ───────────────────────────────────────────────
283+
284+ /// Builds the module/namespace hierarchy tree from the flat type nav items.
285+ ///
286+ /// Each item name is `"class lsp.CodeActionKind"`; the namespace path
287+ /// (`lsp`) becomes folders and the final segment a leaf.
288+ fn build_type_tree ( items : & [ NavItem ] ) -> Vec < NavTreeNode > {
289+ let mut roots: Vec < NavTreeNode > = Vec :: new ( ) ;
290+ for item in items {
291+ let ( kind, full_name) = split_type_name ( & item. name ) ;
292+ let segments: Vec < & str > = full_name. split ( '.' ) . collect ( ) ;
293+ insert_tree ( & mut roots, & segments, item, kind) ;
294+ }
295+ sort_tree ( & mut roots) ;
296+ set_open ( & mut roots) ;
297+ roots
298+ }
299+
300+ fn split_type_name ( name : & str ) -> ( & str , & str ) {
301+ for prefix in [ "class " , "enum " , "alias " ] {
302+ if let Some ( rest) = name. strip_prefix ( prefix) {
303+ return ( prefix. trim ( ) , rest) ;
304+ }
305+ }
306+ ( "class" , name)
307+ }
308+
309+ fn insert_tree ( nodes : & mut Vec < NavTreeNode > , path : & [ & str ] , item : & NavItem , kind : & str ) {
310+ let head = path[ 0 ] ;
311+ if path. len ( ) == 1 {
312+ nodes. push ( NavTreeNode {
313+ label : head. to_string ( ) ,
314+ data_name : Some ( item. name . clone ( ) ) ,
315+ kind : kind. to_string ( ) ,
316+ href : Some ( item. href . clone ( ) ) ,
317+ active : item. active ,
318+ open : false ,
319+ children : Vec :: new ( ) ,
320+ } ) ;
321+ return ;
322+ }
323+ let idx = match nodes
324+ . iter ( )
325+ . position ( |n| n. href . is_none ( ) && n. label == head)
326+ {
327+ Some ( idx) => idx,
328+ None => {
329+ nodes. push ( NavTreeNode {
330+ label : head. to_string ( ) ,
331+ data_name : None ,
332+ kind : String :: new ( ) ,
333+ href : None ,
334+ active : false ,
335+ open : false ,
336+ children : Vec :: new ( ) ,
337+ } ) ;
338+ nodes. len ( ) - 1
339+ }
340+ } ;
341+ insert_tree ( & mut nodes[ idx] . children , & path[ 1 ..] , item, kind) ;
342+ }
343+
344+ fn sort_tree ( nodes : & mut [ NavTreeNode ] ) {
345+ nodes. sort_by ( |a, b| a. label . cmp ( & b. label ) ) ;
346+ for node in nodes. iter_mut ( ) {
347+ sort_tree ( & mut node. children ) ;
348+ }
349+ }
350+
351+ /// Marks folders on the active branch as open. Returns whether `nodes` contain
352+ /// an active item.
353+ fn set_open ( nodes : & mut [ NavTreeNode ] ) -> bool {
354+ let mut has_active = false ;
355+ for node in nodes. iter_mut ( ) {
356+ if node. active {
357+ has_active = true ;
358+ } else if set_open ( & mut node. children ) {
359+ node. open = true ;
360+ has_active = true ;
361+ }
362+ }
363+ has_active
364+ }
365+
366+ /// Renders the full sidebar: the type hierarchy tree plus flat module/global
367+ /// lists. Kept in Rust (not the template) so the tree can recurse freely.
368+ fn build_sidebar_html ( nav : & NavModel ) -> String {
369+ let mut html = String :: new ( ) ;
370+ if !nav. type_tree . is_empty ( ) {
371+ html. push_str (
372+ "<div class=\" sidebar-category\" ><div class=\" category-title\" >Types</div><ul>" ,
373+ ) ;
374+ html. push_str ( & render_tree_html ( & nav. type_tree , & nav. root_prefix ) ) ;
375+ html. push_str ( "</ul></div>" ) ;
376+ }
377+ if !nav. modules . is_empty ( ) {
378+ html. push_str (
379+ "<div class=\" sidebar-category\" ><div class=\" category-title\" >Modules</div><ul>" ,
380+ ) ;
381+ for item in & nav. modules {
382+ html. push_str ( & render_flat_item ( item, & nav. root_prefix ) ) ;
383+ }
384+ html. push_str ( "</ul></div>" ) ;
385+ }
386+ if !nav. globals . is_empty ( ) {
387+ html. push_str (
388+ "<div class=\" sidebar-category\" ><div class=\" category-title\" >Globals</div><ul>" ,
389+ ) ;
390+ for item in & nav. globals {
391+ html. push_str ( & render_flat_item ( item, & nav. root_prefix ) ) ;
392+ }
393+ html. push_str ( "</ul></div>" ) ;
394+ }
395+ html
396+ }
397+
398+ fn render_flat_item ( item : & NavItem , root_prefix : & str ) -> String {
399+ let active_cls = if item. active { " active" } else { "" } ;
400+ format ! (
401+ "<li><a data-name=\" {}\" href=\" {}{}\" class=\" nav-item{active_cls}\" >{}</a></li>" ,
402+ render:: html_escape( & item. name) ,
403+ root_prefix,
404+ render:: html_escape( & item. href) ,
405+ render:: html_escape( & item. name)
406+ )
407+ }
408+
409+ fn render_tree_html ( nodes : & [ NavTreeNode ] , root_prefix : & str ) -> String {
410+ let mut html = String :: new ( ) ;
411+ for node in nodes {
412+ if let Some ( href) = & node. href {
413+ let active_cls = if node. active { " active" } else { "" } ;
414+ let data = node. data_name . as_deref ( ) . unwrap_or ( & node. label ) ;
415+ html. push_str ( & format ! (
416+ "<li><a data-name=\" {}\" href=\" {}{}\" class=\" nav-item{active_cls}\" ><span class=\" kind-dot kind-{}\" ></span>{}</a></li>" ,
417+ render:: html_escape( data) ,
418+ root_prefix,
419+ render:: html_escape( href) ,
420+ render:: html_escape( & node. kind) ,
421+ render:: html_escape( & node. label)
422+ ) ) ;
423+ } else {
424+ let open = if node. open { " open" } else { "" } ;
425+ html. push_str ( & format ! (
426+ "<li><details{open}><summary class=\" group-label\" >{}</summary><ul>" ,
427+ render:: html_escape( & node. label)
428+ ) ) ;
429+ html. push_str ( & render_tree_html ( & node. children , root_prefix) ) ;
430+ html. push_str ( "</ul></details></li>" ) ;
431+ }
432+ }
433+ html
434+ }
435+
279436fn render_page (
280437 tl : & Tera ,
281438 template : & str ,
0 commit comments