untitledEditorService.ts 7.4 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.
 *--------------------------------------------------------------------------------------------*/
'use strict';

import URI from 'vs/base/common/uri';
J
Johannes Rieken 已提交
8
import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
9
import arrays = require('vs/base/common/arrays');
J
Johannes Rieken 已提交
10 11
import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorInput';
import Event, { Emitter, once } from 'vs/base/common/event';
12

B
Benjamin Pasero 已提交
13
export const IUntitledEditorService = createDecorator<IUntitledEditorService>('untitledEditorService');
14 15 16

export interface IUntitledEditorService {

17
	_serviceBrand: any;
18

19 20 21 22 23
	/**
	 * Events for when untitled editors content changes (e.g. any keystroke).
	 */
	onDidChangeContent: Event<URI>;

24 25 26
	/**
	 * Events for when untitled editors change (e.g. getting dirty, saved or reverted).
	 */
B
Benjamin Pasero 已提交
27
	onDidChangeDirty: Event<URI>;
28

29 30 31 32 33
	/**
	 * Events for when untitled editor encodings change.
	 */
	onDidChangeEncoding: Event<URI>;

34 35 36 37 38
	/**
	 * Events for when untitled editors are disposed.
	 */
	onDidDisposeModel: Event<URI>;

39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
	/**
	 * Returns the untitled editor input matching the provided resource.
	 */
	get(resource: URI): UntitledEditorInput;

	/**
	 * Returns all untitled editor inputs.
	 */
	getAll(resources?: URI[]): UntitledEditorInput[];

	/**
	 * Returns dirty untitled editors as resource URIs.
	 */
	getDirty(): URI[];

	/**
	 * Returns true iff the provided resource is dirty.
	 */
	isDirty(resource: URI): boolean;

59 60 61 62 63
	/**
	 * Reverts the untitled resources if found.
	 */
	revertAll(resources?: URI[]): URI[];

64 65 66 67 68 69 70
	/**
	 * Creates a new untitled input with the optional resource URI or returns an existing one
	 * if the provided resource exists already as untitled input.
	 *
	 * It is valid to pass in a file resource. In that case the path will be used as identifier.
	 * The use case is to be able to create a new file with a specific path with VSCode.
	 */
71
	createOrGet(resource?: URI, modeId?: string, hasBackupToRestore?: boolean): UntitledEditorInput;
72 73 74 75 76

	/**
	 * A check to find out if a untitled resource has a file path associated or not.
	 */
	hasAssociatedFilePath(resource: URI): boolean;
77 78 79
}

export class UntitledEditorService implements IUntitledEditorService {
80

81
	public _serviceBrand: any;
82 83 84 85

	private static CACHE: { [resource: string]: UntitledEditorInput } = Object.create(null);
	private static KNOWN_ASSOCIATED_FILE_PATHS: { [resource: string]: boolean } = Object.create(null);

86
	private _onDidChangeContent: Emitter<URI>;
B
Benjamin Pasero 已提交
87
	private _onDidChangeDirty: Emitter<URI>;
88
	private _onDidChangeEncoding: Emitter<URI>;
89
	private _onDidDisposeModel: Emitter<URI>;
90

91 92 93
	constructor(
		@IInstantiationService private instantiationService: IInstantiationService
	) {
94
		this._onDidChangeContent = new Emitter<URI>();
B
Benjamin Pasero 已提交
95
		this._onDidChangeDirty = new Emitter<URI>();
96
		this._onDidChangeEncoding = new Emitter<URI>();
97 98 99 100 101
		this._onDidDisposeModel = new Emitter<URI>();
	}

	public get onDidDisposeModel(): Event<URI> {
		return this._onDidDisposeModel.event;
102 103
	}

104 105 106 107
	public get onDidChangeContent(): Event<URI> {
		return this._onDidChangeContent.event;
	}

B
Benjamin Pasero 已提交
108
	public get onDidChangeDirty(): Event<URI> {
109
		return this._onDidChangeDirty.event;
110 111
	}

112 113 114 115
	public get onDidChangeEncoding(): Event<URI> {
		return this._onDidChangeEncoding.event;
	}

116 117 118 119 120 121 122 123 124 125 126 127
	public get(resource: URI): UntitledEditorInput {
		return UntitledEditorService.CACHE[resource.toString()];
	}

	public getAll(resources?: URI[]): UntitledEditorInput[] {
		if (resources) {
			return arrays.coalesce(resources.map((r) => this.get(r)));
		}

		return Object.keys(UntitledEditorService.CACHE).map((key) => UntitledEditorService.CACHE[key]);
	}

128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143
	public revertAll(resources?: URI[], force?: boolean): URI[] {
		const reverted: URI[] = [];

		const untitledInputs = this.getAll(resources);
		untitledInputs.forEach(input => {
			if (input) {
				input.revert();
				input.dispose();

				reverted.push(input.getResource());
			}
		});

		return reverted;
	}

144
	public isDirty(resource: URI): boolean {
145
		const input = this.get(resource);
146 147 148 149 150 151 152 153 154 155 156

		return input && input.isDirty();
	}

	public getDirty(): URI[] {
		return Object.keys(UntitledEditorService.CACHE)
			.map((key) => UntitledEditorService.CACHE[key])
			.filter((i) => i.isDirty())
			.map((i) => i.getResource());
	}

157
	public createOrGet(resource?: URI, modeId?: string, hasBackupToRestore?: boolean): UntitledEditorInput {
158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173
		let hasAssociatedFilePath = false;
		if (resource) {
			hasAssociatedFilePath = (resource.scheme === 'file');
			resource = this.resourceToUntitled(resource); // ensure we have the right scheme

			if (hasAssociatedFilePath) {
				UntitledEditorService.KNOWN_ASSOCIATED_FILE_PATHS[resource.toString()] = true; // remember for future lookups
			}
		}

		// Return existing instance if asked for it
		if (resource && UntitledEditorService.CACHE[resource.toString()]) {
			return UntitledEditorService.CACHE[resource.toString()];
		}

		// Create new otherwise
174
		return this.doCreate(resource, hasAssociatedFilePath, modeId, hasBackupToRestore);
175 176
	}

177
	private doCreate(resource?: URI, hasAssociatedFilePath?: boolean, modeId?: string, hasBackupToRestore?: boolean): UntitledEditorInput {
178 179 180 181 182
		if (!resource) {

			// Create new taking a resource URI that is not already taken
			let counter = Object.keys(UntitledEditorService.CACHE).length + 1;
			do {
183
				resource = URI.from({ scheme: UntitledEditorInput.SCHEMA, path: 'Untitled-' + counter });
184 185 186 187
				counter++;
			} while (Object.keys(UntitledEditorService.CACHE).indexOf(resource.toString()) >= 0);
		}

188
		const input = this.instantiationService.createInstance(UntitledEditorInput, resource, hasAssociatedFilePath, modeId);
B
Benjamin Pasero 已提交
189 190 191 192 193
		if (input.isDirty()) {
			setTimeout(() => {
				this._onDidChangeDirty.fire(resource);
			}, 0 /* prevent race condition between creating input and emitting dirty event */);
		}
194 195 196 197

		const contentListener = input.onDidModelChangeContent(() => {
			this._onDidChangeContent.fire(resource);
		});
198

199
		const dirtyListener = input.onDidChangeDirty(() => {
B
Benjamin Pasero 已提交
200
			this._onDidChangeDirty.fire(resource);
201 202
		});

203 204 205 206
		const encodingListener = input.onDidModelChangeEncoding(() => {
			this._onDidChangeEncoding.fire(resource);
		});

207 208 209 210
		const disposeListener = input.onDispose(() => {
			this._onDidDisposeModel.fire(resource);
		});

211
		// Remove from cache on dispose
212 213
		const onceDispose = once(input.onDispose);
		onceDispose(() => {
214 215
			delete UntitledEditorService.CACHE[input.getResource().toString()];
			delete UntitledEditorService.KNOWN_ASSOCIATED_FILE_PATHS[input.getResource().toString()];
216
			contentListener.dispose();
217 218
			dirtyListener.dispose();
			encodingListener.dispose();
219
			disposeListener.dispose();
220 221 222 223 224 225 226 227 228 229 230 231 232
		});

		// Add to cache
		UntitledEditorService.CACHE[resource.toString()] = input;

		return input;
	}

	private resourceToUntitled(resource: URI): URI {
		if (resource.scheme === UntitledEditorInput.SCHEMA) {
			return resource;
		}

233
		return URI.from({ scheme: UntitledEditorInput.SCHEMA, path: resource.fsPath });
234 235 236 237 238
	}

	public hasAssociatedFilePath(resource: URI): boolean {
		return !!UntitledEditorService.KNOWN_ASSOCIATED_FILE_PATHS[resource.toString()];
	}
239 240

	public dispose(): void {
241
		this._onDidChangeContent.dispose();
242 243
		this._onDidChangeDirty.dispose();
		this._onDidChangeEncoding.dispose();
244
		this._onDidDisposeModel.dispose();
245
	}
246
}