@difizen/libro-lsp 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +0 -0
- package/es/adapters/adapter.d.ts +255 -0
- package/es/adapters/adapter.d.ts.map +1 -0
- package/es/adapters/adapter.js +647 -0
- package/es/adapters/notebook-adapter.d.ts +150 -0
- package/es/adapters/notebook-adapter.d.ts.map +1 -0
- package/es/adapters/notebook-adapter.js +588 -0
- package/es/adapters/status-message.d.ts +48 -0
- package/es/adapters/status-message.d.ts.map +1 -0
- package/es/adapters/status-message.js +110 -0
- package/es/connection-manager.d.ts +199 -0
- package/es/connection-manager.d.ts.map +1 -0
- package/es/connection-manager.js +685 -0
- package/es/connection.d.ts +149 -0
- package/es/connection.d.ts.map +1 -0
- package/es/connection.js +591 -0
- package/es/extractors/index.d.ts +4 -0
- package/es/extractors/index.d.ts.map +1 -0
- package/es/extractors/index.js +6 -0
- package/es/extractors/manager.d.ts +31 -0
- package/es/extractors/manager.d.ts.map +1 -0
- package/es/extractors/manager.js +90 -0
- package/es/extractors/text-extractor.d.ts +56 -0
- package/es/extractors/text-extractor.d.ts.map +1 -0
- package/es/extractors/text-extractor.js +72 -0
- package/es/extractors/types.d.ts +68 -0
- package/es/extractors/types.d.ts.map +1 -0
- package/es/extractors/types.js +1 -0
- package/es/feature.d.ts +29 -0
- package/es/feature.d.ts.map +1 -0
- package/es/feature.js +85 -0
- package/es/index.d.ts +19 -0
- package/es/index.d.ts.map +1 -0
- package/es/index.js +21 -0
- package/es/lsp-app-contribution.d.ts +19 -0
- package/es/lsp-app-contribution.d.ts.map +1 -0
- package/es/lsp-app-contribution.js +155 -0
- package/es/lsp-protocol.d.ts +10 -0
- package/es/lsp-protocol.d.ts.map +1 -0
- package/es/lsp-protocol.js +1 -0
- package/es/lsp.d.ts +136 -0
- package/es/lsp.d.ts.map +1 -0
- package/es/lsp.js +141 -0
- package/es/manager.d.ts +142 -0
- package/es/manager.d.ts.map +1 -0
- package/es/manager.js +423 -0
- package/es/module.d.ts +3 -0
- package/es/module.d.ts.map +1 -0
- package/es/module.js +21 -0
- package/es/plugin.d.ts +56 -0
- package/es/plugin.d.ts.map +1 -0
- package/es/plugin.js +0 -0
- package/es/positioning.d.ts +66 -0
- package/es/positioning.d.ts.map +1 -0
- package/es/positioning.js +96 -0
- package/es/schema.d.ts +240 -0
- package/es/schema.d.ts.map +1 -0
- package/es/schema.js +0 -0
- package/es/tokens.d.ts +677 -0
- package/es/tokens.d.ts.map +1 -0
- package/es/tokens.js +183 -0
- package/es/utils.d.ts +33 -0
- package/es/utils.d.ts.map +1 -0
- package/es/utils.js +168 -0
- package/es/virtual/document.d.ts +546 -0
- package/es/virtual/document.d.ts.map +1 -0
- package/es/virtual/document.js +1263 -0
- package/es/ws-connection/server-capability-registration.d.ts +19 -0
- package/es/ws-connection/server-capability-registration.d.ts.map +1 -0
- package/es/ws-connection/server-capability-registration.js +51 -0
- package/es/ws-connection/types.d.ts +76 -0
- package/es/ws-connection/types.d.ts.map +1 -0
- package/es/ws-connection/types.js +1 -0
- package/es/ws-connection/ws-connection.d.ts +105 -0
- package/es/ws-connection/ws-connection.d.ts.map +1 -0
- package/es/ws-connection/ws-connection.js +301 -0
- package/package.json +67 -0
- package/src/adapters/adapter.ts +611 -0
- package/src/adapters/notebook-adapter.ts +463 -0
- package/src/adapters/status-message.ts +93 -0
- package/src/connection-manager.ts +626 -0
- package/src/connection.ts +570 -0
- package/src/extractors/index.ts +6 -0
- package/src/extractors/manager.ts +82 -0
- package/src/extractors/text-extractor.ts +94 -0
- package/src/extractors/types.ts +78 -0
- package/src/feature.ts +60 -0
- package/src/index.spec.ts +10 -0
- package/src/index.ts +21 -0
- package/src/lsp-app-contribution.ts +83 -0
- package/src/lsp-protocol.ts +10 -0
- package/src/lsp.ts +160 -0
- package/src/manager.ts +358 -0
- package/src/module.ts +32 -0
- package/src/plugin.ts +62 -0
- package/src/positioning.ts +121 -0
- package/src/schema.ts +249 -0
- package/src/tokens.ts +843 -0
- package/src/utils.ts +109 -0
- package/src/virtual/document.ts +1250 -0
- package/src/ws-connection/server-capability-registration.ts +77 -0
- package/src/ws-connection/types.ts +102 -0
- package/src/ws-connection/ws-connection.ts +320 -0
|
@@ -0,0 +1,1250 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-use-before-define */
|
|
2
|
+
// Copyright (c) Jupyter Development Team.
|
|
3
|
+
// Distributed under the terms of the Modified BSD License.
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
IRange,
|
|
7
|
+
IPosition as CodeEditorPosition,
|
|
8
|
+
} from '@difizen/libro-code-editor';
|
|
9
|
+
import { Emitter } from '@difizen/mana-app';
|
|
10
|
+
import type { Disposable, Event } from '@difizen/mana-app';
|
|
11
|
+
|
|
12
|
+
import { DocumentConnectionManager } from '../connection-manager.js';
|
|
13
|
+
import type { IForeignCodeExtractor } from '../extractors/types.js';
|
|
14
|
+
import type { LanguageIdentifier } from '../lsp.js';
|
|
15
|
+
import type {
|
|
16
|
+
Position,
|
|
17
|
+
IEditorPosition,
|
|
18
|
+
IRootPosition,
|
|
19
|
+
ISourcePosition,
|
|
20
|
+
IVirtualPosition,
|
|
21
|
+
} from '../positioning.js';
|
|
22
|
+
import type { Document, ILSPCodeExtractorsManager } from '../tokens.js';
|
|
23
|
+
import { DefaultMap, untilReady } from '../utils.js';
|
|
24
|
+
import type { IDocumentInfo } from '../ws-connection/types.js';
|
|
25
|
+
|
|
26
|
+
type language = string;
|
|
27
|
+
|
|
28
|
+
interface IVirtualLine {
|
|
29
|
+
/**
|
|
30
|
+
* Inspections for which document should be skipped for this virtual line?
|
|
31
|
+
*/
|
|
32
|
+
skipInspect: VirtualDocument.idPath[];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Where does the virtual line belongs to in the source document?
|
|
36
|
+
*/
|
|
37
|
+
sourceLine: number | null;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The editor holding this virtual line
|
|
41
|
+
*/
|
|
42
|
+
editor: Document.IEditor;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type ForeignDocumentsMap = Map<IRange, Document.IVirtualDocumentBlock>;
|
|
46
|
+
|
|
47
|
+
interface ISourceLine {
|
|
48
|
+
/**
|
|
49
|
+
* Line corresponding to the block in the entire foreign document
|
|
50
|
+
*/
|
|
51
|
+
virtualLine: number;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The CM editor associated with this virtual line.
|
|
55
|
+
*/
|
|
56
|
+
editor: Document.IEditor;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Line in the CM editor corresponding to the virtual line.
|
|
60
|
+
*/
|
|
61
|
+
editorLine: number;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Shift of the virtual line
|
|
65
|
+
*/
|
|
66
|
+
editorShift: CodeEditorPosition;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Everything which is not in the range of foreign documents belongs to the host.
|
|
70
|
+
*/
|
|
71
|
+
foreignDocumentsMap: ForeignDocumentsMap;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Check if given position is within range.
|
|
76
|
+
* Both start and end are inclusive.
|
|
77
|
+
* @param position
|
|
78
|
+
* @param range
|
|
79
|
+
*/
|
|
80
|
+
export function isWithinRange(position: CodeEditorPosition, range: IRange): boolean {
|
|
81
|
+
if (range.start.line === range.end.line) {
|
|
82
|
+
return (
|
|
83
|
+
position.line === range.start.line &&
|
|
84
|
+
position.column >= range.start.column &&
|
|
85
|
+
position.column <= range.end.column
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
(position.line === range.start.line &&
|
|
91
|
+
position.column >= range.start.column &&
|
|
92
|
+
position.line < range.end.line) ||
|
|
93
|
+
(position.line > range.start.line &&
|
|
94
|
+
position.column <= range.end.column &&
|
|
95
|
+
position.line === range.end.line) ||
|
|
96
|
+
(position.line > range.start.line && position.line < range.end.line)
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* A virtual implementation of IDocumentInfo
|
|
102
|
+
*/
|
|
103
|
+
export class VirtualDocumentInfo implements IDocumentInfo {
|
|
104
|
+
/**
|
|
105
|
+
* Creates an instance of VirtualDocumentInfo.
|
|
106
|
+
* @param document - the virtual document need to
|
|
107
|
+
* be wrapped.
|
|
108
|
+
*/
|
|
109
|
+
constructor(document: VirtualDocument) {
|
|
110
|
+
this._document = document;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Current version of the virtual document.
|
|
115
|
+
*/
|
|
116
|
+
version = 0;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get the text content of the virtual document.
|
|
120
|
+
*/
|
|
121
|
+
get text(): string {
|
|
122
|
+
return this._document.value;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Get the uri of the virtual document, if the document is not available,
|
|
127
|
+
* it returns an empty string, users need to check for the length of returned
|
|
128
|
+
* value before using it.
|
|
129
|
+
*/
|
|
130
|
+
get uri(): string {
|
|
131
|
+
const uris = DocumentConnectionManager.solveUris(this._document, this.languageId);
|
|
132
|
+
if (!uris) {
|
|
133
|
+
return '';
|
|
134
|
+
}
|
|
135
|
+
return uris.document;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Get the language identifier of the document.
|
|
140
|
+
*/
|
|
141
|
+
get languageId(): string {
|
|
142
|
+
return this._document.language;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* The wrapped virtual document.
|
|
147
|
+
*/
|
|
148
|
+
protected _document: VirtualDocument;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export interface IVirtualDocumentOptions {
|
|
152
|
+
/**
|
|
153
|
+
* The language identifier of the document.
|
|
154
|
+
*/
|
|
155
|
+
language: LanguageIdentifier;
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* The foreign code extractor manager token.
|
|
159
|
+
*/
|
|
160
|
+
foreignCodeExtractors: ILSPCodeExtractorsManager;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Path to the document.
|
|
164
|
+
*/
|
|
165
|
+
path: string;
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* File extension of the document.
|
|
169
|
+
*/
|
|
170
|
+
fileExtension: string | undefined;
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Notebooks or any other aggregates of documents are not supported
|
|
174
|
+
* by the LSP specification, and we need to make appropriate
|
|
175
|
+
* adjustments for them, pretending they are simple files
|
|
176
|
+
* so that the LSP servers do not refuse to cooperate.
|
|
177
|
+
*/
|
|
178
|
+
hasLspSupportedFile: boolean;
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Being standalone is relevant to foreign documents
|
|
182
|
+
* and defines whether following chunks of code in the same
|
|
183
|
+
* language should be appended to this document (false, not standalone)
|
|
184
|
+
* or should be considered separate documents (true, standalone)
|
|
185
|
+
*
|
|
186
|
+
*/
|
|
187
|
+
standalone?: boolean;
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Parent of the current virtual document.
|
|
191
|
+
*/
|
|
192
|
+
parent?: VirtualDocument;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
*
|
|
197
|
+
* A notebook can hold one or more virtual documents; there is always one,
|
|
198
|
+
* "root" document, corresponding to the language of the kernel. All other
|
|
199
|
+
* virtual documents are extracted out of the notebook, based on magics,
|
|
200
|
+
* or other syntax constructs, depending on the kernel language.
|
|
201
|
+
*
|
|
202
|
+
* Virtual documents represent the underlying code in a single language,
|
|
203
|
+
* which has been parsed excluding interactive kernel commands (magics)
|
|
204
|
+
* which could be misunderstood by the specific LSP server.
|
|
205
|
+
*
|
|
206
|
+
* VirtualDocument has no awareness of the notebook or editor it lives in,
|
|
207
|
+
* however it is able to transform its content back to the notebook space,
|
|
208
|
+
* as it keeps editor coordinates for each virtual line.
|
|
209
|
+
*
|
|
210
|
+
* The notebook/editor aware transformations are preferred to be placed in
|
|
211
|
+
* VirtualEditor descendants rather than here.
|
|
212
|
+
*
|
|
213
|
+
* No dependency on editor implementation (such as CodeMirrorEditor)
|
|
214
|
+
* is allowed for VirtualEditor.
|
|
215
|
+
*/
|
|
216
|
+
export class VirtualDocument implements Disposable {
|
|
217
|
+
constructor(options: IVirtualDocumentOptions) {
|
|
218
|
+
this.options = options;
|
|
219
|
+
this.path = this.options.path;
|
|
220
|
+
this.fileExtension = options.fileExtension;
|
|
221
|
+
this.hasLspSupportedFile = options.hasLspSupportedFile;
|
|
222
|
+
this.parent = options.parent;
|
|
223
|
+
this.language = options.language;
|
|
224
|
+
|
|
225
|
+
this.virtualLines = new Map();
|
|
226
|
+
this.sourceLines = new Map();
|
|
227
|
+
this.foreignDocuments = new Map();
|
|
228
|
+
this._editorToSourceLine = new Map();
|
|
229
|
+
this._foreignCodeExtractors = options.foreignCodeExtractors;
|
|
230
|
+
this.standalone = options.standalone || false;
|
|
231
|
+
this.instanceId = VirtualDocument.instancesCount;
|
|
232
|
+
VirtualDocument.instancesCount += 1;
|
|
233
|
+
this.unusedStandaloneDocuments = new DefaultMap(() => new Array<VirtualDocument>());
|
|
234
|
+
this._remainingLifetime = 6;
|
|
235
|
+
|
|
236
|
+
this.unusedDocuments = new Set();
|
|
237
|
+
this.documentInfo = new VirtualDocumentInfo(this);
|
|
238
|
+
this.updateManager = new UpdateManager(this);
|
|
239
|
+
this.updateManager.updateBegan(this._updateBeganSlot, this);
|
|
240
|
+
this.updateManager.blockAdded(this._blockAddedSlot, this);
|
|
241
|
+
this.updateManager.updateFinished(this._updateFinishedSlot, this);
|
|
242
|
+
this.clear();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Convert from code editor position into code mirror position.
|
|
247
|
+
*/
|
|
248
|
+
static ceToCm(position: CodeEditorPosition): Position {
|
|
249
|
+
return { line: position.line, ch: position.column };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Number of blank lines appended to the virtual document between
|
|
254
|
+
* each cell.
|
|
255
|
+
*/
|
|
256
|
+
blankLinesBetweenCells = 2;
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Line number of the last line in the real document.
|
|
260
|
+
*/
|
|
261
|
+
lastSourceLine: number;
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Line number of the last line in the virtual document.
|
|
265
|
+
*/
|
|
266
|
+
lastVirtualLine: number;
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* the remote document uri, version and other server-related info
|
|
270
|
+
*/
|
|
271
|
+
documentInfo: IDocumentInfo;
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Parent of the current virtual document.
|
|
275
|
+
*/
|
|
276
|
+
parent?: VirtualDocument | null;
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* The language identifier of the document.
|
|
280
|
+
*/
|
|
281
|
+
readonly language: string;
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Being standalone is relevant to foreign documents
|
|
285
|
+
* and defines whether following chunks of code in the same
|
|
286
|
+
* language should be appended to this document (false, not standalone)
|
|
287
|
+
* or should be considered separate documents (true, standalone)
|
|
288
|
+
*/
|
|
289
|
+
readonly standalone: boolean;
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Path to the document.
|
|
293
|
+
*/
|
|
294
|
+
readonly path: string;
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* File extension of the document.
|
|
298
|
+
*/
|
|
299
|
+
readonly fileExtension: string | undefined;
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Notebooks or any other aggregates of documents are not supported
|
|
303
|
+
* by the LSP specification, and we need to make appropriate
|
|
304
|
+
* adjustments for them, pretending they are simple files
|
|
305
|
+
* so that the LSP servers do not refuse to cooperate.
|
|
306
|
+
*/
|
|
307
|
+
readonly hasLspSupportedFile: boolean;
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Map holding the children `VirtualDocument` .
|
|
311
|
+
*/
|
|
312
|
+
readonly foreignDocuments: Map<VirtualDocument.virtualId, VirtualDocument>;
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* The update manager object.
|
|
316
|
+
*/
|
|
317
|
+
readonly updateManager: UpdateManager;
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Unique id of the virtual document.
|
|
321
|
+
*/
|
|
322
|
+
readonly instanceId: number;
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Test whether the document is disposed.
|
|
326
|
+
*/
|
|
327
|
+
get isDisposed(): boolean {
|
|
328
|
+
return this._isDisposed;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Signal emitted when the foreign document is closed
|
|
333
|
+
*/
|
|
334
|
+
get foreignDocumentClosed(): Event<Document.IForeignContext> {
|
|
335
|
+
return this._foreignDocumentClosed.event;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Signal emitted when the foreign document is opened
|
|
340
|
+
*/
|
|
341
|
+
get foreignDocumentOpened(): Event<Document.IForeignContext> {
|
|
342
|
+
return this._foreignDocumentOpened.event;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Signal emitted when the foreign document is changed
|
|
347
|
+
*/
|
|
348
|
+
get changed(): Event<VirtualDocument> {
|
|
349
|
+
return this._changed.event;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Id of the virtual document.
|
|
354
|
+
*/
|
|
355
|
+
get virtualId(): VirtualDocument.virtualId {
|
|
356
|
+
// for easier debugging, the language information is included in the ID:
|
|
357
|
+
return this.standalone
|
|
358
|
+
? this.instanceId + '(' + this.language + ')'
|
|
359
|
+
: this.language;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Return the ancestry to this document.
|
|
364
|
+
*/
|
|
365
|
+
get ancestry(): VirtualDocument[] {
|
|
366
|
+
if (!this.parent) {
|
|
367
|
+
return [this];
|
|
368
|
+
}
|
|
369
|
+
return this.parent.ancestry.concat([this]);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Return the id path to the virtual document.
|
|
374
|
+
*/
|
|
375
|
+
get idPath(): VirtualDocument.idPath {
|
|
376
|
+
if (!this.parent) {
|
|
377
|
+
return this.virtualId;
|
|
378
|
+
}
|
|
379
|
+
return this.parent.idPath + '-' + this.virtualId;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Get the uri of the virtual document.
|
|
384
|
+
*/
|
|
385
|
+
get uri(): VirtualDocument.uri {
|
|
386
|
+
const encodedPath = encodeURI(this.path);
|
|
387
|
+
if (!this.parent) {
|
|
388
|
+
return encodedPath;
|
|
389
|
+
}
|
|
390
|
+
return encodedPath + '.' + this.idPath + '.' + this.fileExtension;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Get the text value of the document
|
|
395
|
+
*/
|
|
396
|
+
get value(): string {
|
|
397
|
+
const linesPadding = '\n'.repeat(this.blankLinesBetweenCells);
|
|
398
|
+
return this.lineBlocks.join(linesPadding);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Get the last line in the virtual document
|
|
403
|
+
*/
|
|
404
|
+
get lastLine(): string {
|
|
405
|
+
const linesInLastBlock = this.lineBlocks[this.lineBlocks.length - 1].split('\n');
|
|
406
|
+
return linesInLastBlock[linesInLastBlock.length - 1];
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Get the root document of current virtual document.
|
|
411
|
+
*/
|
|
412
|
+
get root(): VirtualDocument {
|
|
413
|
+
return this.parent ? this.parent.root : this;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Dispose the virtual document.
|
|
418
|
+
*/
|
|
419
|
+
dispose(): void {
|
|
420
|
+
if (this._isDisposed) {
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
this._isDisposed = true;
|
|
424
|
+
|
|
425
|
+
this.parent = null;
|
|
426
|
+
|
|
427
|
+
this.closeAllForeignDocuments();
|
|
428
|
+
|
|
429
|
+
this.updateManager.dispose();
|
|
430
|
+
// clear all the maps
|
|
431
|
+
|
|
432
|
+
this.foreignDocuments.clear();
|
|
433
|
+
this.sourceLines.clear();
|
|
434
|
+
this.unusedDocuments.clear();
|
|
435
|
+
this.unusedStandaloneDocuments.clear();
|
|
436
|
+
this.virtualLines.clear();
|
|
437
|
+
|
|
438
|
+
// just to be sure - if anything is accessed after disposal (it should not) we
|
|
439
|
+
// will get altered by errors in the console AND this will limit memory leaks
|
|
440
|
+
|
|
441
|
+
this.documentInfo = null as any;
|
|
442
|
+
this.lineBlocks = null as any;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Clear the virtual document and all related stuffs
|
|
447
|
+
*/
|
|
448
|
+
clear(): void {
|
|
449
|
+
for (const document of this.foreignDocuments.values()) {
|
|
450
|
+
document.clear();
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// TODO - deep clear (assure that there is no memory leak)
|
|
454
|
+
this.unusedStandaloneDocuments.clear();
|
|
455
|
+
|
|
456
|
+
this.unusedDocuments = new Set();
|
|
457
|
+
this.virtualLines.clear();
|
|
458
|
+
this.sourceLines.clear();
|
|
459
|
+
this.lastVirtualLine = 0;
|
|
460
|
+
this.lastSourceLine = 0;
|
|
461
|
+
this.lineBlocks = [];
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Get the virtual document from the cursor position of the source
|
|
466
|
+
* document
|
|
467
|
+
* @param position - position in source document
|
|
468
|
+
*/
|
|
469
|
+
documentAtSourcePosition(position: ISourcePosition): VirtualDocument {
|
|
470
|
+
const sourceLine = this.sourceLines.get(position.line);
|
|
471
|
+
|
|
472
|
+
if (!sourceLine) {
|
|
473
|
+
return this;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const sourcePositionCe: CodeEditorPosition = {
|
|
477
|
+
line: sourceLine.editorLine,
|
|
478
|
+
column: position.ch,
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
for (const [
|
|
482
|
+
range,
|
|
483
|
+
{ virtualDocument: document },
|
|
484
|
+
] of sourceLine.foreignDocumentsMap) {
|
|
485
|
+
if (isWithinRange(sourcePositionCe, range)) {
|
|
486
|
+
const sourcePositionCm = {
|
|
487
|
+
line: sourcePositionCe.line - range.start.line,
|
|
488
|
+
ch: sourcePositionCe.column - range.start.column,
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
return document.documentAtSourcePosition(sourcePositionCm as ISourcePosition);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return this;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Detect if the input source position is belong to the current
|
|
500
|
+
* virtual document.
|
|
501
|
+
*
|
|
502
|
+
* @param sourcePosition - position in the source document
|
|
503
|
+
*/
|
|
504
|
+
isWithinForeign(sourcePosition: ISourcePosition): boolean {
|
|
505
|
+
const sourceLine = this.sourceLines.get(sourcePosition.line)!;
|
|
506
|
+
|
|
507
|
+
const sourcePositionCe: CodeEditorPosition = {
|
|
508
|
+
line: sourceLine.editorLine,
|
|
509
|
+
column: sourcePosition.ch,
|
|
510
|
+
};
|
|
511
|
+
for (const [range] of sourceLine.foreignDocumentsMap) {
|
|
512
|
+
if (isWithinRange(sourcePositionCe, range)) {
|
|
513
|
+
return true;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
return false;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Compute the position in root document from the position of
|
|
521
|
+
* a child editor.
|
|
522
|
+
*
|
|
523
|
+
* @param editor - the active editor.
|
|
524
|
+
* @param position - position in the active editor.
|
|
525
|
+
*/
|
|
526
|
+
transformFromEditorToRoot(
|
|
527
|
+
editor: Document.IEditor,
|
|
528
|
+
position: IEditorPosition,
|
|
529
|
+
): IRootPosition | null {
|
|
530
|
+
if (!this._editorToSourceLine.has(editor)) {
|
|
531
|
+
console.warn('Editor not found in _editorToSourceLine map');
|
|
532
|
+
return null;
|
|
533
|
+
}
|
|
534
|
+
const shift = this._editorToSourceLine.get(editor)!;
|
|
535
|
+
return {
|
|
536
|
+
...(position as Position),
|
|
537
|
+
line: position.line + shift,
|
|
538
|
+
} as IRootPosition;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Compute the position in virtual document from the position of
|
|
543
|
+
* a child editor.
|
|
544
|
+
*
|
|
545
|
+
* @param editor - the active editor.
|
|
546
|
+
* @param position - position in the active editor.
|
|
547
|
+
*/
|
|
548
|
+
transformEditorToVirtual(
|
|
549
|
+
editor: Document.IEditor,
|
|
550
|
+
position: IEditorPosition,
|
|
551
|
+
): IVirtualPosition | null {
|
|
552
|
+
const rootPosition = this.transformFromEditorToRoot(editor, position);
|
|
553
|
+
if (!rootPosition) {
|
|
554
|
+
return null;
|
|
555
|
+
}
|
|
556
|
+
return this.virtualPositionAtDocument(rootPosition);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Compute the position in the virtual document from the position
|
|
561
|
+
* in the source document.
|
|
562
|
+
*
|
|
563
|
+
* @param sourcePosition - position in source document
|
|
564
|
+
*/
|
|
565
|
+
virtualPositionAtDocument(sourcePosition: ISourcePosition): IVirtualPosition {
|
|
566
|
+
const sourceLine = this.sourceLines.get(sourcePosition.line);
|
|
567
|
+
if (!sourceLine) {
|
|
568
|
+
throw new Error('Source line not mapped to virtual position');
|
|
569
|
+
}
|
|
570
|
+
const virtualLine = sourceLine.virtualLine;
|
|
571
|
+
|
|
572
|
+
// position inside the cell (block)
|
|
573
|
+
const sourcePositionCe: CodeEditorPosition = {
|
|
574
|
+
line: sourceLine.editorLine,
|
|
575
|
+
column: sourcePosition.ch,
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
for (const [range, content] of sourceLine.foreignDocumentsMap) {
|
|
579
|
+
// eslint-disable-next-line @typescript-eslint/no-shadow
|
|
580
|
+
const { virtualLine, virtualDocument: document } = content;
|
|
581
|
+
if (isWithinRange(sourcePositionCe, range)) {
|
|
582
|
+
// position inside the foreign document block
|
|
583
|
+
const sourcePositionCm = {
|
|
584
|
+
line: sourcePositionCe.line - range.start.line,
|
|
585
|
+
ch: sourcePositionCe.column - range.start.column,
|
|
586
|
+
};
|
|
587
|
+
if (document.isWithinForeign(sourcePositionCm as ISourcePosition)) {
|
|
588
|
+
return this.virtualPositionAtDocument(sourcePositionCm as ISourcePosition);
|
|
589
|
+
} else {
|
|
590
|
+
// where in this block in the entire foreign document?
|
|
591
|
+
sourcePositionCm.line += virtualLine;
|
|
592
|
+
return sourcePositionCm as IVirtualPosition;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
return {
|
|
598
|
+
ch: sourcePosition.ch,
|
|
599
|
+
line: virtualLine,
|
|
600
|
+
} as IVirtualPosition;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Append a code block to the end of the virtual document.
|
|
605
|
+
*
|
|
606
|
+
* @param block - block to be appended
|
|
607
|
+
* @param editorShift - position shift in source
|
|
608
|
+
* document
|
|
609
|
+
* @param [virtualShift] - position shift in
|
|
610
|
+
* virtual document.
|
|
611
|
+
*/
|
|
612
|
+
appendCodeBlock(
|
|
613
|
+
block: Document.ICodeBlockOptions,
|
|
614
|
+
editorShift: CodeEditorPosition = { line: 0, column: 0 },
|
|
615
|
+
virtualShift?: CodeEditorPosition,
|
|
616
|
+
): void {
|
|
617
|
+
const cellCode = block.value;
|
|
618
|
+
const ceEditor = block.ceEditor;
|
|
619
|
+
|
|
620
|
+
if (this.isDisposed) {
|
|
621
|
+
console.warn('Cannot append code block: document disposed');
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
const sourceCellLines = cellCode.split('\n');
|
|
625
|
+
const { lines, foreignDocumentsMap } = this.prepareCodeBlock(block, editorShift);
|
|
626
|
+
|
|
627
|
+
for (let i = 0; i < lines.length; i++) {
|
|
628
|
+
this.virtualLines.set(this.lastVirtualLine + i, {
|
|
629
|
+
skipInspect: [],
|
|
630
|
+
editor: ceEditor,
|
|
631
|
+
// TODO this is incorrect, wont work if something was extracted
|
|
632
|
+
sourceLine: this.lastSourceLine + i,
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
for (let i = 0; i < sourceCellLines.length; i++) {
|
|
636
|
+
this.sourceLines.set(this.lastSourceLine + i, {
|
|
637
|
+
editorLine: i,
|
|
638
|
+
editorShift: {
|
|
639
|
+
line: editorShift.line - (virtualShift?.line || 0),
|
|
640
|
+
column: i === 0 ? editorShift.column - (virtualShift?.column || 0) : 0,
|
|
641
|
+
},
|
|
642
|
+
// TODO: move those to a new abstraction layer (DocumentBlock class)
|
|
643
|
+
editor: ceEditor,
|
|
644
|
+
foreignDocumentsMap,
|
|
645
|
+
// TODO this is incorrect, wont work if something was extracted
|
|
646
|
+
virtualLine: this.lastVirtualLine + i,
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
this.lastVirtualLine += lines.length;
|
|
651
|
+
|
|
652
|
+
// one empty line is necessary to separate code blocks, next 'n' lines are to silence linters;
|
|
653
|
+
// the final cell does not get the additional lines (thanks to the use of join, see below)
|
|
654
|
+
|
|
655
|
+
this.lineBlocks.push(lines.join('\n') + '\n');
|
|
656
|
+
|
|
657
|
+
// adding the virtual lines for the blank lines
|
|
658
|
+
for (let i = 0; i < this.blankLinesBetweenCells; i++) {
|
|
659
|
+
this.virtualLines.set(this.lastVirtualLine + i, {
|
|
660
|
+
skipInspect: [this.idPath],
|
|
661
|
+
editor: ceEditor,
|
|
662
|
+
sourceLine: null,
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
this.lastVirtualLine += this.blankLinesBetweenCells;
|
|
667
|
+
this.lastSourceLine += sourceCellLines.length;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Extract a code block into list of string in supported language and
|
|
672
|
+
* a map of foreign document if any.
|
|
673
|
+
* @param block - block to be appended
|
|
674
|
+
* @param editorShift - position shift in source document
|
|
675
|
+
*/
|
|
676
|
+
prepareCodeBlock(
|
|
677
|
+
block: Document.ICodeBlockOptions,
|
|
678
|
+
editorShift: CodeEditorPosition = { line: 0, column: 0 },
|
|
679
|
+
): {
|
|
680
|
+
lines: string[];
|
|
681
|
+
foreignDocumentsMap: Map<IRange, Document.IVirtualDocumentBlock>;
|
|
682
|
+
} {
|
|
683
|
+
const { cellCodeKept, foreignDocumentsMap } = this.extractForeignCode(
|
|
684
|
+
block,
|
|
685
|
+
editorShift,
|
|
686
|
+
);
|
|
687
|
+
const lines = cellCodeKept.split('\n');
|
|
688
|
+
return { lines, foreignDocumentsMap };
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Extract the foreign code from input block by using the registered
|
|
693
|
+
* extractors.
|
|
694
|
+
* @param block - block to be appended
|
|
695
|
+
* @param editorShift - position shift in source document
|
|
696
|
+
*/
|
|
697
|
+
extractForeignCode(
|
|
698
|
+
block: Document.ICodeBlockOptions,
|
|
699
|
+
editorShift: CodeEditorPosition,
|
|
700
|
+
): {
|
|
701
|
+
cellCodeKept: string;
|
|
702
|
+
foreignDocumentsMap: Map<IRange, Document.IVirtualDocumentBlock>;
|
|
703
|
+
} {
|
|
704
|
+
const foreignDocumentsMap = new Map<IRange, Document.IVirtualDocumentBlock>();
|
|
705
|
+
|
|
706
|
+
let cellCode = block.value;
|
|
707
|
+
const extractorsForAnyLang = this._foreignCodeExtractors.getExtractors(
|
|
708
|
+
block.type,
|
|
709
|
+
null,
|
|
710
|
+
);
|
|
711
|
+
const extractorsForCurrentLang = this._foreignCodeExtractors.getExtractors(
|
|
712
|
+
block.type,
|
|
713
|
+
this.language,
|
|
714
|
+
);
|
|
715
|
+
|
|
716
|
+
for (const extractor of [...extractorsForAnyLang, ...extractorsForCurrentLang]) {
|
|
717
|
+
if (!extractor.hasForeignCode(cellCode, block.type)) {
|
|
718
|
+
continue;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
const results = extractor.extractForeignCode(cellCode);
|
|
722
|
+
|
|
723
|
+
let keptCellCode = '';
|
|
724
|
+
|
|
725
|
+
for (const result of results) {
|
|
726
|
+
if (result.foreignCode !== null) {
|
|
727
|
+
// result.range should only be null if result.foregin_code is null
|
|
728
|
+
if (result.range === null) {
|
|
729
|
+
console.warn(
|
|
730
|
+
'Failure in foreign code extraction: `range` is null but `foreign_code` is not!',
|
|
731
|
+
);
|
|
732
|
+
continue;
|
|
733
|
+
}
|
|
734
|
+
const foreignDocument = this.chooseForeignDocument(extractor);
|
|
735
|
+
foreignDocumentsMap.set(result.range, {
|
|
736
|
+
virtualLine: foreignDocument.lastVirtualLine,
|
|
737
|
+
virtualDocument: foreignDocument,
|
|
738
|
+
editor: block.ceEditor,
|
|
739
|
+
});
|
|
740
|
+
const foreignShift = {
|
|
741
|
+
line: editorShift.line + result.range.start.line,
|
|
742
|
+
column: editorShift.column + result.range.start.column,
|
|
743
|
+
};
|
|
744
|
+
foreignDocument.appendCodeBlock(
|
|
745
|
+
{
|
|
746
|
+
value: result.foreignCode,
|
|
747
|
+
ceEditor: block.ceEditor,
|
|
748
|
+
type: 'code',
|
|
749
|
+
},
|
|
750
|
+
foreignShift,
|
|
751
|
+
result.virtualShift!,
|
|
752
|
+
);
|
|
753
|
+
}
|
|
754
|
+
if (result.hostCode !== null) {
|
|
755
|
+
keptCellCode += result.hostCode;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
// not breaking - many extractors are allowed to process the code, one after each other
|
|
759
|
+
// (think JS and CSS in HTML, or %R inside of %%timeit).
|
|
760
|
+
|
|
761
|
+
cellCode = keptCellCode;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
return { cellCodeKept: cellCode, foreignDocumentsMap };
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* Close a foreign document and disconnect all associated signals
|
|
769
|
+
*/
|
|
770
|
+
closeForeign(document: VirtualDocument): void {
|
|
771
|
+
this._foreignDocumentClosed.fire({
|
|
772
|
+
foreignDocument: document,
|
|
773
|
+
parentHost: this,
|
|
774
|
+
});
|
|
775
|
+
// remove it from foreign documents list
|
|
776
|
+
this.foreignDocuments.delete(document.virtualId);
|
|
777
|
+
// and delete the documents within it
|
|
778
|
+
document.closeAllForeignDocuments();
|
|
779
|
+
|
|
780
|
+
// document.foreignDocumentClosed.disconnect(this.forwardClosedSignal, this);
|
|
781
|
+
// document.foreignDocumentOpened.disconnect(this.forwardOpenedSignal, this);
|
|
782
|
+
document.dispose();
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* Close all foreign documents.
|
|
787
|
+
*/
|
|
788
|
+
closeAllForeignDocuments(): void {
|
|
789
|
+
for (const document of this.foreignDocuments.values()) {
|
|
790
|
+
this.closeForeign(document);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* Close all expired documents.
|
|
796
|
+
*/
|
|
797
|
+
closeExpiredDocuments(): void {
|
|
798
|
+
for (const document of this.unusedDocuments.values()) {
|
|
799
|
+
document.remainingLifetime -= 1;
|
|
800
|
+
if (document.remainingLifetime <= 0) {
|
|
801
|
+
document.dispose();
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/**
|
|
807
|
+
* Transform the position of the source to the editor
|
|
808
|
+
* position.
|
|
809
|
+
*
|
|
810
|
+
* @param pos - position in the source document
|
|
811
|
+
* @return position in the editor.
|
|
812
|
+
*/
|
|
813
|
+
transformSourceToEditor(pos: ISourcePosition): IEditorPosition {
|
|
814
|
+
const sourceLine = this.sourceLines.get(pos.line)!;
|
|
815
|
+
const editorLine = sourceLine.editorLine;
|
|
816
|
+
const editorShift = sourceLine.editorShift;
|
|
817
|
+
return {
|
|
818
|
+
// only shift column in the line beginning the virtual document (first list of the editor in cell magics, but might be any line of editor in line magics!)
|
|
819
|
+
ch: pos.ch + (editorLine === 0 ? editorShift.column : 0),
|
|
820
|
+
line: editorLine + editorShift.line,
|
|
821
|
+
// TODO or:
|
|
822
|
+
// line: pos.line + editor_shift.line - this.first_line_of_the_block(editor)
|
|
823
|
+
} as IEditorPosition;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/**
|
|
827
|
+
* Transform the position in the virtual document to the
|
|
828
|
+
* editor position.
|
|
829
|
+
* Can be null because some lines are added as padding/anchors
|
|
830
|
+
* to the virtual document and those do not exist in the source document
|
|
831
|
+
* and thus they are absent in the editor.
|
|
832
|
+
*/
|
|
833
|
+
transformVirtualToEditor(virtualPosition: IVirtualPosition): IEditorPosition | null {
|
|
834
|
+
const sourcePosition = this.transformVirtualToSource(virtualPosition);
|
|
835
|
+
if (!sourcePosition) {
|
|
836
|
+
return null;
|
|
837
|
+
}
|
|
838
|
+
return this.transformSourceToEditor(sourcePosition);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* Transform the position in the virtual document to the source.
|
|
843
|
+
* Can be null because some lines are added as padding/anchors
|
|
844
|
+
* to the virtual document and those do not exist in the source document.
|
|
845
|
+
*/
|
|
846
|
+
transformVirtualToSource(position: IVirtualPosition): ISourcePosition | null {
|
|
847
|
+
const line = this.virtualLines.get(position.line)!.sourceLine;
|
|
848
|
+
if (line === null) {
|
|
849
|
+
return null;
|
|
850
|
+
}
|
|
851
|
+
return {
|
|
852
|
+
ch: position.ch,
|
|
853
|
+
line: line,
|
|
854
|
+
} as ISourcePosition;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* Get the corresponding editor of the virtual line.
|
|
859
|
+
*/
|
|
860
|
+
getEditorAtVirtualLine(pos: IVirtualPosition): Document.IEditor {
|
|
861
|
+
let line = pos.line;
|
|
862
|
+
// tolerate overshot by one (the hanging blank line at the end)
|
|
863
|
+
if (!this.virtualLines.has(line)) {
|
|
864
|
+
line -= 1;
|
|
865
|
+
}
|
|
866
|
+
return this.virtualLines.get(line)!.editor;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
/**
|
|
870
|
+
* Get the corresponding editor of the source line
|
|
871
|
+
*/
|
|
872
|
+
getEditorAtSourceLine(pos: ISourcePosition): Document.IEditor {
|
|
873
|
+
return this.sourceLines.get(pos.line)!.editor;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
/**
|
|
877
|
+
* Recursively emits changed signal from the document or any descendant foreign document.
|
|
878
|
+
*/
|
|
879
|
+
maybeEmitChanged(): void {
|
|
880
|
+
if (this.value !== this.previousValue) {
|
|
881
|
+
this._changed.fire(this);
|
|
882
|
+
}
|
|
883
|
+
this.previousValue = this.value;
|
|
884
|
+
for (const document of this.foreignDocuments.values()) {
|
|
885
|
+
document.maybeEmitChanged();
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/**
|
|
890
|
+
* When this counter goes down to 0, the document will be destroyed and the associated connection will be closed;
|
|
891
|
+
* This is meant to reduce the number of open connections when a a foreign code snippet was removed from the document.
|
|
892
|
+
*
|
|
893
|
+
* Note: top level virtual documents are currently immortal (unless killed by other means); it might be worth
|
|
894
|
+
* implementing culling of unused documents, but if and only if JupyterLab will also implement culling of
|
|
895
|
+
* idle kernels - otherwise the user experience could be a bit inconsistent, and we would need to invent our own rules.
|
|
896
|
+
*/
|
|
897
|
+
protected get remainingLifetime(): number {
|
|
898
|
+
if (!this.parent) {
|
|
899
|
+
return Infinity;
|
|
900
|
+
}
|
|
901
|
+
return this._remainingLifetime;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
protected set remainingLifetime(value: number) {
|
|
905
|
+
if (this.parent) {
|
|
906
|
+
this._remainingLifetime = value;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/**
|
|
911
|
+
* Virtual lines keep all the lines present in the document AND extracted to the foreign document.
|
|
912
|
+
*/
|
|
913
|
+
protected virtualLines: Map<number, IVirtualLine>;
|
|
914
|
+
protected sourceLines: Map<number, ISourceLine>;
|
|
915
|
+
protected lineBlocks: string[];
|
|
916
|
+
|
|
917
|
+
protected unusedDocuments: Set<VirtualDocument>;
|
|
918
|
+
protected unusedStandaloneDocuments: DefaultMap<language, VirtualDocument[]>;
|
|
919
|
+
|
|
920
|
+
protected _isDisposed = false;
|
|
921
|
+
protected _remainingLifetime: number;
|
|
922
|
+
protected _editorToSourceLine: Map<Document.IEditor, number>;
|
|
923
|
+
protected _editorToSourceLineNew: Map<Document.IEditor, number>;
|
|
924
|
+
protected _foreignCodeExtractors: ILSPCodeExtractorsManager;
|
|
925
|
+
protected previousValue: string;
|
|
926
|
+
protected static instancesCount = 0;
|
|
927
|
+
protected readonly options: IVirtualDocumentOptions;
|
|
928
|
+
|
|
929
|
+
/**
|
|
930
|
+
* Get the foreign document that can be opened with the input extractor.
|
|
931
|
+
*/
|
|
932
|
+
protected chooseForeignDocument(extractor: IForeignCodeExtractor): VirtualDocument {
|
|
933
|
+
let foreignDocument: VirtualDocument;
|
|
934
|
+
// if not standalone, try to append to existing document
|
|
935
|
+
const foreignExists = this.foreignDocuments.has(extractor.language);
|
|
936
|
+
if (!extractor.standalone && foreignExists) {
|
|
937
|
+
foreignDocument = this.foreignDocuments.get(extractor.language)!;
|
|
938
|
+
} else {
|
|
939
|
+
// if (previous document does not exists) or (extractor produces standalone documents
|
|
940
|
+
// and no old standalone document could be reused): create a new document
|
|
941
|
+
foreignDocument = this.openForeign(
|
|
942
|
+
extractor.language,
|
|
943
|
+
extractor.standalone,
|
|
944
|
+
extractor.fileExtension,
|
|
945
|
+
);
|
|
946
|
+
}
|
|
947
|
+
return foreignDocument;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
/**
|
|
951
|
+
* Create a foreign document from input language and file extension.
|
|
952
|
+
*
|
|
953
|
+
* @param language - the required language
|
|
954
|
+
* @param standalone - the document type is supported natively by LSP?
|
|
955
|
+
* @param fileExtension - File extension.
|
|
956
|
+
*/
|
|
957
|
+
protected openForeign(
|
|
958
|
+
language: language,
|
|
959
|
+
standalone: boolean,
|
|
960
|
+
fileExtension: string,
|
|
961
|
+
): VirtualDocument {
|
|
962
|
+
const document = new VirtualDocument({
|
|
963
|
+
...this.options,
|
|
964
|
+
parent: this,
|
|
965
|
+
standalone: standalone,
|
|
966
|
+
fileExtension: fileExtension,
|
|
967
|
+
language: language,
|
|
968
|
+
});
|
|
969
|
+
const context: Document.IForeignContext = {
|
|
970
|
+
foreignDocument: document,
|
|
971
|
+
parentHost: this,
|
|
972
|
+
};
|
|
973
|
+
this._foreignDocumentOpened.fire(context);
|
|
974
|
+
// pass through any future signals
|
|
975
|
+
document.foreignDocumentClosed(() => this.forwardClosedSignal(context));
|
|
976
|
+
document.foreignDocumentOpened(() => this.forwardOpenedSignal(context));
|
|
977
|
+
|
|
978
|
+
this.foreignDocuments.set(document.virtualId, document);
|
|
979
|
+
|
|
980
|
+
return document;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
/**
|
|
984
|
+
* Forward the closed signal from the foreign document to the host document's
|
|
985
|
+
* signal
|
|
986
|
+
*/
|
|
987
|
+
protected forwardClosedSignal(context: Document.IForeignContext) {
|
|
988
|
+
this._foreignDocumentClosed.fire(context);
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
/**
|
|
992
|
+
* Forward the opened signal from the foreign document to the host document's
|
|
993
|
+
* signal
|
|
994
|
+
*/
|
|
995
|
+
protected forwardOpenedSignal(context: Document.IForeignContext) {
|
|
996
|
+
this._foreignDocumentOpened.fire(context);
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
/**
|
|
1000
|
+
* Slot of the `updateBegan` signal.
|
|
1001
|
+
*/
|
|
1002
|
+
protected _updateBeganSlot(): void {
|
|
1003
|
+
this._editorToSourceLineNew = new Map();
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
/**
|
|
1007
|
+
* Slot of the `blockAdded` signal.
|
|
1008
|
+
*/
|
|
1009
|
+
protected _blockAddedSlot(blockData: IBlockAddedInfo): void {
|
|
1010
|
+
this._editorToSourceLineNew.set(
|
|
1011
|
+
blockData.block.ceEditor,
|
|
1012
|
+
blockData.virtualDocument.lastSourceLine,
|
|
1013
|
+
);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
/**
|
|
1017
|
+
* Slot of the `updateFinished` signal.
|
|
1018
|
+
*/
|
|
1019
|
+
protected _updateFinishedSlot(): void {
|
|
1020
|
+
this._editorToSourceLine = this._editorToSourceLineNew;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
protected _foreignDocumentClosed = new Emitter<Document.IForeignContext>();
|
|
1024
|
+
protected _foreignDocumentOpened = new Emitter<Document.IForeignContext>();
|
|
1025
|
+
protected _changed = new Emitter<VirtualDocument>();
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
export namespace VirtualDocument {
|
|
1029
|
+
/**
|
|
1030
|
+
* Identifier composed of `virtual_id`s of a nested structure of documents,
|
|
1031
|
+
* used to aide assignment of the connection to the virtual document
|
|
1032
|
+
* handling specific, nested language usage; it will be appended to the file name
|
|
1033
|
+
* when creating a connection.
|
|
1034
|
+
*/
|
|
1035
|
+
export type idPath = string;
|
|
1036
|
+
/**
|
|
1037
|
+
* Instance identifier for standalone documents (snippets), or language identifier
|
|
1038
|
+
* for documents which should be interpreted as one when stretched across cells.
|
|
1039
|
+
*/
|
|
1040
|
+
export type virtualId = string;
|
|
1041
|
+
/**
|
|
1042
|
+
* Identifier composed of the file path and id_path.
|
|
1043
|
+
*/
|
|
1044
|
+
export type uri = string;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
/**
|
|
1048
|
+
* Create foreign documents if available from input virtual documents.
|
|
1049
|
+
* @param virtualDocument - the virtual document to be collected
|
|
1050
|
+
* @return - Set of generated foreign documents
|
|
1051
|
+
*/
|
|
1052
|
+
export function collectDocuments(
|
|
1053
|
+
virtualDocument: VirtualDocument,
|
|
1054
|
+
): Set<VirtualDocument> {
|
|
1055
|
+
const collected = new Set<VirtualDocument>();
|
|
1056
|
+
collected.add(virtualDocument);
|
|
1057
|
+
for (const foreign of virtualDocument.foreignDocuments.values()) {
|
|
1058
|
+
const foreignLanguages = collectDocuments(foreign);
|
|
1059
|
+
foreignLanguages.forEach(collected.add, collected);
|
|
1060
|
+
}
|
|
1061
|
+
return collected;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
export interface IBlockAddedInfo {
|
|
1065
|
+
/**
|
|
1066
|
+
* The virtual document.
|
|
1067
|
+
*/
|
|
1068
|
+
virtualDocument: VirtualDocument;
|
|
1069
|
+
|
|
1070
|
+
/**
|
|
1071
|
+
* Option of the code block.
|
|
1072
|
+
*/
|
|
1073
|
+
block: Document.ICodeBlockOptions;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
export class UpdateManager implements Disposable {
|
|
1077
|
+
// eslint-disable-next-line @typescript-eslint/parameter-properties, @typescript-eslint/no-parameter-properties
|
|
1078
|
+
constructor(protected virtualDocument: VirtualDocument) {
|
|
1079
|
+
this._blockAdded = new Emitter<IBlockAddedInfo>();
|
|
1080
|
+
this._documentUpdated = new Emitter<VirtualDocument>();
|
|
1081
|
+
this._updateBegan = new Emitter<Document.ICodeBlockOptions[]>();
|
|
1082
|
+
this._updateFinished = new Emitter<Document.ICodeBlockOptions[]>();
|
|
1083
|
+
this.documentUpdated(this._onUpdated);
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
/**
|
|
1087
|
+
* Promise resolved when the updating process finishes.
|
|
1088
|
+
*/
|
|
1089
|
+
get updateDone(): Promise<void> {
|
|
1090
|
+
return this._updateDone;
|
|
1091
|
+
}
|
|
1092
|
+
/**
|
|
1093
|
+
* Test whether the document is disposed.
|
|
1094
|
+
*/
|
|
1095
|
+
get isDisposed(): boolean {
|
|
1096
|
+
return this._isDisposed;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
/**
|
|
1100
|
+
* Signal emitted when a code block is added to the document.
|
|
1101
|
+
*/
|
|
1102
|
+
get blockAdded(): Event<IBlockAddedInfo> {
|
|
1103
|
+
return this._blockAdded.event;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
/**
|
|
1107
|
+
* Signal emitted by the editor that triggered the update,
|
|
1108
|
+
* providing the root document of the updated documents.
|
|
1109
|
+
*/
|
|
1110
|
+
get documentUpdated(): Event<VirtualDocument> {
|
|
1111
|
+
return this._documentUpdated.event;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
/**
|
|
1115
|
+
* Signal emitted when the update is started
|
|
1116
|
+
*/
|
|
1117
|
+
get updateBegan(): Event<Document.ICodeBlockOptions[]> {
|
|
1118
|
+
return this._updateBegan.event;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
/**
|
|
1122
|
+
* Signal emitted when the update is finished
|
|
1123
|
+
*/
|
|
1124
|
+
get updateFinished(): Event<Document.ICodeBlockOptions[]> {
|
|
1125
|
+
return this._updateFinished.event;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
/**
|
|
1129
|
+
* Dispose the class
|
|
1130
|
+
*/
|
|
1131
|
+
dispose(): void {
|
|
1132
|
+
if (this._isDisposed) {
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
this._isDisposed = true;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
/**
|
|
1139
|
+
* Execute provided callback within an update-locked context, which guarantees that:
|
|
1140
|
+
* - the previous updates must have finished before the callback call, and
|
|
1141
|
+
* - no update will happen when executing the callback
|
|
1142
|
+
* @param fn - the callback to execute in update lock
|
|
1143
|
+
*/
|
|
1144
|
+
async withUpdateLock(fn: () => void): Promise<void> {
|
|
1145
|
+
await untilReady(() => this._canUpdate(), 12, 10).then(() => {
|
|
1146
|
+
try {
|
|
1147
|
+
this._updateLock = true;
|
|
1148
|
+
fn();
|
|
1149
|
+
} catch (ex) {
|
|
1150
|
+
console.error(ex);
|
|
1151
|
+
} finally {
|
|
1152
|
+
this._updateLock = false;
|
|
1153
|
+
}
|
|
1154
|
+
return;
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
/**
|
|
1159
|
+
* Update all the virtual documents, emit documents updated with root document if succeeded,
|
|
1160
|
+
* and resolve a void promise. The promise does not contain the text value of the root document,
|
|
1161
|
+
* as to avoid an easy trap of ignoring the changes in the virtual documents.
|
|
1162
|
+
*/
|
|
1163
|
+
async updateDocuments(blocks: Document.ICodeBlockOptions[]): Promise<void> {
|
|
1164
|
+
const update = new Promise<void>((resolve, reject) => {
|
|
1165
|
+
// defer the update by up to 50 ms (10 retrials * 5 ms break),
|
|
1166
|
+
// awaiting for the previous update to complete.
|
|
1167
|
+
untilReady(() => this._canUpdate(), 10, 5)
|
|
1168
|
+
.then(() => {
|
|
1169
|
+
if (this.isDisposed || !this.virtualDocument) {
|
|
1170
|
+
resolve();
|
|
1171
|
+
}
|
|
1172
|
+
try {
|
|
1173
|
+
this._isUpdateInProgress = true;
|
|
1174
|
+
this._updateBegan.fire(blocks);
|
|
1175
|
+
|
|
1176
|
+
this.virtualDocument.clear();
|
|
1177
|
+
|
|
1178
|
+
for (const codeBlock of blocks) {
|
|
1179
|
+
this._blockAdded.fire({
|
|
1180
|
+
block: codeBlock,
|
|
1181
|
+
virtualDocument: this.virtualDocument,
|
|
1182
|
+
});
|
|
1183
|
+
this.virtualDocument.appendCodeBlock(codeBlock);
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
this._updateFinished.fire(blocks);
|
|
1187
|
+
|
|
1188
|
+
if (this.virtualDocument) {
|
|
1189
|
+
this._documentUpdated.fire(this.virtualDocument);
|
|
1190
|
+
this.virtualDocument.maybeEmitChanged();
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
resolve();
|
|
1194
|
+
} catch (e) {
|
|
1195
|
+
console.warn('Documents update failed:', e);
|
|
1196
|
+
reject(e);
|
|
1197
|
+
} finally {
|
|
1198
|
+
this._isUpdateInProgress = false;
|
|
1199
|
+
}
|
|
1200
|
+
return;
|
|
1201
|
+
})
|
|
1202
|
+
.catch(console.error);
|
|
1203
|
+
});
|
|
1204
|
+
this._updateDone = update;
|
|
1205
|
+
return update;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
protected _isDisposed = false;
|
|
1209
|
+
|
|
1210
|
+
/**
|
|
1211
|
+
* Promise resolved when the updating process finishes.
|
|
1212
|
+
*/
|
|
1213
|
+
protected _updateDone: Promise<void> = new Promise<void>((resolve) => {
|
|
1214
|
+
resolve();
|
|
1215
|
+
});
|
|
1216
|
+
|
|
1217
|
+
/**
|
|
1218
|
+
* Virtual documents update guard.
|
|
1219
|
+
*/
|
|
1220
|
+
protected _isUpdateInProgress = false;
|
|
1221
|
+
|
|
1222
|
+
/**
|
|
1223
|
+
* Update lock to prevent multiple updates are applied at the same time.
|
|
1224
|
+
*/
|
|
1225
|
+
protected _updateLock = false;
|
|
1226
|
+
|
|
1227
|
+
protected _blockAdded: Emitter<IBlockAddedInfo>;
|
|
1228
|
+
protected _documentUpdated: Emitter<VirtualDocument>;
|
|
1229
|
+
protected _updateBegan: Emitter<Document.ICodeBlockOptions[]>;
|
|
1230
|
+
protected _updateFinished: Emitter<Document.ICodeBlockOptions[]>;
|
|
1231
|
+
|
|
1232
|
+
/**
|
|
1233
|
+
* Once all the foreign documents were refreshed, the unused documents (and their connections)
|
|
1234
|
+
* should be terminated if their lifetime has expired.
|
|
1235
|
+
*/
|
|
1236
|
+
protected _onUpdated(rootDocument: VirtualDocument) {
|
|
1237
|
+
try {
|
|
1238
|
+
rootDocument.closeExpiredDocuments();
|
|
1239
|
+
} catch (e) {
|
|
1240
|
+
console.warn('Failed to close expired documents');
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
/**
|
|
1245
|
+
* Check if the document can be updated.
|
|
1246
|
+
*/
|
|
1247
|
+
protected _canUpdate() {
|
|
1248
|
+
return !this.isDisposed && !this._isUpdateInProgress && !this._updateLock;
|
|
1249
|
+
}
|
|
1250
|
+
}
|