Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts
3296 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 { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
7
import { URI } from '../../../../base/common/uri.js';
8
import { CellUri, IResolvedNotebookEditorModel, NotebookEditorModelCreationOptions, NotebookSetting, NotebookWorkingCopyTypeIdentifier } from './notebookCommon.js';
9
import { NotebookFileWorkingCopyModel, NotebookFileWorkingCopyModelFactory, SimpleNotebookEditorModel } from './notebookEditorModel.js';
10
import { combinedDisposable, DisposableStore, dispose, IDisposable, IReference, ReferenceCollection, toDisposable } from '../../../../base/common/lifecycle.js';
11
import { INotebookService } from './notebookService.js';
12
import { AsyncEmitter, Emitter, Event } from '../../../../base/common/event.js';
13
import { IExtensionService } from '../../../services/extensions/common/extensions.js';
14
import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js';
15
import { INotebookConflictEvent, INotebookEditorModelResolverService, IUntitledNotebookResource } from './notebookEditorModelResolverService.js';
16
import { ResourceMap } from '../../../../base/common/map.js';
17
import { FileWorkingCopyManager, IFileWorkingCopyManager } from '../../../services/workingCopy/common/fileWorkingCopyManager.js';
18
import { Schemas } from '../../../../base/common/network.js';
19
import { NotebookProviderInfo } from './notebookProvider.js';
20
import { assertReturnsDefined } from '../../../../base/common/types.js';
21
import { CancellationToken } from '../../../../base/common/cancellation.js';
22
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
23
import { IFileReadLimits } from '../../../../platform/files/common/files.js';
24
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
25
import { INotebookLoggingService } from './notebookLoggingService.js';
26
import { parse } from '../../../services/notebook/common/notebookDocumentService.js';
27
28
class NotebookModelReferenceCollection extends ReferenceCollection<Promise<IResolvedNotebookEditorModel>> {
29
30
private readonly _disposables = new DisposableStore();
31
private readonly _workingCopyManagers = new Map<string, IFileWorkingCopyManager<NotebookFileWorkingCopyModel, NotebookFileWorkingCopyModel>>();
32
private readonly _modelListener = new Map<IResolvedNotebookEditorModel, IDisposable>();
33
34
private readonly _onDidSaveNotebook = new Emitter<URI>();
35
readonly onDidSaveNotebook: Event<URI> = this._onDidSaveNotebook.event;
36
37
private readonly _onDidChangeDirty = new Emitter<IResolvedNotebookEditorModel>();
38
readonly onDidChangeDirty: Event<IResolvedNotebookEditorModel> = this._onDidChangeDirty.event;
39
40
private readonly _dirtyStates = new ResourceMap<boolean>();
41
42
private readonly modelsToDispose = new Set<string>();
43
constructor(
44
@IInstantiationService private readonly _instantiationService: IInstantiationService,
45
@INotebookService private readonly _notebookService: INotebookService,
46
@IConfigurationService private readonly _configurationService: IConfigurationService,
47
@ITelemetryService private readonly _telemetryService: ITelemetryService,
48
@INotebookLoggingService private readonly _notebookLoggingService: INotebookLoggingService,
49
) {
50
super();
51
}
52
53
dispose(): void {
54
this._disposables.dispose();
55
this._onDidSaveNotebook.dispose();
56
this._onDidChangeDirty.dispose();
57
dispose(this._modelListener.values());
58
dispose(this._workingCopyManagers.values());
59
}
60
61
isDirty(resource: URI): boolean {
62
return this._dirtyStates.get(resource) ?? false;
63
}
64
65
isListeningToModel(uri: URI): boolean {
66
for (const key of this._modelListener.keys()) {
67
if (key.resource.toString() === uri.toString()) {
68
return true;
69
}
70
}
71
return false;
72
}
73
74
protected async createReferencedObject(key: string, notebookType: string, hasAssociatedFilePath: boolean, limits?: IFileReadLimits, isScratchpad?: boolean, viewType?: string): Promise<IResolvedNotebookEditorModel> {
75
// Untrack as being disposed
76
this.modelsToDispose.delete(key);
77
78
const uri = URI.parse(key);
79
80
const workingCopyTypeId = NotebookWorkingCopyTypeIdentifier.create(notebookType, viewType);
81
let workingCopyManager = this._workingCopyManagers.get(workingCopyTypeId);
82
if (!workingCopyManager) {
83
const factory = new NotebookFileWorkingCopyModelFactory(notebookType, this._notebookService, this._configurationService, this._telemetryService, this._notebookLoggingService);
84
workingCopyManager = this._instantiationService.createInstance(
85
FileWorkingCopyManager<NotebookFileWorkingCopyModel, NotebookFileWorkingCopyModel>,
86
workingCopyTypeId,
87
factory,
88
factory,
89
);
90
this._workingCopyManagers.set(workingCopyTypeId, workingCopyManager);
91
}
92
93
const isScratchpadView = isScratchpad || (notebookType === 'interactive' && this._configurationService.getValue<boolean>(NotebookSetting.InteractiveWindowPromptToSave) !== true);
94
const model = this._instantiationService.createInstance(SimpleNotebookEditorModel, uri, hasAssociatedFilePath, notebookType, workingCopyManager, isScratchpadView);
95
const result = await model.load({ limits });
96
97
98
// Whenever a notebook model is dirty we automatically reference it so that
99
// we can ensure that at least one reference exists. That guarantees that
100
// a model with unsaved changes is never disposed.
101
let onDirtyAutoReference: IReference<any> | undefined;
102
103
this._modelListener.set(result, combinedDisposable(
104
result.onDidSave(() => this._onDidSaveNotebook.fire(result.resource)),
105
result.onDidChangeDirty(() => {
106
const isDirty = result.isDirty();
107
this._dirtyStates.set(result.resource, isDirty);
108
109
// isDirty -> add reference
110
// !isDirty -> free reference
111
if (isDirty && !onDirtyAutoReference) {
112
onDirtyAutoReference = this.acquire(key, notebookType);
113
} else if (onDirtyAutoReference) {
114
onDirtyAutoReference.dispose();
115
onDirtyAutoReference = undefined;
116
}
117
118
this._onDidChangeDirty.fire(result);
119
}),
120
toDisposable(() => onDirtyAutoReference?.dispose()),
121
));
122
return result;
123
}
124
125
protected destroyReferencedObject(key: string, object: Promise<IResolvedNotebookEditorModel>): void {
126
this.modelsToDispose.add(key);
127
128
(async () => {
129
try {
130
const model = await object;
131
132
if (!this.modelsToDispose.has(key)) {
133
// return if model has been acquired again meanwhile
134
return;
135
}
136
137
if (model instanceof SimpleNotebookEditorModel) {
138
await model.canDispose();
139
}
140
141
if (!this.modelsToDispose.has(key)) {
142
// return if model has been acquired again meanwhile
143
return;
144
}
145
146
// Finally we can dispose the model
147
this._modelListener.get(model)?.dispose();
148
this._modelListener.delete(model);
149
model.dispose();
150
} catch (err) {
151
this._notebookLoggingService.error('NotebookModelCollection', 'FAILED to destory notebook - ' + err);
152
} finally {
153
this.modelsToDispose.delete(key); // Untrack as being disposed
154
}
155
})();
156
}
157
}
158
159
export class NotebookModelResolverServiceImpl implements INotebookEditorModelResolverService {
160
161
readonly _serviceBrand: undefined;
162
163
private readonly _data: NotebookModelReferenceCollection;
164
165
readonly onDidSaveNotebook: Event<URI>;
166
readonly onDidChangeDirty: Event<IResolvedNotebookEditorModel>;
167
168
private readonly _onWillFailWithConflict = new AsyncEmitter<INotebookConflictEvent>();
169
readonly onWillFailWithConflict = this._onWillFailWithConflict.event;
170
171
constructor(
172
@IInstantiationService instantiationService: IInstantiationService,
173
@INotebookService private readonly _notebookService: INotebookService,
174
@IExtensionService private readonly _extensionService: IExtensionService,
175
@IUriIdentityService private readonly _uriIdentService: IUriIdentityService,
176
) {
177
this._data = instantiationService.createInstance(NotebookModelReferenceCollection);
178
this.onDidSaveNotebook = this._data.onDidSaveNotebook;
179
this.onDidChangeDirty = this._data.onDidChangeDirty;
180
}
181
182
dispose() {
183
this._data.dispose();
184
}
185
186
isDirty(resource: URI): boolean {
187
return this._data.isDirty(resource);
188
}
189
190
private createUntitledUri(notebookType: string) {
191
const info = this._notebookService.getContributedNotebookType(assertReturnsDefined(notebookType));
192
if (!info) {
193
throw new Error('UNKNOWN notebook type: ' + notebookType);
194
}
195
196
const suffix = NotebookProviderInfo.possibleFileEnding(info.selectors) ?? '';
197
for (let counter = 1; ; counter++) {
198
const candidate = URI.from({ scheme: Schemas.untitled, path: `Untitled-${counter}${suffix}`, query: notebookType });
199
if (!this._notebookService.getNotebookTextModel(candidate) && !this._data.isListeningToModel(candidate)) {
200
return candidate;
201
}
202
}
203
}
204
205
private async validateResourceViewType(uri: URI | undefined, viewType: string | undefined) {
206
if (!uri && !viewType) {
207
throw new Error('Must provide at least one of resource or viewType');
208
}
209
210
if (uri?.scheme === CellUri.scheme) {
211
const originalUri = uri;
212
uri = parse(uri)?.notebook;
213
if (!uri) {
214
throw new Error(`CANNOT open a cell-uri as notebook. Tried with ${originalUri.toString()}`);
215
}
216
}
217
218
const resource = this._uriIdentService.asCanonicalUri(uri ?? this.createUntitledUri(viewType!));
219
220
const existingNotebook = this._notebookService.getNotebookTextModel(resource);
221
if (!viewType) {
222
if (existingNotebook) {
223
viewType = existingNotebook.viewType;
224
} else {
225
await this._extensionService.whenInstalledExtensionsRegistered();
226
const providers = this._notebookService.getContributedNotebookTypes(resource);
227
viewType = providers.find(provider => provider.priority === 'exclusive')?.id ??
228
providers.find(provider => provider.priority === 'default')?.id ??
229
providers[0]?.id;
230
}
231
}
232
233
if (!viewType) {
234
throw new Error(`Missing viewType for '${resource}'`);
235
}
236
237
if (existingNotebook && existingNotebook.viewType !== viewType) {
238
239
await this._onWillFailWithConflict.fireAsync({ resource: resource, viewType }, CancellationToken.None);
240
241
// check again, listener should have done cleanup
242
const existingViewType2 = this._notebookService.getNotebookTextModel(resource)?.viewType;
243
if (existingViewType2 && existingViewType2 !== viewType) {
244
throw new Error(`A notebook with view type '${existingViewType2}' already exists for '${resource}', CANNOT create another notebook with view type ${viewType}`);
245
}
246
}
247
return { resource, viewType };
248
}
249
250
public async createUntitledNotebookTextModel(viewType: string) {
251
const resource = this._uriIdentService.asCanonicalUri(this.createUntitledUri(viewType));
252
253
return (await this._notebookService.createNotebookTextModel(viewType, resource));
254
}
255
256
async resolve(resource: URI, viewType?: string, options?: NotebookEditorModelCreationOptions): Promise<IReference<IResolvedNotebookEditorModel>>;
257
async resolve(resource: IUntitledNotebookResource, viewType: string, options: NotebookEditorModelCreationOptions): Promise<IReference<IResolvedNotebookEditorModel>>;
258
async resolve(arg0: URI | IUntitledNotebookResource, viewType?: string, options?: NotebookEditorModelCreationOptions): Promise<IReference<IResolvedNotebookEditorModel>> {
259
let resource: URI | undefined;
260
let hasAssociatedFilePath;
261
if (URI.isUri(arg0)) {
262
resource = arg0;
263
} else if (arg0.untitledResource) {
264
if (arg0.untitledResource.scheme === Schemas.untitled) {
265
resource = arg0.untitledResource;
266
} else {
267
resource = arg0.untitledResource.with({ scheme: Schemas.untitled });
268
hasAssociatedFilePath = true;
269
}
270
}
271
272
const validated = await this.validateResourceViewType(resource, viewType);
273
274
const reference = this._data.acquire(validated.resource.toString(), validated.viewType, hasAssociatedFilePath, options?.limits, options?.scratchpad, options?.viewType);
275
try {
276
const model = await reference.object;
277
return {
278
object: model,
279
dispose() { reference.dispose(); }
280
};
281
} catch (err) {
282
reference.dispose();
283
throw err;
284
}
285
}
286
}
287
288