/*--------------------------------------------------------------------------------------------- * 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/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'); import widgets = require('vs/workbench/parts/git/browser/gitWidgets'); import wbar = require('vs/workbench/common/actionRegistry'); import gitoutput = require('vs/workbench/parts/git/browser/gitOutput'); import output = require('vs/workbench/parts/output/common/output'); import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; import confregistry = require('vs/platform/configuration/common/configurationRegistry'); import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; 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/viewletService'; 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; export class StatusUpdater implements ext.IWorkbenchContribution { static ID = 'vs.git.statusUpdater'; private gitService: IGitService; private eventService: IEventService; private activityService: IActivityService; private messageService: IMessageService; private configurationService: IConfigurationService; private progressBadgeDelayer: async.Delayer; private toDispose: lifecycle.IDisposable[]; constructor( @IGitService gitService: IGitService, @IEventService eventService: IEventService, @IActivityService activityService: IActivityService, @IMessageService messageService: IMessageService, @IConfigurationService configurationService: IConfigurationService ) { this.gitService = gitService; this.eventService = eventService; this.activityService = activityService; this.messageService = messageService; this.configurationService = configurationService; this.progressBadgeDelayer = new async.Delayer(200); this.toDispose = []; this.toDispose.push(this.configurationService.onDidUpdateConfiguration(e => this.onGitServiceChange())); this.toDispose.push(this.gitService.addBulkListener2(e => this.onGitServiceChange())); } private onGitServiceChange(): void { if (this.gitService.getState() !== git.ServiceState.OK) { this.progressBadgeDelayer.cancel(); this.activityService.showActivity('workbench.view.git', null, 'git-viewlet-label'); } else if (this.gitService.isIdle()) { this.showChangesBadge(); } else { this.progressBadgeDelayer.trigger(() => { this.activityService.showActivity('workbench.view.git', new ProgressBadge(() => nls.localize('gitProgressBadge', 'Running git status')), 'git-viewlet-label-progress'); }); } } private showChangesBadge(): void { this.progressBadgeDelayer.cancel(); const { countBadge } = this.configurationService.getConfiguration('git'); if (countBadge === 'off') { return; } const filter = countBadge === 'tracked' ? s => s.getStatus() !== git.Status.UNTRACKED : () => true; const statuses = this.gitService.getModel().getStatus().getGroups() .map(g => g.all()) .reduce((r, g) => r.concat(g), []) .filter(filter); const badge = new NumberBadge(statuses.length, num => nls.localize('gitPendingChangesBadge', '{0} pending changes', num)); this.activityService.showActivity('workbench.view.git', badge, 'git-viewlet-label'); } public getId(): string { return StatusUpdater.ID; } public dispose(): void { this.toDispose = lifecycle.dispose(this.toDispose); } } 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 { public static ID = VIEWLET_ID; public static LABEL = nls.localize('toggleGitViewlet', "Show Git"); constructor(id: string, label: string, @IViewletService viewletService: IViewletService, @IWorkbenchEditorService editorService: IWorkbenchEditorService) { super(id, label, VIEWLET_ID, viewletService, editorService); } } export function registerContributions(): void { // Register Statusbar item (platform.Registry.as(statusbar.Extensions.Statusbar)).registerStatusbarItem(new statusbar.StatusbarItemDescriptor( widgets.GitStatusbarItem, statusbar.StatusbarAlignment.LEFT, 100 /* High Priority */ )); // Register Output Channel var outputChannelRegistry = platform.Registry.as(output.Extensions.OutputChannels); outputChannelRegistry.registerChannel('Git', nls.localize('git', "Git")); // Register Git Output (platform.Registry.as(ext.Extensions.Workbench)).registerWorkbenchContribution( gitoutput.GitOutput ); // Register Viewlet (platform.Registry.as(viewlet.Extensions.Viewlets)).registerViewlet(new viewlet.ViewletDescriptor( 'vs/workbench/parts/git/browser/gitViewlet', 'GitViewlet', VIEWLET_ID, nls.localize('git', "Git"), 'git', 35 )); // Register Action to Open Viewlet (platform.Registry.as(wbar.Extensions.WorkbenchActions)).registerWorkbenchAction( new SyncActionDescriptor(OpenGitViewletAction, OpenGitViewletAction.ID, OpenGitViewletAction.LABEL, { primary: null, win: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_G }, linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_G }, mac: { primary: KeyMod.WinCtrl | KeyMod.Shift | KeyCode.KEY_G } }), 'View: Show Git', nls.localize('view', "View") ); // Register StatusUpdater (platform.Registry.as(ext.Extensions.Workbench)).registerWorkbenchContribution( 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( 'vs/workbench/parts/git/browser/gitQuickOpen', 'GitCommandQuickOpenHandler', 'git ', nls.localize('gitCommands', "Git Commands") ) ); // Register configuration var configurationRegistry = platform.Registry.as(confregistry.Extensions.Configuration); configurationRegistry.registerConfiguration({ id: 'git', order: 15, title: nls.localize('gitConfigurationTitle', "Git"), type: 'object', properties: { 'git.enabled': { type: 'boolean', description: nls.localize('gitEnabled', "Is git enabled"), default: true }, 'git.path': { type: ['string', 'null'], description: nls.localize('gitPath', "Path to the git executable"), default: null }, 'git.autorefresh': { type: 'boolean', description: nls.localize('gitAutoRefresh', "Whether auto refreshing is enabled"), default: true }, 'git.autofetch': { type: 'boolean', description: nls.localize('gitAutoFetch', "Whether auto fetching is enabled."), default: true }, 'git.enableLongCommitWarning': { type: 'boolean', description: nls.localize('gitLongCommit', "Whether long commit messages should be warned about."), default: true }, 'git.allowLargeRepositories': { type: 'boolean', description: nls.localize('gitLargeRepos', "Always allow large repositories to be managed by Code."), default: false }, 'git.confirmSync': { type: 'boolean', description: nls.localize('confirmSync', "Confirm before synchronizing git repositories."), default: true }, 'git.countBadge': { type: 'string', enum: ['all', 'tracked', 'off'], default: 'all', description: nls.localize('countBadge', "Controls the git badge counter."), } } }); }