UNPKG

14.8 kBJavaScriptView Raw
1// Copyright (c) Jupyter Development Team.
2// Distributed under the terms of the Modified BSD License.
3import { IEditorMimeTypeService } from '@jupyterlab/codeeditor';
4import { untilReady, VirtualDocument, WidgetLSPAdapter } from '@jupyterlab/lsp';
5import { PromiseDelegate } from '@lumino/coreutils';
6import { Signal } from '@lumino/signaling';
7export class NotebookAdapter extends WidgetLSPAdapter {
8 constructor(editorWidget, options) {
9 super(editorWidget, options);
10 this.editorWidget = editorWidget;
11 this.options = options;
12 this._type = 'code';
13 this._readyDelegate = new PromiseDelegate();
14 this._editorToCell = new Map();
15 this.editor = editorWidget.content;
16 this._cellToEditor = new WeakMap();
17 Promise.all([
18 this.widget.context.sessionContext.ready,
19 this.connectionManager.ready
20 ])
21 .then(async () => {
22 await this.initOnceReady();
23 this._readyDelegate.resolve();
24 })
25 .catch(console.error);
26 }
27 /**
28 * Get current path of the document.
29 */
30 get documentPath() {
31 return this.widget.context.path;
32 }
33 /**
34 * Get the mime type of the document.
35 */
36 get mimeType() {
37 var _a;
38 let mimeType;
39 let languageMetadata = this.language_info();
40 if (!languageMetadata || !languageMetadata.mimetype) {
41 // fallback to the code cell mime type if no kernel in use
42 mimeType = this.widget.content.codeMimetype;
43 }
44 else {
45 mimeType = languageMetadata.mimetype;
46 }
47 return Array.isArray(mimeType)
48 ? (_a = mimeType[0]) !== null && _a !== void 0 ? _a : IEditorMimeTypeService.defaultMimeType
49 : mimeType;
50 }
51 /**
52 * Get the file extension of the document.
53 */
54 get languageFileExtension() {
55 let languageMetadata = this.language_info();
56 if (!languageMetadata || !languageMetadata.file_extension) {
57 return;
58 }
59 return languageMetadata.file_extension.replace('.', '');
60 }
61 /**
62 * Get the inner HTMLElement of the document widget.
63 */
64 get wrapperElement() {
65 return this.widget.node;
66 }
67 /**
68 * Get the list of CM editor with its type in the document,
69 */
70 get editors() {
71 if (this.isDisposed) {
72 return [];
73 }
74 let notebook = this.widget.content;
75 this._editorToCell.clear();
76 if (notebook.isDisposed) {
77 return [];
78 }
79 return notebook.widgets.map(cell => {
80 return {
81 ceEditor: this._getCellEditor(cell),
82 type: cell.model.type,
83 value: cell.model.sharedModel.getSource()
84 };
85 });
86 }
87 /**
88 * Get the activated CM editor.
89 */
90 get activeEditor() {
91 return this.editor.activeCell
92 ? this._getCellEditor(this.editor.activeCell)
93 : undefined;
94 }
95 /**
96 * Promise that resolves once the adapter is initialized
97 */
98 get ready() {
99 return this._readyDelegate.promise;
100 }
101 /**
102 * Get the index of editor from the cursor position in the virtual
103 * document.
104 * @deprecated This is error-prone and will be removed in JupyterLab 5.0, use `getEditorIndex()` with `virtualDocument.getEditorAtVirtualLine(position)` instead.
105 *
106 * @param position - the position of cursor in the virtual document.
107 */
108 getEditorIndexAt(position) {
109 let cell = this._getCellAt(position);
110 let notebook = this.widget.content;
111 return notebook.widgets.findIndex(otherCell => {
112 return cell === otherCell;
113 });
114 }
115 /**
116 * Get the index of input editor
117 *
118 * @param ceEditor - instance of the code editor
119 */
120 getEditorIndex(ceEditor) {
121 let cell = this._editorToCell.get(ceEditor);
122 return this.editor.widgets.findIndex(otherCell => {
123 return cell === otherCell;
124 });
125 }
126 /**
127 * Get the wrapper of input editor.
128 *
129 * @param ceEditor - instance of the code editor
130 */
131 getEditorWrapper(ceEditor) {
132 let cell = this._editorToCell.get(ceEditor);
133 return cell.node;
134 }
135 /**
136 * Callback on kernel changed event, it will disconnect the
137 * document with the language server and then reconnect.
138 *
139 * @param _session - Session context of changed kernel
140 * @param change - Changed data
141 */
142 async onKernelChanged(_session, change) {
143 if (!change.newValue) {
144 return;
145 }
146 try {
147 // note: we need to wait until ready before updating language info
148 const oldLanguageInfo = this._languageInfo;
149 await untilReady(this.isReady, -1);
150 await this._updateLanguageInfo();
151 const newLanguageInfo = this._languageInfo;
152 if ((oldLanguageInfo === null || oldLanguageInfo === void 0 ? void 0 : oldLanguageInfo.name) != newLanguageInfo.name ||
153 (oldLanguageInfo === null || oldLanguageInfo === void 0 ? void 0 : oldLanguageInfo.mimetype) != (newLanguageInfo === null || newLanguageInfo === void 0 ? void 0 : newLanguageInfo.mimetype) ||
154 (oldLanguageInfo === null || oldLanguageInfo === void 0 ? void 0 : oldLanguageInfo.file_extension) != (newLanguageInfo === null || newLanguageInfo === void 0 ? void 0 : newLanguageInfo.file_extension)) {
155 console.log(`Changed to ${this._languageInfo.name} kernel, reconnecting`);
156 this.reloadConnection();
157 }
158 else {
159 console.log('Keeping old LSP connection as the new kernel uses the same langauge');
160 }
161 }
162 catch (err) {
163 console.warn(err);
164 // try to reconnect anyway
165 this.reloadConnection();
166 }
167 }
168 /**
169 * Dispose the widget.
170 */
171 dispose() {
172 if (this.isDisposed) {
173 return;
174 }
175 this.widget.context.sessionContext.kernelChanged.disconnect(this.onKernelChanged, this);
176 this.widget.content.activeCellChanged.disconnect(this._activeCellChanged, this);
177 super.dispose();
178 // editors are needed for the parent dispose() to unbind signals, so they are the last to go
179 this._editorToCell.clear();
180 Signal.clearData(this);
181 }
182 /**
183 * Method to check if the notebook context is ready.
184 */
185 isReady() {
186 var _a;
187 return (!this.widget.isDisposed &&
188 this.widget.context.isReady &&
189 this.widget.content.isVisible &&
190 this.widget.content.widgets.length > 0 &&
191 ((_a = this.widget.context.sessionContext.session) === null || _a === void 0 ? void 0 : _a.kernel) != null);
192 }
193 /**
194 * Update the virtual document on cell changing event.
195 *
196 * @param cells - Observable list of changed cells
197 * @param change - Changed data
198 */
199 async handleCellChange(cells, change) {
200 let cellsAdded = [];
201 let cellsRemoved = [];
202 const type = this._type;
203 if (change.type === 'set') {
204 // handling of conversions is important, because the editors get re-used and their handlers inherited,
205 // so we need to clear our handlers from editors of e.g. markdown cells which previously were code cells.
206 let convertedToMarkdownOrRaw = [];
207 let convertedToCode = [];
208 if (change.newValues.length === change.oldValues.length) {
209 // during conversion the cells should not get deleted nor added
210 for (let i = 0; i < change.newValues.length; i++) {
211 if (change.oldValues[i].type === type &&
212 change.newValues[i].type !== type) {
213 convertedToMarkdownOrRaw.push(change.newValues[i]);
214 }
215 else if (change.oldValues[i].type !== type &&
216 change.newValues[i].type === type) {
217 convertedToCode.push(change.newValues[i]);
218 }
219 }
220 cellsAdded = convertedToCode;
221 cellsRemoved = convertedToMarkdownOrRaw;
222 }
223 }
224 else if (change.type == 'add') {
225 cellsAdded = change.newValues.filter(cellModel => cellModel.type === type);
226 }
227 // note: editorRemoved is not emitted for removal of cells by change of type 'remove' (but only during cell type conversion)
228 // because there is no easy way to get the widget associated with the removed cell(s) - because it is no
229 // longer in the notebook widget list! It would need to be tracked on our side, but it is not necessary
230 // as (except for a tiny memory leak) it should not impact the functionality in any way
231 if (cellsRemoved.length ||
232 cellsAdded.length ||
233 change.type === 'set' ||
234 change.type === 'move' ||
235 change.type === 'remove') {
236 // in contrast to the file editor document which can be only changed by the modification of the editor content,
237 // the notebook document cna also get modified by a change in the number or arrangement of editors themselves;
238 // for this reason each change has to trigger documents update (so that LSP mirror is in sync).
239 await this.updateDocuments();
240 }
241 for (let cellModel of cellsAdded) {
242 let cellWidget = this.widget.content.widgets.find(cell => cell.model.id === cellModel.id);
243 if (!cellWidget) {
244 console.warn(`Widget for added cell with ID: ${cellModel.id} not found!`);
245 continue;
246 }
247 // Add editor to the mapping if needed
248 this._getCellEditor(cellWidget);
249 }
250 }
251 /**
252 * Generate the virtual document associated with the document.
253 */
254 createVirtualDocument() {
255 return new VirtualDocument({
256 language: this.language,
257 foreignCodeExtractors: this.options.foreignCodeExtractorsManager,
258 path: this.documentPath,
259 fileExtension: this.languageFileExtension,
260 // notebooks are continuous, each cell is dependent on the previous one
261 standalone: false,
262 // notebooks are not supported by LSP servers
263 hasLspSupportedFile: false
264 });
265 }
266 /**
267 * Get the metadata of notebook.
268 */
269 language_info() {
270 return this._languageInfo;
271 }
272 /**
273 * Initialization function called once the editor and the LSP connection
274 * manager is ready. This function will create the virtual document and
275 * connect various signals.
276 */
277 async initOnceReady() {
278 await untilReady(this.isReady.bind(this), -1);
279 await this._updateLanguageInfo();
280 this.initVirtual();
281 // connect the document, but do not open it as the adapter will handle this
282 // after registering all features
283 this.connectDocument(this.virtualDocument, false).catch(console.warn);
284 this.widget.context.sessionContext.kernelChanged.connect(this.onKernelChanged, this);
285 this.widget.content.activeCellChanged.connect(this._activeCellChanged, this);
286 this._connectModelSignals(this.widget);
287 this.editor.modelChanged.connect(notebook => {
288 // note: this should not usually happen;
289 // there is no default action that would trigger this,
290 // its just a failsafe in case if another extension decides
291 // to swap the notebook model
292 console.warn('Model changed, connecting cell change handler; this is not something we were expecting');
293 this._connectModelSignals(notebook);
294 });
295 }
296 /**
297 * Connect the cell changed event to its handler
298 *
299 * @param notebook - The notebook that emitted event.
300 */
301 _connectModelSignals(notebook) {
302 if (notebook.model === null) {
303 console.warn(`Model is missing for notebook ${notebook}, cannot connect cell changed signal!`);
304 }
305 else {
306 notebook.model.cells.changed.connect(this.handleCellChange, this);
307 }
308 }
309 /**
310 * Update the stored language info with the one from the notebook.
311 */
312 async _updateLanguageInfo() {
313 var _a, _b, _c, _d;
314 const language_info = (_d = (await ((_c = (_b = (_a = this.widget.context.sessionContext) === null || _a === void 0 ? void 0 : _a.session) === null || _b === void 0 ? void 0 : _b.kernel) === null || _c === void 0 ? void 0 : _c.info))) === null || _d === void 0 ? void 0 : _d.language_info;
315 if (language_info) {
316 this._languageInfo = language_info;
317 }
318 else {
319 throw new Error('Language info update failed (no session, kernel, or info available)');
320 }
321 }
322 /**
323 * Handle the cell changed event
324 * @param notebook - The notebook that emitted event
325 * @param cell - Changed cell.
326 */
327 _activeCellChanged(notebook, cell) {
328 if (!cell || cell.model.type !== this._type) {
329 return;
330 }
331 this._activeEditorChanged.emit({
332 editor: this._getCellEditor(cell)
333 });
334 }
335 /**
336 * Get the cell at the cursor position of the virtual document.
337 * @param pos - Position in the virtual document.
338 */
339 _getCellAt(pos) {
340 let editor = this.virtualDocument.getEditorAtVirtualLine(pos);
341 return this._editorToCell.get(editor);
342 }
343 /**
344 * Get the cell editor and add new ones to the mappings.
345 *
346 * @param cell Cell widget
347 * @returns Cell editor accessor
348 */
349 _getCellEditor(cell) {
350 if (!this._cellToEditor.has(cell)) {
351 const editor = Object.freeze({
352 getEditor: () => cell.editor,
353 ready: async () => {
354 await cell.ready;
355 return cell.editor;
356 },
357 reveal: async () => {
358 await this.editor.scrollToCell(cell);
359 return cell.editor;
360 }
361 });
362 this._cellToEditor.set(cell, editor);
363 this._editorToCell.set(editor, cell);
364 cell.disposed.connect(() => {
365 this._cellToEditor.delete(cell);
366 this._editorToCell.delete(editor);
367 this._editorRemoved.emit({
368 editor
369 });
370 });
371 this._editorAdded.emit({
372 editor
373 });
374 }
375 return this._cellToEditor.get(cell);
376 }
377}
378//# sourceMappingURL=notebooklspadapter.js.map
\No newline at end of file