fileEditorInput.ts 10.0 KB
Newer Older
E
Erich Gamma 已提交
1 2 3 4 5 6
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
'use strict';

7
import {TPromise} from 'vs/base/common/winjs.base';
E
Erich Gamma 已提交
8 9 10 11 12 13 14 15
import {Registry} from 'vs/platform/platform';
import types = require('vs/base/common/types');
import paths = require('vs/base/common/paths');
import {guessMimeTypes} from 'vs/base/common/mime';
import labels = require('vs/base/common/labels');
import URI from 'vs/base/common/uri';
import strings = require('vs/base/common/strings');
import assert = require('vs/base/common/assert');
B
Benjamin Pasero 已提交
16
import {IEditorRegistry, Extensions, EditorModel, EncodingMode, ConfirmResult, IEditorDescriptor} from 'vs/workbench/common/editor';
17
import {BinaryEditorModel} from 'vs/workbench/common/editor/binaryEditorModel';
18
import {IFileOperationResult, FileOperationResult} from 'vs/platform/files/common/files';
19
import {ITextFileService, BINARY_FILE_EDITOR_ID, FILE_EDITOR_INPUT_ID, FileEditorInput as CommonFileEditorInput, AutoSaveMode, ModelState, EventType as FileEventType, TextFileChangeEvent, IFileEditorDescriptor} from 'vs/workbench/parts/files/common/files';
20
import {IWorkspaceContextService} from 'vs/platform/workspace/common/workspace';
E
Erich Gamma 已提交
21
import {IInstantiationService} from 'vs/platform/instantiation/common/instantiation';
22 23
import {IDisposable, dispose} from 'vs/base/common/lifecycle';
import {IEventService} from 'vs/platform/event/common/event';
E
Erich Gamma 已提交
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40

/**
 * A file editor input is the input type for the file editor of file system resources.
 */
export class FileEditorInput extends CommonFileEditorInput {

	// Do ref counting for all inputs that resolved to a model to be able to dispose when count = 0
	private static FILE_EDITOR_MODEL_CLIENTS: { [resource: string]: FileEditorInput[]; } = Object.create(null);

	private resource: URI;
	private mime: string;
	private preferredEncoding: string;

	private name: string;
	private description: string;
	private verboseDescription: string;

41 42
	private toUnbind: IDisposable[];

E
Erich Gamma 已提交
43
	/**
44
	 * An editor input who's contents are retrieved from file services.
E
Erich Gamma 已提交
45 46 47 48 49
	 */
	constructor(
		resource: URI,
		mime: string,
		preferredEncoding: string,
50
		@IEventService private eventService: IEventService,
E
Erich Gamma 已提交
51
		@IInstantiationService private instantiationService: IInstantiationService,
52 53
		@IWorkspaceContextService private contextService: IWorkspaceContextService,
		@ITextFileService private textFileService: ITextFileService
E
Erich Gamma 已提交
54 55 56
	) {
		super();

57 58
		this.toUnbind = [];

E
Erich Gamma 已提交
59 60 61 62 63
		if (resource) {
			this.setResource(resource);
			this.setMime(mime || guessMimeTypes(this.resource.fsPath).join(', '));
			this.preferredEncoding = preferredEncoding;
		}
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78

		this.registerListeners();
	}

	private registerListeners(): void {
		this.toUnbind.push(this.eventService.addListener2(FileEventType.FILE_DIRTY, (e: TextFileChangeEvent) => this.onDirtyStateChange(e)));
		this.toUnbind.push(this.eventService.addListener2(FileEventType.FILE_SAVE_ERROR, (e: TextFileChangeEvent) => this.onDirtyStateChange(e)));
		this.toUnbind.push(this.eventService.addListener2(FileEventType.FILE_SAVED, (e: TextFileChangeEvent) => this.onDirtyStateChange(e)));
		this.toUnbind.push(this.eventService.addListener2(FileEventType.FILE_REVERTED, (e: TextFileChangeEvent) => this.onDirtyStateChange(e)));
	}

	private onDirtyStateChange(e: TextFileChangeEvent): void {
		if (e.resource.toString() === this.resource.toString()) {
			this._onDidChangeDirty.fire();
		}
E
Erich Gamma 已提交
79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107
	}

	public setResource(resource: URI): void {
		if (resource.scheme !== 'file') {
			throw new Error('FileEditorInput can only handle file:// resources.');
		}

		this.resource = resource;

		// Reset resource dependent properties
		this.name = null;
		this.description = null;
		this.verboseDescription = null;
	}

	public getResource(): URI {
		return this.resource;
	}

	public getMime(): string {
		return this.mime;
	}

	public setMime(mime: string): void {
		assert.ok(mime, 'Editor input needs mime type');

		this.mime = mime;
	}

108 109 110 111
	public setPreferredEncoding(encoding: string): void {
		this.preferredEncoding = encoding;
	}

E
Erich Gamma 已提交
112
	public getEncoding(): string {
B
Benjamin Pasero 已提交
113
		const textModel = this.textFileService.models.get(this.resource);
E
Erich Gamma 已提交
114 115 116 117 118 119 120 121 122 123
		if (textModel) {
			return textModel.getEncoding();
		}

		return this.preferredEncoding;
	}

	public setEncoding(encoding: string, mode: EncodingMode): void {
		this.preferredEncoding = encoding;

B
Benjamin Pasero 已提交
124
		const textModel = this.textFileService.models.get(this.resource);
E
Erich Gamma 已提交
125 126 127 128 129
		if (textModel) {
			textModel.setEncoding(encoding, mode);
		}
	}

130
	public getTypeId(): string {
E
Erich Gamma 已提交
131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157
		return FILE_EDITOR_INPUT_ID;
	}

	public getName(): string {
		if (!this.name) {
			this.name = paths.basename(this.resource.fsPath);
		}

		return this.name;
	}

	public getDescription(verbose?: boolean): string {
		if (!verbose) {
			if (!this.description) {
				this.description = labels.getPathLabel(paths.dirname(this.resource.fsPath), this.contextService);
			}

			return this.description;
		}

		if (!this.verboseDescription) {
			this.verboseDescription = labels.getPathLabel(this.resource.fsPath);
		}

		return this.verboseDescription;
	}

158
	public isDirty(): boolean {
159
		const model = this.textFileService.models.get(this.resource);
B
Benjamin Pasero 已提交
160 161 162 163 164 165 166 167 168
		if (!model) {
			return false;
		}

		const state = model.getState();
		if (state === ModelState.CONFLICT || state === ModelState.ERROR) {
			return true; // always indicate dirty state if we are in conflict or error state
		}

B
Benjamin Pasero 已提交
169
		if (this.textFileService.getAutoSaveMode() === AutoSaveMode.AFTER_SHORT_DELAY) {
B
Benjamin Pasero 已提交
170
			return false; // fast auto save enabled so we do not declare dirty
B
Benjamin Pasero 已提交
171 172
		}

B
Benjamin Pasero 已提交
173
		return model.isDirty();
174 175 176 177 178 179 180 181 182 183 184 185 186 187
	}

	public confirmSave(): ConfirmResult {
		return this.textFileService.confirmSave([this.resource]);
	}

	public save(): TPromise<boolean> {
		return this.textFileService.save(this.resource);
	}

	public revert(): TPromise<boolean> {
		return this.textFileService.revert(this.resource);
	}

E
Erich Gamma 已提交
188
	public getPreferredEditorId(candidates: string[]): string {
B
Benjamin Pasero 已提交
189
		const editorRegistry = (<IEditorRegistry>Registry.as(Extensions.Editors));
E
Erich Gamma 已提交
190 191

		// Lookup Editor by Mime
B
Benjamin Pasero 已提交
192
		let descriptor: IEditorDescriptor;
B
Benjamin Pasero 已提交
193
		const mimes = this.mime.split(',');
E
Erich Gamma 已提交
194
		for (let m = 0; m < mimes.length; m++) {
B
Benjamin Pasero 已提交
195
			const mime = strings.trim(mimes[m]);
E
Erich Gamma 已提交
196 197 198 199

			for (let i = 0; i < candidates.length; i++) {
				descriptor = editorRegistry.getEditorById(candidates[i]);

200
				if (types.isFunction((<IFileEditorDescriptor>descriptor).getMimeTypes)) {
B
Benjamin Pasero 已提交
201
					const mimetypes = (<IFileEditorDescriptor>descriptor).getMimeTypes();
E
Erich Gamma 已提交
202
					for (let j = 0; j < mimetypes.length; j++) {
B
Benjamin Pasero 已提交
203
						const mimetype = mimetypes[j];
E
Erich Gamma 已提交
204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223

						// Check for direct mime match
						if (mime === mimetype) {
							return descriptor.getId();
						}

						// Otherwise check for wildcard mime matches
						if (strings.endsWith(mimetype, '/*') && strings.startsWith(mime, mimetype.substring(0, mimetype.length - 1))) {
							return descriptor.getId();
						}
					}
				}
			}
		}

		// Otherwise use default editor
		return BINARY_FILE_EDITOR_ID;
	}

	public resolve(refresh?: boolean): TPromise<EditorModel> {
B
Benjamin Pasero 已提交
224
		const resource = this.resource.toString();
E
Erich Gamma 已提交
225 226

		// Keep clients who resolved the input to support proper disposal
B
Benjamin Pasero 已提交
227
		const clients = FileEditorInput.FILE_EDITOR_MODEL_CLIENTS[resource];
E
Erich Gamma 已提交
228
		if (types.isUndefinedOrNull(clients)) {
B
Benjamin Pasero 已提交
229
			FileEditorInput.FILE_EDITOR_MODEL_CLIENTS[resource] = [this];
E
Erich Gamma 已提交
230
		} else if (this.indexOfClient() === -1) {
B
Benjamin Pasero 已提交
231
			FileEditorInput.FILE_EDITOR_MODEL_CLIENTS[resource].push(this);
E
Erich Gamma 已提交
232 233
		}

234
		return this.textFileService.models.loadOrCreate(this.resource, this.preferredEncoding, refresh).then(null, error => {
E
Erich Gamma 已提交
235

236 237 238
			// In case of an error that indicates that the file is binary or too large, just return with the binary editor model
			if ((<IFileOperationResult>error).fileOperationResult === FileOperationResult.FILE_IS_BINARY || (<IFileOperationResult>error).fileOperationResult === FileOperationResult.FILE_TOO_LARGE) {
				return this.instantiationService.createInstance(BinaryEditorModel, this.resource, this.getName()).load();
E
Erich Gamma 已提交
239 240
			}

241
			// Bubble any other error up
242
			return TPromise.wrapError(error);
E
Erich Gamma 已提交
243 244 245 246
		});
	}

	private indexOfClient(): number {
247 248 249
		const inputs = FileEditorInput.FILE_EDITOR_MODEL_CLIENTS[this.resource.toString()];
		if (inputs) {
			for (let i = 0; i < inputs.length; i++) {
B
Benjamin Pasero 已提交
250
				const client = inputs[i];
E
Erich Gamma 已提交
251 252 253 254 255 256 257 258 259
				if (client === this) {
					return i;
				}
			}
		}

		return -1;
	}

260
	public dispose(): void {
E
Erich Gamma 已提交
261

262 263 264
		// Listeners
		dispose(this.toUnbind);

265 266 267 268
		// Clear from our input cache
		const index = this.indexOfClient();
		if (index >= 0) {
			FileEditorInput.FILE_EDITOR_MODEL_CLIENTS[this.resource.toString()].splice(index, 1);
E
Erich Gamma 已提交
269 270 271 272 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
		}

		super.dispose();
	}

	public matches(otherInput: any): boolean {
		if (super.matches(otherInput) === true) {
			return true;
		}

		if (otherInput) {

			// Note that we can not test for the mime type here because we cache resolved file editor input models by resource. And
			// these models have a fixed mode association that can not be changed afterwards. As such, we always treat this input
			// equal if the resource is equal so that there is always just one text editor model (with undo hisotry etc.) around.
			//
			// !!! DO NOT CHANGE THIS ASSUMPTION !!!
			//
			return otherInput instanceof FileEditorInput && (<FileEditorInput>otherInput).resource.toString() === this.resource.toString();
		}

		return false;
	}

	/**
	 * Exposed so that other internal file API can access the list of all file editor inputs
	 * that have been loaded during the session.
	 */
	public static getAll(desiredFileOrFolderResource: URI): FileEditorInput[] {
B
Benjamin Pasero 已提交
298
		const inputsContainingResource: FileEditorInput[] = [];
E
Erich Gamma 已提交
299

B
Benjamin Pasero 已提交
300 301 302
		const clients = FileEditorInput.FILE_EDITOR_MODEL_CLIENTS;
		for (const resource in clients) {
			const inputs = clients[resource];
E
Erich Gamma 已提交
303 304 305 306 307 308 309 310 311 312

			// Check if path is identical or path is a folder that the content is inside
			if (paths.isEqualOrParent(resource, desiredFileOrFolderResource.toString())) {
				inputsContainingResource.push(...inputs);
			}
		}

		return inputsContainingResource;
	}
}