/*--------------------------------------------------------------------------------------------- * 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 {TPromise} from 'vs/base/common/winjs.base'; import nls = require('vs/nls'); import errors = require('vs/base/common/errors'); import {MIME_BINARY, MIME_TEXT} from 'vs/base/common/mime'; import types = require('vs/base/common/types'); import paths = require('vs/base/common/paths'); import {IEditorViewState} from 'vs/editor/common/editorCommon'; import {Action} from 'vs/base/common/actions'; import {Scope} from 'vs/workbench/common/memento'; import {IEditorOptions} from 'vs/editor/common/editorCommon'; import {VIEWLET_ID, TEXT_FILE_EDITOR_ID} from 'vs/workbench/parts/files/common/files'; import {SaveErrorHandler} from 'vs/workbench/parts/files/browser/saveErrorHandler'; import {BaseTextEditor} from 'vs/workbench/browser/parts/editor/textEditor'; import {EditorInput, EditorOptions, TextEditorOptions, EditorModel} from 'vs/workbench/common/editor'; import {TextFileEditorModel} from 'vs/workbench/parts/files/common/editors/textFileEditorModel'; import {BinaryEditorModel} from 'vs/workbench/common/editor/binaryEditorModel'; import {FileEditorInput} from 'vs/workbench/parts/files/common/editors/fileEditorInput'; import {ExplorerViewlet} from 'vs/workbench/parts/files/browser/explorerViewlet'; import {IViewletService} from 'vs/workbench/services/viewlet/common/viewletService'; import {IFileOperationResult, FileOperationResult, FileChangesEvent, EventType, IFileService} from 'vs/platform/files/common/files'; import {ITelemetryService} from 'vs/platform/telemetry/common/telemetry'; import {IWorkspaceContextService} from 'vs/platform/workspace/common/workspace'; import {IStorageService} from 'vs/platform/storage/common/storage'; import {IConfigurationService} from 'vs/platform/configuration/common/configuration'; import {IEventService} from 'vs/platform/event/common/event'; import {IInstantiationService} from 'vs/platform/instantiation/common/instantiation'; import {IMessageService, CancelAction} from 'vs/platform/message/common/message'; import {IWorkbenchEditorService} from 'vs/workbench/services/editor/common/editorService'; import {IThemeService} from 'vs/workbench/services/themes/common/themeService'; const TEXT_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'textEditorViewState'; interface ITextEditorViewState { 0?: IEditorViewState; 1?: IEditorViewState; 2?: IEditorViewState; } /** * An implementation of editor for file system resources. */ export class TextFileEditor extends BaseTextEditor { public static ID = TEXT_FILE_EDITOR_ID; constructor( @ITelemetryService telemetryService: ITelemetryService, @IFileService private fileService: IFileService, @IViewletService private viewletService: IViewletService, @IInstantiationService instantiationService: IInstantiationService, @IWorkspaceContextService contextService: IWorkspaceContextService, @IStorageService storageService: IStorageService, @IMessageService messageService: IMessageService, @IConfigurationService configurationService: IConfigurationService, @IEventService eventService: IEventService, @IWorkbenchEditorService editorService: IWorkbenchEditorService, @IThemeService themeService: IThemeService ) { super(TextFileEditor.ID, telemetryService, instantiationService, contextService, storageService, messageService, configurationService, eventService, editorService, themeService); // Since we are the one providing save-support for models, we hook up the error handler for saving TextFileEditorModel.setSaveErrorHandler(instantiationService.createInstance(SaveErrorHandler)); // Clear view state for deleted files this.toUnbind.push(this.eventService.addListener2(EventType.FILE_CHANGES, (e: FileChangesEvent) => this.onFilesChanged(e))); } private onFilesChanged(e: FileChangesEvent): void { const deleted = e.getDeleted(); if (deleted && deleted.length) { this.clearTextEditorViewState(this.storageService, deleted.map((d) => d.resource.toString())); } } public getTitle(): string { return this.getInput() ? this.getInput().getName() : nls.localize('textFileEditor', "Text File Editor"); } public setInput(input: EditorInput, options: EditorOptions): TPromise { const oldInput = this.getInput(); super.setInput(input, options); // Detect options const forceOpen = options && options.forceOpen; // Same Input if (!forceOpen && input.matches(oldInput)) { // TextOptions (avoiding instanceof here for a reason, do not change!) if (options && types.isFunction((options).apply)) { (options).apply(this.getControl()); } return TPromise.as(null); } // Remember view settings if input changes if (oldInput) { this.saveTextEditorViewState(this.storageService, (oldInput).getResource().toString()); } // Different Input (Reload) return this.editorService.resolveEditorModel(input, true /* Reload */).then((resolvedModel: EditorModel) => { // There is a special case where the text editor has to handle binary file editor input: if a file with application/unknown // mime has been resolved and cached before, it maybe an actual instance of BinaryEditorModel. In this case our text // editor has to open this model using the binary editor. We return early in this case. if (resolvedModel instanceof BinaryEditorModel && this.openAsBinary(input, options)) { return null; } // Assert Model interface if (!(resolvedModel instanceof TextFileEditorModel)) { return TPromise.wrapError('Invalid editor input. Text file editor requires a model instance of TextFileEditorModel.'); } // Check Model state const textFileModel = resolvedModel; if ( !this.getInput() || // editor got hidden meanwhile textFileModel.isDisposed() || // input got disposed meanwhile (this.getInput()).getResource().toString() !== textFileModel.getResource().toString() // a different input was set meanwhile ) { return null; } // log the time it takes the editor to render the resource const mode = textFileModel.textEditorModel.getMode(); const setModelEvent = this.telemetryService.timedPublicLog('editorSetModel', { mode: mode && mode.getId(), resource: textFileModel.textEditorModel.uri.toString(), }); // Editor const textEditor = this.getControl(); textEditor.setModel(textFileModel.textEditorModel); // stop the event setModelEvent.stop(); // TextOptions (avoiding instanceof here for a reason, do not change!) let optionsGotApplied = false; if (options && types.isFunction((options).apply)) { optionsGotApplied = (options).apply(textEditor); } // Otherwise restore View State if (!optionsGotApplied) { const editorViewState = this.loadTextEditorViewState(this.storageService, (this.getInput()).getResource().toString()); if (editorViewState) { textEditor.restoreViewState(editorViewState); } } }, (error) => { // In case we tried to open a file inside the text editor and the response // indicates that this is not a text file, reopen the file through the binary // editor by using application/octet-stream as mime. if ((error).fileOperationResult === FileOperationResult.FILE_IS_BINARY && this.openAsBinary(input, options)) { return; } // Similar, handle case where we were asked to open a folder in the text editor. if ((error).fileOperationResult === FileOperationResult.FILE_IS_DIRECTORY && this.openAsFolder(input)) { return; } // Offer to create a file from the error if we have a file not found and the name is valid if ((error).fileOperationResult === FileOperationResult.FILE_NOT_FOUND && paths.isValidBasename(paths.basename((input).getResource().fsPath))) { return TPromise.wrapError(errors.create(errors.toErrorMessage(error), { actions: [ new Action('workbench.files.action.createMissingFile', nls.localize('createFile', "Create File"), null, true, () => { return this.fileService.updateContent((input).getResource(), '').then(() => { // Open return this.editorService.openEditor({ resource: (input).getResource(), mime: MIME_TEXT, options: { pinned: true // new file gets pinned by default } }); }); }), CancelAction ] })); } // Otherwise make sure the error bubbles up return TPromise.wrapError(error); }); } private openAsBinary(input: EditorInput, options: EditorOptions): boolean { if (input instanceof FileEditorInput) { const fileEditorInput = input; const fileInputBinary = this.instantiationService.createInstance(FileEditorInput, fileEditorInput.getResource(), MIME_BINARY, void 0); this.editorService.openEditor(fileInputBinary, options, this.position).done(null, errors.onUnexpectedError); return true; } return false; } private openAsFolder(input: EditorInput): boolean { // Since we cannot open a folder, we have to restore the previous input if any and close the editor this.editorService.closeEditor(this.position, this.input).done(() => { // Best we can do is to reveal the folder in the explorer if (input instanceof FileEditorInput) { const fileEditorInput = input; // Reveal if we have a workspace path if (this.contextService.isInsideWorkspace(fileEditorInput.getResource())) { this.viewletService.openViewlet(VIEWLET_ID, true).done((viewlet: ExplorerViewlet) => { return viewlet.getExplorerView().select(fileEditorInput.getResource(), true); }, errors.onUnexpectedError); } } }, errors.onUnexpectedError); return true; // in any case we handled it } protected getCodeEditorOptions(): IEditorOptions { const options = super.getCodeEditorOptions(); const input = this.getInput(); const inputName = input && input.getName(); options.ariaLabel = inputName ? nls.localize('fileEditorWithInputAriaLabel', "{0}. Text file editor.", inputName) : nls.localize('fileEditorAriaLabel', "Text file editor."); return options; } /** * Saves the text editor view state under the given key. */ private saveTextEditorViewState(storageService: IStorageService, key: string): void { const memento = this.getMemento(storageService, Scope.WORKSPACE); let textEditorViewStateMemento = memento[TEXT_EDITOR_VIEW_STATE_PREFERENCE_KEY]; if (!textEditorViewStateMemento) { textEditorViewStateMemento = Object.create(null); memento[TEXT_EDITOR_VIEW_STATE_PREFERENCE_KEY] = textEditorViewStateMemento; } const editorViewState = this.getControl().saveViewState(); let fileViewState: ITextEditorViewState = textEditorViewStateMemento[key]; if (!fileViewState) { fileViewState = Object.create(null); textEditorViewStateMemento[key] = fileViewState; } if (typeof this.position === 'number') { fileViewState[this.position] = editorViewState; } } /** * Clears the text editor view state under the given key. */ private clearTextEditorViewState(storageService: IStorageService, keys: string[]): void { const memento = this.getMemento(storageService, Scope.WORKSPACE); const textEditorViewStateMemento = memento[TEXT_EDITOR_VIEW_STATE_PREFERENCE_KEY]; if (textEditorViewStateMemento) { keys.forEach(key => delete textEditorViewStateMemento[key]); } } /** * Loads the text editor view state for the given key and returns it. */ private loadTextEditorViewState(storageService: IStorageService, key: string): IEditorViewState { const memento = this.getMemento(storageService, Scope.WORKSPACE); const textEditorViewStateMemento = memento[TEXT_EDITOR_VIEW_STATE_PREFERENCE_KEY]; if (textEditorViewStateMemento) { const fileViewState: ITextEditorViewState = textEditorViewStateMemento[key]; if (fileViewState) { return fileViewState[this.position]; } } return null; } public clearInput(): void { // Keep editor view state in settings to restore when coming back if (this.input) { this.saveTextEditorViewState(this.storageService, (this.input).getResource().toString()); } // Clear Model this.getControl().setModel(null); // Pass to super super.clearInput(); } public shutdown(): void { // Save View State if (this.input) { this.saveTextEditorViewState(this.storageService, (this.input).getResource().toString()); } // Call Super super.shutdown(); } }