@@ -251,7 +251,7 @@ class CommandCompleter(Completer):
251251 # Commands whose argument is a filesystem path. The completer
252252 # delegates to ``PathCompleter`` once the user has typed past the
253253 # command name.
254- PATH_ARG_COMMANDS = frozenset ({"load" })
254+ PATH_ARG_COMMANDS = frozenset ({"load" , "save" })
255255
256256 def __init__ (self , commands : dict ) -> None :
257257 self .commands = commands
@@ -348,6 +348,10 @@ def __init__(self, parser_name: str | None, settings: dict[str, Any]) -> None:
348348 "help" : "Load hyperedges from a .jsonl parse-results file" ,
349349 "handler" : self .cmd_load ,
350350 },
351+ "save" : {
352+ "help" : "Save in-memory edges to a .jsonl file" ,
353+ "handler" : self .cmd_save ,
354+ },
351355 "edges" : {
352356 "help" : "Show in-memory edges (count and source file)" ,
353357 "handler" : self .cmd_edges ,
@@ -754,6 +758,49 @@ def cmd_load(self, args: list) -> bool:
754758 )
755759 return False
756760
761+ def cmd_save (self , args : list ) -> bool :
762+ if len (args ) < 1 :
763+ self .console .print (
764+ "[red]Error:[/red] /save requires a file path: "
765+ "[cyan]/save <path>[/cyan]"
766+ )
767+ return False
768+ if not self .edges :
769+ self .console .print ("[yellow]No edges loaded.[/yellow]" )
770+ return False
771+
772+ path = Path (" " .join (args )).expanduser ()
773+ with_metadata = 0
774+ try :
775+ with open (path , "w" ) as f :
776+ for edge in self .edges :
777+ d : dict [str , Any ] = {"edge" : str (edge )}
778+ if edge .tokens is not None and edge .text is not None :
779+ d ["text" ] = edge .text
780+ d ["tokens" ] = list (edge .tokens )
781+ d ["tok_pos" ] = str (self ._build_tok_pos_tree (edge ))
782+ with_metadata += 1
783+ f .write (json .dumps (d , ensure_ascii = False ) + "\n " )
784+ except OSError as e :
785+ self .console .print (f"[red]Error:[/red] failed to write { path } : { e } " )
786+ return False
787+
788+ self .console .print (
789+ f"[green]✓[/green] Saved [cyan]{ len (self .edges )} [/cyan] edge(s) "
790+ f"to [cyan]{ path } [/cyan] "
791+ f"[dim]({ with_metadata } with source-position metadata)[/dim]"
792+ )
793+ return False
794+
795+ def _build_tok_pos_tree (self , edge : Hyperedge ) -> Hyperedge :
796+ """Build a tok_pos mirror tree where each atom is replaced by its
797+ ``tok_pos`` (or ``-1`` for synthetic atoms with no position)."""
798+ if edge .atom :
799+ atom = cast (Atom , edge )
800+ pos = atom .tok_pos if atom .tok_pos is not None else - 1
801+ return Atom (str (pos ))
802+ return Hyperedge (tuple (self ._build_tok_pos_tree (sub ) for sub in edge ))
803+
757804 def cmd_edges (self , args : list ) -> bool :
758805 if not self .edges :
759806 self .console .print ("[yellow]No edges loaded.[/yellow]" )
0 commit comments