UNPKG

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