Skip to content

Commit adc6ed5

Browse files
committed
Add jupter lexical commands
1 parent e57291d commit adc6ed5

File tree

2 files changed

+195
-0
lines changed

2 files changed

+195
-0
lines changed

packages/lexical/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@
131131
"prettier": "^3.5.3",
132132
"process": "^0.11.10",
133133
"raw-loader": "^4.0.2",
134+
"react-is": "^19.2.0",
134135
"rimraf": "^6.0.1",
135136
"stream": "^0.0.2",
136137
"stream-browserify": "^2.0.2",

packages/lexical/src/plugins/JupyterInputOutputPlugin.tsx

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import {
2424
$createNodeSelection,
2525
$setSelection,
2626
SELECT_ALL_COMMAND,
27+
$getRoot,
28+
$isElementNode,
2729
} from 'lexical';
2830
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
2931
import {
@@ -84,6 +86,30 @@ export type JupyterInputOutputPluginProps = {
8486
export 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+
87113
export 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

Comments
 (0)