Skip to content

Commit 13366c1

Browse files
committed
Fix state not being updated after notebook rename/restart
1 parent 748a24c commit 13366c1

File tree

7 files changed

+191
-101
lines changed

7 files changed

+191
-101
lines changed

atest/03_Notebook.robot

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,41 @@ Foreign Extractors
6666
Code Overrides
6767
${file} = Set Variable Code overrides.ipynb
6868
Setup Notebook Python ${file}
69-
${virtual_path} = Set Variable ${VIRTUALDOCS DIR}${/}Code overrides.ipynb
69+
${virtual_path} = Set Variable ${VIRTUALDOCS DIR}${/}${file}
7070
Wait Until Created ${virtual_path}
7171
Wait Until Keyword Succeeds 10x 1s File Should Not Be Empty ${virtual_path}
7272
${document} = Get File ${virtual_path}
7373
Should Be Equal ${document} get_ipython().run_line_magic("ls", "")\n\n\nget_ipython().run_line_magic("pip", " freeze")\n
74+
[Teardown] Clean Up After Working With File Code overrides.ipynb
75+
76+
Adding Text To Cells Is Reflected In Virtual Document
77+
${file} = Set Variable Empty.ipynb
78+
Setup Notebook Python ${file}
79+
${virtual_path} = Set Variable ${VIRTUALDOCS DIR}${/}${file}
80+
Wait Until Created ${virtual_path}
81+
Enter Cell Editor 1
82+
Press Keys None cell_1
83+
Lab Command Insert Cell Below
84+
Enter Cell Editor 2
85+
Press Keys None cell_2
86+
Wait Until Keyword Succeeds 3x 1s File Content Should Be Equal ${virtual_path} cell_1\n\n\ncell_2\n
87+
[Teardown] Clean Up After Working With File Empty.ipynb
88+
89+
Adding Text To Cells After Kernel Restart
90+
${file} = Set Variable Empty.ipynb
91+
Setup Notebook Python ${file}
92+
${virtual_path} = Set Variable ${VIRTUALDOCS DIR}${/}${file}
93+
Wait Until Created ${virtual_path}
94+
Enter Cell Editor 1
95+
Lab Command Insert Cell Below
96+
Enter Cell Editor 2 line=1
97+
Press Keys None text
98+
Wait Until Keyword Succeeds 3x 1s File Content Should Be Equal ${virtual_path} \n\n\ntext\n
99+
[Teardown] Clean Up After Working With File Empty.ipynb
100+
101+
*** Keywords ***
102+
File Content Should Be Equal
103+
[Arguments] ${file} ${text}
104+
Wait Until Keyword Succeeds 5x 1s File Should Not Be Empty ${file}
105+
${document} = Get File ${file}
106+
Should Be Equal ${document} ${text}

atest/04_Interface/Statusbar.robot

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,7 @@ Statusbar Popup Opens
2222
Status Changes Between Notebooks
2323
Setup Notebook Python Python.ipynb
2424
Wait Until Fully Initialized
25-
Lab Command New Notebook
26-
Wait For Dialog
27-
# Kernel selection dialog shows up, accept Python as default kernel
28-
Accept Default Dialog Option
25+
Open New Notebook
2926
Element Should Contain ${STATUSBAR} Waiting...
3027
Wait Until Fully Initialized
3128
Switch To Tab Python.ipynb

atest/Keywords.robot

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,3 +413,9 @@ Measure Cursor Position
413413
Switch To Tab
414414
[Arguments] ${file}
415415
Click Element ${JLAB XP DOCK TAB}\[contains(., '${file}')]
416+
417+
Open New Notebook
418+
Lab Command New Notebook
419+
Wait For Dialog
420+
# Kernel selection dialog shows up, accept Python as default kernel
421+
Accept Default Dialog Option

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,14 @@ export abstract class WidgetAdapter<T extends IDocumentWidget> {
221221
this
222222
);
223223

224+
// pretend that all editors were removed to trigger the disconnection of even handlers
225+
// they will be connected again on new connection
226+
for (let editor of this.editors) {
227+
this.editorRemoved.emit({
228+
editor: editor
229+
});
230+
}
231+
224232
for (let adapter of this.adapters.values()) {
225233
adapter.dispose();
226234
}
@@ -392,6 +400,7 @@ export abstract class WidgetAdapter<T extends IDocumentWidget> {
392400
return;
393401
}
394402
this.virtual_editor = virtual_editor;
403+
this.connect_contentChanged_signal();
395404
}
396405

397406
/**
@@ -538,7 +547,7 @@ export abstract class WidgetAdapter<T extends IDocumentWidget> {
538547
* (range) updates this can be still implemented by comparison of before/after states of the
539548
* virtual documents, which is even more resilient and -obviously - editor-independent.
540549
*/
541-
connect_contentChanged_signal() {
550+
private connect_contentChanged_signal() {
542551
this.widget.context.model.contentChanged.connect(
543552
this.onContentChanged,
544553
this

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ export class FileEditorAdapter extends WidgetAdapter<
4646
this.editor = editor_widget.content;
4747

4848
this.init_virtual();
49-
this.connect_contentChanged_signal();
5049

5150
this.console.log('file ready for connection:', this.path);
5251

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

Lines changed: 90 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ import { ICommandContext } from '../../command_manager';
1313
import { CodeEditor } from '@jupyterlab/codeeditor';
1414
import IEditor = CodeEditor.IEditor;
1515
import { VirtualDocument } from '../../virtual/document';
16+
import {
17+
IObservableUndoableList,
18+
IObservableList
19+
} from '@jupyterlab/observables';
1620

1721
export class NotebookAdapter extends WidgetAdapter<NotebookPanel> {
1822
editor: Notebook;
@@ -125,7 +129,6 @@ export class NotebookAdapter extends WidgetAdapter<NotebookPanel> {
125129
this.console.log(this.document_path, 'ready for connection');
126130

127131
this.init_virtual();
128-
this.connect_contentChanged_signal();
129132

130133
// connect the document, but do not open it as the adapter will handle this
131134
// after registering all features
@@ -139,83 +142,98 @@ export class NotebookAdapter extends WidgetAdapter<NotebookPanel> {
139142
);
140143

141144
this.widget.content.activeCellChanged.connect(this.activeCellChanged, this);
142-
this.widget.model.cells.changed.connect(async (cells, change) => {
143-
let cellsAdded: ICellModel[] = [];
144-
let cellsRemoved: ICellModel[] = [];
145-
const type = this.type;
146-
147-
if (change.type === 'set') {
148-
// handling of conversions is important, because the editors get re-used and their handlers inherited,
149-
// so we need to clear our handlers from editors of e.g. markdown cells which previously were code cells.
150-
let convertedToMarkdownOrRaw = [];
151-
let convertedToCode = [];
152-
153-
if (change.newValues.length === change.oldValues.length) {
154-
// during conversion the cells should not get deleted nor added
155-
for (let i = 0; i < change.newValues.length; i++) {
156-
if (
157-
change.oldValues[i].type === type &&
158-
change.newValues[i].type !== type
159-
) {
160-
convertedToMarkdownOrRaw.push(change.newValues[i]);
161-
} else if (
162-
change.oldValues[i].type !== type &&
163-
change.newValues[i].type === type
164-
) {
165-
convertedToCode.push(change.newValues[i]);
166-
}
145+
this.widget.model.cells.changed.connect(this.handle_cell_change, this);
146+
this.editor.modelChanged.connect(notebook => {
147+
// note: this should not usually happen;
148+
// there is no default action that would trigger this,
149+
// its just a failsafe in case if another extension decides
150+
// to swap the notebook model
151+
this.console.warn(
152+
'Model changed, connecting cell change handler; this is not something we were expecting'
153+
);
154+
notebook.model.cells.changed.connect(this.handle_cell_change, this);
155+
});
156+
}
157+
158+
async handle_cell_change(
159+
cells: IObservableUndoableList<ICellModel>,
160+
change: IObservableList.IChangedArgs<ICellModel>
161+
) {
162+
let cellsAdded: ICellModel[] = [];
163+
let cellsRemoved: ICellModel[] = [];
164+
const type = this.type;
165+
166+
if (change.type === 'set') {
167+
// handling of conversions is important, because the editors get re-used and their handlers inherited,
168+
// so we need to clear our handlers from editors of e.g. markdown cells which previously were code cells.
169+
let convertedToMarkdownOrRaw = [];
170+
let convertedToCode = [];
171+
172+
if (change.newValues.length === change.oldValues.length) {
173+
// during conversion the cells should not get deleted nor added
174+
for (let i = 0; i < change.newValues.length; i++) {
175+
if (
176+
change.oldValues[i].type === type &&
177+
change.newValues[i].type !== type
178+
) {
179+
convertedToMarkdownOrRaw.push(change.newValues[i]);
180+
} else if (
181+
change.oldValues[i].type !== type &&
182+
change.newValues[i].type === type
183+
) {
184+
convertedToCode.push(change.newValues[i]);
167185
}
168-
cellsAdded = convertedToCode;
169-
cellsRemoved = convertedToMarkdownOrRaw;
170186
}
171-
} else if (change.type == 'add') {
172-
cellsAdded = change.newValues.filter(
173-
cellModel => cellModel.type === type
174-
);
175-
}
176-
// note: editorRemoved is not emitted for removal of cells by change of type 'remove' (but only during cell type conversion)
177-
// because there is no easy way to get the widget associated with the removed cell(s) - because it is no
178-
// longer in the notebook widget list! It would need to be tracked on our side, but it is not necessary
179-
// as (except for a tiny memory leak) it should not impact the functionality in any way
180-
181-
if (
182-
cellsRemoved.length ||
183-
cellsAdded.length ||
184-
change.type === 'move' ||
185-
change.type === 'remove'
186-
) {
187-
// in contrast to the file editor document which can be only changed by the modification of the editor content,
188-
// the notebook document cna also get modified by a change in the number or arrangement of editors themselves;
189-
// for this reason each change has to trigger documents update (so that LSP mirror is in sync).
190-
await this.update_documents();
187+
cellsAdded = convertedToCode;
188+
cellsRemoved = convertedToMarkdownOrRaw;
191189
}
190+
} else if (change.type == 'add') {
191+
cellsAdded = change.newValues.filter(
192+
cellModel => cellModel.type === type
193+
);
194+
}
195+
// note: editorRemoved is not emitted for removal of cells by change of type 'remove' (but only during cell type conversion)
196+
// because there is no easy way to get the widget associated with the removed cell(s) - because it is no
197+
// longer in the notebook widget list! It would need to be tracked on our side, but it is not necessary
198+
// as (except for a tiny memory leak) it should not impact the functionality in any way
199+
200+
if (
201+
cellsRemoved.length ||
202+
cellsAdded.length ||
203+
change.type === 'move' ||
204+
change.type === 'remove'
205+
) {
206+
// in contrast to the file editor document which can be only changed by the modification of the editor content,
207+
// the notebook document cna also get modified by a change in the number or arrangement of editors themselves;
208+
// for this reason each change has to trigger documents update (so that LSP mirror is in sync).
209+
await this.update_documents();
210+
}
192211

193-
for (let cellModel of cellsRemoved) {
194-
let cellWidget = this.widget.content.widgets.find(
195-
cell => cell.model.id === cellModel.id
196-
);
197-
this.known_editors_ids.delete(cellWidget.editor.uuid);
198-
199-
// for practical purposes this editor got removed from our consideration;
200-
// it might seem that we should instead look for the editor indicated by
201-
// the oldValues[i] cellModel, but this one got already transferred to the
202-
// markdown cell in newValues[i]
203-
this.editorRemoved.emit({
204-
editor: cellWidget.editor
205-
});
206-
}
212+
for (let cellModel of cellsRemoved) {
213+
let cellWidget = this.widget.content.widgets.find(
214+
cell => cell.model.id === cellModel.id
215+
);
216+
this.known_editors_ids.delete(cellWidget.editor.uuid);
217+
218+
// for practical purposes this editor got removed from our consideration;
219+
// it might seem that we should instead look for the editor indicated by
220+
// the oldValues[i] cellModel, but this one got already transferred to the
221+
// markdown cell in newValues[i]
222+
this.editorRemoved.emit({
223+
editor: cellWidget.editor
224+
});
225+
}
207226

208-
for (let cellModel of cellsAdded) {
209-
let cellWidget = this.widget.content.widgets.find(
210-
cell => cell.model.id === cellModel.id
211-
);
212-
this.known_editors_ids.add(cellWidget.editor.uuid);
227+
for (let cellModel of cellsAdded) {
228+
let cellWidget = this.widget.content.widgets.find(
229+
cell => cell.model.id === cellModel.id
230+
);
231+
this.known_editors_ids.add(cellWidget.editor.uuid);
213232

214-
this.editorAdded.emit({
215-
editor: cellWidget.editor
216-
});
217-
}
218-
});
233+
this.editorAdded.emit({
234+
editor: cellWidget.editor
235+
});
236+
}
219237
}
220238

221239
get editors(): CodeEditor.IEditor[] {

0 commit comments

Comments
 (0)