Selaa lähdekoodia

Remembers editor annotation state

main
Eric Amodio 7 vuotta sitten
vanhempi
commit
a3912b7d89
7 muutettua tiedostoa jossa 214 lisäystä ja 142 poistoa
  1. +159
    -119
      src/annotations/annotationController.ts
  2. +38
    -6
      src/annotations/annotationProvider.ts
  3. +6
    -6
      src/annotations/gutterBlameAnnotationProvider.ts
  4. +5
    -5
      src/annotations/hoverBlameAnnotationProvider.ts
  5. +4
    -4
      src/annotations/recentChangesAnnotationProvider.ts
  6. +1
    -1
      src/commands/clearFileAnnotations.ts
  7. +1
    -1
      src/comparers.ts

+ 159
- 119
src/annotations/annotationController.ts Näytä tiedosto

@ -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);
}
}
}

+ 38
- 6
src/annotations/annotationProvider.ts Näytä tiedosto

@ -1,15 +1,23 @@
'use strict';
import { Disposable, ExtensionContext, TextDocument, TextEditor, TextEditorDecorationType, TextEditorSelectionChangeEvent, window, workspace } from 'vscode';
import { DecorationOptions, Disposable, ExtensionContext, TextDocument, TextEditor, TextEditorDecorationType, TextEditorSelectionChangeEvent, Uri, window, workspace } from 'vscode';
import { FileAnnotationType } from '../annotations/annotationController';
import { TextDocumentComparer } from '../comparers';
import { ExtensionKey, IConfig } from '../configuration';
export abstract class AnnotationProviderBase extends Disposable {
export type TextEditorCorrelationKey = string;
export abstract class AnnotationProviderBase extends Disposable {
static getCorrelationKey(editor: TextEditor | undefined): TextEditorCorrelationKey {
return editor !== undefined ? (editor as any).id : '';
}
public annotationType: FileAnnotationType;
public correlationKey: TextEditorCorrelationKey;
public document: TextDocument;
protected _config: IConfig;
protected _decorations: DecorationOptions[] | undefined;
protected _disposable: Disposable;
constructor(
@ -20,14 +28,14 @@ import { ExtensionKey, IConfig } from '../configuration';
) {
super(() => this.dispose());
this.correlationKey = AnnotationProviderBase.getCorrelationKey(this.editor);
this.document = this.editor.document;
this._config = workspace.getConfiguration().get<IConfig>(ExtensionKey)!;
const subscriptions: Disposable[] = [];
subscriptions.push(window.onDidChangeTextEditorSelection(this._onTextEditorSelectionChanged, this));
const subscriptions: Disposable[] = [
window.onDidChangeTextEditorSelection(this._onTextEditorSelectionChanged, this)
];
this._disposable = Disposable.from(...subscriptions);
}
@ -43,6 +51,16 @@ import { ExtensionKey, IConfig } from '../configuration';
return this.selection(e.selections[0].active.line);
}
get editorId(): string {
if (this.editor === undefined || this.editor.document === undefined) return '';
return (this.editor as any).id;
}
get editorUri(): Uri | undefined {
if (this.editor === undefined || this.editor.document === undefined) return undefined;
return this.editor.document.uri;
}
async clear() {
if (this.editor !== undefined) {
try {
@ -68,6 +86,20 @@ import { ExtensionKey, IConfig } from '../configuration';
await this.provideAnnotation(this.editor === undefined ? undefined : this.editor.selection.active.line);
}
restore(editor: TextEditor, force: boolean = false) {
// If the editor isn't disposed then we don't need to do anything
// Explicitly check for `false`
if (!force && (this.editor as any)._disposed === false) return;
this.editor = editor;
this.correlationKey = AnnotationProviderBase.getCorrelationKey(editor);
this.document = editor.document;
if (this._decorations !== undefined && this._decorations.length) {
this.editor.setDecorations(this.decoration!, this._decorations);
}
}
abstract async provideAnnotation(shaOrLine?: string | number): Promise<boolean>;
abstract async selection(shaOrLine?: string | number): Promise<void>;
abstract async validate(): Promise<boolean>;

+ 6
- 6
src/annotations/gutterBlameAnnotationProvider.ts Näytä tiedosto

@ -36,7 +36,7 @@ export class GutterBlameAnnotationProvider extends BlameAnnotationProviderBase {
const renderOptions = Annotations.gutterRenderOptions(this._config.theme, cfg.heatmap, options);
const separateLines = this._config.theme.annotations.file.gutter.separateLines;
const decorations: DecorationOptions[] = [];
this._decorations = [];
const decorationsMap: { [sha: string]: DecorationOptions | undefined } = Object.create(null);
let commit: GitBlameCommit | undefined;
@ -77,7 +77,7 @@ export class GutterBlameAnnotationProvider extends BlameAnnotationProviderBase {
gutter.range = new Range(line, 0, line, 0);
decorations.push(gutter);
this._decorations.push(gutter);
continue;
}
@ -92,7 +92,7 @@ export class GutterBlameAnnotationProvider extends BlameAnnotationProviderBase {
range: new Range(line, 0, line, 0)
} as DecorationOptions;
decorations.push(gutter);
this._decorations.push(gutter);
continue;
}
@ -108,12 +108,12 @@ export class GutterBlameAnnotationProvider extends BlameAnnotationProviderBase {
gutter.range = new Range(line, 0, line, 0);
decorations.push(gutter);
this._decorations.push(gutter);
decorationsMap[l.sha] = gutter;
}
if (decorations.length) {
this.editor.setDecorations(this.decoration!, decorations);
if (this._decorations.length) {
this.editor.setDecorations(this.decoration!, this._decorations);
}
const duration = process.hrtime(start);

+ 5
- 5
src/annotations/hoverBlameAnnotationProvider.ts Näytä tiedosto

@ -22,7 +22,7 @@ export class HoverBlameAnnotationProvider extends BlameAnnotationProviderBase {
const now = Date.now();
const renderOptions = Annotations.hoverRenderOptions(this._config.theme, cfg.heatmap);
const decorations: DecorationOptions[] = [];
this._decorations = [];
const decorationsMap: { [sha: string]: DecorationOptions } = Object.create(null);
let commit: GitBlameCommit | undefined;
@ -39,7 +39,7 @@ export class HoverBlameAnnotationProvider extends BlameAnnotationProviderBase {
range: new Range(line, 0, line, 0)
} as DecorationOptions;
decorations.push(hover);
this._decorations.push(hover);
continue;
}
@ -50,13 +50,13 @@ export class HoverBlameAnnotationProvider extends BlameAnnotationProviderBase {
hover = Annotations.hover(commit, renderOptions, now);
hover.range = new Range(line, 0, line, 0);
decorations.push(hover);
this._decorations.push(hover);
decorationsMap[l.sha] = hover;
}
if (decorations.length) {
this.editor.setDecorations(this.decoration!, decorations);
if (this._decorations.length) {
this.editor.setDecorations(this.decoration!, this._decorations);
}
const duration = process.hrtime(start);

+ 4
- 4
src/annotations/recentChangesAnnotationProvider.ts Näytä tiedosto

@ -33,7 +33,7 @@ export class RecentChangesAnnotationProvider extends AnnotationProviderBase {
const cfg = this._config.annotations.file.recentChanges;
const dateFormat = this._config.defaultDateFormat;
const decorators: DecorationOptions[] = [];
this._decorations = [];
for (const chunk of diff.chunks) {
let count = chunk.currentPosition.start - 2;
@ -47,7 +47,7 @@ export class RecentChangesAnnotationProvider extends AnnotationProviderBase {
const range = this.editor.document.validateRange(new Range(new Position(count, 0), new Position(count, endOfLineIndex)));
if (cfg.hover.details) {
decorators.push({
this._decorations.push({
hoverMessage: Annotations.getHoverMessage(commit, dateFormat, this.git.hasRemotes(commit.repoPath), this._config.blame.file.annotationType),
range: range
} as DecorationOptions);
@ -58,14 +58,14 @@ export class RecentChangesAnnotationProvider extends AnnotationProviderBase {
message = Annotations.getHoverDiffMessage(commit, line);
}
decorators.push({
this._decorations.push({
hoverMessage: message,
range: range
} as DecorationOptions);
}
}
this.editor.setDecorations(this.highlightDecoration!, decorators);
this.editor.setDecorations(this.highlightDecoration!, this._decorations);
const duration = process.hrtime(start);
Logger.log(`${(duration[0] * 1000) + Math.floor(duration[1] / 1000000)} ms to compute recent changes annotations`);

+ 1
- 1
src/commands/clearFileAnnotations.ts Näytä tiedosto

@ -14,7 +14,7 @@ export class ClearFileAnnotationsCommand extends EditorCommand {
if (editor === undefined || editor.document === undefined || editor.document.isDirty) return undefined;
try {
return this.annotationController.clear(editor.viewColumn || -1);
return this.annotationController.clear(editor);
}
catch (ex) {
Logger.error(ex, 'ClearFileAnnotationsCommand');

+ 1
- 1
src/comparers.ts Näytä tiedosto

@ -34,7 +34,7 @@ class TextEditorComparer extends Comparer {
if (options.usePosition && (lhs.viewColumn !== rhs.viewColumn)) return false;
if (options.useId && (!lhs.document || !rhs.document)) {
if ((lhs as any)._id !== (rhs as any)._id) return false;
if ((lhs as any).id !== (rhs as any).id) return false;
return true;
}

Ladataan…
Peruuta
Tallenna