Skip to content

Commit d441550

Browse files
committed
Handle cell changes explicitly rater than via activeCellChanged
This fixes a ton of bugs related to how the state of document was not being picked up by the virtual document system.
1 parent de2182f commit d441550

File tree

3 files changed

+81
-30
lines changed

3 files changed

+81
-30
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ export abstract class WidgetAdapter<T extends IDocumentWidget> {
9696
protected app: JupyterFrontEnd;
9797

9898
public activeEditorChanged: Signal<WidgetAdapter<T>, IEditorChangedData>;
99+
public editorAdded: Signal<WidgetAdapter<T>, IEditorChangedData>;
99100
public editorRemoved: Signal<WidgetAdapter<T>, IEditorChangedData>;
100101
public update_finished: Promise<void>;
101102

@@ -119,6 +120,7 @@ export abstract class WidgetAdapter<T extends IDocumentWidget> {
119120
this.adapterConnected = new Signal(this);
120121
this.activeEditorChanged = new Signal(this);
121122
this.editorRemoved = new Signal(this);
123+
this.editorAdded = new Signal(this);
122124
this.adapters = new Map();
123125
this.status_message = new StatusMessage();
124126
this.isConnected = false;

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

Lines changed: 67 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { WidgetAdapter } from '../adapter';
22
import { Notebook, NotebookPanel } from '@jupyterlab/notebook';
33
import { until_ready } from '../../utils';
4-
import { Cell } from '@jupyterlab/cells';
4+
import { Cell, ICellModel } from '@jupyterlab/cells';
55
import * as nbformat from '@jupyterlab/nbformat';
66
import ILanguageInfoMetadata = nbformat.ILanguageInfoMetadata;
77
import { Session } from '@jupyterlab/services';
@@ -17,13 +17,15 @@ import { VirtualDocument } from '../../virtual/document';
1717
export class NotebookAdapter extends WidgetAdapter<NotebookPanel> {
1818
editor: Notebook;
1919
private ce_editor_to_cell: Map<IEditor, Cell>;
20+
private known_editors_ids: Set<string>;
2021

2122
private _language_info: ILanguageInfoMetadata;
2223

2324
constructor(extension: LSPExtension, editor_widget: NotebookPanel) {
2425
super(extension, editor_widget);
2526
this.ce_editor_to_cell = new Map();
2627
this.editor = editor_widget.content;
28+
this.known_editors_ids = new Set();
2729
this.init_once_ready().catch(console.warn);
2830
}
2931

@@ -136,32 +138,57 @@ export class NotebookAdapter extends WidgetAdapter<NotebookPanel> {
136138
);
137139

138140
this.widget.content.activeCellChanged.connect(this.activeCellChanged, this);
139-
this.widget.model.cells.changed.connect((cells, change) => {
140-
console.log(change);
141+
this.widget.model.cells.changed.connect(async (cells, change) => {
142+
let cellsAdded: ICellModel[] = [];
143+
let cellsRemoved: ICellModel[] = [];
144+
145+
if (change.type === 'set') {
146+
// handling of conversions is important, because the editors get re-used and their handlers inherited,
147+
// so we need to clear our handlers from editors of e.g. markdown cells which previously were code cells.
148+
let convertedToMarkdownOrRaw = [];
149+
let convertedToCode = [];
150+
151+
if (change.newValues.length !== change.oldValues.length) {
152+
// during conversion the cells should not get deleted nor added
153+
return;
154+
}
141155

142-
if (change.type !== 'set') {
143-
return;
144-
}
145-
let convertedToMarkdown = [];
156+
for (let i = 0; i < change.newValues.length; i++) {
157+
if (
158+
change.oldValues[i].type === 'code' &&
159+
change.newValues[i].type !== 'code'
160+
) {
161+
convertedToMarkdownOrRaw.push(change.newValues[i]);
162+
} else if (
163+
change.oldValues[i].type !== 'code' &&
164+
change.newValues[i].type === 'code'
165+
) {
166+
convertedToCode.push(change.newValues[i]);
167+
}
168+
}
169+
cellsAdded = convertedToCode;
170+
cellsRemoved = convertedToMarkdownOrRaw;
146171

147-
if (change.newValues.length !== change.oldValues.length) {
148-
// during conversion the cells should not get deleted nor added
149-
return;
172+
} else if (change.type == 'add') {
173+
cellsAdded = change.newValues.filter((cellModel) => cellModel.type === 'code')
150174
}
151-
152-
for (let i = 0; i < change.newValues.length; i++) {
153-
if (
154-
change.oldValues[i].type === 'code' &&
155-
change.newValues[i].type === 'markdown'
156-
) {
157-
convertedToMarkdown.push(change.newValues[i]);
158-
}
175+
// note: editorRemoved is not emitted for removal of cells by change of type 'remove' (but only during cell type conversion)
176+
// because there is no easy way to get the widget associated with the removed cell(s) - because it is no
177+
// longer in the notebook widget list! It would need to be tracked on our side, but it is not necessary
178+
// as (except for a tiny memory leak) it should not impact the functionality in any way
179+
180+
if (cellsRemoved.length || cellsAdded.length || change.type === 'move' || change.type === 'remove') {
181+
// in contrast to the file editor document which can be only changed by the modification of the editor content,
182+
// the notebook document cna also get modified by a change in the number or arrangement of editors themselves;
183+
// for this reason each change has to trigger documents update (so that LSP mirror is in sync).
184+
await this.update_documents();
159185
}
160186

161-
for (let cellModel of convertedToMarkdown) {
187+
for (let cellModel of cellsRemoved) {
162188
let cellWidget = this.widget.content.widgets.find(
163189
cell => cell.model.id === cellModel.id
164190
);
191+
this.known_editors_ids.delete(cellWidget.editor.uuid)
165192

166193
// for practical purposes this editor got removed from our consideration;
167194
// it might seem that we should instead look for the editor indicated by
@@ -171,6 +198,18 @@ export class NotebookAdapter extends WidgetAdapter<NotebookPanel> {
171198
editor: cellWidget.editor
172199
});
173200
}
201+
202+
for (let cellModel of cellsAdded) {
203+
let cellWidget = this.widget.content.widgets.find(
204+
cell => cell.model.id === cellModel.id
205+
);
206+
this.known_editors_ids.add(cellWidget.editor.uuid)
207+
208+
this.editorAdded.emit({
209+
editor: cellWidget.editor
210+
});
211+
}
212+
174213
});
175214
}
176215

@@ -214,6 +253,15 @@ export class NotebookAdapter extends WidgetAdapter<NotebookPanel> {
214253
}
215254

216255
private activeCellChanged(notebook: Notebook, cell: Cell) {
256+
if (cell.model.type !== 'code') {
257+
return;
258+
}
259+
if (!this.known_editors_ids.has(cell.editor.uuid)) {
260+
this.known_editors_ids.add(cell.editor.uuid);
261+
this.editorAdded.emit({
262+
editor: cell.editor
263+
});
264+
}
217265
this.activeEditorChanged.emit({
218266
editor: cell.editor
219267
});

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

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -519,18 +519,19 @@ export class CodeMirrorVirtualEditor
519519
callback(cm_editor);
520520
}
521521
if (monitor_for_new_blocks) {
522-
this.adapter.activeEditorChanged.connect(
523-
(adapter, data: IEditorChangedData) => {
524-
let { editor } = data;
525-
if (editor == null) {
526-
return;
527-
}
528-
let cm_editor = (editor as CodeMirrorEditor).editor;
529-
if (!editors_with_handlers.has(cm_editor)) {
530-
callback(cm_editor);
531-
}
522+
523+
let activation_callback = (adapter: WidgetAdapter<IDocumentWidget>, data: IEditorChangedData) => {
524+
let { editor } = data;
525+
if (editor == null) {
526+
return;
532527
}
533-
);
528+
let cm_editor = (editor as CodeMirrorEditor).editor;
529+
if (!editors_with_handlers.has(cm_editor)) {
530+
callback(cm_editor);
531+
}
532+
}
533+
534+
this.adapter.editorAdded.connect(activation_callback)
534535
this.adapter.editorRemoved.connect(
535536
(adapter, data: IEditorChangedData) => {
536537
let { editor } = data;

0 commit comments

Comments
 (0)