Skip to content

Commit 6916e04

Browse files
committed
Add support for undo/redo on datalayer notebooks
1 parent 8f5d8fa commit 6916e04

File tree

6 files changed

+190
-14
lines changed

6 files changed

+190
-14
lines changed

package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,7 @@
411411
"@codemirror/view": "^6.38.3",
412412
"@datalayer/core": "^0.0.13",
413413
"@datalayer/jupyter-lexical": "^1.0.6",
414+
"@datalayer/jupyter-react": "^1.1.7",
414415
"@datalayer/lexical-loro": "^0.1.0",
415416
"@jupyter/ydoc": "^2.0.0",
416417
"@jupyterlab/services": "^7.0.0",

patches/@datalayer+jupyter-react+1.1.7.patch

Lines changed: 128 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,132 @@
1+
diff --git a/node_modules/@datalayer/jupyter-react/lib/components/lumino/Lumino.js b/node_modules/@datalayer/jupyter-react/lib/components/lumino/Lumino.js
2+
index 850306f..138e9c6 100644
3+
--- a/node_modules/@datalayer/jupyter-react/lib/components/lumino/Lumino.js
4+
+++ b/node_modules/@datalayer/jupyter-react/lib/components/lumino/Lumino.js
5+
@@ -6,9 +6,8 @@ import { jsx as _jsx } from "react/jsx-runtime";
6+
*/
7+
import { useRef, useEffect } from 'react';
8+
import { Widget } from '@lumino/widgets';
9+
-export const Lumino = (props) => {
10+
+export const Lumino = ({ id = 'lumino-id', height = '100%', children }) => {
11+
const ref = useRef(null);
12+
- const { children, id, height } = props;
13+
useEffect(() => {
14+
console.log('Lumino useEffect - ref.current:', ref.current, 'children:', children, 'children.isAttached:', children?.isAttached);
15+
if (ref && ref.current && children) {
16+
@@ -59,9 +58,5 @@ export const Lumino = (props) => {
17+
}, [ref, children]);
18+
return (_jsx("div", { id: id, ref: ref, style: { height: height, minHeight: height } }));
19+
};
20+
-Lumino.defaultProps = {
21+
- id: 'lumino-id',
22+
- height: '100%',
23+
-};
24+
export default Lumino;
25+
//# sourceMappingURL=Lumino.js.map
26+
\ No newline at end of file
27+
diff --git a/node_modules/@datalayer/jupyter-react/lib/components/notebook/Notebook2Adapter.d.ts b/node_modules/@datalayer/jupyter-react/lib/components/notebook/Notebook2Adapter.d.ts
28+
index 841a2cf..05e0d52 100644
29+
--- a/node_modules/@datalayer/jupyter-react/lib/components/notebook/Notebook2Adapter.d.ts
30+
+++ b/node_modules/@datalayer/jupyter-react/lib/components/notebook/Notebook2Adapter.d.ts
31+
@@ -54,6 +54,14 @@ export declare class Notebook2Adapter {
32+
* Get the notebook model.
33+
*/
34+
get model(): NotebookModel | null;
35+
+ /**
36+
+ * Undo the last change in the notebook.
37+
+ */
38+
+ undo(): void;
39+
+ /**
40+
+ * Redo the last undone change in the notebook.
41+
+ */
42+
+ redo(): void;
43+
/**
44+
* Dispose of the adapter.
45+
*/
46+
diff --git a/node_modules/@datalayer/jupyter-react/lib/components/notebook/Notebook2Adapter.js b/node_modules/@datalayer/jupyter-react/lib/components/notebook/Notebook2Adapter.js
47+
index a6c29d2..8d6d435 100644
48+
--- a/node_modules/@datalayer/jupyter-react/lib/components/notebook/Notebook2Adapter.js
49+
+++ b/node_modules/@datalayer/jupyter-react/lib/components/notebook/Notebook2Adapter.js
50+
@@ -110,6 +110,36 @@ export class Notebook2Adapter {
51+
get model() {
52+
return this._context.model;
53+
}
54+
+ /**
55+
+ * Undo the last change in the notebook.
56+
+ */
57+
+ undo() {
58+
+ const notebook = this._notebook;
59+
+ // If in edit mode and active cell has an editor, undo within the cell editor (CodeMirror)
60+
+ // Otherwise, undo structural changes (add/delete/move cells)
61+
+ if (notebook.mode === 'edit' && notebook.activeCell?.editor) {
62+
+ notebook.activeCell.editor.undo();
63+
+ }
64+
+ else {
65+
+ // Structural undo (cell operations)
66+
+ NotebookActions.undo(notebook);
67+
+ }
68+
+ }
69+
+ /**
70+
+ * Redo the last undone change in the notebook.
71+
+ */
72+
+ redo() {
73+
+ const notebook = this._notebook;
74+
+ // If in edit mode and active cell has an editor, redo within the cell editor (CodeMirror)
75+
+ // Otherwise, redo structural changes (add/delete/move cells)
76+
+ if (notebook.mode === 'edit' && notebook.activeCell?.editor) {
77+
+ notebook.activeCell.editor.redo();
78+
+ }
79+
+ else {
80+
+ // Structural redo (cell operations)
81+
+ NotebookActions.redo(notebook);
82+
+ }
83+
+ }
84+
/**
85+
* Dispose of the adapter.
86+
*/
87+
diff --git a/node_modules/@datalayer/jupyter-react/lib/components/notebook/Notebook2State.d.ts b/node_modules/@datalayer/jupyter-react/lib/components/notebook/Notebook2State.d.ts
88+
index 1ea7f63..c6f6cb7 100644
89+
--- a/node_modules/@datalayer/jupyter-react/lib/components/notebook/Notebook2State.d.ts
90+
+++ b/node_modules/@datalayer/jupyter-react/lib/components/notebook/Notebook2State.d.ts
91+
@@ -22,6 +22,8 @@ export type Notebook2State = INotebooks2State & {
92+
insertBelow: (mutation: CellMutation) => void;
93+
delete: (id: string) => void;
94+
changeCellType: (mutation: CellMutation) => void;
95+
+ undo: (id: string) => void;
96+
+ redo: (id: string) => void;
97+
reset: () => void;
98+
};
99+
export declare const notebookStore2: import("zustand").StoreApi<Notebook2State>;
100+
diff --git a/node_modules/@datalayer/jupyter-react/lib/components/notebook/Notebook2State.js b/node_modules/@datalayer/jupyter-react/lib/components/notebook/Notebook2State.js
101+
index 33a060c..543392e 100644
102+
--- a/node_modules/@datalayer/jupyter-react/lib/components/notebook/Notebook2State.js
103+
+++ b/node_modules/@datalayer/jupyter-react/lib/components/notebook/Notebook2State.js
104+
@@ -54,6 +54,20 @@ export const notebookStore2 = createStore((set, get) => ({
105+
.notebooks.get(mutation.id)
106+
?.adapter?.changeCellType(mutation.cellType);
107+
},
108+
+ undo: (id) => {
109+
+ // Directly call adapter's undo method which uses NotebookActions
110+
+ // This works for both local notebooks and collaborative notebooks
111+
+ get()
112+
+ .notebooks.get(id)
113+
+ ?.adapter?.undo();
114+
+ },
115+
+ redo: (id) => {
116+
+ // Directly call adapter's redo method which uses NotebookActions
117+
+ // This works for both local notebooks and collaborative notebooks
118+
+ get()
119+
+ .notebooks.get(id)
120+
+ ?.adapter?.redo();
121+
+ },
122+
reset: () => set((state) => ({
123+
notebooks: new Map(),
124+
})),
1125
diff --git a/node_modules/@datalayer/jupyter-react/lib/jupyter/JupyterConfig.d.ts b/node_modules/@datalayer/jupyter-react/lib/jupyter/JupyterConfig.d.ts
2-
index 1234567..abcdefg 100644
126+
index 2e6c71b..a1551d1 100644
3127
--- a/node_modules/@datalayer/jupyter-react/lib/jupyter/JupyterConfig.d.ts
4128
+++ b/node_modules/@datalayer/jupyter-react/lib/jupyter/JupyterConfig.d.ts
5-
@@ -25,6 +25,12 @@ export declare const setJupyterServerToken: (jupyterServerToken: string) => voi
129+
@@ -24,6 +24,12 @@ export declare const setJupyterServerToken: (jupyterServerToken: string) => void
6130
* Getter for jupyterServerToken.
7131
*/
8132
export declare const getJupyterServerToken: () => string;
@@ -16,10 +140,10 @@ index 1234567..abcdefg 100644
16140
* Method to load the Jupyter configuration from the host HTML page.
17141
*/
18142
diff --git a/node_modules/@datalayer/jupyter-react/lib/jupyter/JupyterConfig.js b/node_modules/@datalayer/jupyter-react/lib/jupyter/JupyterConfig.js
19-
index 1234567..abcdefg 100644
143+
index c6ea148..c5cbbe3 100644
20144
--- a/node_modules/@datalayer/jupyter-react/lib/jupyter/JupyterConfig.js
21145
+++ b/node_modules/@datalayer/jupyter-react/lib/jupyter/JupyterConfig.js
22-
@@ -60,6 +60,14 @@ export const getJupyterServerToken = () => {
146+
@@ -48,6 +48,14 @@ export const getJupyterServerToken = () => {
23147
}
24148
return config.jupyterServerToken;
25149
};

webpack.config.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,19 @@ const webviewConfig = {
8181
path: path.resolve(__dirname, "dist"),
8282
filename: "webview.js",
8383
},
84+
optimization: {
85+
// Split React into a separate chunk to ensure single instance
86+
splitChunks: {
87+
cacheGroups: {
88+
react: {
89+
test: /[\\/]node_modules[\\/](react|react-dom|scheduler)[\\/]/,
90+
name: "react-vendors",
91+
chunks: "all",
92+
priority: 20,
93+
},
94+
},
95+
},
96+
},
8497
// Suppress warnings from external dependencies
8598
ignoreWarnings: [
8699
{
@@ -98,6 +111,9 @@ const webviewConfig = {
98111
},
99112
// Deduplicate CodeMirror modules to prevent multiple instances
100113
alias: {
114+
// Force all React imports to use the same instance
115+
react: path.resolve(__dirname, "./node_modules/react"),
116+
"react-dom": path.resolve(__dirname, "./node_modules/react-dom"),
101117
"@codemirror/state": path.resolve(
102118
__dirname,
103119
"./node_modules/@codemirror/state",
@@ -343,6 +359,9 @@ const lexicalWebviewConfig = {
343359
},
344360
// Deduplicate CodeMirror modules to prevent multiple instances
345361
alias: {
362+
// Force all React imports to use the same instance
363+
react: path.resolve(__dirname, "./node_modules/react"),
364+
"react-dom": path.resolve(__dirname, "./node_modules/react-dom"),
346365
"@codemirror/state": path.resolve(
347366
__dirname,
348367
"./node_modules/@codemirror/state",

webview/notebook/NotebookEditor.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
useJupyterReactStore,
2121
CellSidebarExtension,
2222
resetJupyterConfig,
23+
notebookStore2,
2324
} from "@datalayer/jupyter-react";
2425
import { DatalayerCollaborationProvider } from "@datalayer/core/lib/collaboration";
2526
import {
@@ -269,6 +270,36 @@ function NotebookEditorCore(): JSX.Element {
269270
return undefined;
270271
}, [store.isDatalayerNotebook]);
271272

273+
// Handle Cmd+Z/Ctrl+Z (undo) and Cmd+Shift+Z/Ctrl+Y (redo)
274+
useEffect(() => {
275+
const handleKeyDown = (e: KeyboardEvent) => {
276+
const notebookId = store.documentId || store.notebookId;
277+
if (!notebookId) return;
278+
279+
// Cmd+Z (macOS) or Ctrl+Z (Windows/Linux) - Undo
280+
if ((e.metaKey || e.ctrlKey) && e.key === "z" && !e.shiftKey) {
281+
e.preventDefault();
282+
notebookStore2.getState().undo(notebookId);
283+
return;
284+
}
285+
286+
// Cmd+Shift+Z (macOS) or Ctrl+Y (Windows/Linux) - Redo
287+
if (
288+
((e.metaKey || e.ctrlKey) && e.key === "z" && e.shiftKey) ||
289+
(e.ctrlKey && e.key === "y" && !e.metaKey)
290+
) {
291+
e.preventDefault();
292+
notebookStore2.getState().redo(notebookId);
293+
return;
294+
}
295+
};
296+
297+
document.addEventListener("keydown", handleKeyDown, true);
298+
return () => {
299+
document.removeEventListener("keydown", handleKeyDown, true);
300+
};
301+
}, [store.documentId, store.notebookId]);
302+
272303
// Loading state
273304
if (!store.isInitialized || !store.nbformat) {
274305
return (

webview/notebook/NotebookToolbar.tsx

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212

1313
import React, { useState, useEffect, useContext } from "react";
1414
import { notebookStore2 } from "@datalayer/jupyter-react";
15-
import { NotebookActions } from "@jupyterlab/notebook";
1615
import { MessageHandlerContext } from "../services/messageHandler";
1716
import type { RuntimeJSON } from "@datalayer/core/lib/client/models/Runtime";
1817

@@ -180,19 +179,20 @@ export const NotebookToolbar: React.FC<NotebookToolbarProps> = ({
180179
};
181180

182181
const handleAddCodeCell = () => {
183-
if (notebookId && notebook?.adapter?.panel?.content) {
184-
NotebookActions.insertBelow(notebook.adapter.panel.content);
185-
NotebookActions.changeCellType(notebook.adapter.panel.content, "code");
182+
if (notebookId) {
183+
notebookStore2.getState().insertBelow({
184+
id: notebookId,
185+
cellType: "code",
186+
});
186187
}
187188
};
188189

189190
const handleAddMarkdownCell = () => {
190-
if (notebookId && notebook?.adapter?.panel?.content) {
191-
NotebookActions.insertBelow(notebook.adapter.panel.content);
192-
NotebookActions.changeCellType(
193-
notebook.adapter.panel.content,
194-
"markdown",
195-
);
191+
if (notebookId) {
192+
notebookStore2.getState().insertBelow({
193+
id: notebookId,
194+
cellType: "markdown",
195+
});
196196
}
197197
};
198198

0 commit comments

Comments
 (0)