Skip to content

Commit 924ce7b

Browse files
committed
Backport PR jupyter-server#148: Fix notebook undo scope
1 parent 8854a43 commit 924ce7b

File tree

5 files changed

+409
-208
lines changed

5 files changed

+409
-208
lines changed

.github/workflows/test.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ jobs:
3030
test:
3131
name: Run tests on ${{ matrix.os }}
3232
runs-on: ${{ matrix.os }}
33+
timeout-minutes: 10
3334

3435
strategy:
3536
matrix:

javascript/src/api.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,11 @@ export interface ISharedBase extends IDisposable {
6262
/**
6363
* Perform a transaction. While the function f is called, all changes to the shared
6464
* document are bundled into a single event.
65+
*
66+
* @param f Transaction to execute
67+
* @param undoable Whether to track the change in the action history or not (default `true`)
6568
*/
66-
transact(f: () => void): void;
69+
transact(f: () => void, undoable?: boolean): void;
6770
}
6871

6972
/**

javascript/src/ymodels.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -777,7 +777,7 @@ export class YBaseCell<Metadata extends nbformat.IBaseCellMetadata>
777777
}
778778
this.transact(() => {
779779
this.ymodel.set('metadata', clone);
780-
});
780+
}, false);
781781
} else {
782782
const clone = JSONExt.deepCopy(metadata) as any;
783783
if (clone.collapsed != null) {
@@ -789,7 +789,7 @@ export class YBaseCell<Metadata extends nbformat.IBaseCellMetadata>
789789
if (!JSONExt.deepEqual(clone, this.getMetadata())) {
790790
this.transact(() => {
791791
this.ymodel.set('metadata', clone);
792-
});
792+
}, false);
793793
}
794794
}
795795
}
@@ -811,11 +811,11 @@ export class YBaseCell<Metadata extends nbformat.IBaseCellMetadata>
811811
* document are bundled into a single event.
812812
*/
813813
transact(f: () => void, undoable = true): void {
814-
this.notebook && undoable
815-
? this.notebook.transact(f)
816-
: this.ymodel.doc == null
817-
? f()
818-
: this.ymodel.doc.transact(f, this);
814+
!this.notebook || this.notebook.disableDocumentWideUndoRedo
815+
? this.ymodel.doc == null
816+
? f()
817+
: this.ymodel.doc.transact(f, undoable ? this : null)
818+
: this.notebook.transact(f, undoable);
819819
}
820820

821821
/**
@@ -971,7 +971,7 @@ export class YCodeCell
971971
if (this.ymodel.get('execution_count') !== count) {
972972
this.transact(() => {
973973
this.ymodel.set('execution_count', count);
974-
});
974+
}, false);
975975
}
976976
}
977977

@@ -1106,7 +1106,7 @@ class YAttachmentCell
11061106
} else {
11071107
this.ymodel.set('attachments', attachments);
11081108
}
1109-
});
1109+
}, false);
11101110
}
11111111

11121112
/**

javascript/test/ycell.spec.ts

Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
/* eslint-disable camelcase */
2+
// Copyright (c) Jupyter Development Team.
3+
// Distributed under the terms of the Modified BSD License.
4+
5+
import { IMapChange, YCodeCell, YNotebook } from '../src';
6+
7+
describe('@jupyter/ydoc', () => {
8+
describe('YCell standalone', () => {
9+
test('should set source', () => {
10+
const codeCell = YCodeCell.create();
11+
codeCell.setSource('test');
12+
expect(codeCell.getSource()).toBe('test');
13+
codeCell.dispose();
14+
});
15+
16+
test('should update source', () => {
17+
const codeCell = YCodeCell.create();
18+
codeCell.setSource('test');
19+
codeCell.updateSource(0, 0, 'hello');
20+
expect(codeCell.getSource()).toBe('hellotest');
21+
codeCell.dispose();
22+
});
23+
24+
test('should get metadata', () => {
25+
const cell = YCodeCell.create();
26+
const metadata = {
27+
collapsed: true,
28+
editable: false,
29+
name: 'cell-name'
30+
};
31+
32+
cell.setMetadata(metadata);
33+
34+
expect(cell.metadata).toEqual({
35+
...metadata,
36+
jupyter: { outputs_hidden: true }
37+
});
38+
cell.dispose();
39+
});
40+
41+
test('should get all metadata', () => {
42+
const cell = YCodeCell.create();
43+
const metadata = {
44+
jupyter: { outputs_hidden: true },
45+
editable: false,
46+
name: 'cell-name'
47+
};
48+
49+
cell.setMetadata(metadata);
50+
51+
expect(cell.getMetadata()).toEqual({ ...metadata, collapsed: true });
52+
cell.dispose();
53+
});
54+
55+
test('should get one metadata', () => {
56+
const cell = YCodeCell.create();
57+
const metadata = {
58+
collapsed: true,
59+
editable: false,
60+
name: 'cell-name'
61+
};
62+
63+
cell.setMetadata(metadata);
64+
65+
expect(cell.getMetadata('editable')).toEqual(metadata.editable);
66+
cell.dispose();
67+
});
68+
69+
it.each([null, undefined, 1, true, 'string', { a: 1 }, [1, 2]])(
70+
'should get single metadata %s',
71+
value => {
72+
const cell = YCodeCell.create();
73+
const metadata = {
74+
collapsed: true,
75+
editable: false,
76+
name: 'cell-name',
77+
test: value
78+
};
79+
80+
cell.setMetadata(metadata);
81+
82+
expect(cell.getMetadata('test')).toEqual(value);
83+
cell.dispose();
84+
}
85+
);
86+
87+
test('should set one metadata', () => {
88+
const cell = YCodeCell.create();
89+
const metadata = {
90+
collapsed: true,
91+
editable: false,
92+
name: 'cell-name'
93+
};
94+
95+
cell.setMetadata(metadata);
96+
cell.setMetadata('test', 'banana');
97+
98+
expect(cell.getMetadata('test')).toEqual('banana');
99+
cell.dispose();
100+
});
101+
102+
test('should emit all metadata changes', () => {
103+
const notebook = YNotebook.create();
104+
105+
const metadata = {
106+
collapsed: true,
107+
editable: false,
108+
name: 'cell-name'
109+
};
110+
111+
const changes: IMapChange[] = [];
112+
notebook.metadataChanged.connect((_, c) => {
113+
changes.push(c);
114+
});
115+
notebook.metadata = metadata;
116+
117+
expect(changes).toHaveLength(3);
118+
expect(changes).toEqual([
119+
{
120+
type: 'add',
121+
key: 'collapsed',
122+
newValue: metadata.collapsed,
123+
oldValue: undefined
124+
},
125+
{
126+
type: 'add',
127+
key: 'editable',
128+
newValue: metadata.editable,
129+
oldValue: undefined
130+
},
131+
{
132+
type: 'add',
133+
key: 'name',
134+
newValue: metadata.name,
135+
oldValue: undefined
136+
}
137+
]);
138+
139+
notebook.dispose();
140+
});
141+
142+
test('should emit a add metadata change', () => {
143+
const cell = YCodeCell.create();
144+
const metadata = {
145+
collapsed: true,
146+
editable: false,
147+
name: 'cell-name'
148+
};
149+
cell.metadata = metadata;
150+
151+
const changes: IMapChange[] = [];
152+
cell.metadataChanged.connect((_, c) => {
153+
changes.push(c);
154+
});
155+
cell.setMetadata('test', 'banana');
156+
157+
try {
158+
expect(changes).toHaveLength(1);
159+
expect(changes).toEqual([
160+
{ type: 'add', key: 'test', newValue: 'banana', oldValue: undefined }
161+
]);
162+
} finally {
163+
cell.dispose();
164+
}
165+
});
166+
167+
test('should emit a delete metadata change', () => {
168+
const cell = YCodeCell.create();
169+
const metadata = {
170+
collapsed: true,
171+
editable: false,
172+
name: 'cell-name'
173+
};
174+
cell.metadata = metadata;
175+
176+
const changes: IMapChange[] = [];
177+
cell.setMetadata('test', 'banana');
178+
179+
cell.metadataChanged.connect((_, c) => {
180+
changes.push(c);
181+
});
182+
cell.deleteMetadata('test');
183+
184+
try {
185+
expect(changes).toHaveLength(1);
186+
expect(changes).toEqual([
187+
{
188+
type: 'remove',
189+
key: 'test',
190+
newValue: undefined,
191+
oldValue: 'banana'
192+
}
193+
]);
194+
} finally {
195+
cell.dispose();
196+
}
197+
});
198+
199+
test('should emit an update metadata change', () => {
200+
const cell = YCodeCell.create();
201+
const metadata = {
202+
collapsed: true,
203+
editable: false,
204+
name: 'cell-name'
205+
};
206+
cell.metadata = metadata;
207+
208+
const changes: IMapChange[] = [];
209+
cell.setMetadata('test', 'banana');
210+
211+
cell.metadataChanged.connect((_, c) => {
212+
changes.push(c);
213+
});
214+
cell.setMetadata('test', 'orange');
215+
216+
try {
217+
expect(changes).toHaveLength(1);
218+
expect(changes).toEqual([
219+
{
220+
type: 'change',
221+
key: 'test',
222+
newValue: 'orange',
223+
oldValue: 'banana'
224+
}
225+
]);
226+
} finally {
227+
cell.dispose();
228+
}
229+
});
230+
});
231+
232+
describe('#undo', () => {
233+
test('should undo source change', () => {
234+
const codeCell = YCodeCell.create();
235+
codeCell.setSource('test');
236+
codeCell.undoManager?.stopCapturing();
237+
codeCell.updateSource(0, 0, 'hello');
238+
239+
codeCell.undo();
240+
241+
expect(codeCell.getSource()).toEqual('test');
242+
});
243+
244+
test('should not undo execution count change', () => {
245+
const codeCell = YCodeCell.create();
246+
codeCell.setSource('test');
247+
codeCell.undoManager?.stopCapturing();
248+
codeCell.execution_count = 22;
249+
codeCell.undoManager?.stopCapturing();
250+
codeCell.execution_count = 42;
251+
252+
codeCell.undo();
253+
254+
expect(codeCell.execution_count).toEqual(42);
255+
});
256+
257+
test('should not undo output change', () => {
258+
const codeCell = YCodeCell.create();
259+
codeCell.setSource('test');
260+
codeCell.undoManager?.stopCapturing();
261+
const outputs = [
262+
{
263+
data: {
264+
'application/geo+json': {
265+
geometry: {
266+
coordinates: [-118.4563712, 34.0163116],
267+
type: 'Point'
268+
},
269+
type: 'Feature'
270+
},
271+
'text/plain': ['<IPython.display.GeoJSON object>']
272+
},
273+
metadata: {
274+
'application/geo+json': {
275+
expanded: false
276+
}
277+
},
278+
output_type: 'display_data'
279+
},
280+
{
281+
data: {
282+
'application/vnd.jupyter.widget-view+json': {
283+
model_id: '4619c172d65e496baa5d1230894b535a',
284+
version_major: 2,
285+
version_minor: 0
286+
},
287+
'text/plain': [
288+
"HBox(children=(Text(value='text input', layout=Layout(border='1px dashed red', width='80px')), Button(descript…"
289+
]
290+
},
291+
metadata: {},
292+
output_type: 'display_data'
293+
}
294+
];
295+
codeCell.setOutputs(outputs);
296+
codeCell.undoManager?.stopCapturing();
297+
298+
codeCell.undo();
299+
300+
expect(codeCell.getOutputs()).toEqual(outputs);
301+
});
302+
303+
test('should not undo metadata change', () => {
304+
const codeCell = YCodeCell.create();
305+
codeCell.setSource('test');
306+
codeCell.undoManager?.stopCapturing();
307+
codeCell.setMetadata({ collapsed: false });
308+
codeCell.undoManager?.stopCapturing();
309+
codeCell.setMetadata({ collapsed: true });
310+
311+
codeCell.undo();
312+
313+
expect(codeCell.getMetadata('collapsed')).toEqual(true);
314+
});
315+
});
316+
});

0 commit comments

Comments
 (0)