notebookEditorModel.ts 10.9 KB
Newer Older
1 2 3 4 5 6 7
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

import { EditorModel, IRevertOptions } from 'vs/workbench/common/editor';
import { Emitter, Event } from 'vs/base/common/event';
8
import { INotebookEditorModel, NotebookDocumentBackupData } from 'vs/workbench/contrib/notebook/common/notebookCommon';
9 10 11 12 13 14
import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel';
import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle';
import { ResourceMap } from 'vs/base/common/map';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService';
import { URI } from 'vs/base/common/uri';
15
import { IWorkingCopyService, IWorkingCopy, IWorkingCopyBackup } from 'vs/workbench/services/workingCopy/common/workingCopyService';
16
import { basename } from 'vs/base/common/resources';
R
rebornix 已提交
17
import { CancellationTokenSource } from 'vs/base/common/cancellation';
R
rebornix 已提交
18 19
import { IBackupFileService } from 'vs/workbench/services/backup/common/backup';
import { DefaultEndOfLine, ITextBuffer, EndOfLinePreference } from 'vs/editor/common/model';
20
import { Schemas } from 'vs/base/common/network';
21 22 23 24

export interface INotebookEditorModelManager {
	models: NotebookEditorModel[];

25
	resolve(resource: URI, viewType: string, editorId?: string): Promise<NotebookEditorModel>;
26 27 28 29

	get(resource: URI): NotebookEditorModel | undefined;
}

30
export interface INotebookLoadOptions {
R
revert.  
rebornix 已提交
31 32 33 34
	/**
	 * Go to disk bypassing any cache of the model if any.
	 */
	forceReadFromDisk?: boolean;
35 36

	editorId?: string;
R
revert.  
rebornix 已提交
37 38
}

39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56

export class NotebookEditorModel extends EditorModel implements IWorkingCopy, INotebookEditorModel {
	private _dirty = false;
	protected readonly _onDidChangeDirty = this._register(new Emitter<void>());
	readonly onDidChangeDirty = this._onDidChangeDirty.event;
	private readonly _onDidChangeContent = this._register(new Emitter<void>());
	readonly onDidChangeContent: Event<void> = this._onDidChangeContent.event;
	private _notebook!: NotebookTextModel;

	get notebook() {
		return this._notebook;
	}

	private _name!: string;

	get name() {
		return this._name;
	}
57 58 59

	private _workingCopyResource: URI;

60 61 62 63
	constructor(
		public readonly resource: URI,
		public readonly viewType: string,
		@INotebookService private readonly notebookService: INotebookService,
R
rebornix 已提交
64 65
		@IWorkingCopyService private readonly workingCopyService: IWorkingCopyService,
		@IBackupFileService private readonly backupFileService: IBackupFileService
66 67
	) {
		super();
R
rebornix 已提交
68 69

		const input = this;
70
		this._workingCopyResource = resource.with({ scheme: Schemas.vscodeNotebook });
R
rebornix 已提交
71
		const workingCopyAdapter = new class implements IWorkingCopy {
72
			readonly resource = input._workingCopyResource;
R
rebornix 已提交
73 74 75 76 77 78 79 80 81 82 83
			get name() { return input.name; }
			readonly capabilities = input.capabilities;
			readonly onDidChangeDirty = input.onDidChangeDirty;
			readonly onDidChangeContent = input.onDidChangeContent;
			isDirty(): boolean { return input.isDirty(); }
			backup(): Promise<IWorkingCopyBackup> { return input.backup(); }
			save(): Promise<boolean> { return input.save(); }
			revert(options?: IRevertOptions): Promise<void> { return input.revert(options); }
		};

		this._register(this.workingCopyService.registerWorkingCopy(workingCopyAdapter));
84 85
	}

R
rebornix 已提交
86
	capabilities = 0;
87

88
	async backup(): Promise<IWorkingCopyBackup<NotebookDocumentBackupData>> {
89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108
		if (this._notebook.supportBackup) {
			const tokenSource = new CancellationTokenSource();
			const backupId = await this.notebookService.backup(this.viewType, this.resource, tokenSource.token);

			return {
				meta: {
					name: this._name,
					viewType: this._notebook.viewType,
					backupId: backupId
				}
			};
		} else {
			return {
				meta: {
					name: this._name,
					viewType: this._notebook.viewType
				},
				content: this._notebook.createSnapshot(true)
			};
		}
109 110 111
	}

	async revert(options?: IRevertOptions | undefined): Promise<void> {
R
rebornix 已提交
112 113 114 115 116
		if (options?.soft) {
			await this.backupFileService.discardBackup(this.resource);
			return;
		}

117 118 119 120 121 122 123
		if (this._notebook.supportBackup) {
			const tokenSource = new CancellationTokenSource();
			await this.notebookService.revert(this.viewType, this.resource, tokenSource.token);
		} else {
			await this.load({ forceReadFromDisk: true });
		}

R
revert.  
rebornix 已提交
124 125
		this._dirty = false;
		this._onDidChangeDirty.fire();
126 127
	}

128
	async load(options?: INotebookLoadOptions): Promise<NotebookEditorModel> {
R
revert.  
rebornix 已提交
129
		if (options?.forceReadFromDisk) {
130
			return this.loadFromProvider(true, undefined, undefined);
R
revert.  
rebornix 已提交
131
		}
132

R
rebornix 已提交
133 134 135 136
		if (this.isResolved()) {
			return this;
		}

137
		const backup = await this.backupFileService.resolve<NotebookDocumentBackupData>(this._workingCopyResource);
R
rebornix 已提交
138 139 140 141 142

		if (this.isResolved()) {
			return this; // Make sure meanwhile someone else did not succeed in loading
		}

143
		if (backup && backup.meta?.backupId === undefined) {
R
rebornix 已提交
144
			try {
145
				return await this.loadFromBackup(backup.value.create(DefaultEndOfLine.LF), options?.editorId);
R
rebornix 已提交
146 147 148 149 150
			} catch (error) {
				// this.logService.error('[text file model] load() from backup', error); // ignore error and continue to load as file below
			}
		}

151
		return this.loadFromProvider(false, options?.editorId, backup?.meta?.backupId);
R
rebornix 已提交
152 153
	}

154
	private async loadFromBackup(content: ITextBuffer, editorId?: string): Promise<NotebookEditorModel> {
R
rebornix 已提交
155 156 157
		const fullRange = content.getRangeAt(0, content.getLength());
		const data = JSON.parse(content.getValueInRange(fullRange, EndOfLinePreference.LF));

158
		const notebook = await this.notebookService.createNotebookFromBackup(this.viewType!, this.resource, data.metadata, data.languages, data.cells, editorId);
R
rebornix 已提交
159 160 161 162 163 164 165 166
		this._notebook = notebook!;

		this._name = basename(this._notebook!.uri);

		this._register(this._notebook.onDidChangeContent(() => {
			this.setDirty(true);
			this._onDidChangeContent.fire();
		}));
167 168 169
		this._register(this._notebook.onDidChangeUnknown(() => {
			this.setDirty(true);
		}));
R
rebornix 已提交
170

171
		await this.backupFileService.discardBackup(this._workingCopyResource);
R
rebornix 已提交
172 173 174 175 176
		this.setDirty(true);

		return this;
	}

177 178
	private async loadFromProvider(forceReloadFromDisk: boolean, editorId: string | undefined, backupId: string | undefined) {
		const notebook = await this.notebookService.resolveNotebook(this.viewType!, this.resource, forceReloadFromDisk, editorId, backupId);
179 180 181 182
		this._notebook = notebook!;

		this._name = basename(this._notebook!.uri);

183
		this._register(this._notebook.onDidChangeContent(() => {
R
rebornix 已提交
184
			this.setDirty(true);
185 186
			this._onDidChangeContent.fire();
		}));
187 188 189
		this._register(this._notebook.onDidChangeUnknown(() => {
			this.setDirty(true);
		}));
190

191 192 193 194 195
		if (backupId) {
			await this.backupFileService.discardBackup(this._workingCopyResource);
			this.setDirty(true);
		}

196 197 198
		return this;
	}

R
rebornix 已提交
199 200 201 202 203 204 205 206 207 208 209
	isResolved(): boolean {
		return !!this._notebook;
	}

	setDirty(newState: boolean) {
		if (this._dirty !== newState) {
			this._dirty = newState;
			this._onDidChangeDirty.fire();
		}
	}

210 211 212 213 214
	isDirty() {
		return this._dirty;
	}

	async save(): Promise<boolean> {
R
rebornix 已提交
215 216
		const tokenSource = new CancellationTokenSource();
		await this.notebookService.save(this.notebook.viewType, this.notebook.uri, tokenSource.token);
217 218 219 220
		this._dirty = false;
		this._onDidChangeDirty.fire();
		return true;
	}
R
revert.  
rebornix 已提交
221 222 223

	async saveAs(targetResource: URI): Promise<boolean> {
		const tokenSource = new CancellationTokenSource();
R
saveAs  
rebornix 已提交
224
		await this.notebookService.saveAs(this.notebook.viewType, this.notebook.uri, targetResource, tokenSource.token);
R
revert.  
rebornix 已提交
225 226 227 228
		this._dirty = false;
		this._onDidChangeDirty.fire();
		return true;
	}
229 230 231 232 233 234 235 236 237 238 239 240
}

export class NotebookEditorModelManager extends Disposable implements INotebookEditorModelManager {

	private readonly mapResourceToModel = new ResourceMap<NotebookEditorModel>();
	private readonly mapResourceToModelListeners = new ResourceMap<IDisposable>();
	private readonly mapResourceToDisposeListener = new ResourceMap<IDisposable>();
	private readonly mapResourceToPendingModelLoaders = new ResourceMap<Promise<NotebookEditorModel>>();

	// private readonly modelLoadQueue = this._register(new ResourceQueue());

	get models(): NotebookEditorModel[] {
241
		return [...this.mapResourceToModel.values()];
242 243 244 245 246 247 248
	}
	constructor(
		@IInstantiationService readonly instantiationService: IInstantiationService
	) {
		super();
	}

249
	async resolve(resource: URI, viewType: string, editorId?: string): Promise<NotebookEditorModel> {
250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271
		// Return early if model is currently being loaded
		const pendingLoad = this.mapResourceToPendingModelLoaders.get(resource);
		if (pendingLoad) {
			return pendingLoad;
		}

		let modelPromise: Promise<NotebookEditorModel>;
		let model = this.get(resource);
		// let didCreateModel = false;

		// Model exists
		if (model) {
			// if (options?.reload) {
			// } else {
			modelPromise = Promise.resolve(model);
			// }
		}

		// Model does not exist
		else {
			// didCreateModel = true;
			const newModel = model = this.instantiationService.createInstance(NotebookEditorModel, resource, viewType);
R
rebornix 已提交
272
			modelPromise = model.load({ editorId });
273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345

			this.registerModel(newModel);
		}

		// Store pending loads to avoid race conditions
		this.mapResourceToPendingModelLoaders.set(resource, modelPromise);

		// Make known to manager (if not already known)
		this.add(resource, model);

		// dispose and bind new listeners

		try {
			const resolvedModel = await modelPromise;

			// Remove from pending loads
			this.mapResourceToPendingModelLoaders.delete(resource);
			return resolvedModel;
		} catch (error) {
			// Free resources of this invalid model
			if (model) {
				model.dispose();
			}

			// Remove from pending loads
			this.mapResourceToPendingModelLoaders.delete(resource);

			throw error;
		}
	}

	add(resource: URI, model: NotebookEditorModel): void {
		const knownModel = this.mapResourceToModel.get(resource);
		if (knownModel === model) {
			return; // already cached
		}

		// dispose any previously stored dispose listener for this resource
		const disposeListener = this.mapResourceToDisposeListener.get(resource);
		if (disposeListener) {
			disposeListener.dispose();
		}

		// store in cache but remove when model gets disposed
		this.mapResourceToModel.set(resource, model);
		this.mapResourceToDisposeListener.set(resource, model.onDispose(() => this.remove(resource)));
	}

	remove(resource: URI): void {
		this.mapResourceToModel.delete(resource);

		const disposeListener = this.mapResourceToDisposeListener.get(resource);
		if (disposeListener) {
			dispose(disposeListener);
			this.mapResourceToDisposeListener.delete(resource);
		}

		const modelListener = this.mapResourceToModelListeners.get(resource);
		if (modelListener) {
			dispose(modelListener);
			this.mapResourceToModelListeners.delete(resource);
		}
	}


	private registerModel(model: NotebookEditorModel): void {

	}

	get(resource: URI): NotebookEditorModel | undefined {
		return this.mapResourceToModel.get(resource);
	}
}