Skip to content

Commit 807aae3

Browse files
committed
Fix race in VirtualDocument.dispose() / async update_documents()
1 parent 3e1afc8 commit 807aae3

File tree

4 files changed

+50
-14
lines changed

4 files changed

+50
-14
lines changed

packages/jupyterlab-lsp/src/adapters/adapter.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ export abstract class WidgetAdapter<T extends IDocumentWidget> {
144144
if (this.isDisposed) {
145145
return;
146146
}
147+
147148
if (this.virtual_editor?.virtual_document) {
148149
this.disconnect_adapter(this.virtual_editor?.virtual_document);
149150
}
@@ -173,7 +174,6 @@ export abstract class WidgetAdapter<T extends IDocumentWidget> {
173174
this.connection_manager = null;
174175
this.widget = null;
175176

176-
// actually disposed
177177
this.isDisposed = true;
178178
}
179179

@@ -251,6 +251,10 @@ export abstract class WidgetAdapter<T extends IDocumentWidget> {
251251
* public for use in tests (but otherwise could be private)
252252
*/
253253
public update_documents() {
254+
if (this.isDisposed) {
255+
console.warn('Cannot update documents: adapter disposed');
256+
return;
257+
}
254258
return this.virtual_editor.virtual_document.update_manager.update_documents(
255259
this.editors.map(ce_editor => {
256260
return {
@@ -388,6 +392,11 @@ export abstract class WidgetAdapter<T extends IDocumentWidget> {
388392
document: VirtualDocument,
389393
is_init = false
390394
) {
395+
if (this.isDisposed) {
396+
console.warn('Cannot swap document: adapter disposed');
397+
return;
398+
}
399+
391400
// TODO only send the difference, using connection.sendSelectiveChange()
392401
let connection = this.connection_manager.connections.get(
393402
virtual_document.id_path

packages/jupyterlab-lsp/src/adapters/notebook/notebook.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ export class NotebookAdapter extends WidgetAdapter<NotebookPanel> {
6161
if (this.isDisposed) {
6262
return;
6363
}
64+
6465
this.widget.context.sessionContext.kernelChanged.disconnect(
6566
this.on_kernel_changed,
6667
this
@@ -69,8 +70,11 @@ export class NotebookAdapter extends WidgetAdapter<NotebookPanel> {
6970
this.activeCellChanged,
7071
this
7172
);
72-
this.ce_editor_to_cell.clear();
73+
7374
super.dispose();
75+
76+
// editors are needed for the parent dispose() to unbind signals, so they are the last to go
77+
this.ce_editor_to_cell.clear();
7478
}
7579

7680
is_ready = () => {

packages/jupyterlab-lsp/src/virtual/document.spec.ts

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { is_within_range, VirtualDocument } from './document';
33
import { ISourcePosition, IVirtualPosition } from '../positioning';
44
import { CodeEditor } from '@jupyterlab/codeeditor';
55
import { foreign_code_extractors } from '../extractors/defaults';
6+
import Mock = jest.Mock;
67

78
let R_LINE_MAGICS = `%R df = data.frame()
89
print("df created")
@@ -45,14 +46,34 @@ describe('is_within_range', () => {
4546
});
4647

4748
describe('VirtualDocument', () => {
48-
let document = new VirtualDocument({
49-
language: 'python',
50-
path: 'test.ipynb',
51-
overrides_registry: {},
52-
foreign_code_extractors: foreign_code_extractors,
53-
standalone: false,
54-
file_extension: 'py',
55-
has_lsp_supported_file: false
49+
let document: VirtualDocument;
50+
beforeEach(() => {
51+
document = new VirtualDocument({
52+
language: 'python',
53+
path: 'test.ipynb',
54+
overrides_registry: {},
55+
foreign_code_extractors: foreign_code_extractors,
56+
standalone: false,
57+
file_extension: 'py',
58+
has_lsp_supported_file: false
59+
});
60+
});
61+
62+
describe('#dispose', () => {
63+
it('disposes, but does not break methods which can be called from async callbacks', () => {
64+
expect(document.isDisposed).to.equal(false);
65+
// appending code block here should work fine
66+
document.append_code_block('code', {} as CodeEditor.IEditor);
67+
document.dispose();
68+
expect(document.isDisposed).to.equal(true);
69+
// mock console.warn
70+
console.warn = jest.fn();
71+
// this one should not raise, but just warn
72+
document.append_code_block('code', {} as CodeEditor.IEditor);
73+
expect((console.warn as Mock).mock.calls[0][0]).to.equal(
74+
'Cannot append code block: document disposed'
75+
);
76+
});
5677
});
5778

5879
describe('#extract_foreign_code', () => {
@@ -113,7 +134,7 @@ describe('VirtualDocument', () => {
113134
);
114135
};
115136

116-
describe('transform_virtual_to_editor', () => {
137+
describe('#transform_virtual_to_editor', () => {
117138
it('transforms positions for the top level document', () => {
118139
init_document_with_Python_and_R();
119140
// The first (Python) line in the first block

packages/jupyterlab-lsp/src/virtual/document.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,7 @@ export class VirtualDocument {
251251
if (this.isDisposed) {
252252
return;
253253
}
254+
this.isDisposed = true;
254255

255256
this.parent = null;
256257

@@ -276,9 +277,6 @@ export class VirtualDocument {
276277
this.line_magics_overrides = null;
277278
this.lines = null;
278279
this.overrides_registry = null;
279-
280-
// actually disposed now
281-
this.isDisposed = true;
282280
}
283281

284282
/**
@@ -595,6 +593,10 @@ export class VirtualDocument {
595593
editor_shift: CodeEditor.IPosition = { line: 0, column: 0 },
596594
virtual_shift?: CodeEditor.IPosition
597595
) {
596+
if (this.isDisposed) {
597+
console.warn('Cannot append code block: document disposed');
598+
return;
599+
}
598600
let source_cell_lines = cell_code.split('\n');
599601

600602
let { lines, foreign_document_map, skip_inspect } = this.prepare_code_block(

0 commit comments

Comments
 (0)