Path: blob/main/extensions/git/src/decorationProvider.ts
3314 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { window, workspace, Uri, Disposable, Event, EventEmitter, FileDecoration, FileDecorationProvider, ThemeColor, l10n, SourceControlHistoryItemRef } from 'vscode';6import * as path from 'path';7import { Repository, GitResourceGroup } from './repository';8import { Model } from './model';9import { debounce } from './decorators';10import { filterEvent, dispose, anyEvent, fireEvent, PromiseSource, combinedDisposable, runAndSubscribeEvent } from './util';11import { Change, GitErrorCodes, Status } from './api/git';1213function equalSourceControlHistoryItemRefs(ref1?: SourceControlHistoryItemRef, ref2?: SourceControlHistoryItemRef): boolean {14if (ref1 === ref2) {15return true;16}1718return ref1?.id === ref2?.id &&19ref1?.name === ref2?.name &&20ref1?.revision === ref2?.revision;21}2223class GitIgnoreDecorationProvider implements FileDecorationProvider {2425private static Decoration: FileDecoration = { color: new ThemeColor('gitDecoration.ignoredResourceForeground') };2627readonly onDidChangeFileDecorations: Event<Uri[]>;28private queue = new Map<string, { repository: Repository; queue: Map<string, PromiseSource<FileDecoration | undefined>> }>();29private disposables: Disposable[] = [];3031constructor(private model: Model) {32this.onDidChangeFileDecorations = fireEvent(anyEvent<any>(33filterEvent(workspace.onDidSaveTextDocument, e => /\.gitignore$|\.git\/info\/exclude$/.test(e.uri.path)),34model.onDidOpenRepository,35model.onDidCloseRepository36));3738this.disposables.push(window.registerFileDecorationProvider(this));39}4041async provideFileDecoration(uri: Uri): Promise<FileDecoration | undefined> {42const repository = this.model.getRepository(uri);4344if (!repository) {45return;46}4748let queueItem = this.queue.get(repository.root);4950if (!queueItem) {51queueItem = { repository, queue: new Map<string, PromiseSource<FileDecoration | undefined>>() };52this.queue.set(repository.root, queueItem);53}5455let promiseSource = queueItem.queue.get(uri.fsPath);5657if (!promiseSource) {58promiseSource = new PromiseSource();59queueItem!.queue.set(uri.fsPath, promiseSource);60this.checkIgnoreSoon();61}6263return await promiseSource.promise;64}6566@debounce(500)67private checkIgnoreSoon(): void {68const queue = new Map(this.queue.entries());69this.queue.clear();7071for (const [, item] of queue) {72const paths = [...item.queue.keys()];7374item.repository.checkIgnore(paths).then(ignoreSet => {75for (const [path, promiseSource] of item.queue.entries()) {76promiseSource.resolve(ignoreSet.has(path) ? GitIgnoreDecorationProvider.Decoration : undefined);77}78}, err => {79if (err.gitErrorCode !== GitErrorCodes.IsInSubmodule) {80console.error(err);81}8283for (const [, promiseSource] of item.queue.entries()) {84promiseSource.reject(err);85}86});87}88}8990dispose(): void {91this.disposables.forEach(d => d.dispose());92this.queue.clear();93}94}9596class GitDecorationProvider implements FileDecorationProvider {9798private static SubmoduleDecorationData: FileDecoration = {99tooltip: 'Submodule',100badge: 'S',101color: new ThemeColor('gitDecoration.submoduleResourceForeground')102};103104private readonly _onDidChangeDecorations = new EventEmitter<Uri[]>();105readonly onDidChangeFileDecorations: Event<Uri[]> = this._onDidChangeDecorations.event;106107private disposables: Disposable[] = [];108private decorations = new Map<string, FileDecoration>();109110constructor(private repository: Repository) {111this.disposables.push(112window.registerFileDecorationProvider(this),113runAndSubscribeEvent(repository.onDidRunGitStatus, () => this.onDidRunGitStatus())114);115}116117private onDidRunGitStatus(): void {118const newDecorations = new Map<string, FileDecoration>();119120this.collectDecorationData(this.repository.indexGroup, newDecorations);121this.collectDecorationData(this.repository.untrackedGroup, newDecorations);122this.collectDecorationData(this.repository.workingTreeGroup, newDecorations);123this.collectDecorationData(this.repository.mergeGroup, newDecorations);124this.collectSubmoduleDecorationData(newDecorations);125126const uris = new Set([...this.decorations.keys()].concat([...newDecorations.keys()]));127this.decorations = newDecorations;128this._onDidChangeDecorations.fire([...uris.values()].map(value => Uri.parse(value, true)));129}130131private collectDecorationData(group: GitResourceGroup, bucket: Map<string, FileDecoration>): void {132for (const r of group.resourceStates) {133const decoration = r.resourceDecoration;134135if (decoration) {136// not deleted and has a decoration137bucket.set(r.original.toString(), decoration);138139if (r.type === Status.DELETED && r.rightUri) {140bucket.set(r.rightUri.toString(), decoration);141}142143if (r.type === Status.INDEX_RENAMED || r.type === Status.INTENT_TO_RENAME) {144bucket.set(r.resourceUri.toString(), decoration);145}146}147}148}149150private collectSubmoduleDecorationData(bucket: Map<string, FileDecoration>): void {151for (const submodule of this.repository.submodules) {152bucket.set(Uri.file(path.join(this.repository.root, submodule.path)).toString(), GitDecorationProvider.SubmoduleDecorationData);153}154}155156provideFileDecoration(uri: Uri): FileDecoration | undefined {157return this.decorations.get(uri.toString());158}159160dispose(): void {161this.disposables.forEach(d => d.dispose());162}163}164165class GitIncomingChangesFileDecorationProvider implements FileDecorationProvider {166167private readonly _onDidChangeDecorations = new EventEmitter<Uri[]>();168readonly onDidChangeFileDecorations: Event<Uri[]> = this._onDidChangeDecorations.event;169170private _currentHistoryItemRef: SourceControlHistoryItemRef | undefined;171private _currentHistoryItemRemoteRef: SourceControlHistoryItemRef | undefined;172173private _decorations = new Map<string, FileDecoration>();174private readonly disposables: Disposable[] = [];175176constructor(private readonly repository: Repository) {177this.disposables.push(178window.registerFileDecorationProvider(this),179runAndSubscribeEvent(repository.historyProvider.onDidChangeCurrentHistoryItemRefs, () => this.onDidChangeCurrentHistoryItemRefs())180);181}182183private async onDidChangeCurrentHistoryItemRefs(): Promise<void> {184const historyProvider = this.repository.historyProvider;185const currentHistoryItemRef = historyProvider.currentHistoryItemRef;186const currentHistoryItemRemoteRef = historyProvider.currentHistoryItemRemoteRef;187188if (equalSourceControlHistoryItemRefs(this._currentHistoryItemRef, currentHistoryItemRef) &&189equalSourceControlHistoryItemRefs(this._currentHistoryItemRemoteRef, currentHistoryItemRemoteRef)) {190return;191}192193const decorations = new Map<string, FileDecoration>();194await this.collectIncomingChangesFileDecorations(decorations);195const uris = new Set([...this._decorations.keys()].concat([...decorations.keys()]));196197this._decorations = decorations;198this._currentHistoryItemRef = currentHistoryItemRef;199this._currentHistoryItemRemoteRef = currentHistoryItemRemoteRef;200201this._onDidChangeDecorations.fire([...uris.values()].map(value => Uri.parse(value, true)));202}203204private async collectIncomingChangesFileDecorations(bucket: Map<string, FileDecoration>): Promise<void> {205for (const change of await this.getIncomingChanges()) {206switch (change.status) {207case Status.INDEX_ADDED:208bucket.set(change.uri.toString(), {209badge: '↓A',210tooltip: l10n.t('Incoming Changes (added)'),211});212break;213case Status.DELETED:214bucket.set(change.uri.toString(), {215badge: '↓D',216tooltip: l10n.t('Incoming Changes (deleted)'),217});218break;219case Status.INDEX_RENAMED:220bucket.set(change.originalUri.toString(), {221badge: '↓R',222tooltip: l10n.t('Incoming Changes (renamed)'),223});224break;225case Status.MODIFIED:226bucket.set(change.uri.toString(), {227badge: '↓M',228tooltip: l10n.t('Incoming Changes (modified)'),229});230break;231default: {232bucket.set(change.uri.toString(), {233badge: '↓~',234tooltip: l10n.t('Incoming Changes'),235});236break;237}238}239}240}241242private async getIncomingChanges(): Promise<Change[]> {243try {244const historyProvider = this.repository.historyProvider;245const currentHistoryItemRef = historyProvider.currentHistoryItemRef;246const currentHistoryItemRemoteRef = historyProvider.currentHistoryItemRemoteRef;247248if (!currentHistoryItemRef || !currentHistoryItemRemoteRef) {249return [];250}251252const ancestor = await historyProvider.resolveHistoryItemRefsCommonAncestor([currentHistoryItemRef.id, currentHistoryItemRemoteRef.id]);253if (!ancestor) {254return [];255}256257const changes = await this.repository.diffBetween(ancestor, currentHistoryItemRemoteRef.id);258return changes;259} catch (err) {260return [];261}262}263264provideFileDecoration(uri: Uri): FileDecoration | undefined {265return this._decorations.get(uri.toString());266}267268dispose(): void {269dispose(this.disposables);270}271}272273export class GitDecorations {274275private enabled = false;276private disposables: Disposable[] = [];277private modelDisposables: Disposable[] = [];278private providers = new Map<Repository, Disposable>();279280constructor(private model: Model) {281this.disposables.push(new GitIgnoreDecorationProvider(model));282283const onEnablementChange = filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git.decorations.enabled'));284onEnablementChange(this.update, this, this.disposables);285this.update();286}287288private update(): void {289const config = workspace.getConfiguration('git');290const enabled = config.get<boolean>('decorations.enabled') === true;291if (this.enabled === enabled) {292return;293}294295if (enabled) {296this.enable();297} else {298this.disable();299}300301this.enabled = enabled;302}303304private enable(): void {305this.model.onDidOpenRepository(this.onDidOpenRepository, this, this.modelDisposables);306this.model.onDidCloseRepository(this.onDidCloseRepository, this, this.modelDisposables);307this.model.repositories.forEach(this.onDidOpenRepository, this);308}309310private disable(): void {311this.modelDisposables = dispose(this.modelDisposables);312this.providers.forEach(value => value.dispose());313this.providers.clear();314}315316private onDidOpenRepository(repository: Repository): void {317const providers = combinedDisposable([318new GitDecorationProvider(repository),319new GitIncomingChangesFileDecorationProvider(repository)320]);321322this.providers.set(repository, providers);323}324325private onDidCloseRepository(repository: Repository): void {326const provider = this.providers.get(repository);327328if (provider) {329provider.dispose();330this.providers.delete(repository);331}332}333334dispose(): void {335this.disable();336this.disposables = dispose(this.disposables);337}338}339340341