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