'use strict'; import { Functions } from './system'; import { DecorationRenderOptions, Disposable, Event, EventEmitter, ExtensionContext, OverviewRulerLane, TextDocument, TextEditor, TextEditorDecorationType, TextEditorViewColumnChangeEvent, window, workspace } from 'vscode'; import { BlameAnnotationProvider } from './blameAnnotationProvider'; import { TextDocumentComparer, TextEditorComparer } from './comparers'; import { IBlameConfig } from './configuration'; import GitProvider from './gitProvider'; import { Logger } from './logger'; import WhitespaceController from './whitespaceController'; export const blameDecoration: TextEditorDecorationType = window.createTextEditorDecorationType({ before: { margin: '0 1.75em 0 0' }, after: { margin: '0 0 0 4em' } } as DecorationRenderOptions); export let highlightDecoration: TextEditorDecorationType; export default class BlameAnnotationController extends Disposable { private _onDidToggleBlameAnnotationsEmitter = new EventEmitter(); get onDidToggleBlameAnnotations(): Event { return this._onDidToggleBlameAnnotationsEmitter.event; } private _annotationProviders: Map = new Map(); private _blameAnnotationsDisposable: Disposable; private _config: IBlameConfig; private _disposable: Disposable; private _whitespaceController: WhitespaceController | undefined; constructor(private context: ExtensionContext, private git: GitProvider) { super(() => this.dispose()); this._onConfigurationChanged(); const subscriptions: Disposable[] = []; subscriptions.push(workspace.onDidChangeConfiguration(this._onConfigurationChanged, this)); this._disposable = Disposable.from(...subscriptions); } dispose() { this._annotationProviders.forEach(async (p, i) => await this.clear(i)); this._blameAnnotationsDisposable && this._blameAnnotationsDisposable.dispose(); this._whitespaceController && this._whitespaceController.dispose(); this._disposable && this._disposable.dispose(); } private _onConfigurationChanged() { let toggleWhitespace = workspace.getConfiguration('gitlens.advanced.toggleWhitespace').get('enabled'); if (!toggleWhitespace) { // Until https://github.com/Microsoft/vscode/issues/11485 is fixed we need to toggle whitespace for non-monospace fonts and ligatures // TODO: detect monospace font toggleWhitespace = workspace.getConfiguration('editor').get('fontLigatures'); } if (toggleWhitespace && !this._whitespaceController) { this._whitespaceController = new WhitespaceController(); } else if (!toggleWhitespace && this._whitespaceController) { this._whitespaceController.dispose(); this._whitespaceController = undefined; } const config = workspace.getConfiguration('gitlens').get('blame'); if (config.annotation.highlight !== (this._config && this._config.annotation.highlight)) { highlightDecoration && highlightDecoration.dispose(); switch (config.annotation.highlight) { case 'none': highlightDecoration = undefined; break; case 'gutter': highlightDecoration = window.createTextEditorDecorationType({ dark: { gutterIconPath: this.context.asAbsolutePath('images/blame-dark.svg'), overviewRulerColor: 'rgba(255, 255, 255, 0.75)' }, light: { gutterIconPath: this.context.asAbsolutePath('images/blame-light.svg'), overviewRulerColor: 'rgba(0, 0, 0, 0.75)' }, gutterIconSize: 'contain', overviewRulerLane: OverviewRulerLane.Right }); break; case 'line': highlightDecoration = window.createTextEditorDecorationType({ dark: { backgroundColor: 'rgba(255, 255, 255, 0.15)', overviewRulerColor: 'rgba(255, 255, 255, 0.75)' }, light: { backgroundColor: 'rgba(0, 0, 0, 0.15)', overviewRulerColor: 'rgba(0, 0, 0, 0.75)' }, overviewRulerLane: OverviewRulerLane.Right, isWholeLine: true }); break; case 'both': highlightDecoration = window.createTextEditorDecorationType({ dark: { backgroundColor: 'rgba(255, 255, 255, 0.15)', gutterIconPath: this.context.asAbsolutePath('images/blame-dark.svg'), overviewRulerColor: 'rgba(255, 255, 255, 0.75)' }, light: { backgroundColor: 'rgba(0, 0, 0, 0.15)', gutterIconPath: this.context.asAbsolutePath('images/blame-light.svg'), overviewRulerColor: 'rgba(0, 0, 0, 0.75)' }, gutterIconSize: 'contain', overviewRulerLane: OverviewRulerLane.Right, isWholeLine: true }); break; } } this._config = config; } async clear(column: number) { const provider = this._annotationProviders.get(column); if (!provider) return; this._annotationProviders.delete(column); await provider.dispose(); if (this._annotationProviders.size === 0) { Logger.log(`Remove listener registrations for blame annotations`); this._blameAnnotationsDisposable && this._blameAnnotationsDisposable.dispose(); this._blameAnnotationsDisposable = undefined; } this._onDidToggleBlameAnnotationsEmitter.fire(); } async showBlameAnnotation(editor: TextEditor, shaOrLine?: string | number): Promise { if (!editor || !editor.document) return false; if (editor.viewColumn === undefined && !this.git.hasGitUriForFile(editor)) return false; const currentProvider = this._annotationProviders.get(editor.viewColumn || -1); if (currentProvider && TextEditorComparer.equals(currentProvider.editor, editor)) { await currentProvider.setSelection(shaOrLine); return true; } const provider = new BlameAnnotationProvider(this.context, this.git, this._whitespaceController, editor); if (!await provider.supportsBlame()) return false; if (currentProvider) { await this.clear(currentProvider.editor.viewColumn || -1); } if (!this._blameAnnotationsDisposable && this._annotationProviders.size === 0) { Logger.log(`Add listener registrations for blame annotations`); const subscriptions: Disposable[] = []; subscriptions.push(window.onDidChangeVisibleTextEditors(Functions.debounce(this._onVisibleTextEditorsChanged, 100), this)); subscriptions.push(window.onDidChangeTextEditorViewColumn(this._onTextEditorViewColumnChanged, this)); subscriptions.push(workspace.onDidCloseTextDocument(this._onTextDocumentClosed, this)); this._blameAnnotationsDisposable = Disposable.from(...subscriptions); } this._annotationProviders.set(editor.viewColumn || -1, provider); if (await provider.provideBlameAnnotation(shaOrLine)) { this._onDidToggleBlameAnnotationsEmitter.fire(); return true; } return false; } isAnnotating(editor: TextEditor): boolean { if (!editor || !editor.document) return false; if (editor.viewColumn === undefined && !this.git.hasGitUriForFile(editor)) return false; return !!this._annotationProviders.get(editor.viewColumn || -1); } async toggleBlameAnnotation(editor: TextEditor, shaOrLine?: string | number): Promise { if (!editor || !editor.document) return false; if (editor.viewColumn === undefined && !this.git.hasGitUriForFile(editor)) return false; let provider = this._annotationProviders.get(editor.viewColumn || -1); if (!provider) return this.showBlameAnnotation(editor, shaOrLine); await this.clear(provider.editor.viewColumn || -1); return false; } private _onTextDocumentClosed(e: TextDocument) { for (const [key, p] of this._annotationProviders) { if (!TextDocumentComparer.equals(p.document, e)) continue; Logger.log('TextDocumentClosed:', `Clear blame annotations for column ${key}`); this.clear(key); } } private async _onTextEditorViewColumnChanged(e: TextEditorViewColumnChangeEvent) { const viewColumn = e.viewColumn || -1; Logger.log('TextEditorViewColumnChanged:', `Clear blame annotations for column ${viewColumn}`); await this.clear(viewColumn); for (const [key, p] of this._annotationProviders) { if (!TextEditorComparer.equals(p.editor, e.textEditor)) continue; Logger.log('TextEditorViewColumnChanged:', `Clear blame annotations for column ${key}`); await this.clear(key); } } private async _onVisibleTextEditorsChanged(e: TextEditor[]) { if (e.every(_ => _.document.uri.scheme === 'inmemory')) return; for (const [key, p] of this._annotationProviders) { if (e.some(_ => TextEditorComparer.equals(p.editor, _))) continue; Logger.log('VisibleTextEditorsChanged:', `Clear blame annotations for column ${key}`); this.clear(key); } } }