Path: blob/main/extensions/git/src/decorationProvider.ts
5240 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, 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') };2627private readonly _onDidChangeDecorations = new EventEmitter<undefined | Uri | Uri[]>();28readonly onDidChangeFileDecorations: Event<undefined | Uri | Uri[]> = this._onDidChangeDecorations.event;2930private queue = new Map<string, { repository: Repository; queue: Map<string, PromiseSource<FileDecoration | undefined>> }>();31private disposables: Disposable[] = [];3233constructor(private model: Model) {34const onDidChangeRepository = anyEvent<unknown>(35filterEvent(workspace.onDidSaveTextDocument, e => /\.gitignore$|\.git\/info\/exclude$/.test(e.uri.path)),36model.onDidOpenRepository,37model.onDidCloseRepository38);39this.disposables.push(onDidChangeRepository(() => this._onDidChangeDecorations.fire(undefined)));40this.disposables.push(window.registerFileDecorationProvider(this));41}4243async provideFileDecoration(uri: Uri): Promise<FileDecoration | undefined> {44const repository = this.model.getRepository(uri);4546if (!repository) {47return;48}4950let queueItem = this.queue.get(repository.root);5152if (!queueItem) {53queueItem = { repository, queue: new Map<string, PromiseSource<FileDecoration | undefined>>() };54this.queue.set(repository.root, queueItem);55}5657let promiseSource = queueItem.queue.get(uri.fsPath);5859if (!promiseSource) {60promiseSource = new PromiseSource();61queueItem!.queue.set(uri.fsPath, promiseSource);62this.checkIgnoreSoon();63}6465return await promiseSource.promise;66}6768@debounce(500)69private checkIgnoreSoon(): void {70const queue = new Map(this.queue.entries());71this.queue.clear();7273for (const [, item] of queue) {74const paths = [...item.queue.keys()];7576item.repository.checkIgnore(paths).then(ignoreSet => {77for (const [path, promiseSource] of item.queue.entries()) {78promiseSource.resolve(ignoreSet.has(path) ? GitIgnoreDecorationProvider.Decoration : undefined);79}80}, err => {81if (err.gitErrorCode !== GitErrorCodes.IsInSubmodule) {82console.error(err);83}8485for (const [, promiseSource] of item.queue.entries()) {86promiseSource.reject(err);87}88});89}90}9192dispose(): void {93this.disposables.forEach(d => d.dispose());94this.queue.clear();95}96}9798class GitDecorationProvider implements FileDecorationProvider {99100private static SubmoduleDecorationData: FileDecoration = {101tooltip: 'Submodule',102badge: 'S',103color: new ThemeColor('gitDecoration.submoduleResourceForeground')104};105106private readonly _onDidChangeDecorations = new EventEmitter<Uri[]>();107readonly onDidChangeFileDecorations: Event<Uri[]> = this._onDidChangeDecorations.event;108109private disposables: Disposable[] = [];110private decorations = new Map<string, FileDecoration>();111112constructor(private repository: Repository) {113this.disposables.push(114window.registerFileDecorationProvider(this),115runAndSubscribeEvent(repository.onDidRunGitStatus, () => this.onDidRunGitStatus())116);117}118119private onDidRunGitStatus(): void {120const newDecorations = new Map<string, FileDecoration>();121122this.collectDecorationData(this.repository.indexGroup, newDecorations);123this.collectDecorationData(this.repository.untrackedGroup, newDecorations);124this.collectDecorationData(this.repository.workingTreeGroup, newDecorations);125this.collectDecorationData(this.repository.mergeGroup, newDecorations);126this.collectSubmoduleDecorationData(newDecorations);127128const uris = new Set([...this.decorations.keys()].concat([...newDecorations.keys()]));129this.decorations = newDecorations;130this._onDidChangeDecorations.fire([...uris.values()].map(value => Uri.parse(value, true)));131}132133private collectDecorationData(group: GitResourceGroup, bucket: Map<string, FileDecoration>): void {134for (const r of group.resourceStates) {135const decoration = r.resourceDecoration;136137if (decoration) {138// not deleted and has a decoration139bucket.set(r.original.toString(), decoration);140141if (r.type === Status.DELETED && r.rightUri) {142bucket.set(r.rightUri.toString(), decoration);143}144145if (r.type === Status.INDEX_RENAMED || r.type === Status.INTENT_TO_RENAME) {146bucket.set(r.resourceUri.toString(), decoration);147}148}149}150}151152private collectSubmoduleDecorationData(bucket: Map<string, FileDecoration>): void {153for (const submodule of this.repository.submodules) {154bucket.set(Uri.file(path.join(this.repository.root, submodule.path)).toString(), GitDecorationProvider.SubmoduleDecorationData);155}156}157158provideFileDecoration(uri: Uri): FileDecoration | undefined {159return this.decorations.get(uri.toString());160}161162dispose(): void {163this.disposables.forEach(d => d.dispose());164}165}166167class GitIncomingChangesFileDecorationProvider implements FileDecorationProvider {168169private readonly _onDidChangeDecorations = new EventEmitter<Uri[]>();170readonly onDidChangeFileDecorations: Event<Uri[]> = this._onDidChangeDecorations.event;171172private _currentHistoryItemRef: SourceControlHistoryItemRef | undefined;173private _currentHistoryItemRemoteRef: SourceControlHistoryItemRef | undefined;174175private _decorations = new Map<string, FileDecoration>();176private readonly disposables: Disposable[] = [];177178constructor(private readonly repository: Repository) {179this.disposables.push(180window.registerFileDecorationProvider(this),181runAndSubscribeEvent(repository.historyProvider.onDidChangeCurrentHistoryItemRefs, () => this.onDidChangeCurrentHistoryItemRefs())182);183}184185private async onDidChangeCurrentHistoryItemRefs(): Promise<void> {186const historyProvider = this.repository.historyProvider;187const currentHistoryItemRef = historyProvider.currentHistoryItemRef;188const currentHistoryItemRemoteRef = historyProvider.currentHistoryItemRemoteRef;189190if (equalSourceControlHistoryItemRefs(this._currentHistoryItemRef, currentHistoryItemRef) &&191equalSourceControlHistoryItemRefs(this._currentHistoryItemRemoteRef, currentHistoryItemRemoteRef)) {192return;193}194195const decorations = new Map<string, FileDecoration>();196await this.collectIncomingChangesFileDecorations(decorations);197const uris = new Set([...this._decorations.keys()].concat([...decorations.keys()]));198199this._decorations = decorations;200this._currentHistoryItemRef = currentHistoryItemRef;201this._currentHistoryItemRemoteRef = currentHistoryItemRemoteRef;202203this._onDidChangeDecorations.fire([...uris.values()].map(value => Uri.parse(value, true)));204}205206private async collectIncomingChangesFileDecorations(bucket: Map<string, FileDecoration>): Promise<void> {207for (const change of await this.getIncomingChanges()) {208switch (change.status) {209case Status.INDEX_ADDED:210bucket.set(change.uri.toString(), {211badge: '↓A',212tooltip: l10n.t('Incoming Changes (added)'),213});214break;215case Status.DELETED:216bucket.set(change.uri.toString(), {217badge: '↓D',218tooltip: l10n.t('Incoming Changes (deleted)'),219});220break;221case Status.INDEX_RENAMED:222bucket.set(change.originalUri.toString(), {223badge: '↓R',224tooltip: l10n.t('Incoming Changes (renamed)'),225});226break;227case Status.MODIFIED:228bucket.set(change.uri.toString(), {229badge: '↓M',230tooltip: l10n.t('Incoming Changes (modified)'),231});232break;233default: {234bucket.set(change.uri.toString(), {235badge: '↓~',236tooltip: l10n.t('Incoming Changes'),237});238break;239}240}241}242}243244private async getIncomingChanges(): Promise<Change[]> {245try {246const historyProvider = this.repository.historyProvider;247const currentHistoryItemRef = historyProvider.currentHistoryItemRef;248const currentHistoryItemRemoteRef = historyProvider.currentHistoryItemRemoteRef;249250if (!currentHistoryItemRef || !currentHistoryItemRemoteRef) {251return [];252}253254const ancestor = await historyProvider.resolveHistoryItemRefsCommonAncestor([currentHistoryItemRef.id, currentHistoryItemRemoteRef.id]);255if (!ancestor) {256return [];257}258259const changes = await this.repository.diffBetweenWithStats(ancestor, currentHistoryItemRemoteRef.id);260return changes;261} catch (err) {262return [];263}264}265266provideFileDecoration(uri: Uri): FileDecoration | undefined {267return this._decorations.get(uri.toString());268}269270dispose(): void {271dispose(this.disposables);272}273}274275export class GitDecorations {276277private enabled = false;278private disposables: Disposable[] = [];279private modelDisposables: Disposable[] = [];280private providers = new Map<Repository, Disposable>();281282constructor(private model: Model) {283this.disposables.push(new GitIgnoreDecorationProvider(model));284285const onEnablementChange = filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git.decorations.enabled'));286onEnablementChange(this.update, this, this.disposables);287this.update();288}289290private update(): void {291const config = workspace.getConfiguration('git');292const enabled = config.get<boolean>('decorations.enabled') === true;293if (this.enabled === enabled) {294return;295}296297if (enabled) {298this.enable();299} else {300this.disable();301}302303this.enabled = enabled;304}305306private enable(): void {307this.model.onDidOpenRepository(this.onDidOpenRepository, this, this.modelDisposables);308this.model.onDidCloseRepository(this.onDidCloseRepository, this, this.modelDisposables);309this.model.repositories.forEach(this.onDidOpenRepository, this);310}311312private disable(): void {313this.modelDisposables = dispose(this.modelDisposables);314this.providers.forEach(value => value.dispose());315this.providers.clear();316}317318private onDidOpenRepository(repository: Repository): void {319const providers = combinedDisposable([320new GitDecorationProvider(repository),321new GitIncomingChangesFileDecorationProvider(repository)322]);323324this.providers.set(repository, providers);325}326327private onDidCloseRepository(repository: Repository): void {328const provider = this.providers.get(repository);329330if (provider) {331provider.dispose();332this.providers.delete(repository);333}334}335336dispose(): void {337this.disable();338this.disposables = dispose(this.disposables);339}340}341342343