Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/inlineEdits/vscode-node/parts/vscodeWorkspace.ts
13405 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { CancellationToken, CodeAction, CodeActionKind, commands, Diagnostic, DiagnosticSeverity, languages, NotebookCell, NotebookCellKind, NotebookDocument, Range, TextDocument, TextEditor, Uri, window, workspace } from 'vscode';
7
import { ConfigKey, IConfigurationService } from '../../../../platform/configuration/common/configurationService';
8
import { CodeActionData } from '../../../../platform/inlineEdits/common/dataTypes/codeActionData';
9
import { DiagnosticData } from '../../../../platform/inlineEdits/common/dataTypes/diagnosticData';
10
import { DocumentId } from '../../../../platform/inlineEdits/common/dataTypes/documentId';
11
import { LanguageId } from '../../../../platform/inlineEdits/common/dataTypes/languageId';
12
import { EditReason } from '../../../../platform/inlineEdits/common/editReason';
13
import { IObservableDocument, ObservableWorkspace, StringEditWithReason } from '../../../../platform/inlineEdits/common/observableWorkspace';
14
import { createAlternativeNotebookDocument, IAlternativeNotebookDocument, toAltNotebookCellChangeEdit, toAltNotebookChangeEdit } from '../../../../platform/notebook/common/alternativeNotebookTextDocument';
15
import { getDefaultLanguage } from '../../../../platform/notebook/common/helpers';
16
import { IExperimentationService } from '../../../../platform/telemetry/common/nullExperimentationService';
17
import { IWorkspaceService } from '../../../../platform/workspace/common/workspaceService';
18
import { getLanguage } from '../../../../util/common/languages';
19
import { findNotebook } from '../../../../util/common/notebooks';
20
import { coalesce } from '../../../../util/vs/base/common/arrays';
21
import { asPromise, raceCancellation, raceTimeout } from '../../../../util/vs/base/common/async';
22
import { diffMaps } from '../../../../util/vs/base/common/collections';
23
import { onUnexpectedError } from '../../../../util/vs/base/common/errors';
24
import { Lazy } from '../../../../util/vs/base/common/lazy';
25
import { DisposableStore, IDisposable } from '../../../../util/vs/base/common/lifecycle';
26
import { ResourceSet } from '../../../../util/vs/base/common/map';
27
import { Schemas } from '../../../../util/vs/base/common/network';
28
import { autorun, derived, IObservable, IReader, ISettableObservable, mapObservableArrayCached, observableFromEvent, observableValue, transaction } from '../../../../util/vs/base/common/observableInternal';
29
import { isDefined } from '../../../../util/vs/base/common/types';
30
import { URI } from '../../../../util/vs/base/common/uri';
31
import { TextReplacement } from '../../../../util/vs/editor/common/core/edits/textEdit';
32
import { OffsetRange } from '../../../../util/vs/editor/common/core/ranges/offsetRange';
33
import { StringText } from '../../../../util/vs/editor/common/core/text/abstractText';
34
import { IInstantiationService } from '../../../../util/vs/platform/instantiation/common/instantiation';
35
import { toInternalTextEdit } from '../utils/translations';
36
import { editFromTextDocumentContentChangeEvents, stringValueFromDoc } from './common';
37
import { DocumentFilter } from './documentFilter';
38
import { VerifyTextDocumentChanges } from './verifyTextDocumentChanges';
39
40
export class VSCodeWorkspace extends ObservableWorkspace implements IDisposable {
41
private readonly _openDocuments = observableValue<readonly IVSCodeObservableDocument[], { added: readonly IVSCodeObservableDocument[]; removed: readonly IVSCodeObservableDocument[] }>(this, []);
42
public readonly openDocuments = this._openDocuments;
43
44
private readonly _store = new DisposableStore();
45
private readonly _filter = this._instaService.createInstance(DocumentFilter);
46
private get _useAlternativeNotebookFormat(): boolean {
47
return this._configurationService.getExperimentBasedConfig(ConfigKey.Advanced.UseAlternativeNESNotebookFormat, this._experimentationService)
48
|| this._configurationService.getExperimentBasedConfig(ConfigKey.UseAlternativeNESNotebookFormat, this._experimentationService);
49
}
50
private readonly _markdownNotebookCells = new Lazy<ResourceSet>(() => {
51
const markdownCellUris = new ResourceSet();
52
workspace.notebookDocuments.forEach(doc => trackMarkdownCells(doc.getCells(), markdownCellUris));
53
return markdownCellUris;
54
});
55
56
constructor(
57
@IWorkspaceService private readonly _workspaceService: IWorkspaceService,
58
@IInstantiationService private readonly _instaService: IInstantiationService,
59
@IConfigurationService private readonly _configurationService: IConfigurationService,
60
@IExperimentationService private readonly _experimentationService: IExperimentationService,
61
) {
62
super();
63
64
const config = this._configurationService.getExperimentBasedConfigObservable(ConfigKey.TeamInternal.VerifyTextDocumentChanges, this._experimentationService);
65
this._store.add(autorun(reader => {
66
if (config.read(reader)) {
67
reader.store.add(this._instaService.createInstance(VerifyTextDocumentChanges));
68
}
69
}));
70
71
let lastDocs: Map<DocumentId, IVSCodeObservableDocument> = new Map();
72
this._store.add(autorun(reader => {
73
// Manually copy over the documents to get the delta
74
const curDocs = this._obsDocsByDocId.read(reader);
75
const diff = diffMaps(lastDocs, curDocs);
76
lastDocs = curDocs;
77
78
this._openDocuments.set([...curDocs.values()], undefined, {
79
added: diff.added,
80
removed: diff.removed,
81
});
82
}));
83
84
this._store.add(workspace.onDidChangeTextDocument(e => {
85
const doc = this._getDocumentByDocumentAndUpdateShouldTrack(e.document.uri);
86
if (!doc) {
87
return;
88
}
89
if (doc instanceof VSCodeObservableTextDocument) {
90
const edit = editFromTextDocumentContentChangeEvents(e.contentChanges);
91
const editWithReason = new StringEditWithReason(edit.replacements, EditReason.create(e.detailedReason?.metadata as any));
92
transaction(tx => {
93
doc.languageId.set(LanguageId.create(e.document.languageId), tx);
94
doc.value.set(stringValueFromDoc(e.document), tx, editWithReason);
95
doc.version.set(e.document.version, tx);
96
});
97
} else {
98
const edit = toAltNotebookCellChangeEdit(doc.altNotebook, e.document, e.contentChanges);
99
doc.altNotebook.applyCellChanges(e.document, e.contentChanges);
100
const editWithReason = new StringEditWithReason(edit.replacements, EditReason.create(e.detailedReason?.metadata as any));
101
transaction(tx => {
102
doc.value.set(stringValueFromDoc(doc.altNotebook), tx, editWithReason);
103
doc.version.set(doc.notebook.version, tx);
104
});
105
}
106
}));
107
108
if (this._useAlternativeNotebookFormat) {
109
this._store.add(workspace.onDidOpenNotebookDocument(e => trackMarkdownCells(e.getCells(), this._markdownNotebookCells.value)));
110
}
111
112
this._store.add(workspace.onDidChangeNotebookDocument(e => {
113
const doc = this._getDocumentByDocumentAndUpdateShouldTrack(e.notebook.uri);
114
if (!doc || !e.contentChanges.length || doc instanceof VSCodeObservableTextDocument) {
115
return;
116
}
117
const edit = toAltNotebookChangeEdit(doc.altNotebook, e.contentChanges);
118
if (!edit) {
119
return;
120
}
121
doc.altNotebook.applyNotebookChanges(e.contentChanges);
122
const editWithReason = new StringEditWithReason(edit.replacements, EditReason.unknown);
123
transaction(tx => {
124
doc.value.set(stringValueFromDoc(doc.altNotebook), tx, editWithReason);
125
doc.version.set(doc.notebook.version, tx);
126
});
127
128
if (this._useAlternativeNotebookFormat) {
129
e.contentChanges.map(c => c.removedCells).flat().forEach(c => {
130
this._markdownNotebookCells.value.delete(c.document.uri);
131
});
132
trackMarkdownCells(e.contentChanges.map(c => c.addedCells).flat(), this._markdownNotebookCells.value);
133
}
134
}));
135
136
this._store.add(window.onDidChangeTextEditorSelection(e => {
137
const doc = this._getDocumentByDocumentAndUpdateShouldTrack(e.textEditor.document.uri);
138
if (!doc) {
139
return;
140
}
141
const selections = doc instanceof VSCodeObservableTextDocument ?
142
coalesce(e.selections.map(s => doc.toOffsetRange(e.textEditor.document, s))) :
143
this.getNotebookSelections(doc.notebook, e.textEditor);
144
doc.selection.set(selections, undefined);
145
doc.primarySelectionLine.set(e.selections[0]?.start.line, undefined);
146
}));
147
148
this._store.add(window.onDidChangeTextEditorVisibleRanges(e => {
149
const doc = this._getDocumentByDocumentAndUpdateShouldTrack(e.textEditor.document.uri);
150
if (!doc) {
151
return;
152
}
153
const visibleRanges = doc instanceof VSCodeObservableTextDocument ?
154
coalesce(e.visibleRanges.map(r => doc.toOffsetRange(e.textEditor.document, r))) :
155
this.getNotebookVisibleRanges(doc.notebook);
156
doc.visibleRanges.set(visibleRanges, undefined);
157
}));
158
159
this._store.add(languages.onDidChangeDiagnostics(e => {
160
e.uris.forEach(uri => {
161
const document = this._getDocumentByDocumentAndUpdateShouldTrack(uri);
162
if (!document) {
163
return;
164
}
165
const diagnostics = document instanceof VSCodeObservableTextDocument ?
166
this._createTextDocumentDiagnosticData(document) :
167
this._createNotebookDiagnosticData(document.altNotebook);
168
document.diagnostics.set(diagnostics, undefined);
169
});
170
}));
171
}
172
173
public dispose(): void {
174
this._store.dispose();
175
}
176
177
private readonly _obsDocsByDocId = derived(this, reader => {
178
const textDocs = this._textDocsWithShouldTrackFlag.read(reader);
179
const obsDocs = textDocs.map(d => d.obsDoc.read(reader)).filter(isDefined);
180
const map: Map<DocumentId, VSCodeObservableTextDocument | VSCodeObservableNotebookDocument> = new Map(obsDocs.map(d => [d.id, d]));
181
const notebookDocs = this._notebookDocsWithShouldTrackFlag.read(reader);
182
const obsNotebookDocs = notebookDocs.map(d => d.obsDoc.read(reader)).filter(isDefined);
183
obsNotebookDocs.forEach(d => map.set(d.id, d));
184
185
return map;
186
});
187
188
private readonly _vscodeTextDocuments: IObservable<readonly TextDocument[]> = getObservableTextDocumentList(this._useAlternativeNotebookFormat, this._markdownNotebookCells.value);
189
private readonly _textDocsWithShouldTrackFlag = mapObservableArrayCached(this, this._vscodeTextDocuments, (doc, store) => {
190
const shouldTrack = observableValue<boolean>(this, false);
191
const updateShouldTrack = () => {
192
// @ulugbekna: not sure if invoking `isCopilotIgnored` on every textDocument-edit event is a good idea
193
// also not sure if we should be enforcing local copilot-ignore rules (vs only remote-exclusion rules)
194
this._filter.isTrackingEnabled(doc).then(v => {
195
shouldTrack.set(v, undefined);
196
}).catch(e => {
197
onUnexpectedError(e);
198
});
199
};
200
const obsDoc = derived(this, reader => {
201
if (!shouldTrack.read(reader)) {
202
return undefined;
203
}
204
205
const documentId = DocumentId.create(doc.uri.toString());
206
const openedTextEditor = window.visibleTextEditors.find(e => e.document.uri.toString() === doc.uri.toString());
207
const document = new VSCodeObservableTextDocument(documentId, stringValueFromDoc(doc), doc.version, [], [], LanguageId.create(doc.languageId), [], doc);
208
209
const selections = coalesce((openedTextEditor?.selections || []).map(s => document.toOffsetRange(doc, s)));
210
const visibleRanges = coalesce((openedTextEditor?.visibleRanges || []).map(r => document.toOffsetRange(doc, r)));
211
transaction(tx => {
212
document.selection.set(selections, tx);
213
document.primarySelectionLine.set(openedTextEditor?.selections[0]?.start.line, tx);
214
document.visibleRanges.set(visibleRanges, tx);
215
document.diagnostics.set(this._createTextDocumentDiagnosticData(document), tx);
216
});
217
return document;
218
}).recomputeInitiallyAndOnChange(store);
219
220
updateShouldTrack();
221
return {
222
doc,
223
updateShouldTrack,
224
obsDoc,
225
};
226
});
227
228
private getNotebookDocuments() {
229
if (!this._useAlternativeNotebookFormat) {
230
return observableValue('', []);
231
}
232
return getNotebookDocuments();
233
}
234
private readonly _vscodeNotebookDocuments: IObservable<readonly NotebookDocument[]> = this.getNotebookDocuments();
235
private readonly _altNotebookDocs = new WeakMap<NotebookDocument, IAlternativeNotebookDocument>();
236
private getAltNotebookDocument(doc: NotebookDocument): IAlternativeNotebookDocument {
237
let altNotebook = this._altNotebookDocs.get(doc);
238
if (!altNotebook) {
239
altNotebook = createAlternativeNotebookDocument(doc, true);
240
this._altNotebookDocs.set(doc, altNotebook);
241
}
242
return altNotebook;
243
}
244
private readonly _notebookDocsWithShouldTrackFlag = mapObservableArrayCached(this, this._vscodeNotebookDocuments, (doc, store) => {
245
const shouldTrack = observableValue<boolean>(this, false);
246
const updateShouldTrack = () => {
247
// @ulugbekna: not sure if invoking `isCopilotIgnored` on every textDocument-edit event is a good idea
248
// also not sure if we should be enforcing local copilot-ignore rules (vs only remote-exclusion rules)
249
this._filter.isTrackingEnabled(doc).then(v => {
250
shouldTrack.set(v, undefined);
251
}).catch(e => {
252
onUnexpectedError(e);
253
});
254
};
255
const obsDoc = derived(this, reader => {
256
if (!shouldTrack.read(reader)) {
257
return undefined;
258
}
259
const altNotebook = this.getAltNotebookDocument(doc);
260
const documentId = DocumentId.create(doc.uri.toString());
261
const selections = this.getNotebookSelections(doc);
262
const visibleRanges = this.getNotebookVisibleRanges(doc);
263
const diagnostics = this._createNotebookDiagnosticData(altNotebook);
264
const language = getLanguage(getDefaultLanguage(altNotebook.notebook)).languageId;
265
const document = new VSCodeObservableNotebookDocument(documentId, stringValueFromDoc(altNotebook), doc.version, selections ?? [], visibleRanges ?? [], LanguageId.create(language), diagnostics, doc, altNotebook);
266
return document;
267
}).recomputeInitiallyAndOnChange(store);
268
269
updateShouldTrack();
270
return {
271
doc,
272
updateShouldTrack,
273
obsDoc,
274
};
275
});
276
277
private getNotebookSelections(doc: NotebookDocument, activeCellEditor?: TextEditor) {
278
const altNotebook = this.getAltNotebookDocument(doc);
279
const visibleTextEditors = new Map(window.visibleTextEditors.map(e => [e.document, e]));
280
// As notebooks have multiple cells, and each cell can have its own selection,
281
// We should focus on the active cell to determine the cursor position.
282
const selectedCellRange = window.activeNotebookEditor?.selection;
283
const selectedCell = activeCellEditor ? altNotebook.getCell(activeCellEditor.document) : (selectedCellRange && selectedCellRange.start < doc.cellCount ? doc.cellAt(selectedCellRange.start) : undefined);
284
const selectedCellEditor = selectedCell ? visibleTextEditors.get(selectedCell.document) : undefined;
285
// We only care about the active cell where cursor is, don't care about multi-cursor.
286
// As the edits are to be performed around wher the cursor is.
287
return (selectedCellEditor && selectedCell) ? altNotebook.toAltOffsetRange(selectedCell, selectedCellEditor.selections) : [];
288
}
289
290
private getNotebookVisibleRanges(doc: NotebookDocument) {
291
const altNotebook = this.getAltNotebookDocument(doc);
292
const visibleTextEditors = new Map(window.visibleTextEditors.map(e => [e.document, e]));
293
const cellTextEditors = coalesce(doc.getCells().map(cell => visibleTextEditors.has(cell.document) ? [cell, visibleTextEditors.get(cell.document)!] as const : undefined));
294
return cellTextEditors.flatMap(e => altNotebook.toAltOffsetRange(e[0], e[1].visibleRanges));
295
}
296
297
private _getDocumentByDocumentAndUpdateShouldTrack(uri: URI): VSCodeObservableTextDocument | VSCodeObservableNotebookDocument | undefined {
298
const internalDoc = this._getInternalDocument(uri);
299
if (!internalDoc) {
300
return undefined;
301
}
302
internalDoc.updateShouldTrack();
303
return internalDoc.obsDoc.get();
304
}
305
306
private _getInternalDocument(uri: Uri, reader?: IReader) {
307
const document = this._obsDocsWithUpdateIgnored.read(reader).get(uri.toString());
308
return document;
309
}
310
311
private _createTextDocumentDiagnosticData(document: VSCodeObservableTextDocument) {
312
return languages.getDiagnostics(document.textDocument.uri).map(d => this._createDiagnosticData(d, document)).filter(isDefined);
313
}
314
315
private _createDiagnosticData(diagnostic: Diagnostic, doc: VSCodeObservableTextDocument): DiagnosticData | undefined {
316
return toDiagnosticData(diagnostic, doc.textDocument.uri, (range) => doc.toOffsetRange(doc.textDocument, range));
317
}
318
319
private _createNotebookDiagnosticData(altNotebook: IAlternativeNotebookDocument) {
320
return coalesce(altNotebook.notebook.getCells().flatMap(c => languages.getDiagnostics(c.document.uri).map(d => this._createNotebookCellDiagnosticData(d, altNotebook, c.document))));
321
}
322
323
private _createNotebookCellDiagnosticData(diagnostic: Diagnostic, altNotebook: IAlternativeNotebookDocument, doc: TextDocument): DiagnosticData | undefined {
324
const cell = altNotebook.getCell(doc);
325
if (!cell) {
326
return undefined;
327
}
328
return toDiagnosticData(diagnostic, altNotebook.notebook.uri, range => {
329
const offsetRanges = altNotebook.toAltOffsetRange(cell, [range]);
330
return offsetRanges.length ? offsetRanges[0] : undefined;
331
});
332
}
333
334
private readonly _obsDocsWithUpdateIgnored = derived(this, reader => {
335
const docs = this._textDocsWithShouldTrackFlag.read(reader);
336
const map: Map<string, {
337
doc: TextDocument | NotebookDocument;
338
updateShouldTrack: () => void;
339
obsDoc: IObservable<VSCodeObservableTextDocument | VSCodeObservableNotebookDocument | undefined>;
340
}> = new Map(docs.map(d => [d.doc.uri.toString(), d]));
341
const notebookDocs = this._notebookDocsWithShouldTrackFlag.read(reader);
342
notebookDocs.forEach(d => {
343
map.set(d.doc.uri.toString(), d);
344
// Markdown cells will be treated as standalone text documents (old behaviour).
345
d.doc.getCells().filter(cell => cell.kind === NotebookCellKind.Code).forEach(cell => map.set(cell.document.uri.toString(), d));
346
});
347
return map;
348
});
349
350
/**
351
* Returns undefined for documents that are not tracked (e.g. filtered out).
352
*/
353
public getDocumentByTextDocument(doc: TextDocument, reader?: IReader): IVSCodeObservableDocument | undefined {
354
this._store.assertNotDisposed();
355
356
const internalDoc = this._getInternalDocument(doc.uri, reader);
357
if (!internalDoc) {
358
return undefined;
359
}
360
return internalDoc.obsDoc.get();
361
}
362
363
public getWorkspaceRoot(documentId: DocumentId): URI | undefined {
364
let uri = documentId.toUri();
365
if (uri.scheme === Schemas.vscodeNotebookCell) {
366
const notebook = findNotebook(uri, this._workspaceService.notebookDocuments);
367
if (notebook) {
368
uri = notebook.uri;
369
}
370
}
371
return workspace.getWorkspaceFolder(uri)?.uri;
372
}
373
}
374
375
export interface IVSCodeObservableDocument extends IObservableDocument {
376
/**
377
* Converts the OffsetRange of the Observable document to a range within the provided Text document.
378
* If this is a Text Document, performs a simple OffsetRange to Range translation.
379
* If this is a Notebook Document, then this method converts an offset range of the Observable document to a range within the provided notebook cell. If the range provided does not belong to the provided cell (as identified by TextDocument), it returns undefined.
380
*/
381
fromOffsetRange(textDocument: TextDocument, range: OffsetRange): Range | undefined;
382
/**
383
* Converts the OffsetRange of the Observable document to range(s) within the provided Text document.
384
* If this is a Text Document, performs a simple OffsetRange to Range translation.
385
* If this is a Notebook Document, then this method converts an offset range of the Observable document to a range(s) of multiple notebook cells.
386
*/
387
fromOffsetRange(range: OffsetRange): [TextDocument, Range][];
388
389
/**
390
* Converts the Range of the Observable document to a range within the provided Text document.
391
* If this is a Text Document, then this returns the same value passed in.
392
* If this is a Notebook Document, then this method converts a range of the Observable document to a range within the provided notebook cell. If the range provided does not belong to the provided cell (as identified by TextDocument), it returns undefined.
393
*/
394
fromRange(textDocument: TextDocument, range: Range): Range | undefined;
395
/**
396
* Converts the Range of the Observable document to range(s) within the provided Text document.
397
* If this is a Text Document, then this returns the same value passed in.
398
* If this is a Notebook Document, then this method converts a range of the Observable document to a range(s) of multiple notebook cells.
399
*/
400
fromRange(range: Range): [TextDocument, Range][];
401
402
/**
403
* Converts the Range of the provided Text Document into an OffsetRange within the Observable Document.
404
* If this is a Text Document, performs a simple Range to OffsetRange translation.
405
* If this is a Notebook Document, then this method converts a range within the provided Notebook Cell to an Offset within the Observable document. If the provided document does not belong to the Notebook Cell, it returns undefined.
406
*/
407
toOffsetRange(textDocument: TextDocument, range: Range): OffsetRange | undefined;
408
409
/**
410
* Converts the Range of the provided Text Document into a Range within the Observable Document.
411
* If this is a Text Document, then this returns the same value passed in.
412
* If this is a Notebook Document, then this method converts a range within the provided Notebook Cell to a Range within the Observable document. If the provided document does not belong to the Notebook Cell, it returns undefined.
413
*/
414
toRange(textDocument: TextDocument, range: Range): Range | undefined;
415
416
/**
417
* Gets code actions for the specific range within the document
418
* @param itemResolveCount Number of code actions to resolve (too large numbers slow down code actions)
419
*/
420
getCodeActions(range: OffsetRange, itemResolveCount: number, token: CancellationToken): Promise<CodeActionData[] | undefined>;
421
}
422
423
abstract class AbstractVSCodeObservableDocument {
424
public readonly value: ISettableObservable<StringText, StringEditWithReason>;
425
public readonly version: ISettableObservable<number>;
426
public readonly selection: ISettableObservable<readonly OffsetRange[]>;
427
public readonly primarySelectionLine: ISettableObservable<number | undefined>;
428
public readonly visibleRanges: ISettableObservable<readonly OffsetRange[]>;
429
public readonly languageId: ISettableObservable<LanguageId>;
430
public readonly diagnostics: ISettableObservable<readonly DiagnosticData[]>;
431
432
constructor(
433
public readonly id: DocumentId,
434
value: StringText,
435
versionId: number,
436
selection: readonly OffsetRange[],
437
visibleRanges: readonly OffsetRange[],
438
languageId: LanguageId,
439
diagnostics: DiagnosticData[]
440
) {
441
this.value = observableValue(this, value);
442
this.version = observableValue(this, versionId);
443
this.selection = observableValue(this, selection);
444
this.primarySelectionLine = observableValue(this, undefined);
445
this.visibleRanges = observableValue(this, visibleRanges);
446
this.languageId = observableValue(this, languageId);
447
this.diagnostics = observableValue(this, diagnostics);
448
}
449
}
450
451
class VSCodeObservableTextDocument extends AbstractVSCodeObservableDocument implements IVSCodeObservableDocument {
452
constructor(
453
id: DocumentId,
454
value: StringText,
455
versionId: number,
456
selection: readonly OffsetRange[],
457
visibleRanges: readonly OffsetRange[],
458
languageId: LanguageId,
459
diagnostics: DiagnosticData[],
460
public readonly textDocument: TextDocument,
461
) {
462
super(id, value, versionId, selection, visibleRanges, languageId, diagnostics);
463
}
464
465
fromOffsetRange(textDocument: TextDocument, range: OffsetRange): Range | undefined;
466
fromOffsetRange(range: OffsetRange): [TextDocument, Range][];
467
fromOffsetRange(arg1: TextDocument | OffsetRange, range?: OffsetRange): Range | undefined | [TextDocument, Range][] {
468
let textDocument: TextDocument = this.textDocument;
469
let offsetRange: OffsetRange | undefined;
470
471
if (arg1 instanceof OffsetRange) {
472
offsetRange = arg1;
473
} else if (range !== undefined) {
474
textDocument = arg1;
475
offsetRange = range;
476
}
477
if (textDocument !== this.textDocument) {
478
throw new Error('TextDocument does not match the one of this observable document.');
479
}
480
if (!offsetRange) {
481
throw new Error('OffsetRange is not defined.');
482
}
483
const result = new Range(
484
textDocument.positionAt(offsetRange.start),
485
textDocument.positionAt(offsetRange.endExclusive)
486
);
487
if (arg1 instanceof OffsetRange) {
488
return [[this.textDocument, result]];
489
} else {
490
return result;
491
}
492
}
493
toOffsetRange(textDocument: TextDocument, range: Range): OffsetRange | undefined {
494
return new OffsetRange(textDocument.offsetAt(range.start), textDocument.offsetAt(range.end));
495
}
496
497
fromRange(textDocument: TextDocument, range: Range): Range | undefined;
498
fromRange(range: Range): [TextDocument, Range][];
499
fromRange(arg1: TextDocument | Range, range?: Range): Range | undefined | [TextDocument, Range][] {
500
if (arg1 instanceof Range) {
501
return [[this.textDocument, arg1]];
502
} else if (range !== undefined) {
503
if (arg1 !== this.textDocument) {
504
throw new Error('TextDocument does not match the one of this observable document.');
505
}
506
return range;
507
} else {
508
return undefined;
509
}
510
}
511
512
toRange(_textDocument: TextDocument, range: Range): Range | undefined {
513
return range;
514
}
515
516
async getCodeActions(offsetRange: OffsetRange, itemResolveCount: number, token: CancellationToken): Promise<CodeActionData[] | undefined> {
517
const range = this.fromOffsetRange(this.textDocument, offsetRange);
518
if (!range) {
519
return;
520
}
521
const actions = await raceTimeout(
522
raceCancellation(
523
getQuickFixCodeActions(this.textDocument.uri, range, itemResolveCount),
524
token
525
),
526
1000
527
);
528
529
return actions?.map(action => toCodeActionData(action, this, range => this.toOffsetRange(this.textDocument, range)));
530
}
531
}
532
533
class VSCodeObservableNotebookDocument extends AbstractVSCodeObservableDocument implements IVSCodeObservableDocument {
534
constructor(
535
id: DocumentId,
536
value: StringText,
537
versionId: number,
538
selection: readonly OffsetRange[],
539
visibleRanges: readonly OffsetRange[],
540
languageId: LanguageId,
541
diagnostics: DiagnosticData[],
542
public readonly notebook: NotebookDocument,
543
public readonly altNotebook: IAlternativeNotebookDocument,
544
) {
545
super(id, value, versionId, selection, visibleRanges, languageId, diagnostics);
546
}
547
548
fromOffsetRange(textDocument: TextDocument, range: OffsetRange): Range | undefined;
549
fromOffsetRange(range: OffsetRange): [TextDocument, Range][];
550
fromOffsetRange(arg1: TextDocument | OffsetRange, range?: OffsetRange): Range | undefined | [TextDocument, Range][] {
551
if (arg1 instanceof OffsetRange) {
552
return this.altNotebook.fromAltOffsetRange(arg1).map(r => [r[0].document, r[1]]);
553
} else if (range !== undefined) {
554
const cell = this.altNotebook.getCell(arg1);
555
if (!cell) {
556
return undefined;
557
}
558
const results = this.altNotebook.fromAltOffsetRange(range);
559
const found = results.find(r => r[0].document === arg1);
560
return found ? found[1] : undefined;
561
}
562
return undefined;
563
}
564
565
fromRange(textDocument: TextDocument, range: Range): Range | undefined;
566
fromRange(range: Range): [TextDocument, Range][];
567
fromRange(arg1: TextDocument | Range, range?: Range): Range | undefined | [TextDocument, Range][] {
568
if (arg1 instanceof Range) {
569
return this.altNotebook.fromAltRange(arg1).map(r => [r[0].document, r[1]]);
570
} else if (range !== undefined) {
571
const cell = this.altNotebook.getCell(arg1);
572
if (!cell) {
573
return undefined;
574
}
575
const results = this.altNotebook.fromAltRange(range);
576
const found = results.find(r => r[0].document === arg1);
577
return found ? found[1] : undefined;
578
}
579
}
580
581
toOffsetRange(textDocument: TextDocument, range: Range): OffsetRange | undefined {
582
const cell = this.altNotebook.getCell(textDocument);
583
if (!cell) {
584
return undefined;
585
}
586
const offsetRanges = this.altNotebook.toAltOffsetRange(cell, [range]);
587
return offsetRanges.length ? offsetRanges[0] : undefined;
588
}
589
590
toRange(textDocument: TextDocument, range: Range): Range | undefined {
591
const cell = this.altNotebook.getCell(textDocument);
592
if (!cell) {
593
return undefined;
594
}
595
const ranges = this.altNotebook.toAltRange(cell, [range]);
596
return ranges.length > 0 ? ranges[0] : undefined;
597
}
598
599
async getCodeActions(offsetRange: OffsetRange, itemResolveCount: number, token: CancellationToken): Promise<CodeActionData[] | undefined> {
600
const cellRanges = this.fromOffsetRange(offsetRange);
601
if (!cellRanges || cellRanges.length === 0) {
602
return;
603
}
604
return Promise.all(cellRanges.map(async ([cellTextDocument, range]) => {
605
const cell = this.altNotebook.getCell(cellTextDocument);
606
if (!cell) {
607
return undefined;
608
}
609
const actions = await raceTimeout(
610
raceCancellation(
611
getQuickFixCodeActions(cellTextDocument.uri, range, itemResolveCount),
612
token
613
),
614
1000
615
);
616
617
return actions?.map(action => toCodeActionData(action, this, (range) => {
618
const offsetRanges = this.altNotebook.toAltOffsetRange(cell, [range]);
619
return offsetRanges.length ? offsetRanges[0] : undefined;
620
}));
621
})).then(results => coalesce(results.flat()));
622
}
623
}
624
625
function getObservableTextDocumentList(excludeNotebookCells: boolean, markdownCellUris: ResourceSet): IObservable<readonly TextDocument[]> {
626
return observableFromEvent(undefined, e => {
627
const d1 = workspace.onDidOpenTextDocument(e);
628
const d2 = workspace.onDidCloseTextDocument(e);
629
return {
630
dispose: () => {
631
d1.dispose();
632
d2.dispose();
633
}
634
};
635
// If we're meant to exclude notebook cells, we will still include the markdown cells as separate documents.
636
// Thats because markdown cells will be treated as standalone text documents in the editor.
637
}, () => excludeNotebookCells ? workspace.textDocuments.filter(doc => doc.uri.scheme !== Schemas.vscodeNotebookCell || markdownCellUris.has(doc.uri)) : workspace.textDocuments);
638
}
639
640
function getNotebookDocuments(): IObservable<readonly NotebookDocument[]> {
641
return observableFromEvent(undefined, e => {
642
const d1 = workspace.onDidOpenNotebookDocument(e);
643
const d2 = workspace.onDidCloseNotebookDocument(e);
644
return {
645
dispose: () => {
646
d1.dispose();
647
d2.dispose();
648
}
649
};
650
}, () => workspace.notebookDocuments);
651
}
652
653
function trackMarkdownCells(cells: NotebookCell[], resources: ResourceSet): void {
654
cells.forEach(c => {
655
if (c.kind === NotebookCellKind.Markup) {
656
resources.add(c.document.uri);
657
}
658
});
659
}
660
661
function toDiagnosticData(diagnostic: Diagnostic, uri: URI, translateRange: (range: Range) => OffsetRange | undefined) {
662
if (!diagnostic.source || (diagnostic.severity !== DiagnosticSeverity.Error && diagnostic.severity !== DiagnosticSeverity.Warning)) {
663
return undefined;
664
}
665
666
const range = translateRange(diagnostic.range);
667
if (!range) {
668
return undefined;
669
}
670
return new DiagnosticData(
671
uri,
672
diagnostic.message,
673
diagnostic.severity === DiagnosticSeverity.Error ? 'error' : 'warning',
674
range,
675
diagnostic.code && !(typeof diagnostic.code === 'number') && !(typeof diagnostic.code === 'string') ? diagnostic.code.value : diagnostic.code,
676
diagnostic.source
677
);
678
}
679
680
function toCodeActionData(codeAction: CodeAction, workspaceDocument: IVSCodeObservableDocument, translateRange: (range: Range) => OffsetRange | undefined): CodeActionData {
681
const uri = workspaceDocument.id.toUri();
682
const diagnostics = coalesce((codeAction.diagnostics || []).map(d => toDiagnosticData(d, uri, translateRange))).concat(
683
getCommandDiagnostics(codeAction, uri, translateRange)
684
);
685
686
// remove no-op edits
687
let documentEdits = getDocumentEdits(codeAction, workspaceDocument);
688
if (documentEdits) {
689
const documentContent = workspaceDocument.value.get();
690
documentEdits = documentEdits.filter(edit => documentContent.getValueOfRange(edit.range) !== edit.text);
691
}
692
693
const codeActionData = new CodeActionData(
694
codeAction.title,
695
diagnostics,
696
documentEdits,
697
);
698
return codeActionData;
699
}
700
701
function getCommandDiagnostics(codeAction: CodeAction, uri: URI, translateRange: (range: Range) => OffsetRange | undefined): DiagnosticData[] {
702
const commandArgs = codeAction.command?.arguments || [];
703
704
return coalesce(commandArgs.map(arg => {
705
if (arg && typeof arg === 'object' && 'diagnostic' in arg) {
706
const diagnostic = arg.diagnostic;
707
// @benibenj Can we not check `diagnostic instanceOf vscode.Diagnostic?
708
if (diagnostic && typeof diagnostic === 'object' && 'range' in diagnostic && 'message' in diagnostic && 'severity' in diagnostic) {
709
return toDiagnosticData(diagnostic, uri, translateRange);
710
}
711
}
712
}));
713
}
714
715
function getDocumentEdits(codeAction: CodeAction, workspaceDocument: IVSCodeObservableDocument): TextReplacement[] | undefined {
716
const edit = codeAction.edit;
717
if (!edit) {
718
return undefined;
719
}
720
721
if (workspaceDocument instanceof VSCodeObservableTextDocument) {
722
return edit.get(workspaceDocument.id.toUri()).map(e => toInternalTextEdit(e.range, e.newText));
723
} else if (workspaceDocument instanceof VSCodeObservableNotebookDocument) {
724
const edits = coalesce(workspaceDocument.notebook.getCells().flatMap(cell =>
725
edit.get(cell.document.uri).map(e => {
726
const range = workspaceDocument.toRange(cell.document, e.range);
727
return range ? toInternalTextEdit(range, e.newText) : undefined;
728
})
729
));
730
return edits.length ? edits : undefined;
731
}
732
}
733
734
async function getQuickFixCodeActions(uri: Uri, range: Range, itemResolveCount: number): Promise<CodeAction[]> {
735
return asPromise(
736
() => commands.executeCommand<CodeAction[]>(
737
'vscode.executeCodeActionProvider',
738
uri,
739
range,
740
CodeActionKind.QuickFix.value,
741
itemResolveCount
742
)
743
);
744
}
745
746