|
|
@ -1,9 +1,9 @@ |
|
|
|
'use strict'; |
|
|
|
import { Functions, Objects } from '../system'; |
|
|
|
import { Iterables, Objects } from '../system'; |
|
|
|
import { DecorationRenderOptions, Disposable, Event, EventEmitter, ExtensionContext, OverviewRulerLane, Progress, ProgressLocation, TextDocument, TextDocumentChangeEvent, TextEditor, TextEditorDecorationType, TextEditorViewColumnChangeEvent, window, workspace } from 'vscode'; |
|
|
|
import { AnnotationProviderBase } from './annotationProvider'; |
|
|
|
import { AnnotationProviderBase, TextEditorCorrelationKey } from './annotationProvider'; |
|
|
|
import { Keyboard, KeyboardScope, KeyCommand, Keys } from '../keyboard'; |
|
|
|
import { TextDocumentComparer, TextEditorComparer } from '../comparers'; |
|
|
|
import { TextDocumentComparer } from '../comparers'; |
|
|
|
import { ExtensionKey, IConfig, LineHighlightLocations, themeDefaults } from '../configuration'; |
|
|
|
import { CommandContext, setCommandContext } from '../constants'; |
|
|
|
import { BlameabilityChangeEvent, GitContextTracker, GitService, GitUri } from '../gitService'; |
|
|
@ -19,6 +19,20 @@ export enum FileAnnotationType { |
|
|
|
RecentChanges = 'recentChanges' |
|
|
|
} |
|
|
|
|
|
|
|
export enum AnnotationClearReason { |
|
|
|
User = 'User', |
|
|
|
BlameabilityChanged = 'BlameabilityChanged', |
|
|
|
ColumnChanged = 'ColumnChanged', |
|
|
|
Disposing = 'Disposing', |
|
|
|
DocumentChanged = 'DocumentChanged', |
|
|
|
DocumentClosed = 'DocumentClosed' |
|
|
|
} |
|
|
|
|
|
|
|
enum AnnotationStatus { |
|
|
|
Computing = 'computing', |
|
|
|
Computed = 'computed' |
|
|
|
} |
|
|
|
|
|
|
|
export const Decorations = { |
|
|
|
blameAnnotation: window.createTextEditorDecorationType({ |
|
|
|
isWholeLine: true, |
|
|
@ -37,24 +51,28 @@ export class AnnotationController extends Disposable { |
|
|
|
} |
|
|
|
|
|
|
|
private _annotationsDisposable: Disposable | undefined; |
|
|
|
private _annotationProviders: Map<number, AnnotationProviderBase> = new Map(); |
|
|
|
private _annotationProviders: Map<TextEditorCorrelationKey, AnnotationProviderBase> = new Map(); |
|
|
|
private _config: IConfig; |
|
|
|
private _disposable: Disposable; |
|
|
|
private _keyboardScope: KeyboardScope | undefined = undefined; |
|
|
|
|
|
|
|
constructor(private context: ExtensionContext, private git: GitService, private gitContextTracker: GitContextTracker) { |
|
|
|
constructor( |
|
|
|
private context: ExtensionContext, |
|
|
|
private git: GitService, |
|
|
|
private gitContextTracker: GitContextTracker |
|
|
|
) { |
|
|
|
super(() => this.dispose()); |
|
|
|
|
|
|
|
this._onConfigurationChanged(); |
|
|
|
|
|
|
|
const subscriptions: Disposable[] = []; |
|
|
|
|
|
|
|
subscriptions.push(workspace.onDidChangeConfiguration(this._onConfigurationChanged, this)); |
|
|
|
|
|
|
|
const subscriptions: Disposable[] = [ |
|
|
|
workspace.onDidChangeConfiguration(this._onConfigurationChanged, this) |
|
|
|
]; |
|
|
|
this._disposable = Disposable.from(...subscriptions); |
|
|
|
} |
|
|
|
|
|
|
|
dispose() { |
|
|
|
this._annotationProviders.forEach(async (p, i) => await this.clear(i)); |
|
|
|
this._annotationProviders.forEach(async (p, key) => await this.clearCore(key, AnnotationClearReason.Disposing)); |
|
|
|
|
|
|
|
Decorations.blameAnnotation && Decorations.blameAnnotation.dispose(); |
|
|
|
Decorations.blameHighlight && Decorations.blameHighlight.dispose(); |
|
|
@ -166,20 +184,119 @@ export class AnnotationController extends Disposable { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
async clear(column: number) { |
|
|
|
const provider = this._annotationProviders.get(column); |
|
|
|
private async _onActiveTextEditorChanged(e: TextEditor) { |
|
|
|
const provider = this.getProvider(e); |
|
|
|
if (provider === undefined) { |
|
|
|
await setCommandContext(CommandContext.AnnotationStatus, undefined); |
|
|
|
await this.detachKeyboardHook(); |
|
|
|
} |
|
|
|
else { |
|
|
|
await setCommandContext(CommandContext.AnnotationStatus, AnnotationStatus.Computed); |
|
|
|
await this.attachKeyboardHook(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private _onBlameabilityChanged(e: BlameabilityChangeEvent) { |
|
|
|
if (e.blameable || e.editor === undefined) return; |
|
|
|
|
|
|
|
this.clear(e.editor, AnnotationClearReason.BlameabilityChanged); |
|
|
|
} |
|
|
|
|
|
|
|
private _onTextDocumentChanged(e: TextDocumentChangeEvent) { |
|
|
|
for (const [key, p] of this._annotationProviders) { |
|
|
|
if (!TextDocumentComparer.equals(p.document, e.document)) continue; |
|
|
|
|
|
|
|
// We have to defer because isDirty is not reliable inside this event
|
|
|
|
// https://github.com/Microsoft/vscode/issues/27231
|
|
|
|
setTimeout(() => { |
|
|
|
// If the document is dirty all is fine, just kick out since the GitContextTracker will handle it
|
|
|
|
if (e.document.isDirty) return; |
|
|
|
|
|
|
|
// If the document isn't dirty, it is very likely this event was triggered by an outside edit of this document
|
|
|
|
// Which means the document has been reloaded and the annotations have been removed, so we need to update (clear) our state tracking
|
|
|
|
this.clearCore(key, AnnotationClearReason.DocumentChanged); |
|
|
|
}, 1); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private _onTextDocumentClosed(e: TextDocument) { |
|
|
|
for (const [key, p] of this._annotationProviders) { |
|
|
|
if (!TextDocumentComparer.equals(p.document, e)) continue; |
|
|
|
|
|
|
|
this.clearCore(key, AnnotationClearReason.DocumentClosed); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private _onTextEditorViewColumnChanged(e: TextEditorViewColumnChangeEvent) { |
|
|
|
// FYI https://github.com/Microsoft/vscode/issues/35602
|
|
|
|
const provider = this.getProvider(e.textEditor); |
|
|
|
if (provider === undefined) { |
|
|
|
// If we don't find an exact match, do a fuzzy match (since we can't properly track editors)
|
|
|
|
const fuzzyProvider = Iterables.find(this._annotationProviders.values(), p => p.editor.document === e.textEditor.document); |
|
|
|
if (fuzzyProvider == null) return; |
|
|
|
|
|
|
|
this.clearCore(fuzzyProvider.correlationKey, AnnotationClearReason.ColumnChanged); |
|
|
|
|
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
provider.restore(e.textEditor); |
|
|
|
} |
|
|
|
|
|
|
|
private async _onVisibleTextEditorsChanged(editors: TextEditor[]) { |
|
|
|
let provider: AnnotationProviderBase | undefined; |
|
|
|
for (const e of editors) { |
|
|
|
provider = this.getProvider(e); |
|
|
|
if (provider === undefined) continue; |
|
|
|
|
|
|
|
provider.restore(e); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private async attachKeyboardHook() { |
|
|
|
// Allows pressing escape to exit the annotations
|
|
|
|
if (this._keyboardScope === undefined) { |
|
|
|
this._keyboardScope = await Keyboard.instance.beginScope({ |
|
|
|
escape: { |
|
|
|
onDidPressKey: async (key: Keys) => { |
|
|
|
const e = window.activeTextEditor; |
|
|
|
if (e === undefined) return undefined; |
|
|
|
|
|
|
|
await this.clear(e, AnnotationClearReason.User); |
|
|
|
return undefined; |
|
|
|
} |
|
|
|
} as KeyCommand |
|
|
|
}); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private async detachKeyboardHook() { |
|
|
|
if (this._keyboardScope === undefined) return; |
|
|
|
|
|
|
|
await this._keyboardScope.dispose(); |
|
|
|
this._keyboardScope = undefined; |
|
|
|
} |
|
|
|
|
|
|
|
async clear(editor: TextEditor, reason: AnnotationClearReason = AnnotationClearReason.User) { |
|
|
|
this.clearCore(AnnotationProviderBase.getCorrelationKey(editor), reason); |
|
|
|
} |
|
|
|
|
|
|
|
private async clearCore(key: TextEditorCorrelationKey, reason: AnnotationClearReason) { |
|
|
|
const provider = this._annotationProviders.get(key); |
|
|
|
if (provider === undefined) return; |
|
|
|
|
|
|
|
this._annotationProviders.delete(column); |
|
|
|
await provider.dispose(); |
|
|
|
Logger.log(`${reason}:`, `Clear annotations for ${key}`); |
|
|
|
|
|
|
|
if (this._annotationProviders.size === 0) { |
|
|
|
Logger.log(`Remove listener registrations for annotations`); |
|
|
|
this._annotationProviders.delete(key); |
|
|
|
await provider.dispose(); |
|
|
|
|
|
|
|
if (key === AnnotationProviderBase.getCorrelationKey(window.activeTextEditor)) { |
|
|
|
await setCommandContext(CommandContext.AnnotationStatus, undefined); |
|
|
|
await this.detachKeyboardHook(); |
|
|
|
} |
|
|
|
|
|
|
|
this._keyboardScope && this._keyboardScope.dispose(); |
|
|
|
this._keyboardScope = undefined; |
|
|
|
if (this._annotationProviders.size === 0) { |
|
|
|
Logger.log(`Remove all listener registrations for annotations`); |
|
|
|
|
|
|
|
this._annotationsDisposable && this._annotationsDisposable.dispose(); |
|
|
|
this._annotationsDisposable = undefined; |
|
|
@ -190,39 +307,36 @@ export class AnnotationController extends Disposable { |
|
|
|
|
|
|
|
getAnnotationType(editor: TextEditor | undefined): FileAnnotationType | undefined { |
|
|
|
const provider = this.getProvider(editor); |
|
|
|
return provider === undefined ? undefined : provider.annotationType; |
|
|
|
return provider !== undefined && this.git.isEditorBlameable(editor!) ? provider.annotationType : undefined; |
|
|
|
} |
|
|
|
|
|
|
|
getProvider(editor: TextEditor | undefined): AnnotationProviderBase | undefined { |
|
|
|
if (editor === undefined || editor.document === undefined || !this.git.isEditorBlameable(editor)) return undefined; |
|
|
|
|
|
|
|
return this._annotationProviders.get(editor.viewColumn || -1); |
|
|
|
if (editor === undefined || editor.document === undefined) return undefined; |
|
|
|
return this._annotationProviders.get(AnnotationProviderBase.getCorrelationKey(editor)); |
|
|
|
} |
|
|
|
|
|
|
|
private _keyboardScope: KeyboardScope | undefined = undefined; |
|
|
|
|
|
|
|
async showAnnotations(editor: TextEditor, type: FileAnnotationType, shaOrLine?: string | number): Promise<boolean> { |
|
|
|
if (!editor || !editor.document || !this.git.isEditorBlameable(editor)) return false; |
|
|
|
if (editor === undefined || editor.document === undefined || !this.git.isEditorBlameable(editor)) return false; |
|
|
|
|
|
|
|
const currentProvider = this._annotationProviders.get(editor.viewColumn || -1); |
|
|
|
if (currentProvider !== undefined && TextEditorComparer.equals(currentProvider.editor, editor)) { |
|
|
|
const currentProvider = this.getProvider(editor); |
|
|
|
if (currentProvider !== undefined && currentProvider.annotationType === type) { |
|
|
|
await currentProvider.selection(shaOrLine); |
|
|
|
return true; |
|
|
|
} |
|
|
|
|
|
|
|
return window.withProgress({ location: ProgressLocation.Window }, async (progress: Progress<{ message: string }>) => { |
|
|
|
await setCommandContext(CommandContext.AnnotationStatus, 'computing'); |
|
|
|
await setCommandContext(CommandContext.AnnotationStatus, AnnotationStatus.Computing); |
|
|
|
|
|
|
|
const computingAnnotations = this._showAnnotationsCore(currentProvider, editor, type, shaOrLine, progress); |
|
|
|
const computingAnnotations = this.showAnnotationsCore(currentProvider, editor, type, shaOrLine, progress); |
|
|
|
const result = await computingAnnotations; |
|
|
|
|
|
|
|
await setCommandContext(CommandContext.AnnotationStatus, result ? 'computed' : undefined); |
|
|
|
await setCommandContext(CommandContext.AnnotationStatus, result ? AnnotationStatus.Computed : undefined); |
|
|
|
|
|
|
|
return computingAnnotations; |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
private async _showAnnotationsCore(currentProvider: AnnotationProviderBase | undefined, editor: TextEditor, type: FileAnnotationType, shaOrLine?: string | number, progress?: Progress<{ message: string}>): Promise<boolean> { |
|
|
|
private async showAnnotationsCore(currentProvider: AnnotationProviderBase | undefined, editor: TextEditor, type: FileAnnotationType, shaOrLine?: string | number, progress?: Progress<{ message: string}>): Promise<boolean> { |
|
|
|
if (progress !== undefined) { |
|
|
|
let annotationsLabel = 'annotations'; |
|
|
|
switch (type) { |
|
|
@ -240,19 +354,7 @@ export class AnnotationController extends Disposable { |
|
|
|
} |
|
|
|
|
|
|
|
// Allows pressing escape to exit the annotations
|
|
|
|
if (this._keyboardScope === undefined) { |
|
|
|
this._keyboardScope = await Keyboard.instance.beginScope({ |
|
|
|
escape: { |
|
|
|
onDidPressKey: (key: Keys) => { |
|
|
|
const e = window.activeTextEditor; |
|
|
|
if (e === undefined) return Promise.resolve(undefined); |
|
|
|
|
|
|
|
this.clear(e.viewColumn || -1); |
|
|
|
return Promise.resolve(undefined); |
|
|
|
} |
|
|
|
} as KeyCommand |
|
|
|
}); |
|
|
|
} |
|
|
|
this.attachKeyboardHook(); |
|
|
|
|
|
|
|
const gitUri = await GitUri.fromUri(editor.document.uri, this.git); |
|
|
|
|
|
|
@ -272,25 +374,26 @@ export class AnnotationController extends Disposable { |
|
|
|
} |
|
|
|
if (provider === undefined || !(await provider.validate())) return false; |
|
|
|
|
|
|
|
if (currentProvider) { |
|
|
|
await this.clear(currentProvider.editor.viewColumn || -1); |
|
|
|
if (currentProvider !== undefined) { |
|
|
|
await this.clearCore(currentProvider.correlationKey, AnnotationClearReason.User); |
|
|
|
} |
|
|
|
|
|
|
|
if (!this._annotationsDisposable && this._annotationProviders.size === 0) { |
|
|
|
Logger.log(`Add listener registrations for annotations`); |
|
|
|
|
|
|
|
const subscriptions: Disposable[] = []; |
|
|
|
|
|
|
|
subscriptions.push(window.onDidChangeVisibleTextEditors(Functions.debounce(this._onVisibleTextEditorsChanged, 100), this)); |
|
|
|
subscriptions.push(window.onDidChangeTextEditorViewColumn(this._onTextEditorViewColumnChanged, this)); |
|
|
|
subscriptions.push(workspace.onDidChangeTextDocument(this._onTextDocumentChanged, this)); |
|
|
|
subscriptions.push(workspace.onDidCloseTextDocument(this._onTextDocumentClosed, this)); |
|
|
|
subscriptions.push(this.gitContextTracker.onDidChangeBlameability(this._onBlameabilityChanged, this)); |
|
|
|
const subscriptions: Disposable[] = [ |
|
|
|
window.onDidChangeActiveTextEditor(this._onActiveTextEditorChanged, this), |
|
|
|
window.onDidChangeTextEditorViewColumn(this._onTextEditorViewColumnChanged, this), |
|
|
|
window.onDidChangeVisibleTextEditors(this._onVisibleTextEditorsChanged, this), |
|
|
|
workspace.onDidChangeTextDocument(this._onTextDocumentChanged, this), |
|
|
|
workspace.onDidCloseTextDocument(this._onTextDocumentClosed, this), |
|
|
|
this.gitContextTracker.onDidChangeBlameability(this._onBlameabilityChanged, this) |
|
|
|
]; |
|
|
|
|
|
|
|
this._annotationsDisposable = Disposable.from(...subscriptions); |
|
|
|
} |
|
|
|
|
|
|
|
this._annotationProviders.set(editor.viewColumn || -1, provider); |
|
|
|
this._annotationProviders.set(provider.correlationKey, provider); |
|
|
|
if (await provider.provideAnnotation(shaOrLine)) { |
|
|
|
this._onDidToggleAnnotations.fire(); |
|
|
|
return true; |
|
|
@ -302,77 +405,14 @@ export class AnnotationController extends Disposable { |
|
|
|
async toggleAnnotations(editor: TextEditor, type: FileAnnotationType, shaOrLine?: string | number): Promise<boolean> { |
|
|
|
if (!editor || !editor.document || (type === FileAnnotationType.RecentChanges ? !this.git.isTrackable(editor.document.uri) : !this.git.isEditorBlameable(editor))) return false; |
|
|
|
|
|
|
|
const provider = this._annotationProviders.get(editor.viewColumn || -1); |
|
|
|
const provider = this.getProvider(editor); |
|
|
|
if (provider === undefined) return this.showAnnotations(editor, type, shaOrLine); |
|
|
|
|
|
|
|
const reopen = provider.annotationType !== type; |
|
|
|
await this.clear(provider.editor.viewColumn || -1); |
|
|
|
await this.clearCore(provider.correlationKey, AnnotationClearReason.User); |
|
|
|
|
|
|
|
if (!reopen) return false; |
|
|
|
|
|
|
|
return this.showAnnotations(editor, type, shaOrLine); |
|
|
|
} |
|
|
|
|
|
|
|
private _onBlameabilityChanged(e: BlameabilityChangeEvent) { |
|
|
|
if (e.blameable || !e.editor) return; |
|
|
|
|
|
|
|
for (const [key, p] of this._annotationProviders) { |
|
|
|
if (!TextDocumentComparer.equals(p.document, e.editor.document)) continue; |
|
|
|
|
|
|
|
Logger.log('BlameabilityChanged:', `Clear annotations for column ${key}`); |
|
|
|
this.clear(key); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private _onTextDocumentChanged(e: TextDocumentChangeEvent) { |
|
|
|
for (const [key, p] of this._annotationProviders) { |
|
|
|
if (!TextDocumentComparer.equals(p.document, e.document)) continue; |
|
|
|
|
|
|
|
// TODO: Rework this once https://github.com/Microsoft/vscode/issues/27231 is released in v1.13
|
|
|
|
// We have to defer because isDirty is not reliable inside this event
|
|
|
|
setTimeout(() => { |
|
|
|
// If the document is dirty all is fine, just kick out since the GitContextTracker will handle it
|
|
|
|
if (e.document.isDirty) return; |
|
|
|
|
|
|
|
// If the document isn't dirty, it is very likely this event was triggered by an outside edit of this document
|
|
|
|
// Which means the document has been reloaded and the annotations have been removed, so we need to update (clear) our state tracking
|
|
|
|
Logger.log('TextDocumentChanged:', `Clear annotations for column ${key}`); |
|
|
|
this.clear(key); |
|
|
|
}, 1); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private _onTextDocumentClosed(e: TextDocument) { |
|
|
|
for (const [key, p] of this._annotationProviders) { |
|
|
|
if (!TextDocumentComparer.equals(p.document, e)) continue; |
|
|
|
|
|
|
|
Logger.log('TextDocumentClosed:', `Clear annotations for column ${key}`); |
|
|
|
this.clear(key); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private async _onTextEditorViewColumnChanged(e: TextEditorViewColumnChangeEvent) { |
|
|
|
const viewColumn = e.viewColumn || -1; |
|
|
|
|
|
|
|
Logger.log('TextEditorViewColumnChanged:', `Clear 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 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 annotations for column ${key}`); |
|
|
|
this.clear(key); |
|
|
|
} |
|
|
|
} |
|
|
|
} |