1/*--------------------------------------------------------------------------------------------- 2 * Copyright (c) Microsoft Corporation. All rights reserved. 3 * Licensed under the MIT License. See License.txt in the project root for license information. 4 *--------------------------------------------------------------------------------------------*/ 5 6import { window, workspace, Uri, Disposable, Event, EventEmitter, FileDecoration, FileDecorationProvider, ThemeColor } from 'vscode'; 7import * as path from 'path'; 8import { Repository, GitResourceGroup } from './repository'; 9import { Model } from './model'; 10import { debounce } from './decorators'; 11import { filterEvent, dispose, anyEvent, fireEvent, PromiseSource } from './util'; 12import { GitErrorCodes, Status } from './api/git'; 13 14class GitIgnoreDecorationProvider implements FileDecorationProvider { 15 16 private static Decoration: FileDecoration = { color: new ThemeColor('gitDecoration.ignoredResourceForeground') }; 17 18 readonly onDidChangeFileDecorations: Event<Uri[]>; 19 private queue = new Map<string, { repository: Repository; queue: Map<string, PromiseSource<FileDecoration | undefined>>; }>(); 20 private disposables: Disposable[] = []; 21 22 constructor(private model: Model) { 23 this.onDidChangeFileDecorations = fireEvent(anyEvent<any>( 24 filterEvent(workspace.onDidSaveTextDocument, e => /\.gitignore$|\.git\/info\/exclude$/.test(e.uri.path)), 25 model.onDidOpenRepository, 26 model.onDidCloseRepository 27 )); 28 29 this.disposables.push(window.registerFileDecorationProvider(this)); 30 } 31 32 async provideFileDecoration(uri: Uri): Promise<FileDecoration | undefined> { 33 const repository = this.model.getRepository(uri); 34 35 if (!repository) { 36 return; 37 } 38 39 let queueItem = this.queue.get(repository.root); 40 41 if (!queueItem) { 42 queueItem = { repository, queue: new Map<string, PromiseSource<FileDecoration | undefined>>() }; 43 this.queue.set(repository.root, queueItem); 44 } 45 46 let promiseSource = queueItem.queue.get(uri.fsPath); 47 48 if (!promiseSource) { 49 promiseSource = new PromiseSource(); 50 queueItem!.queue.set(uri.fsPath, promiseSource); 51 this.checkIgnoreSoon(); 52 } 53 54 return await promiseSource.promise; 55 } 56 57 @debounce(500) 58 private checkIgnoreSoon(): void { 59 const queue = new Map(this.queue.entries()); 60 this.queue.clear(); 61 62 for (const [, item] of queue) { 63 const paths = [...item.queue.keys()]; 64 65 item.repository.checkIgnore(paths).then(ignoreSet => { 66 for (const [path, promiseSource] of item.queue.entries()) { 67 promiseSource.resolve(ignoreSet.has(path) ? GitIgnoreDecorationProvider.Decoration : undefined); 68 } 69 }, err => { 70 if (err.gitErrorCode !== GitErrorCodes.IsInSubmodule) { 71 console.error(err); 72 } 73 74 for (const [, promiseSource] of item.queue.entries()) { 75 promiseSource.reject(err); 76 } 77 }); 78 } 79 } 80 81 dispose(): void { 82 this.disposables.forEach(d => d.dispose()); 83 this.queue.clear(); 84 } 85} 86 87class GitDecorationProvider implements FileDecorationProvider { 88 89 private static SubmoduleDecorationData: FileDecoration = { 90 tooltip: 'Submodule', 91 badge: 'S', 92 color: new ThemeColor('gitDecoration.submoduleResourceForeground') 93 }; 94 95 private readonly _onDidChangeDecorations = new EventEmitter<Uri[]>(); 96 readonly onDidChangeFileDecorations: Event<Uri[]> = this._onDidChangeDecorations.event; 97 98 private disposables: Disposable[] = []; 99 private decorations = new Map<string, FileDecoration>(); 100 101 constructor(private repository: Repository) { 102 this.disposables.push( 103 window.registerFileDecorationProvider(this), 104 repository.onDidRunGitStatus(this.onDidRunGitStatus, this) 105 ); 106 } 107 108 private onDidRunGitStatus(): void { 109 let newDecorations = new Map<string, FileDecoration>(); 110 111 this.collectSubmoduleDecorationData(newDecorations); 112 this.collectDecorationData(this.repository.indexGroup, newDecorations); 113 this.collectDecorationData(this.repository.untrackedGroup, newDecorations); 114 this.collectDecorationData(this.repository.workingTreeGroup, newDecorations); 115 this.collectDecorationData(this.repository.mergeGroup, newDecorations); 116 117 const uris = new Set([...this.decorations.keys()].concat([...newDecorations.keys()])); 118 this.decorations = newDecorations; 119 this._onDidChangeDecorations.fire([...uris.values()].map(value => Uri.parse(value, true))); 120 } 121 122 private collectDecorationData(group: GitResourceGroup, bucket: Map<string, FileDecoration>): void { 123 for (const r of group.resourceStates) { 124 const decoration = r.resourceDecoration; 125 126 if (decoration) { 127 // not deleted and has a decoration 128 bucket.set(r.original.toString(), decoration); 129 130 if (r.type === Status.INDEX_RENAMED) { 131 bucket.set(r.resourceUri.toString(), decoration); 132 } 133 } 134 } 135 } 136 137 private collectSubmoduleDecorationData(bucket: Map<string, FileDecoration>): void { 138 for (const submodule of this.repository.submodules) { 139 bucket.set(Uri.file(path.join(this.repository.root, submodule.path)).toString(), GitDecorationProvider.SubmoduleDecorationData); 140 } 141 } 142 143 provideFileDecoration(uri: Uri): FileDecoration | undefined { 144 return this.decorations.get(uri.toString()); 145 } 146 147 dispose(): void { 148 this.disposables.forEach(d => d.dispose()); 149 } 150} 151 152 153export class GitDecorations { 154 155 private disposables: Disposable[] = []; 156 private modelDisposables: Disposable[] = []; 157 private providers = new Map<Repository, Disposable>(); 158 159 constructor(private model: Model) { 160 this.disposables.push(new GitIgnoreDecorationProvider(model)); 161 162 const onEnablementChange = filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git.decorations.enabled')); 163 onEnablementChange(this.update, this, this.disposables); 164 this.update(); 165 } 166 167 private update(): void { 168 const enabled = workspace.getConfiguration('git').get('decorations.enabled'); 169 170 if (enabled) { 171 this.enable(); 172 } else { 173 this.disable(); 174 } 175 } 176 177 private enable(): void { 178 this.model.onDidOpenRepository(this.onDidOpenRepository, this, this.modelDisposables); 179 this.model.onDidCloseRepository(this.onDidCloseRepository, this, this.modelDisposables); 180 this.model.repositories.forEach(this.onDidOpenRepository, this); 181 } 182 183 private disable(): void { 184 this.modelDisposables = dispose(this.modelDisposables); 185 this.providers.forEach(value => value.dispose()); 186 this.providers.clear(); 187 } 188 189 private onDidOpenRepository(repository: Repository): void { 190 const provider = new GitDecorationProvider(repository); 191 this.providers.set(repository, provider); 192 } 193 194 private onDidCloseRepository(repository: Repository): void { 195 const provider = this.providers.get(repository); 196 197 if (provider) { 198 provider.dispose(); 199 this.providers.delete(repository); 200 } 201 } 202 203 dispose(): void { 204 this.disable(); 205 this.disposables = dispose(this.disposables); 206 } 207} 208