11import { WidgetAdapter } from '../adapter' ;
22import { Notebook , NotebookPanel } from '@jupyterlab/notebook' ;
33import { until_ready } from '../../utils' ;
4- import { Cell } from '@jupyterlab/cells' ;
4+ import { Cell , ICellModel } from '@jupyterlab/cells' ;
55import * as nbformat from '@jupyterlab/nbformat' ;
66import ILanguageInfoMetadata = nbformat . ILanguageInfoMetadata ;
77import { Session } from '@jupyterlab/services' ;
@@ -17,13 +17,16 @@ import { VirtualDocument } from '../../virtual/document';
1717export 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 ;
23+ private type : nbformat . CellType = 'code' ;
2224
2325 constructor ( extension : LSPExtension , editor_widget : NotebookPanel ) {
2426 super ( extension , editor_widget ) ;
2527 this . ce_editor_to_cell = new Map ( ) ;
2628 this . editor = editor_widget . content ;
29+ this . known_editors_ids = new Set ( ) ;
2730 this . init_once_ready ( ) . catch ( console . warn ) ;
2831 }
2932
@@ -136,6 +139,83 @@ export class NotebookAdapter extends WidgetAdapter<NotebookPanel> {
136139 ) ;
137140
138141 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+ }
167+ }
168+ cellsAdded = convertedToCode ;
169+ cellsRemoved = convertedToMarkdownOrRaw ;
170+ }
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 ( ) ;
191+ }
192+
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+ }
207+
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 ) ;
213+
214+ this . editorAdded . emit ( {
215+ editor : cellWidget . editor
216+ } ) ;
217+ }
218+ } ) ;
139219 }
140220
141221 get editors ( ) : CodeEditor . IEditor [ ] {
@@ -178,13 +258,29 @@ export class NotebookAdapter extends WidgetAdapter<NotebookPanel> {
178258 }
179259
180260 private activeCellChanged ( notebook : Notebook , cell : Cell ) {
261+ if ( cell . model . type !== this . type ) {
262+ return ;
263+ }
264+ if ( ! this . known_editors_ids . has ( cell . editor . uuid ) ) {
265+ this . known_editors_ids . add ( cell . editor . uuid ) ;
266+ this . editorAdded . emit ( {
267+ editor : cell . editor
268+ } ) ;
269+ }
181270 this . activeEditorChanged . emit ( {
182271 editor : cell . editor
183272 } ) ;
184273 }
185274
186275 context_from_active_document ( ) : ICommandContext | null {
187276 let cell = this . widget . content . activeCell ;
277+ if ( cell . model . type !== this . type ) {
278+ // context will be sought on all cells to verify if the context menu should be visible,
279+ // thus it is ok to just return null; it seems to stem from the implementation detail
280+ // upstream, i.e. the markdown cells appear to be created by transforming the code cells
281+ // but do not quote me on that.
282+ return null ;
283+ }
188284 let editor = cell . editor ;
189285 let ce_cursor = editor . getCursorPosition ( ) ;
190286 let cm_cursor = PositionConverter . ce_to_cm ( ce_cursor ) as IEditorPosition ;
0 commit comments