/*--------------------------------------------------------------------------------------------- * 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 'vs/css!./media/textdiffeditor'; import {Promise, TPromise} from 'vs/base/common/winjs.base'; import nls = require('vs/nls'); import {Builder} from 'vs/base/browser/builder'; import {Action, IAction} from 'vs/base/common/actions'; import {onUnexpectedError} from 'vs/base/common/errors'; import types = require('vs/base/common/types'); import {IDiffEditor} from 'vs/editor/browser/editorBrowser'; import {IDiffEditorOptions, IEditorOptions} from 'vs/editor/common/editorCommon'; import {BaseEditor} from 'vs/workbench/browser/parts/editor/baseEditor'; import {BaseTextEditor} from 'vs/workbench/browser/parts/editor/textEditor'; import {TextEditorOptions, TextDiffEditorOptions, EditorModel, EditorInput, EditorOptions} from 'vs/workbench/common/editor'; import {StringEditorInput} from 'vs/workbench/browser/parts/editor/stringEditorInput'; import {ResourceEditorInput} from 'vs/workbench/browser/parts/editor/resourceEditorInput'; import {DiffEditorInput} from 'vs/workbench/browser/parts/editor/diffEditorInput'; import {DiffNavigator} from 'vs/editor/contrib/diffNavigator/common/diffNavigator'; import {DiffEditorWidget} from 'vs/editor/browser/widget/diffEditorWidget'; import {TextDiffEditorModel} from 'vs/workbench/browser/parts/editor/textDiffEditorModel'; import {DelegatingWorkbenchEditorService} from 'vs/workbench/services/editor/browser/editorService'; import {IFileOperationResult, FileOperationResult} from 'vs/platform/files/common/files'; import {ITelemetryService} from 'vs/platform/telemetry/common/telemetry'; import {IWorkspaceContextService} from 'vs/workbench/services/workspace/common/contextService'; import {IStorageService} from 'vs/platform/storage/common/storage'; import {IConfigurationService, IConfigurationServiceEvent, ConfigurationServiceEventTypes} from 'vs/platform/configuration/common/configuration'; import {IEventService} from 'vs/platform/event/common/event'; import {IInstantiationService} from 'vs/platform/instantiation/common/instantiation'; import {IMessageService} from 'vs/platform/message/common/message'; import {IWorkbenchEditorService} from 'vs/workbench/services/editor/common/editorService'; import {IModeService} from 'vs/editor/common/services/modeService'; /** * The text editor that leverages the monaco diff text editor for the editing experience. */ export class TextDiffEditor extends BaseTextEditor { public static ID = 'workbench.editors.textDiffEditor'; private diffNavigator: DiffNavigator; private nextDiffAction: NavigateAction; private previousDiffAction: NavigateAction; constructor( @ITelemetryService telemetryService: ITelemetryService, @IInstantiationService instantiationService: IInstantiationService, @IWorkspaceContextService contextService: IWorkspaceContextService, @IStorageService storageService: IStorageService, @IMessageService messageService: IMessageService, @IConfigurationService configurationService: IConfigurationService, @IEventService eventService: IEventService, @IWorkbenchEditorService editorService: IWorkbenchEditorService, @IModeService modeService: IModeService ) { super(TextDiffEditor.ID, telemetryService, instantiationService, contextService, storageService, messageService, configurationService, eventService, editorService, modeService); } public getTitle(): string { if (this.input) { return this.input.getName(); } return nls.localize('textDiffEditor', "Text Diff Editor"); } public createEditorControl(parent: Builder): IDiffEditor { this.nextDiffAction = new NavigateAction(this, true); this.previousDiffAction = new NavigateAction(this, false); let delegatingService = this.instantiationService.createInstance(DelegatingWorkbenchEditorService, this, (editor: BaseEditor, input: EditorInput, options?: EditorOptions, arg4?: any) => { // Check if arg4 is a position argument that differs from this editors position if (types.isUndefinedOrNull(arg4) || arg4 === false || arg4 === this.position) { let activeDiffInput = this.getInput(); if (input && options && activeDiffInput) { // Input matches modified side of the diff editor: perform the action on modified side if (input.matches(activeDiffInput.getModifiedInput())) { return this.setInput(this.getInput(), options).then(() => { return true; }); } // Input matches original side of the diff editor: perform the action on original side else if (input.matches(activeDiffInput.getOriginalInput())) { let originalEditor = this.getControl().getOriginalEditor(); if (options instanceof TextEditorOptions) { (options).apply(originalEditor); return TPromise.as(true); } } } } return TPromise.as(false); }); // Create a special child of instantiator that will delegate all calls to openEditor() to the same diff editor if the input matches with the modified one let diffEditorInstantiator = this.instantiationService.createChild({ editorService: delegatingService }); return diffEditorInstantiator.createInstance(DiffEditorWidget, parent.getHTMLElement(), this.getCodeEditorOptions()); } public setInput(input: EditorInput, options: EditorOptions): TPromise { let oldInput = this.getInput(); super.setInput(input, options); // Detect options let forceOpen = options && options.forceOpen; // Same Input if (!forceOpen && input.matches(oldInput)) { // TextOptions (avoiding instanceof here for a reason, do not change!) let textOptions = options; if (textOptions && types.isFunction(textOptions.apply)) { textOptions.apply(this.getControl()); } return TPromise.as(null); } // Dispose previous diff navigator if (this.diffNavigator) { this.diffNavigator.dispose(); } // Different Input (Reload) return this.editorService.resolveEditorModel(input, true /* Reload */).then((resolvedModel: EditorModel) => { // Assert Model Instance if (!(resolvedModel instanceof TextDiffEditorModel) && this.openAsBinary(input, options)) { return null; } // Assert that the current input is still the one we expect. This prevents a race condition when loading a diff takes long and another input was set meanwhile if (!this.getInput() || this.getInput() !== input) { return null; } // Editor let diffEditor = this.getControl(); diffEditor.setModel((resolvedModel).textDiffEditorModel); // Respect text diff editor options let autoRevealFirstChange = true; if (options instanceof TextDiffEditorOptions) { let textDiffOptions = (options); autoRevealFirstChange = !types.isUndefinedOrNull(textDiffOptions.autoRevealFirstChange) ? textDiffOptions.autoRevealFirstChange : autoRevealFirstChange; } // listen on diff updated changes to reveal the first change this.diffNavigator = new DiffNavigator(diffEditor, { alwaysRevealFirst: autoRevealFirstChange }); this.diffNavigator.addListener(DiffNavigator.Events.UPDATED, () => { this.nextDiffAction.updateEnablement(); this.previousDiffAction.updateEnablement(); }); // Handle TextOptions if (options && types.isFunction((options).apply)) { (options).apply(diffEditor); } // Apply options again because input has changed diffEditor.updateOptions(this.getCodeEditorOptions()); }, (error) => { // In case we tried to open a file and the response indicates that this is not a text file, fallback to binary diff. if (this.isFileBinaryError(error) && this.openAsBinary(input, options)) { return null; } // Otherwise make sure the error bubbles up return Promise.wrapError(error); }); } private openAsBinary(input: EditorInput, options: EditorOptions): boolean { if (input instanceof DiffEditorInput) { let originalInput = input.originalInput; let modifiedInput = input.modifiedInput; let binaryDiffInput = new DiffEditorInput(input.getName(), input.getDescription(), originalInput, modifiedInput, true); this.editorService.openEditor(binaryDiffInput, options, this.position).done(null, onUnexpectedError); return true; } return false; } protected getCodeEditorOptions(): IEditorOptions { let options: IDiffEditorOptions = super.getCodeEditorOptions(); let input = this.input; if (input && types.isFunction((input).getModifiedInput)) { let modifiedInput = (input).getModifiedInput(); let readOnly = modifiedInput instanceof StringEditorInput || modifiedInput instanceof ResourceEditorInput; options.readOnly = readOnly; } return options; } private isFileBinaryError(error: Error[]): boolean; private isFileBinaryError(error: Error): boolean; private isFileBinaryError(error: any): boolean { if (types.isArray(error)) { let errors = error; return errors.some((e) => this.isFileBinaryError(e)); } return (error).fileOperationResult === FileOperationResult.FILE_IS_BINARY; } public clearInput(): void { // Dispose previous diff navigator if (this.diffNavigator) { this.diffNavigator.dispose(); } // Clear Model this.getControl().setModel(null); // Pass to super super.clearInput(); } public getDiffNavigator(): DiffNavigator { return this.diffNavigator; } public getActions(): IAction[] { return [ this.previousDiffAction, this.nextDiffAction ]; } public getSecondaryActions(): IAction[] { let actions = super.getSecondaryActions(); const control = this.getControl(); let inlineModeActive = control && !control.renderSideBySide; let inlineLabel = nls.localize('inlineDiffLabel', "Switch to Inline View"); let sideBySideLabel = nls.localize('sideBySideDiffLabel', "Switch to Side by Side View"); // Action to toggle editor mode from inline to side by side let toggleEditorModeAction = new Action('toggle.diff.editorMode', inlineModeActive ? sideBySideLabel : inlineLabel, null, true, () => { this.getControl().updateOptions({ renderSideBySide: inlineModeActive }); inlineModeActive = !inlineModeActive; toggleEditorModeAction.label = inlineModeActive ? sideBySideLabel : inlineLabel; return Promise.as(true); }); toggleEditorModeAction.order = 50; // Closer to the end actions.push(...[ toggleEditorModeAction ]); return actions; } public getControl(): IDiffEditor { return super.getControl(); } public dispose(): void { // Dispose previous diff navigator if (this.diffNavigator) { this.diffNavigator.dispose(); } super.dispose(); } } class NavigateAction extends Action { static ID_NEXT = 'workbench.action.nextDiff'; static ID_PREV = 'workbench.action.previousDiff'; private editor: TextDiffEditor; private next: boolean; constructor(editor: TextDiffEditor, next: boolean) { super(next ? NavigateAction.ID_NEXT : NavigateAction.ID_PREV); this.editor = editor; this.next = next; this.label = this.next ? nls.localize('navigate.next.label', "Next Change") : nls.localize('navigate.prev.label', "Previous Change"); this.class = this.next ? 'textdiff-editor-action next' : 'textdiff-editor-action previous'; this.enabled = false; } public run(): Promise { if (this.next) { this.editor.getDiffNavigator().next(); } else { this.editor.getDiffNavigator().previous(); } return null; } public updateEnablement(): void { this.enabled = this.editor.getDiffNavigator().canNavigate(); } }