From 92dd82088f0a18824d94c6928d52ba1b436041f1 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Wed, 16 Nov 2016 10:24:03 +0100 Subject: [PATCH] wip: dirtydiff as a workbench concept --- .../electron-browser/workbench.main.ts | 2 + .../workbench/electron-browser/workbench.ts | 5 + .../git/browser/gitWorkbenchContributions.ts | 342 ------------------ .../parts/git/common/gitDirtyDiff.ts | 80 ++++ .../git/electron-browser/git.contribution.ts | 11 +- .../parts/scm/browser/dirtydiffDecorator.ts | 218 +++++++++++ .../parts/scm/browser/scm.contribution.ts | 13 + .../services/scm/common/dirtydiff.ts | 25 ++ .../services/scm/common/dirtydiffService.ts | 43 +++ 9 files changed, 394 insertions(+), 345 deletions(-) create mode 100644 src/vs/workbench/parts/git/common/gitDirtyDiff.ts create mode 100644 src/vs/workbench/parts/scm/browser/dirtydiffDecorator.ts create mode 100644 src/vs/workbench/parts/scm/browser/scm.contribution.ts create mode 100644 src/vs/workbench/services/scm/common/dirtydiff.ts create mode 100644 src/vs/workbench/services/scm/common/dirtydiffService.ts diff --git a/src/vs/workbench/electron-browser/workbench.main.ts b/src/vs/workbench/electron-browser/workbench.main.ts index efc06c1ca57..07149fa38b8 100644 --- a/src/vs/workbench/electron-browser/workbench.main.ts +++ b/src/vs/workbench/electron-browser/workbench.main.ts @@ -40,6 +40,8 @@ import 'vs/workbench/parts/search/browser/search.contribution'; import 'vs/workbench/parts/search/browser/searchViewlet'; // can be packaged separately import 'vs/workbench/parts/search/browser/openAnythingHandler'; // can be packaged separately +import 'vs/workbench/parts/scm/browser/scm.contribution'; + import 'vs/workbench/parts/git/electron-browser/git.contribution'; import 'vs/workbench/parts/git/browser/gitQuickOpen'; import 'vs/workbench/parts/git/browser/gitActions.contribution'; diff --git a/src/vs/workbench/electron-browser/workbench.ts b/src/vs/workbench/electron-browser/workbench.ts index f9bbd8e4713..33a5f2ac864 100644 --- a/src/vs/workbench/electron-browser/workbench.ts +++ b/src/vs/workbench/electron-browser/workbench.ts @@ -75,6 +75,8 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { TextFileService } from 'vs/workbench/services/textfile/electron-browser/textFileService'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { IDirtyDiffService } from 'vs/workbench/services/scm/common/dirtydiff'; +import { DirtyDiffService } from 'vs/workbench/services/scm/common/dirtydiffService'; import { TextModelResolverService } from 'vs/workbench/services/textmodelResolver/common/textModelResolverService'; import { ITextModelResolverService } from 'vs/platform/textmodelResolver/common/resolver'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; @@ -456,6 +458,9 @@ export class Workbench implements IPartService { // Text File Service serviceCollection.set(ITextFileService, this.instantiationService.createInstance(TextFileService)); + // DirtyDiff Service + serviceCollection.set(IDirtyDiffService, this.instantiationService.createInstance(DirtyDiffService)); + // Backup Model Service serviceCollection.set(IBackupModelService, this.instantiationService.createInstance(BackupModelService)); diff --git a/src/vs/workbench/parts/git/browser/gitWorkbenchContributions.ts b/src/vs/workbench/parts/git/browser/gitWorkbenchContributions.ts index 32d184db3d6..f0b0644c178 100644 --- a/src/vs/workbench/parts/git/browser/gitWorkbenchContributions.ts +++ b/src/vs/workbench/parts/git/browser/gitWorkbenchContributions.ts @@ -8,14 +8,9 @@ import 'vs/css!./media/git.contribution'; import nls = require('vs/nls'); import async = require('vs/base/common/async'); -import errors = require('vs/base/common/errors'); -import paths = require('vs/base/common/paths'); import lifecycle = require('vs/base/common/lifecycle'); -import winjs = require('vs/base/common/winjs.base'); import ext = require('vs/workbench/common/contributions'); import git = require('vs/workbench/parts/git/common/git'); -import common = require('vs/editor/common/editorCommon'); -import widget = require('vs/editor/browser/codeEditor'); import viewlet = require('vs/workbench/browser/viewlet'); import statusbar = require('vs/workbench/browser/parts/statusbar/statusbar'); import platform = require('vs/platform/platform'); @@ -30,18 +25,10 @@ import quickopen = require('vs/workbench/browser/quickopen'); import 'vs/workbench/parts/git/browser/gitEditorContributions'; import { IActivityService, ProgressBadge, NumberBadge } from 'vs/workbench/services/activity/common/activityService'; 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 { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; -import { IModelService } from 'vs/editor/common/services/modelService'; -import { RawText } from 'vs/editor/common/model/textModel'; -import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; -import URI from 'vs/base/common/uri'; -import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService'; -import { Schemas } from 'vs/base/common/network'; import IGitService = git.IGitService; @@ -120,330 +107,6 @@ export class StatusUpdater implements ext.IWorkbenchContribution { } } -class DirtyDiffModelDecorator { - static ID = 'vs.git.editor.dirtyDiffDecorator'; - static MODIFIED_DECORATION_OPTIONS: common.IModelDecorationOptions = { - linesDecorationsClassName: 'git-dirty-modified-diff-glyph', - isWholeLine: true, - overviewRuler: { - color: 'rgba(0, 122, 204, 0.6)', - darkColor: 'rgba(0, 122, 204, 0.6)', - position: common.OverviewRulerLane.Left - } - }; - static ADDED_DECORATION_OPTIONS: common.IModelDecorationOptions = { - linesDecorationsClassName: 'git-dirty-added-diff-glyph', - isWholeLine: true, - overviewRuler: { - color: 'rgba(0, 122, 204, 0.6)', - darkColor: 'rgba(0, 122, 204, 0.6)', - position: common.OverviewRulerLane.Left - } - }; - static DELETED_DECORATION_OPTIONS: common.IModelDecorationOptions = { - linesDecorationsClassName: 'git-dirty-deleted-diff-glyph', - isWholeLine: true, - overviewRuler: { - color: 'rgba(0, 122, 204, 0.6)', - darkColor: 'rgba(0, 122, 204, 0.6)', - position: common.OverviewRulerLane.Left - } - }; - - private modelService: IModelService; - private editorWorkerService: IEditorWorkerService; - private editorService: IWorkbenchEditorService; - private contextService: IWorkspaceContextService; - private gitService: IGitService; - - private model: common.IModel; - private _originalContentsURI: URI; - private path: string; - private decorations: string[]; - - private delayer: async.ThrottledDelayer; - private diffDelayer: async.ThrottledDelayer; - private toDispose: lifecycle.IDisposable[]; - - constructor(model: common.IModel, path: string, - @IModelService modelService: IModelService, - @IEditorWorkerService editorWorkerService: IEditorWorkerService, - @IWorkbenchEditorService editorService: IWorkbenchEditorService, - @IWorkspaceContextService contextService: IWorkspaceContextService, - @IGitService gitService: IGitService - ) { - this.modelService = modelService; - this.editorWorkerService = editorWorkerService; - this.editorService = editorService; - this.contextService = contextService; - this.gitService = gitService; - - this.model = model; - this._originalContentsURI = model.uri.with({ scheme: Schemas.internal }); - this.path = path; - this.decorations = []; - - this.delayer = new async.ThrottledDelayer(500); - this.diffDelayer = new async.ThrottledDelayer(200); - - this.toDispose = []; - this.toDispose.push(model.onDidChangeContent(() => this.triggerDiff())); - this.toDispose.push(this.gitService.addListener2(git.ServiceEvents.STATE_CHANGED, () => this.onChanges())); - this.toDispose.push(this.gitService.addListener2(git.ServiceEvents.OPERATION_END, e => { - if (e.operation.id !== git.ServiceOperations.BACKGROUND_FETCH) { - this.onChanges(); - } - })); - - this.onChanges(); - } - - private onChanges(): void { - if (!this.gitService) { - return; - } - - if (this.gitService.getState() !== git.ServiceState.OK) { - return; - } - - // go through all interesting models - this.trigger(); - } - - private trigger(): void { - this.delayer - .trigger(() => this.diffOriginalContents()) - .done(null, errors.onUnexpectedError); - } - - private diffOriginalContents(): winjs.TPromise { - return this.getOriginalContents() - .then(contents => { - if (!this.model || this.model.isDisposed()) { - return; // disposed - } - - if (!contents) { - // untracked file - this.modelService.destroyModel(this._originalContentsURI); - return this.triggerDiff(); - } - - let originalModel = this.modelService.getModel(this._originalContentsURI); - if (originalModel) { - let contentsRawText = RawText.fromStringWithModelOptions(contents, originalModel); - - // return early if nothing has changed - if (originalModel.equals(contentsRawText)) { - return winjs.TPromise.as(null); - } - - // we already have the original contents - originalModel.setValueFromRawText(contentsRawText); - } else { - // this is the first time we load the original contents - this.modelService.createModel(contents, null, this._originalContentsURI); - } - - return this.triggerDiff(); - }); - } - - private getOriginalContents(): winjs.TPromise { - var gitModel = this.gitService.getModel(); - var treeish = gitModel.getStatus().find(this.path, git.StatusType.INDEX) ? '~' : 'HEAD'; - - return this.gitService.buffer(this.path, treeish); - } - - private triggerDiff(): winjs.Promise { - if (!this.diffDelayer) { - return winjs.TPromise.as(null); - } - - return this.diffDelayer.trigger(() => { - if (!this.model || this.model.isDisposed()) { - return winjs.TPromise.as([]); // disposed - } - - return this.editorWorkerService.computeDirtyDiff(this._originalContentsURI, this.model.uri, true); - }).then((diff: common.IChange[]) => { - if (!this.model || this.model.isDisposed()) { - return; // disposed - } - - return this.decorations = this.model.deltaDecorations(this.decorations, DirtyDiffModelDecorator.changesToDecorations(diff || [])); - }); - } - - private static changesToDecorations(diff: common.IChange[]): common.IModelDeltaDecoration[] { - return diff.map((change) => { - var startLineNumber = change.modifiedStartLineNumber; - var endLineNumber = change.modifiedEndLineNumber || startLineNumber; - - // Added - if (change.originalEndLineNumber === 0) { - return { - range: { - startLineNumber: startLineNumber, startColumn: 1, - endLineNumber: endLineNumber, endColumn: 1 - }, - options: DirtyDiffModelDecorator.ADDED_DECORATION_OPTIONS - }; - } - - // Removed - if (change.modifiedEndLineNumber === 0) { - return { - range: { - startLineNumber: startLineNumber, startColumn: 1, - endLineNumber: startLineNumber, endColumn: 1 - }, - options: DirtyDiffModelDecorator.DELETED_DECORATION_OPTIONS - }; - } - - // Modified - return { - range: { - startLineNumber: startLineNumber, startColumn: 1, - endLineNumber: endLineNumber, endColumn: 1 - }, - options: DirtyDiffModelDecorator.MODIFIED_DECORATION_OPTIONS - }; - }); - } - - public dispose(): void { - this.modelService.destroyModel(this._originalContentsURI); - this.toDispose = lifecycle.dispose(this.toDispose); - if (this.model && !this.model.isDisposed()) { - this.model.deltaDecorations(this.decorations, []); - } - this.model = null; - this.decorations = null; - if (this.delayer) { - this.delayer.cancel(); - this.delayer = null; - } - if (this.diffDelayer) { - this.diffDelayer.cancel(); - this.diffDelayer = null; - } - } -} - -export class DirtyDiffDecorator implements ext.IWorkbenchContribution { - - private gitService: IGitService; - private messageService: IMessageService; - private editorService: IWorkbenchEditorService; - private eventService: IEventService; - private contextService: IWorkspaceContextService; - private instantiationService: IInstantiationService; - private models: common.IModel[]; - private decorators: { [modelId: string]: DirtyDiffModelDecorator }; - private toDispose: lifecycle.IDisposable[]; - - constructor( - @IGitService gitService: IGitService, - @IMessageService messageService: IMessageService, - @IWorkbenchEditorService editorService: IWorkbenchEditorService, - @IEditorGroupService editorGroupService: IEditorGroupService, - @IEventService eventService: IEventService, - @IWorkspaceContextService contextService: IWorkspaceContextService, - @IInstantiationService instantiationService: IInstantiationService - ) { - this.gitService = gitService; - this.messageService = messageService; - this.editorService = editorService; - this.eventService = eventService; - this.contextService = contextService; - this.instantiationService = instantiationService; - - this.models = []; - this.decorators = Object.create(null); - this.toDispose = []; - this.toDispose.push(editorGroupService.onEditorsChanged(() => this.onEditorsChanged())); - this.toDispose.push(gitService.addListener2(git.ServiceEvents.DISPOSE, () => this.dispose())); - } - - public getId(): string { - return 'git.DirtyDiffModelDecorator'; - } - - private onEditorsChanged(): void { - // HACK: This is the best current way of figuring out whether to draw these decorations - // or not. Needs context from the editor, to know whether it is a diff editor, in place editor - // etc. - - const repositoryRoot = this.gitService.getModel().getRepositoryRoot(); - - // If there is no repository root, just wait until that changes - if (typeof repositoryRoot !== 'string') { - this.gitService.addOneTimeDisposableListener(git.ServiceEvents.STATE_CHANGED, () => this.onEditorsChanged()); - - this.models.forEach(m => this.onModelInvisible(m)); - this.models = []; - return; - } - - const models = this.editorService.getVisibleEditors() - - // map to the editor controls - .map(e => e.getControl()) - - // only interested in code editor widgets - .filter(c => c instanceof widget.CodeEditor) - - // map to models - .map(e => (e).getModel()) - - // remove nulls and duplicates - .filter((m, i, a) => !!m && a.indexOf(m, i + 1) === -1) - - // get the associated resource - .map(m => ({ model: m, resource: m.uri })) - - // remove nulls - .filter(p => !!p.resource && - // and invalid resources - (p.resource.scheme === 'file' && paths.isEqualOrParent(p.resource.fsPath, repositoryRoot)) - ) - - // get paths - .map(p => ({ model: p.model, path: paths.normalize(paths.relative(repositoryRoot, p.resource.fsPath)) })) - - // remove nulls and inside .git files - .filter(p => !!p.path && p.path.indexOf('.git/') === -1); - - var newModels = models.filter(p => this.models.every(m => p.model !== m)); - var oldModels = this.models.filter(m => models.every(p => p.model !== m)); - - newModels.forEach(p => this.onModelVisible(p.model, p.path)); - oldModels.forEach(m => this.onModelInvisible(m)); - - this.models = models.map(p => p.model); - } - - private onModelVisible(model: common.IModel, path: string): void { - this.decorators[model.id] = this.instantiationService.createInstance(DirtyDiffModelDecorator, model, path); - } - - private onModelInvisible(model: common.IModel): void { - this.decorators[model.id].dispose(); - delete this.decorators[model.id]; - } - - public dispose(): void { - this.toDispose = lifecycle.dispose(this.toDispose); - this.models.forEach(m => this.decorators[m.id].dispose()); - this.models = null; - this.decorators = null; - } -} - export const VIEWLET_ID = 'workbench.view.git'; class OpenGitViewletAction extends viewlet.ToggleViewletAction { @@ -500,11 +163,6 @@ export function registerContributions(): void { StatusUpdater ); - // Register DirtyDiffDecorator - (platform.Registry.as(ext.Extensions.Workbench)).registerWorkbenchContribution( - DirtyDiffDecorator - ); - // Register Quick Open for git (platform.Registry.as(quickopen.Extensions.Quickopen)).registerQuickOpenHandler( new quickopen.QuickOpenHandlerDescriptor( diff --git a/src/vs/workbench/parts/git/common/gitDirtyDiff.ts b/src/vs/workbench/parts/git/common/gitDirtyDiff.ts new file mode 100644 index 00000000000..ecc113cbeea --- /dev/null +++ b/src/vs/workbench/parts/git/common/gitDirtyDiff.ts @@ -0,0 +1,80 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { IModelService } from 'vs/editor/common/services/modelService'; +import URI from 'vs/base/common/uri'; +import { dispose } from 'vs/base/common/lifecycle'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { IModel } from 'vs/editor/common/editorCommon'; +import { ITextModelResolverService, ITextModelContentProvider } from 'vs/platform/textmodelResolver/common/resolver'; +import { IDirtyDiffTextDocumentProvider, IDirtyDiffService } from 'vs/workbench/services/scm/common/dirtydiff'; +import { IGitService, StatusType, ServiceEvents, ServiceOperations, ServiceState } from 'vs/workbench/parts/git/common/git'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; + +export class GitContentProvider implements IWorkbenchContribution, ITextModelContentProvider, IDirtyDiffTextDocumentProvider { + + constructor( + @ITextModelResolverService textModelResolverService: ITextModelResolverService, + @IModelService private modelService: IModelService, + @IDirtyDiffService private dirtyDiffService: IDirtyDiffService, + @IGitService private gitService: IGitService + ) { + this.dirtyDiffService.registerDirtyDiffTextDocumentProvider(this); + textModelResolverService.registerTextModelContentProvider('git-index', this); + } + + getDirtyDiffTextDocument(resource: URI): TPromise { + return TPromise.as(resource.with({ scheme: 'git-index' })); + } + + provideTextContent(uri: URI): TPromise { + if (uri.scheme !== 'git-index') { + return null; + } + + const gitModel = this.gitService.getModel(); + const path = uri.fsPath; + const treeish = gitModel.getStatus().find(path, StatusType.INDEX) ? '~' : 'HEAD'; + + return this.gitService.buffer(path, treeish) + .then(contents => this.modelService.createModel(contents, null, uri)) + .then(model => { + const trigger = () => { + this.gitService.buffer(path, treeish). + then(contents => model.setValue(contents)) + .done(null, onUnexpectedError); + }; + + const onChanges = () => { + if (this.gitService.getState() !== ServiceState.OK) { + return; + } + + trigger(); + }; + + const disposables = [ + this.gitService.addListener2(ServiceEvents.STATE_CHANGED, onChanges), + this.gitService.addListener2(ServiceEvents.OPERATION_END, e => { + if (e.operation.id !== ServiceOperations.BACKGROUND_FETCH) { + onChanges(); + } + }) + ]; + + model.onWillDispose(() => { + dispose(disposables); + }); + + return model; + }); + } + + getId(): string { + return 'git.contentprovider'; + } +} \ No newline at end of file diff --git a/src/vs/workbench/parts/git/electron-browser/git.contribution.ts b/src/vs/workbench/parts/git/electron-browser/git.contribution.ts index 0516fbf8f54..bd96031dca3 100644 --- a/src/vs/workbench/parts/git/electron-browser/git.contribution.ts +++ b/src/vs/workbench/parts/git/electron-browser/git.contribution.ts @@ -10,15 +10,20 @@ import { IGitService } from 'vs/workbench/parts/git/common/git'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; import { Registry } from 'vs/platform/platform'; -import { Extensions as WorkbenchExtensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actionRegistry'; import { CloneAction } from './gitActions'; +import { GitContentProvider } from '../common/gitDirtyDiff'; +import { IWorkbenchActionRegistry, Extensions as WorkbenchActionExtensions } from 'vs/workbench/common/actionRegistry'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; registerContributions(); // Register Service registerSingleton(IGitService, ElectronGitService); -const workbenchActionRegistry = Registry.as(WorkbenchExtensions.WorkbenchActions); const category = localize('git', "Git"); -workbenchActionRegistry.registerWorkbenchAction(new SyncActionDescriptor(CloneAction, CloneAction.ID, CloneAction.LABEL), 'Git: Clone', category); \ No newline at end of file +Registry.as(WorkbenchActionExtensions.WorkbenchActions) + .registerWorkbenchAction(new SyncActionDescriptor(CloneAction, CloneAction.ID, CloneAction.LABEL), 'Git: Clone', category); + +Registry.as(WorkbenchExtensions.Workbench) + .registerWorkbenchContribution(GitContentProvider); \ No newline at end of file diff --git a/src/vs/workbench/parts/scm/browser/dirtydiffDecorator.ts b/src/vs/workbench/parts/scm/browser/dirtydiffDecorator.ts new file mode 100644 index 00000000000..1b0ceeb86e9 --- /dev/null +++ b/src/vs/workbench/parts/scm/browser/dirtydiffDecorator.ts @@ -0,0 +1,218 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { ThrottledDelayer } from 'vs/base/common/async'; +import * as lifecycle from 'vs/base/common/lifecycle'; +import * as winjs from 'vs/base/common/winjs.base'; +import * as ext from 'vs/workbench/common/contributions'; +import * as common from 'vs/editor/common/editorCommon'; +import * as widget from 'vs/editor/browser/codeEditor'; +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 { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IModelService } from 'vs/editor/common/services/modelService'; +import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; +import URI from 'vs/base/common/uri'; +import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService'; +import { IDirtyDiffService } from 'vs/workbench/services/scm/common/dirtydiff'; + +class DirtyDiffModelDecorator { + + static MODIFIED_DECORATION_OPTIONS: common.IModelDecorationOptions = { + linesDecorationsClassName: 'git-dirty-modified-diff-glyph', + isWholeLine: true, + overviewRuler: { + color: 'rgba(0, 122, 204, 0.6)', + darkColor: 'rgba(0, 122, 204, 0.6)', + position: common.OverviewRulerLane.Left + } + }; + + static ADDED_DECORATION_OPTIONS: common.IModelDecorationOptions = { + linesDecorationsClassName: 'git-dirty-added-diff-glyph', + isWholeLine: true, + overviewRuler: { + color: 'rgba(0, 122, 204, 0.6)', + darkColor: 'rgba(0, 122, 204, 0.6)', + position: common.OverviewRulerLane.Left + } + }; + + static DELETED_DECORATION_OPTIONS: common.IModelDecorationOptions = { + linesDecorationsClassName: 'git-dirty-deleted-diff-glyph', + isWholeLine: true, + overviewRuler: { + color: 'rgba(0, 122, 204, 0.6)', + darkColor: 'rgba(0, 122, 204, 0.6)', + position: common.OverviewRulerLane.Left + } + }; + + private decorations: string[]; + private diffDelayer: ThrottledDelayer; + private toDispose: lifecycle.IDisposable[]; + + constructor( + private model: common.IModel, + private uri: URI, + @IDirtyDiffService private dirtyDiffService: IDirtyDiffService, + @IModelService private modelService: IModelService, + @IEditorWorkerService private editorWorkerService: IEditorWorkerService, + @IWorkbenchEditorService private editorService: IWorkbenchEditorService, + @IWorkspaceContextService private contextService: IWorkspaceContextService + ) { + this.decorations = []; + this.diffDelayer = new ThrottledDelayer(200); + this.toDispose = []; + this.triggerDiff(); + this.toDispose.push(model.onDidChangeContent(() => this.triggerDiff())); + } + + private triggerDiff(): winjs.Promise { + if (!this.diffDelayer) { + return winjs.TPromise.as(null); + } + + return this.dirtyDiffService.getDirtyDiffTextDocument(this.uri).then(originalUri => { + return this.diffDelayer.trigger(() => { + if (!this.model || this.model.isDisposed()) { + return winjs.TPromise.as([]); // disposed + } + + return this.editorWorkerService.computeDirtyDiff(originalUri, this.model.uri, true); + }).then((diff: common.IChange[]) => { + if (!this.model || this.model.isDisposed()) { + return; // disposed + } + + return this.decorations = this.model.deltaDecorations(this.decorations, DirtyDiffModelDecorator.changesToDecorations(diff || [])); + }); + }); + } + + private static changesToDecorations(diff: common.IChange[]): common.IModelDeltaDecoration[] { + return diff.map((change) => { + const startLineNumber = change.modifiedStartLineNumber; + const endLineNumber = change.modifiedEndLineNumber || startLineNumber; + + // Added + if (change.originalEndLineNumber === 0) { + return { + range: { + startLineNumber: startLineNumber, startColumn: 1, + endLineNumber: endLineNumber, endColumn: 1 + }, + options: DirtyDiffModelDecorator.ADDED_DECORATION_OPTIONS + }; + } + + // Removed + if (change.modifiedEndLineNumber === 0) { + return { + range: { + startLineNumber: startLineNumber, startColumn: 1, + endLineNumber: startLineNumber, endColumn: 1 + }, + options: DirtyDiffModelDecorator.DELETED_DECORATION_OPTIONS + }; + } + + // Modified + return { + range: { + startLineNumber: startLineNumber, startColumn: 1, + endLineNumber: endLineNumber, endColumn: 1 + }, + options: DirtyDiffModelDecorator.MODIFIED_DECORATION_OPTIONS + }; + }); + } + + dispose(): void { + this.toDispose = lifecycle.dispose(this.toDispose); + if (this.model && !this.model.isDisposed()) { + this.model.deltaDecorations(this.decorations, []); + } + this.model = null; + this.decorations = null; + if (this.diffDelayer) { + this.diffDelayer.cancel(); + this.diffDelayer = null; + } + } +} + +export class DirtyDiffDecorator implements ext.IWorkbenchContribution { + + private models: common.IModel[] = []; + private decorators: { [modelId: string]: DirtyDiffModelDecorator } = Object.create(null); + private toDispose: lifecycle.IDisposable[] = []; + + constructor( + @IMessageService private messageService: IMessageService, + @IWorkbenchEditorService private editorService: IWorkbenchEditorService, + @IEditorGroupService editorGroupService: IEditorGroupService, + @IEventService private eventService: IEventService, + @IWorkspaceContextService private contextService: IWorkspaceContextService, + @IInstantiationService private instantiationService: IInstantiationService + ) { + this.toDispose.push(editorGroupService.onEditorsChanged(() => this.onEditorsChanged())); + } + + getId(): string { + return 'git.DirtyDiffModelDecorator'; + } + + private onEditorsChanged(): void { + // HACK: This is the best current way of figuring out whether to draw these decorations + // or not. Needs context from the editor, to know whether it is a diff editor, in place editor + // etc. + + const models = this.editorService.getVisibleEditors() + + // map to the editor controls + .map(e => e.getControl()) + + // only interested in code editor widgets + .filter(c => c instanceof widget.CodeEditor) + + // map to models + .map(e => (e).getModel()) + + // remove nulls and duplicates + .filter((m, i, a) => !!m && !!m.uri && a.indexOf(m, i + 1) === -1) + + // get the associated resource + .map(m => ({ model: m, uri: m.uri })); + + const newModels = models.filter(p => this.models.every(m => p.model !== m)); + const oldModels = this.models.filter(m => models.every(p => p.model !== m)); + + newModels.forEach(({ model, uri }) => this.onModelVisible(model, uri)); + oldModels.forEach(m => this.onModelInvisible(m)); + + this.models = models.map(p => p.model); + } + + private onModelVisible(model: common.IModel, uri: URI): void { + this.decorators[model.id] = this.instantiationService.createInstance(DirtyDiffModelDecorator, model, uri); + } + + private onModelInvisible(model: common.IModel): void { + this.decorators[model.id].dispose(); + delete this.decorators[model.id]; + } + + dispose(): void { + this.toDispose = lifecycle.dispose(this.toDispose); + this.models.forEach(m => this.decorators[m.id].dispose()); + this.models = null; + this.decorators = null; + } +} diff --git a/src/vs/workbench/parts/scm/browser/scm.contribution.ts b/src/vs/workbench/parts/scm/browser/scm.contribution.ts new file mode 100644 index 00000000000..40d189f227c --- /dev/null +++ b/src/vs/workbench/parts/scm/browser/scm.contribution.ts @@ -0,0 +1,13 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Registry } from 'vs/platform/platform'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; +import { DirtyDiffDecorator } from './dirtydiffDecorator'; + +Registry.as(WorkbenchExtensions.Workbench) + .registerWorkbenchContribution(DirtyDiffDecorator); \ No newline at end of file diff --git a/src/vs/workbench/services/scm/common/dirtydiff.ts b/src/vs/workbench/services/scm/common/dirtydiff.ts new file mode 100644 index 00000000000..bfe45f39c80 --- /dev/null +++ b/src/vs/workbench/services/scm/common/dirtydiff.ts @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * 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 URI from 'vs/base/common/uri'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IDisposable } from 'vs/base/common/lifecycle'; + +export interface IDirtyDiffTextDocumentProvider { + getDirtyDiffTextDocument(resource: URI): TPromise; +} + +export const IDirtyDiffService = createDecorator('dirtyDiff'); + +export interface IDirtyDiffService { + + _serviceBrand: any; + + getDirtyDiffTextDocument(resource: URI): TPromise; + registerDirtyDiffTextDocumentProvider(provider: IDirtyDiffTextDocumentProvider): IDisposable; +} \ No newline at end of file diff --git a/src/vs/workbench/services/scm/common/dirtydiffService.ts b/src/vs/workbench/services/scm/common/dirtydiffService.ts new file mode 100644 index 00000000000..18e25db657f --- /dev/null +++ b/src/vs/workbench/services/scm/common/dirtydiffService.ts @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * 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 URI from 'vs/base/common/uri'; +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { IDirtyDiffService, IDirtyDiffTextDocumentProvider } from './dirtydiff'; + +export class DirtyDiffService implements IDirtyDiffService { + + _serviceBrand; + + private providers: IDirtyDiffTextDocumentProvider[] = []; + + getDirtyDiffTextDocument(resource: URI): TPromise { + // TODO@Joao: just take the first + const [provider] = this.providers; + + if (!provider) { + return TPromise.as(null); + } + + return provider.getDirtyDiffTextDocument(resource); + } + + registerDirtyDiffTextDocumentProvider(provider: IDirtyDiffTextDocumentProvider): IDisposable { + this.providers = [provider, ...this.providers]; + + return toDisposable(() => { + const index = this.providers.indexOf(provider); + + if (index < 0) { + return; + } + + this.providers.splice(index, 1); + }); + } +} \ No newline at end of file -- GitLab