77"""
88
99import re
10+ import shutil
1011import sys
1112from pathlib import Path
1213
@@ -237,6 +238,7 @@ def build_symbol_index(docs_dir):
237238 lines = content .split ("\n " )
238239
239240 class_name = md_file .stem .capitalize ()
241+ is_enum_doc = md_file .stem == "enums"
240242 section = ""
241243 subtitle = ""
242244 in_code_block = False
@@ -276,6 +278,40 @@ def build_symbol_index(docs_dir):
276278 elif stripped .startswith ("## " ) and not stripped .startswith ("### " ):
277279 section = stripped .lstrip ("# " ).strip ()
278280
281+ # docs/src/helpers/enums.md stores enum "classes" as ## headings.
282+ # Index them as top-level symbols so queries like "ItemFlag" work.
283+ if is_enum_doc and " " not in section :
284+ class_name = section
285+ desc = _get_desc_after (lines , i )
286+ symbols .append ({
287+ "class_name" : class_name ,
288+ "name" : None ,
289+ "section" : None ,
290+ "qualified" : class_name ,
291+ "desc" : desc ,
292+ "file" : str (md_file .relative_to (docs_dir ).with_suffix ('' )),
293+ "line" : i + 1 ,
294+ "enum_section_line" : i + 1 ,
295+ })
296+
297+ elif is_enum_doc and class_name and stripped .startswith ("| `" ):
298+ # docs/src/helpers/enums.md stores enum values in markdown tables.
299+ # Index values as members so queries like "ItemFlag." work.
300+ m = re .match (r"\|\s*`([^`]+)`\s*\|" , stripped )
301+ if m :
302+ member = m .group (1 ).strip ()
303+ if member and member != "Name" :
304+ symbols .append ({
305+ "class_name" : class_name ,
306+ "name" : member ,
307+ "section" : "Values" ,
308+ "qualified" : f"{ class_name } .{ member } " ,
309+ "desc" : "" ,
310+ "sig" : None ,
311+ "file" : str (md_file .relative_to (docs_dir ).with_suffix ('' )),
312+ "line" : i + 1 ,
313+ })
314+
279315 elif stripped .startswith ("### " ):
280316 member = stripped .lstrip ("# " ).strip ()
281317 # Only index code-like symbols (no spaces = identifier)
@@ -329,6 +365,73 @@ def _get_signature(lines, heading_idx, member):
329365 return f"({ m .group (1 )} )"
330366 return None
331367
368+ def _load_stub_enum_values ():
369+ """Load enum constants from bridge __init__.pyi for richer completion."""
370+ candidates = [
371+ SCRIPT_DIR .parent / "src" / "main" / "resources" / "python" / "bridge" / "__init__.pyi" ,
372+ SCRIPT_DIR / "src" / "main" / "resources" / "python" / "bridge" / "__init__.pyi" ,
373+ Path .cwd () / "src" / "main" / "resources" / "python" / "bridge" / "__init__.pyi" ,
374+ ]
375+ stub_path = next ((p for p in candidates if p .is_file ()), None )
376+ if stub_path is None :
377+ return {}
378+
379+ out = {}
380+ current_class = None
381+ for raw in stub_path .read_text (encoding = "utf-8" ).splitlines ():
382+ m_class = re .match (r"^class\s+(\w+)\(EnumValue\):" , raw )
383+ if m_class :
384+ current_class = m_class .group (1 )
385+ out .setdefault (current_class , [])
386+ continue
387+
388+ if current_class is None :
389+ continue
390+
391+ if raw .startswith ("class " ):
392+ current_class = None
393+ continue
394+
395+ m_member = re .match (r"^\s+([A-Z][A-Z0-9_]*)\s*:\s*ClassVar\[" , raw )
396+ if m_member :
397+ out [current_class ].append (m_member .group (1 ))
398+
399+ return out
400+
401+ def _merge_stub_enum_symbols (symbols ):
402+ """Add enum constants from stubs when docs are incomplete."""
403+ enum_values = _load_stub_enum_values ()
404+ if not enum_values :
405+ return symbols
406+
407+ class_meta = {}
408+ for s in symbols :
409+ if s .get ("name" ) is None :
410+ class_meta .setdefault (s ["class_name" ], s )
411+
412+ existing = {(s ["class_name" ], s .get ("name" )) for s in symbols }
413+ for class_name , members in enum_values .items ():
414+ meta = class_meta .get (class_name )
415+ if meta is None :
416+ continue
417+ for member in members :
418+ key = (class_name , member )
419+ if key in existing :
420+ continue
421+ symbols .append ({
422+ "class_name" : class_name ,
423+ "name" : member ,
424+ "section" : "Values" ,
425+ "qualified" : f"{ class_name } .{ member } " ,
426+ "desc" : "" ,
427+ "sig" : None ,
428+ "file" : meta .get ("file" , "helpers/enums" ),
429+ "line" : meta .get ("line" , 1 ),
430+ })
431+ existing .add (key )
432+
433+ return symbols
434+
332435def _strip_md (line ):
333436 """Strip markdown formatting from a line for terminal display."""
334437 line = re .sub (r'\[([^\]]*)\]\([^)]*\)' , r'\1' , line ) # links
@@ -341,6 +444,34 @@ def _sort_key(s):
341444 """Sort key that orders _ after letters (replace _ with ~ which sorts after z)."""
342445 return s .lower ().replace ('_' , '~' )
343446
447+ def _print_compact_members (members ):
448+ """Print members as a width-wrapped comma-separated list."""
449+ values = []
450+ for s in members :
451+ sig = s .get ('sig' , '' ) or ''
452+ values .append (f".{ s ['name' ]} { sig } " )
453+
454+ term_cols = shutil .get_terminal_size ((120 , 20 )).columns
455+ indent = " "
456+ max_width = max (30 , term_cols - len (indent ))
457+
458+ line_parts = []
459+ line_len = 0
460+ for value in values :
461+ part = value if not line_parts else f", { value } "
462+ part_len = len (part )
463+
464+ if line_parts and line_len + part_len > max_width :
465+ print (f"{ indent } \033 [36m{ '' .join (line_parts )} \033 [0m" )
466+ line_parts = [value ]
467+ line_len = len (value )
468+ else :
469+ line_parts .append (part )
470+ line_len += part_len
471+
472+ if line_parts :
473+ print (f"{ indent } \033 [36m{ '' .join (line_parts )} \033 [0m" )
474+
344475
345476# Extension doc file stems (matches Extensions section in docs/build.py)
346477_EXT_FILES = {
@@ -572,14 +703,57 @@ def _get_member_doc(md_path, heading_line):
572703
573704 return "\n " .join (collapsed )
574705
706+ def _get_enum_section_doc (md_path , heading_line ):
707+ """Extract a ## section block (used for enum docs in enums.md)."""
708+ content = md_path .read_text (encoding = "utf-8" )
709+ lines = content .split ("\n " )
710+
711+ out = []
712+ in_code_block = False
713+ start = max (0 , heading_line - 1 )
714+
715+ for j in range (start , len (lines )):
716+ line = lines [j ]
717+ stripped = line .strip ()
718+
719+ if stripped .startswith ("```" ):
720+ in_code_block = not in_code_block
721+ continue
722+
723+ if not in_code_block and j > start and stripped .startswith ("## " ):
724+ break
725+
726+ if in_code_block :
727+ out .append (f" { _highlight_python (line .rstrip ())} " )
728+ elif stripped .startswith ("## " ) and j == start :
729+ title = stripped .lstrip ('# ' ).strip ()
730+ out .append (f" \033 [36;1m# { title } \033 [0m" )
731+ elif stripped .startswith ("- " ):
732+ out .append (f" { _strip_md (line .rstrip ())} " )
733+ elif stripped :
734+ out .append (f" { _strip_md (line .rstrip ())} " )
735+ else :
736+ out .append ("" )
737+
738+ while out and not out [- 1 ].strip ():
739+ out .pop ()
740+
741+ collapsed = []
742+ for line in out :
743+ if not line .strip () and collapsed and not collapsed [- 1 ].strip ():
744+ continue
745+ collapsed .append (line )
746+
747+ return "\n " .join (collapsed )
748+
575749def cmd_search (query ):
576750 docs_dir = find_docs_dir ()
577751 if docs_dir is None :
578752 print ("Error: Could not find docs/src directory." )
579753 print ("Run this from the PyJavaBridge project root." )
580754 sys .exit (1 )
581755
582- symbols = build_symbol_index (docs_dir )
756+ symbols = _merge_stub_enum_symbols ( build_symbol_index (docs_dir ) )
583757
584758 if query .endswith ("." ):
585759 # "Player." → list all members of that class
@@ -591,14 +765,28 @@ def cmd_search(query):
591765 ext = _EXT_TAG if Path (members [0 ]['file' ]).stem in _EXT_FILES else ""
592766 print (f"\033 [1m{ members [0 ]['class_name' ]} \033 [0m \033 [90m({ members [0 ]['file' ]} .md)\033 [0m{ ext } " )
593767 current_section = None
594- for s in members :
595- section = s ['section' ] or ''
768+ section_members = []
769+ for s in members + [None ]:
770+ if s is None :
771+ section = None
772+ else :
773+ section = s ['section' ] or ''
774+
596775 if section != current_section :
776+ if current_section and section_members :
777+ if current_section == "Values" :
778+ _print_compact_members (section_members )
779+ else :
780+ for m in section_members :
781+ sig = m .get ('sig' , '' ) or ''
782+ print (f" \033 [90m###\033 [0m \033 [36m.{ m ['name' ]} { sig } \033 [0m" )
783+ section_members = []
597784 current_section = section
598785 if section :
599786 print (f" \033 [90m## { section } \033 [0m" )
600- sig = s .get ('sig' , '' ) or ''
601- print (f" \033 [90m###\033 [0m \033 [36m.{ s ['name' ]} { sig } \033 [0m" )
787+
788+ if s is not None :
789+ section_members .append (s )
602790
603791 elif "." in query :
604792 # "Player.name" → find specific member(s)
@@ -647,13 +835,22 @@ def cmd_search(query):
647835 if any (s ["class_name" ].lower () == q for s in classes ):
648836 classes = [s for s in classes if s ["class_name" ].lower () == q ]
649837 if classes :
838+ members_by_class = {}
839+ for sym in symbols :
840+ if sym .get ("name" ):
841+ members_by_class .setdefault (sym ["class_name" ], []).append (sym )
650842 lines_printed = 0
651843 for s in classes :
652844 ext = _EXT_TAG if Path (s ['file' ]).stem in _EXT_FILES else ""
653845 # Show the class-level doc text (everything before first ### member)
654846 md_path = docs_dir / f"{ s ['file' ]} .md"
655847 if md_path .exists ():
656- doc = _get_class_doc (md_path , ext )
848+ if s .get ("enum_section_line" ):
849+ doc = _get_enum_section_doc (md_path , s ["enum_section_line" ])
850+ if ext :
851+ doc = doc .replace ("\033 [0m" , f"\033 [0m{ ext } " , 1 )
852+ else :
853+ doc = _get_class_doc (md_path , ext )
657854 else :
658855 doc = f" \033 [36;1m{ s ['class_name' ]} \033 [0m{ ext } "
659856 doc_lines = doc .count ('\n ' ) + 1
@@ -664,6 +861,14 @@ def cmd_search(query):
664861 print (doc )
665862 lines_printed += doc_lines
666863
864+ # For exact class matches, also show indexed members (useful for enums).
865+ if s ["class_name" ].lower () == q :
866+ members = members_by_class .get (s ["class_name" ], [])
867+ if members :
868+ members = sorted (members , key = lambda m : _sort_key (m ["name" ]))
869+ print (" \033 [90m## Values\033 [0m" )
870+ _print_compact_members (members )
871+
667872 else :
668873 # No class match → search member names across all classes
669874 results = []
0 commit comments