@@ -24,6 +24,8 @@ import {
2424 $createNodeSelection ,
2525 $setSelection ,
2626 SELECT_ALL_COMMAND ,
27+ $getRoot ,
28+ $isElementNode ,
2729} from 'lexical' ;
2830import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' ;
2931import {
@@ -84,6 +86,30 @@ export type JupyterInputOutputPluginProps = {
8486export const INSERT_JUPYTER_INPUT_OUTPUT_COMMAND =
8587 createCommand < JupyterInputOutputProps > ( ) ;
8688
89+ /**
90+ * Command to execute the currently focused/selected Jupyter cell.
91+ * Dispatching this command will execute the code in the cell where the cursor is located.
92+ */
93+ export const RUN_JUPYTER_CELL_COMMAND = createCommand < void > ( ) ;
94+
95+ /**
96+ * Command to execute all Jupyter cells in the document.
97+ * Dispatching this command will execute all cells in sequential order.
98+ */
99+ export const RUN_ALL_JUPYTER_CELLS_COMMAND = createCommand < void > ( ) ;
100+
101+ /**
102+ * Command to restart the Jupyter kernel.
103+ * Dispatching this command will restart the kernel session.
104+ */
105+ export const RESTART_JUPYTER_KERNEL_COMMAND = createCommand < void > ( ) ;
106+
107+ /**
108+ * Command to clear all outputs from all Jupyter cells in the document.
109+ * Dispatching this command will clear the outputs of all cells without affecting the code.
110+ */
111+ export const CLEAR_ALL_OUTPUTS_COMMAND = createCommand < void > ( ) ;
112+
87113export const JupyterInputOutputPlugin = (
88114 props ?: JupyterInputOutputPluginProps ,
89115) => {
@@ -614,6 +640,174 @@ export const JupyterInputOutputPlugin = (
614640 ) ;
615641 } , [ editor , kernel ] ) ;
616642
643+ /**
644+ * Helper function to execute code for a given input node.
645+ * Centralizes adapter/kernel setup and code execution logic.
646+ */
647+ const executeInputNode = useCallback (
648+ ( node : JupyterInputNode , kernelToUse : Kernel ) => {
649+ const code = node . getTextContent ( ) ;
650+ const jupyterInputNodeUuid = node . getJupyterInputNodeUuid ( ) ;
651+ const jupyterOutputNodeKey =
652+ INPUT_UUID_TO_OUTPUT_KEY . get ( jupyterInputNodeUuid ) ;
653+
654+ if ( jupyterOutputNodeKey ) {
655+ const jupyterOutputNode = $getNodeByKey ( jupyterOutputNodeKey ) ;
656+ if ( jupyterOutputNode ) {
657+ // Update kernel using public API before execution
658+ ( jupyterOutputNode as JupyterOutputNode ) . updateKernel ( kernelToUse ) ;
659+ ( jupyterOutputNode as JupyterOutputNode ) . executeCode ( code ) ;
660+ return true ;
661+ }
662+ }
663+ return false ;
664+ } ,
665+ [ ] ,
666+ ) ;
667+
668+ // Handle RUN_JUPYTER_CELL_COMMAND - execute currently focused cell
669+ useEffect ( ( ) => {
670+ return editor . registerCommand (
671+ RUN_JUPYTER_CELL_COMMAND ,
672+ ( ) => {
673+ if ( ! kernel ) {
674+ console . warn ( '❌ No kernel available for cell execution' ) ;
675+ return false ;
676+ }
677+
678+ const selection = $getSelection ( ) ;
679+ if ( ! selection ) return false ;
680+
681+ const nodes = selection . getNodes ( ) ;
682+ const node = nodes [ 0 ] ;
683+
684+ // Find parent JupyterInputNode using public API
685+ const parentNode = node ?. getParent ( ) ;
686+ if ( parentNode && $isJupyterInputNode ( parentNode ) ) {
687+ // Use shared helper to execute the cell
688+ return executeInputNode ( parentNode , kernel ) ;
689+ }
690+ return false ;
691+ } ,
692+ COMMAND_PRIORITY_EDITOR ,
693+ ) ;
694+ } , [ editor , kernel , executeInputNode ] ) ;
695+
696+ // Handle RUN_ALL_JUPYTER_CELLS_COMMAND - execute all cells in document order
697+ useEffect ( ( ) => {
698+ return editor . registerCommand (
699+ RUN_ALL_JUPYTER_CELLS_COMMAND ,
700+ ( ) => {
701+ if ( ! kernel ) {
702+ console . warn ( '❌ No kernel available for running all cells' ) ;
703+ return false ;
704+ }
705+
706+ // Collect all JupyterInputNodes in document order using public API
707+ const inputNodes : JupyterInputNode [ ] = [ ] ;
708+
709+ function collectJupyterInputNodes ( node : LexicalNode ) {
710+ if ( $isJupyterInputNode ( node ) ) {
711+ inputNodes . push ( node ) ;
712+ }
713+ if ( $isElementNode ( node ) ) {
714+ const children = node . getChildren ( ) ;
715+ for ( const child of children ) {
716+ collectJupyterInputNodes ( child ) ;
717+ }
718+ }
719+ }
720+
721+ const root = $getRoot ( ) ;
722+ collectJupyterInputNodes ( root ) ;
723+
724+ if ( inputNodes . length === 0 ) {
725+ console . warn ( '❌ No Jupyter cells found to execute' ) ;
726+ return false ;
727+ }
728+
729+ console . log (
730+ `🚀 Executing ${ inputNodes . length } cells in document order` ,
731+ ) ;
732+
733+ // Execute each cell in document order using shared helper
734+ inputNodes . forEach ( ( node : JupyterInputNode ) => {
735+ executeInputNode ( node , kernel ) ;
736+ } ) ;
737+
738+ return true ;
739+ } ,
740+ COMMAND_PRIORITY_EDITOR ,
741+ ) ;
742+ } , [ editor , kernel , executeInputNode ] ) ;
743+
744+ // Handle RESTART_JUPYTER_KERNEL_COMMAND - restart the kernel
745+ useEffect ( ( ) => {
746+ return editor . registerCommand (
747+ RESTART_JUPYTER_KERNEL_COMMAND ,
748+ ( ) => {
749+ if ( ! kernel ?. session ?. kernel ) {
750+ console . warn ( '❌ No kernel session available to restart' ) ;
751+ return false ;
752+ }
753+
754+ // Execute restart asynchronously without blocking
755+ ( async ( ) => {
756+ try {
757+ if ( ! kernel . session . kernel ) {
758+ console . error ( '❌ Kernel became null during restart' ) ;
759+ return ;
760+ }
761+ console . log ( '🔄 Restarting kernel...' ) ;
762+ await kernel . session . kernel . restart ( ) ;
763+ console . log ( '✅ Kernel restarted successfully' ) ;
764+ } catch ( error ) {
765+ console . error ( '❌ Failed to restart kernel:' , error ) ;
766+ }
767+ } ) ( ) ;
768+
769+ return true ;
770+ } ,
771+ COMMAND_PRIORITY_EDITOR ,
772+ ) ;
773+ } , [ editor , kernel ] ) ;
774+
775+ // Handle CLEAR_ALL_OUTPUTS_COMMAND - clear outputs from all cells
776+ useEffect ( ( ) => {
777+ return editor . registerCommand (
778+ CLEAR_ALL_OUTPUTS_COMMAND ,
779+ ( ) => {
780+ let clearedCount = 0 ;
781+
782+ // Traverse document tree and clear all JupyterOutputNode outputs
783+ function clearJupyterOutputs ( node : LexicalNode ) {
784+ if ( node instanceof JupyterOutputNode ) {
785+ node . setOutputs ( [ ] ) ;
786+ clearedCount ++ ;
787+ }
788+ if ( $isElementNode ( node ) ) {
789+ const children = node . getChildren ( ) ;
790+ for ( const child of children ) {
791+ clearJupyterOutputs ( child ) ;
792+ }
793+ }
794+ }
795+
796+ const root = $getRoot ( ) ;
797+ clearJupyterOutputs ( root ) ;
798+
799+ if ( clearedCount > 0 ) {
800+ console . log ( `✅ Cleared outputs from ${ clearedCount } cells` ) ;
801+ } else {
802+ console . warn ( '❌ No Jupyter cells found to clear' ) ;
803+ }
804+
805+ return true ;
806+ } ,
807+ COMMAND_PRIORITY_EDITOR ,
808+ ) ;
809+ } , [ editor ] ) ;
810+
617811 return null ;
618812} ;
619813
0 commit comments