mirror of
https://github.com/ckaczor/vscode-gitlens.git
synced 2026-01-16 01:25:42 -05:00
Major refactor/rework -- many new features and breaking changes
Adds all-new, beautiful, highly customizable and themeable, file blame annotations Adds all-new configurability and themeability to the current line blame annotations Adds all-new configurability to the status bar blame information Adds all-new configurability over which commands are added to which menus via the `gitlens.advanced.menus` setting Adds better configurability over where Git code lens will be shown -- both by default and per language Adds an all-new `changes` (diff) hover annotation to the current line - provides instant access to the line's previous version Adds `Toggle Line Blame Annotations` command (`gitlens.toggleLineBlame`) - toggles the current line blame annotations on and off Adds `Show Line Blame Annotations` command (`gitlens.showLineBlame`) - shows the current line blame annotations Adds `Toggle File Blame Annotations` command (`gitlens.toggleFileBlame`) - toggles the file blame annotations on and off Adds `Show File Blame Annotations` command (`gitlens.showFileBlame`) - shows the file blame annotations Adds `Open File in Remote` command (`gitlens.openFileInRemote`) to the `editor/title` context menu Adds `Open Repo in Remote` command (`gitlens.openRepoInRemote`) to the `editor/title` context menu Changes the position of the `Open File in Remote` command (`gitlens.openFileInRemote`) in the context menus - now in the `navigation` group Changes the `Toggle Git Code Lens` command (`gitlens.toggleCodeLens`) to always toggle the Git code lens on and off Removes the on-demand `trailing` file blame annotations -- didn't work out and just ended up with a ton of visual noise Removes `Toggle Blame Annotations` command (`gitlens.toggleBlame`) - replaced by the `Toggle File Blame Annotations` command (`gitlens.toggleFileBlame`) Removes `Show Blame Annotations` command (`gitlens.showBlame`) - replaced by the `Show File Blame Annotations` command (`gitlens.showFileBlame`)
This commit is contained in:
282
src/annotations/annotationController.ts
Normal file
282
src/annotations/annotationController.ts
Normal file
@@ -0,0 +1,282 @@
|
||||
'use strict';
|
||||
import { Functions, Objects } from '../system';
|
||||
import { DecorationRenderOptions, Disposable, Event, EventEmitter, ExtensionContext, OverviewRulerLane, TextDocument, TextDocumentChangeEvent, TextEditor, TextEditorDecorationType, TextEditorViewColumnChangeEvent, window, workspace } from 'vscode';
|
||||
import { AnnotationProviderBase } from './annotationProvider';
|
||||
import { TextDocumentComparer, TextEditorComparer } from '../comparers';
|
||||
import { BlameLineHighlightLocations, ExtensionKey, FileAnnotationType, IConfig, themeDefaults } from '../configuration';
|
||||
import { BlameabilityChangeEvent, GitContextTracker, GitService, GitUri } from '../gitService';
|
||||
import { GutterBlameAnnotationProvider } from './gutterBlameAnnotationProvider';
|
||||
import { HoverBlameAnnotationProvider } from './hoverBlameAnnotationProvider';
|
||||
import { Logger } from '../logger';
|
||||
import { WhitespaceController } from './whitespaceController';
|
||||
|
||||
export const Decorations = {
|
||||
annotation: window.createTextEditorDecorationType({
|
||||
isWholeLine: true
|
||||
} as DecorationRenderOptions),
|
||||
highlight: undefined as TextEditorDecorationType | undefined
|
||||
};
|
||||
|
||||
export class AnnotationController extends Disposable {
|
||||
|
||||
private _onDidToggleAnnotations = new EventEmitter<void>();
|
||||
get onDidToggleAnnotations(): Event<void> {
|
||||
return this._onDidToggleAnnotations.event;
|
||||
}
|
||||
|
||||
private _annotationsDisposable: Disposable | undefined;
|
||||
private _annotationProviders: Map<number, AnnotationProviderBase> = new Map();
|
||||
private _config: IConfig;
|
||||
private _disposable: Disposable;
|
||||
private _whitespaceController: WhitespaceController | undefined;
|
||||
|
||||
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));
|
||||
|
||||
this._disposable = Disposable.from(...subscriptions);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._annotationProviders.forEach(async (p, i) => await this.clear(i));
|
||||
|
||||
Decorations.annotation && Decorations.annotation.dispose();
|
||||
Decorations.highlight && Decorations.highlight.dispose();
|
||||
|
||||
this._annotationsDisposable && this._annotationsDisposable.dispose();
|
||||
this._whitespaceController && this._whitespaceController.dispose();
|
||||
this._disposable && this._disposable.dispose();
|
||||
}
|
||||
|
||||
private _onConfigurationChanged() {
|
||||
let toggleWhitespace = workspace.getConfiguration(`${ExtensionKey}.advanced.toggleWhitespace`).get<boolean>('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<boolean>('fontLigatures');
|
||||
}
|
||||
|
||||
if (toggleWhitespace && !this._whitespaceController) {
|
||||
this._whitespaceController = new WhitespaceController();
|
||||
}
|
||||
else if (!toggleWhitespace && this._whitespaceController) {
|
||||
this._whitespaceController.dispose();
|
||||
this._whitespaceController = undefined;
|
||||
}
|
||||
|
||||
const cfg = workspace.getConfiguration().get<IConfig>(ExtensionKey)!;
|
||||
const cfgHighlight = cfg.blame.file.lineHighlight;
|
||||
const cfgTheme = cfg.theme.lineHighlight;
|
||||
|
||||
let changed = false;
|
||||
|
||||
if (!Objects.areEquivalent(cfgHighlight, this._config && this._config.blame.file.lineHighlight) ||
|
||||
!Objects.areEquivalent(cfgTheme, this._config && this._config.theme.lineHighlight)) {
|
||||
changed = true;
|
||||
|
||||
Decorations.highlight && Decorations.highlight.dispose();
|
||||
|
||||
if (cfgHighlight.enabled) {
|
||||
Decorations.highlight = window.createTextEditorDecorationType({
|
||||
gutterIconSize: 'contain',
|
||||
isWholeLine: true,
|
||||
overviewRulerLane: OverviewRulerLane.Right,
|
||||
dark: {
|
||||
backgroundColor: cfgHighlight.locations.includes(BlameLineHighlightLocations.Line)
|
||||
? cfgTheme.dark.backgroundColor || themeDefaults.lineHighlight.dark.backgroundColor
|
||||
: undefined,
|
||||
gutterIconPath: cfgHighlight.locations.includes(BlameLineHighlightLocations.Gutter)
|
||||
? this.context.asAbsolutePath('images/blame-dark.svg')
|
||||
: undefined,
|
||||
overviewRulerColor: cfgHighlight.locations.includes(BlameLineHighlightLocations.OverviewRuler)
|
||||
? cfgTheme.dark.overviewRulerColor || themeDefaults.lineHighlight.dark.overviewRulerColor
|
||||
: undefined
|
||||
},
|
||||
light: {
|
||||
backgroundColor: cfgHighlight.locations.includes(BlameLineHighlightLocations.Line)
|
||||
? cfgTheme.light.backgroundColor || themeDefaults.lineHighlight.light.backgroundColor
|
||||
: undefined,
|
||||
gutterIconPath: cfgHighlight.locations.includes(BlameLineHighlightLocations.Gutter)
|
||||
? this.context.asAbsolutePath('images/blame-light.svg')
|
||||
: undefined,
|
||||
overviewRulerColor: cfgHighlight.locations.includes(BlameLineHighlightLocations.OverviewRuler)
|
||||
? cfgTheme.light.overviewRulerColor || themeDefaults.lineHighlight.light.overviewRulerColor
|
||||
: undefined
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
Decorations.highlight = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
if (!Objects.areEquivalent(cfg.blame.file, this._config && this._config.blame.file) ||
|
||||
!Objects.areEquivalent(cfg.annotations, this._config && this._config.annotations) ||
|
||||
!Objects.areEquivalent(cfg.theme.annotations, this._config && this._config.theme.annotations)) {
|
||||
changed = true;
|
||||
}
|
||||
|
||||
this._config = cfg;
|
||||
|
||||
if (changed) {
|
||||
// Since the configuration has changed -- reset any visible annotations
|
||||
for (const provider of this._annotationProviders.values()) {
|
||||
if (provider === undefined) continue;
|
||||
|
||||
provider.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 annotations`);
|
||||
this._annotationsDisposable && this._annotationsDisposable.dispose();
|
||||
this._annotationsDisposable = undefined;
|
||||
}
|
||||
|
||||
this._onDidToggleAnnotations.fire();
|
||||
}
|
||||
|
||||
getAnnotationType(editor: TextEditor): FileAnnotationType | undefined {
|
||||
const provider = this.getProvider(editor);
|
||||
return provider === undefined ? undefined : provider.annotationType;
|
||||
}
|
||||
|
||||
getProvider(editor: TextEditor): AnnotationProviderBase | undefined {
|
||||
if (!editor || !editor.document || !this.git.isEditorBlameable(editor)) return undefined;
|
||||
|
||||
return this._annotationProviders.get(editor.viewColumn || -1);
|
||||
}
|
||||
|
||||
async showAnnotations(editor: TextEditor, type: FileAnnotationType, shaOrLine?: string | number): Promise<boolean> {
|
||||
if (!editor || !editor.document || !this.git.isEditorBlameable(editor)) return false;
|
||||
|
||||
const currentProvider = this._annotationProviders.get(editor.viewColumn || -1);
|
||||
if (currentProvider && TextEditorComparer.equals(currentProvider.editor, editor)) {
|
||||
await currentProvider.selection(shaOrLine);
|
||||
return true;
|
||||
}
|
||||
|
||||
const gitUri = await GitUri.fromUri(editor.document.uri, this.git);
|
||||
|
||||
let provider: AnnotationProviderBase | undefined = undefined;
|
||||
switch (type) {
|
||||
case FileAnnotationType.Gutter:
|
||||
provider = new GutterBlameAnnotationProvider(this.context, editor, Decorations.annotation, Decorations.highlight, this._whitespaceController, this.git, gitUri);
|
||||
break;
|
||||
case FileAnnotationType.Hover:
|
||||
provider = new HoverBlameAnnotationProvider(this.context, editor, Decorations.annotation, Decorations.highlight, this._whitespaceController, this.git, gitUri);
|
||||
break;
|
||||
}
|
||||
if (provider === undefined || !(await provider.validate())) return false;
|
||||
|
||||
if (currentProvider) {
|
||||
await this.clear(currentProvider.editor.viewColumn || -1);
|
||||
}
|
||||
|
||||
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.onDidBlameabilityChange(this._onBlameabilityChanged, this));
|
||||
|
||||
this._annotationsDisposable = Disposable.from(...subscriptions);
|
||||
}
|
||||
|
||||
this._annotationProviders.set(editor.viewColumn || -1, provider);
|
||||
if (await provider.provideAnnotation(shaOrLine)) {
|
||||
this._onDidToggleAnnotations.fire();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async toggleAnnotations(editor: TextEditor, type: FileAnnotationType, shaOrLine?: string | number): Promise<boolean> {
|
||||
if (!editor || !editor.document || !this.git.isEditorBlameable(editor)) return false;
|
||||
|
||||
const provider = this._annotationProviders.get(editor.viewColumn || -1);
|
||||
if (provider === undefined) return this.showAnnotations(editor, type, shaOrLine);
|
||||
|
||||
await this.clear(provider.editor.viewColumn || -1);
|
||||
return false;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
74
src/annotations/annotationProvider.ts
Normal file
74
src/annotations/annotationProvider.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
'use strict';
|
||||
import { Functions } from '../system';
|
||||
import { Disposable, ExtensionContext, TextDocument, TextEditor, TextEditorDecorationType, TextEditorSelectionChangeEvent, window, workspace } from 'vscode';
|
||||
import { TextDocumentComparer } from '../comparers';
|
||||
import { ExtensionKey, FileAnnotationType, IConfig } from '../configuration';
|
||||
import { WhitespaceController } from './whitespaceController';
|
||||
|
||||
export abstract class AnnotationProviderBase extends Disposable {
|
||||
|
||||
public annotationType: FileAnnotationType;
|
||||
public document: TextDocument;
|
||||
|
||||
protected _config: IConfig;
|
||||
protected _disposable: Disposable;
|
||||
|
||||
constructor(context: ExtensionContext, public editor: TextEditor, protected decoration: TextEditorDecorationType, protected highlightDecoration: TextEditorDecorationType | undefined, protected whitespaceController: WhitespaceController | undefined) {
|
||||
super(() => this.dispose());
|
||||
|
||||
this.document = this.editor.document;
|
||||
|
||||
this._config = workspace.getConfiguration().get<IConfig>(ExtensionKey)!;
|
||||
|
||||
const subscriptions: Disposable[] = [];
|
||||
|
||||
subscriptions.push(window.onDidChangeTextEditorSelection(this._onTextEditorSelectionChanged, this));
|
||||
|
||||
this._disposable = Disposable.from(...subscriptions);
|
||||
}
|
||||
|
||||
async dispose() {
|
||||
await this.clear();
|
||||
|
||||
this._disposable && this._disposable.dispose();
|
||||
}
|
||||
|
||||
private async _onTextEditorSelectionChanged(e: TextEditorSelectionChangeEvent) {
|
||||
if (!TextDocumentComparer.equals(this.document, e.textEditor && e.textEditor.document)) return;
|
||||
|
||||
return this.selection(e.selections[0].active.line);
|
||||
}
|
||||
|
||||
async clear() {
|
||||
if (this.editor !== undefined) {
|
||||
try {
|
||||
this.editor.setDecorations(this.decoration, []);
|
||||
this.highlightDecoration && this.editor.setDecorations(this.highlightDecoration, []);
|
||||
// I have no idea why the decorators sometimes don't get removed, but if they don't try again with a tiny delay
|
||||
if (this.highlightDecoration !== undefined) {
|
||||
await Functions.wait(1);
|
||||
|
||||
if (this.highlightDecoration === undefined) return;
|
||||
|
||||
this.editor.setDecorations(this.highlightDecoration, []);
|
||||
}
|
||||
}
|
||||
catch (ex) { }
|
||||
}
|
||||
|
||||
// HACK: Until https://github.com/Microsoft/vscode/issues/11485 is fixed -- restore whitespace
|
||||
this.whitespaceController && await this.whitespaceController.restore();
|
||||
}
|
||||
|
||||
async reset() {
|
||||
await this.clear();
|
||||
|
||||
this._config = workspace.getConfiguration().get<IConfig>(ExtensionKey)!;
|
||||
|
||||
await this.provideAnnotation(this.editor === undefined ? undefined : this.editor.selection.active.line);
|
||||
}
|
||||
|
||||
abstract async provideAnnotation(shaOrLine?: string | number): Promise<boolean>;
|
||||
abstract async selection(shaOrLine?: string | number): Promise<void>;
|
||||
abstract async validate(): Promise<boolean>;
|
||||
}
|
||||
189
src/annotations/annotations.ts
Normal file
189
src/annotations/annotations.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
import { DecorationInstanceRenderOptions, DecorationOptions, ThemableDecorationRenderOptions } from 'vscode';
|
||||
import { IThemeConfig, themeDefaults } from '../configuration';
|
||||
import { CommitFormatter, GitCommit, GitService, GitUri, ICommitFormatOptions } from '../gitService';
|
||||
import * as moment from 'moment';
|
||||
|
||||
interface IHeatmapConfig {
|
||||
enabled: boolean;
|
||||
location?: 'left' | 'right';
|
||||
}
|
||||
|
||||
interface IRenderOptions {
|
||||
uncommittedForegroundColor?: {
|
||||
dark: string;
|
||||
light: string;
|
||||
};
|
||||
|
||||
before?: DecorationInstanceRenderOptions & ThemableDecorationRenderOptions & { height?: string };
|
||||
dark?: DecorationInstanceRenderOptions;
|
||||
light?: DecorationInstanceRenderOptions;
|
||||
}
|
||||
|
||||
export const endOfLineIndex = 1000000;
|
||||
|
||||
export class Annotations {
|
||||
|
||||
static applyHeatmap(decoration: DecorationOptions, date: Date, now: moment.Moment) {
|
||||
const color = this._getHeatmapColor(now, date);
|
||||
(decoration.renderOptions!.before! as any).borderColor = color;
|
||||
}
|
||||
|
||||
private static _getHeatmapColor(now: moment.Moment, date: Date) {
|
||||
const days = now.diff(moment(date), 'days');
|
||||
|
||||
if (days <= 2) return '#ffeca7';
|
||||
if (days <= 7) return '#ffdd8c';
|
||||
if (days <= 14) return '#ffdd7c';
|
||||
if (days <= 30) return '#fba447';
|
||||
if (days <= 60) return '#f68736';
|
||||
if (days <= 90) return '#f37636';
|
||||
if (days <= 180) return '#ca6632';
|
||||
if (days <= 365) return '#c0513f';
|
||||
if (days <= 730) return '#a2503a';
|
||||
return '#793738';
|
||||
}
|
||||
|
||||
static async changesHover(commit: GitCommit, line: number, uri: GitUri, git: GitService): Promise<DecorationOptions> {
|
||||
let message: string | undefined = undefined;
|
||||
if (commit.isUncommitted) {
|
||||
const [previous, current] = await git.getDiffForLine(uri, line + uri.offset);
|
||||
message = CommitFormatter.toHoverDiff(commit, previous, current);
|
||||
}
|
||||
else if (commit.previousSha !== undefined) {
|
||||
const [previous, current] = await git.getDiffForLine(uri, line + uri.offset, commit.previousSha);
|
||||
message = CommitFormatter.toHoverDiff(commit, previous, current);
|
||||
}
|
||||
|
||||
return {
|
||||
hoverMessage: message
|
||||
} as DecorationOptions;
|
||||
}
|
||||
|
||||
static detailsHover(commit: GitCommit): DecorationOptions {
|
||||
const message = CommitFormatter.toHoverAnnotation(commit);
|
||||
return {
|
||||
hoverMessage: message
|
||||
} as DecorationOptions;
|
||||
}
|
||||
|
||||
static gutter(commit: GitCommit, format: string, dateFormatOrFormatOptions: string | null | ICommitFormatOptions, renderOptions: IRenderOptions, compact: boolean): DecorationOptions {
|
||||
let content = `\u00a0${CommitFormatter.fromTemplate(format, commit, dateFormatOrFormatOptions)}\u00a0`;
|
||||
if (compact) {
|
||||
content = '\u00a0'.repeat(content.length);
|
||||
}
|
||||
|
||||
return {
|
||||
renderOptions: {
|
||||
before: {
|
||||
...renderOptions.before,
|
||||
...{
|
||||
contentText: content,
|
||||
margin: '0 26px 0 0'
|
||||
}
|
||||
},
|
||||
dark: {
|
||||
before: commit.isUncommitted
|
||||
? { ...renderOptions.dark, ...{ color: renderOptions.uncommittedForegroundColor!.dark } }
|
||||
: { ...renderOptions.dark }
|
||||
},
|
||||
light: {
|
||||
before: commit.isUncommitted
|
||||
? { ...renderOptions.light, ...{ color: renderOptions.uncommittedForegroundColor!.light } }
|
||||
: { ...renderOptions.light }
|
||||
}
|
||||
} as DecorationInstanceRenderOptions
|
||||
} as DecorationOptions;
|
||||
}
|
||||
|
||||
static gutterRenderOptions(cfgTheme: IThemeConfig, heatmap: IHeatmapConfig): IRenderOptions {
|
||||
const cfgFileTheme = cfgTheme.annotations.file.gutter;
|
||||
|
||||
let borderStyle = undefined;
|
||||
let borderWidth = undefined;
|
||||
if (heatmap.enabled) {
|
||||
borderStyle = 'solid';
|
||||
borderWidth = heatmap.location === 'left' ? '0 0 0 2px' : '0 2px 0 0';
|
||||
}
|
||||
|
||||
return {
|
||||
uncommittedForegroundColor: {
|
||||
dark: cfgFileTheme.dark.uncommittedForegroundColor || cfgFileTheme.dark.foregroundColor || themeDefaults.annotations.file.gutter.dark.foregroundColor,
|
||||
light: cfgFileTheme.light.uncommittedForegroundColor || cfgFileTheme.light.foregroundColor || themeDefaults.annotations.file.gutter.light.foregroundColor
|
||||
},
|
||||
before: {
|
||||
borderStyle: borderStyle,
|
||||
borderWidth: borderWidth,
|
||||
height: cfgFileTheme.separateLines ? 'calc(100% - 1px)' : '100%'
|
||||
},
|
||||
dark: {
|
||||
backgroundColor: cfgFileTheme.dark.backgroundColor || undefined,
|
||||
color: cfgFileTheme.dark.foregroundColor || themeDefaults.annotations.file.gutter.dark.foregroundColor
|
||||
} as DecorationInstanceRenderOptions,
|
||||
light: {
|
||||
backgroundColor: cfgFileTheme.light.backgroundColor || undefined,
|
||||
color: cfgFileTheme.light.foregroundColor || themeDefaults.annotations.file.gutter.light.foregroundColor
|
||||
} as DecorationInstanceRenderOptions
|
||||
};
|
||||
}
|
||||
|
||||
static hover(commit: GitCommit, renderOptions: IRenderOptions, heatmap: boolean): DecorationOptions {
|
||||
return {
|
||||
hoverMessage: CommitFormatter.toHoverAnnotation(commit),
|
||||
renderOptions: heatmap ? { before: { ...renderOptions.before } } : undefined
|
||||
} as DecorationOptions;
|
||||
}
|
||||
|
||||
static hoverRenderOptions(cfgTheme: IThemeConfig, heatmap: IHeatmapConfig): IRenderOptions {
|
||||
if (!heatmap.enabled) return { before: undefined };
|
||||
|
||||
return {
|
||||
before: {
|
||||
borderStyle: 'solid',
|
||||
borderWidth: '0 0 0 2px',
|
||||
contentText: '\u200B',
|
||||
height: cfgTheme.annotations.file.hover.separateLines ? 'calc(100% - 1px)' : '100%',
|
||||
margin: '0 26px 0 0'
|
||||
}
|
||||
} as IRenderOptions;
|
||||
}
|
||||
|
||||
static trailing(commit: GitCommit, format: string, dateFormat: string | null, cfgTheme: IThemeConfig): DecorationOptions {
|
||||
const message = CommitFormatter.fromTemplate(format, commit, dateFormat);
|
||||
return {
|
||||
renderOptions: {
|
||||
after: {
|
||||
contentText: `\u00a0${message}\u00a0`
|
||||
},
|
||||
dark: {
|
||||
after: {
|
||||
backgroundColor: cfgTheme.annotations.line.trailing.dark.backgroundColor || undefined,
|
||||
color: cfgTheme.annotations.line.trailing.dark.foregroundColor || themeDefaults.annotations.line.trailing.dark.foregroundColor
|
||||
}
|
||||
},
|
||||
light: {
|
||||
after: {
|
||||
backgroundColor: cfgTheme.annotations.line.trailing.light.backgroundColor || undefined,
|
||||
color: cfgTheme.annotations.line.trailing.light.foregroundColor || themeDefaults.annotations.line.trailing.light.foregroundColor
|
||||
}
|
||||
}
|
||||
} as DecorationInstanceRenderOptions
|
||||
} as DecorationOptions;
|
||||
}
|
||||
|
||||
static withRange(decoration: DecorationOptions, start?: number, end?: number): DecorationOptions {
|
||||
let range = decoration.range;
|
||||
if (start !== undefined) {
|
||||
range = range.with({
|
||||
start: range.start.with({ character: start })
|
||||
});
|
||||
}
|
||||
|
||||
if (end !== undefined) {
|
||||
range = range.with({
|
||||
end: range.end.with({ character: end })
|
||||
});
|
||||
}
|
||||
|
||||
return { ...decoration, ...{ range: range } };
|
||||
}
|
||||
}
|
||||
82
src/annotations/blameAnnotationProvider.ts
Normal file
82
src/annotations/blameAnnotationProvider.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
'use strict';
|
||||
import { Iterables } from '../system';
|
||||
import { ExtensionContext, Range, TextEditor, TextEditorDecorationType } from 'vscode';
|
||||
import { AnnotationProviderBase } from './annotationProvider';
|
||||
import { GitService, GitUri, IGitBlame } from '../gitService';
|
||||
import { WhitespaceController } from './whitespaceController';
|
||||
|
||||
export abstract class BlameAnnotationProviderBase extends AnnotationProviderBase {
|
||||
|
||||
protected _blame: Promise<IGitBlame>;
|
||||
|
||||
constructor(context: ExtensionContext, editor: TextEditor, decoration: TextEditorDecorationType, highlightDecoration: TextEditorDecorationType | undefined, whitespaceController: WhitespaceController | undefined, protected git: GitService, protected uri: GitUri) {
|
||||
super(context, editor, decoration, highlightDecoration, whitespaceController);
|
||||
|
||||
this._blame = this.git.getBlameForFile(this.uri);
|
||||
}
|
||||
|
||||
async selection(shaOrLine?: string | number, blame?: IGitBlame) {
|
||||
if (!this.highlightDecoration) return;
|
||||
|
||||
if (blame === undefined) {
|
||||
blame = await this._blame;
|
||||
if (!blame || !blame.lines.length) return;
|
||||
}
|
||||
|
||||
const offset = this.uri.offset;
|
||||
|
||||
let sha: string | undefined = undefined;
|
||||
if (typeof shaOrLine === 'string') {
|
||||
sha = shaOrLine;
|
||||
}
|
||||
else if (typeof shaOrLine === 'number') {
|
||||
const line = shaOrLine - offset;
|
||||
if (line >= 0) {
|
||||
const commitLine = blame.lines[line];
|
||||
sha = commitLine && commitLine.sha;
|
||||
}
|
||||
}
|
||||
else {
|
||||
sha = Iterables.first(blame.commits.values()).sha;
|
||||
}
|
||||
|
||||
if (!sha) {
|
||||
this.editor.setDecorations(this.highlightDecoration, []);
|
||||
return;
|
||||
}
|
||||
|
||||
const highlightDecorationRanges = blame.lines
|
||||
.filter(l => l.sha === sha)
|
||||
.map(l => this.editor.document.validateRange(new Range(l.line + offset, 0, l.line + offset, 1000000)));
|
||||
|
||||
this.editor.setDecorations(this.highlightDecoration, highlightDecorationRanges);
|
||||
}
|
||||
|
||||
async validate(): Promise<boolean> {
|
||||
const blame = await this._blame;
|
||||
return blame !== undefined && blame.lines.length !== 0;
|
||||
}
|
||||
|
||||
protected async getBlame(requiresWhitespaceHack: boolean): Promise<IGitBlame | undefined> {
|
||||
let whitespacePromise: Promise<void> | undefined;
|
||||
// HACK: Until https://github.com/Microsoft/vscode/issues/11485 is fixed -- override whitespace (turn off)
|
||||
if (requiresWhitespaceHack) {
|
||||
whitespacePromise = this.whitespaceController && this.whitespaceController.override();
|
||||
}
|
||||
|
||||
let blame: IGitBlame;
|
||||
if (whitespacePromise) {
|
||||
[blame] = await Promise.all([this._blame, whitespacePromise]);
|
||||
}
|
||||
else {
|
||||
blame = await this._blame;
|
||||
}
|
||||
|
||||
if (!blame || !blame.lines.length) {
|
||||
this.whitespaceController && await this.whitespaceController.restore();
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return blame;
|
||||
}
|
||||
}
|
||||
69
src/annotations/diffAnnotationProvider.ts
Normal file
69
src/annotations/diffAnnotationProvider.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
'use strict';
|
||||
import { DecorationOptions, ExtensionContext, Position, Range, TextEditor, TextEditorDecorationType } from 'vscode';
|
||||
import { AnnotationProviderBase } from './annotationProvider';
|
||||
import { GitService, GitUri } from '../gitService';
|
||||
import { WhitespaceController } from './whitespaceController';
|
||||
|
||||
export class DiffAnnotationProvider extends AnnotationProviderBase {
|
||||
|
||||
constructor(context: ExtensionContext, editor: TextEditor, decoration: TextEditorDecorationType, highlightDecoration: TextEditorDecorationType | undefined, whitespaceController: WhitespaceController | undefined, private git: GitService, private uri: GitUri) {
|
||||
super(context, editor, decoration, highlightDecoration, whitespaceController);
|
||||
}
|
||||
|
||||
async provideAnnotation(shaOrLine?: string | number): Promise<boolean> {
|
||||
// let sha1: string | undefined = undefined;
|
||||
// let sha2: string | undefined = undefined;
|
||||
// if (shaOrLine === undefined) {
|
||||
// const commit = await this.git.getLogCommit(this.uri.repoPath, this.uri.fsPath, { previous: true });
|
||||
// if (commit === undefined) return false;
|
||||
|
||||
// sha1 = commit.previousSha;
|
||||
// }
|
||||
// else if (typeof shaOrLine === 'string') {
|
||||
// sha1 = shaOrLine;
|
||||
// }
|
||||
// else {
|
||||
// const blame = await this.git.getBlameForLine(this.uri, shaOrLine);
|
||||
// if (blame === undefined) return false;
|
||||
|
||||
// sha1 = blame.commit.previousSha;
|
||||
// sha2 = blame.commit.sha;
|
||||
// }
|
||||
|
||||
// if (sha1 === undefined) return false;
|
||||
|
||||
const commit = await this.git.getLogCommit(this.uri.repoPath, this.uri.fsPath, { previous: true });
|
||||
if (commit === undefined) return false;
|
||||
|
||||
const diff = await this.git.getDiffForFile(this.uri, commit.previousSha);
|
||||
if (diff === undefined) return false;
|
||||
|
||||
const decorators: DecorationOptions[] = [];
|
||||
|
||||
for (const chunk of diff.chunks) {
|
||||
let count = chunk.currentStart - 2;
|
||||
for (const change of chunk.current) {
|
||||
if (change === undefined) continue;
|
||||
|
||||
count++;
|
||||
|
||||
if (change.state === 'unchanged') continue;
|
||||
|
||||
decorators.push({
|
||||
range: new Range(new Position(count, 0), new Position(count, 0))
|
||||
} as DecorationOptions);
|
||||
}
|
||||
}
|
||||
|
||||
this.editor.setDecorations(this.decoration, decorators);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async selection(shaOrLine?: string | number): Promise<void> {
|
||||
}
|
||||
|
||||
async validate(): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
76
src/annotations/gutterBlameAnnotationProvider.ts
Normal file
76
src/annotations/gutterBlameAnnotationProvider.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
'use strict';
|
||||
import { Strings } from '../system';
|
||||
import { DecorationOptions, Range } from 'vscode';
|
||||
import { BlameAnnotationProviderBase } from './blameAnnotationProvider';
|
||||
import { Annotations, endOfLineIndex } from './annotations';
|
||||
import { FileAnnotationType } from '../configuration';
|
||||
import { ICommitFormatOptions } from '../gitService';
|
||||
import * as moment from 'moment';
|
||||
|
||||
export class GutterBlameAnnotationProvider extends BlameAnnotationProviderBase {
|
||||
|
||||
async provideAnnotation(shaOrLine?: string | number, type?: FileAnnotationType): Promise<boolean> {
|
||||
this.annotationType = FileAnnotationType.Gutter;
|
||||
|
||||
const blame = await this.getBlame(true);
|
||||
if (blame === undefined) return false;
|
||||
|
||||
const cfg = this._config.annotations.file.gutter;
|
||||
|
||||
// Precalculate the formatting options so we don't need to do it on each iteration
|
||||
const tokenOptions = Strings.getTokensFromTemplate(cfg.format)
|
||||
.reduce((map, token) => {
|
||||
map[token.key] = token.options;
|
||||
return map;
|
||||
}, {} as { [token: string]: ICommitFormatOptions });
|
||||
|
||||
const options: ICommitFormatOptions = {
|
||||
dateFormat: cfg.dateFormat,
|
||||
tokenOptions: tokenOptions
|
||||
};
|
||||
|
||||
const now = moment();
|
||||
const offset = this.uri.offset;
|
||||
let previousLine: string | undefined = undefined;
|
||||
const renderOptions = Annotations.gutterRenderOptions(this._config.theme, cfg.heatmap);
|
||||
|
||||
const decorations: DecorationOptions[] = [];
|
||||
|
||||
for (const l of blame.lines) {
|
||||
const commit = blame.commits.get(l.sha);
|
||||
if (commit === undefined) continue;
|
||||
|
||||
const line = l.line + offset;
|
||||
|
||||
const gutter = Annotations.gutter(commit, cfg.format, options, renderOptions, cfg.compact && previousLine === l.sha);
|
||||
|
||||
if (cfg.compact) {
|
||||
const isEmptyOrWhitespace = this.document.lineAt(line).isEmptyOrWhitespace;
|
||||
previousLine = isEmptyOrWhitespace ? undefined : l.sha;
|
||||
}
|
||||
|
||||
if (cfg.heatmap.enabled) {
|
||||
Annotations.applyHeatmap(gutter, commit.date, now);
|
||||
}
|
||||
|
||||
const firstNonWhitespace = this.editor.document.lineAt(line).firstNonWhitespaceCharacterIndex;
|
||||
gutter.range = this.editor.document.validateRange(new Range(line, 0, line, firstNonWhitespace));
|
||||
decorations.push(gutter);
|
||||
|
||||
if (cfg.hover.details) {
|
||||
const details = Annotations.detailsHover(commit);
|
||||
details.range = cfg.hover.wholeLine
|
||||
? this.editor.document.validateRange(new Range(line, 0, line, endOfLineIndex))
|
||||
: gutter.range;
|
||||
decorations.push(details);
|
||||
}
|
||||
}
|
||||
|
||||
if (decorations.length) {
|
||||
this.editor.setDecorations(this.decoration, decorations);
|
||||
}
|
||||
|
||||
this.selection(shaOrLine, blame);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
49
src/annotations/hoverBlameAnnotationProvider.ts
Normal file
49
src/annotations/hoverBlameAnnotationProvider.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
'use strict';
|
||||
import { DecorationOptions, Range } from 'vscode';
|
||||
import { BlameAnnotationProviderBase } from './blameAnnotationProvider';
|
||||
import { Annotations, endOfLineIndex } from './annotations';
|
||||
import { FileAnnotationType } from '../configuration';
|
||||
import * as moment from 'moment';
|
||||
|
||||
export class HoverBlameAnnotationProvider extends BlameAnnotationProviderBase {
|
||||
|
||||
async provideAnnotation(shaOrLine?: string | number): Promise<boolean> {
|
||||
this.annotationType = FileAnnotationType.Hover;
|
||||
|
||||
const blame = await this.getBlame(this._config.annotations.file.hover.heatmap.enabled);
|
||||
if (blame === undefined) return false;
|
||||
|
||||
const cfg = this._config.annotations.file.hover;
|
||||
|
||||
const now = moment();
|
||||
const offset = this.uri.offset;
|
||||
const renderOptions = Annotations.hoverRenderOptions(this._config.theme, cfg.heatmap);
|
||||
|
||||
const decorations: DecorationOptions[] = [];
|
||||
|
||||
for (const l of blame.lines) {
|
||||
const commit = blame.commits.get(l.sha);
|
||||
if (commit === undefined) continue;
|
||||
|
||||
const line = l.line + offset;
|
||||
|
||||
const hover = Annotations.hover(commit, renderOptions, cfg.heatmap.enabled);
|
||||
|
||||
const endIndex = cfg.wholeLine ? endOfLineIndex : this.editor.document.lineAt(line).firstNonWhitespaceCharacterIndex;
|
||||
hover.range = this.editor.document.validateRange(new Range(line, 0, line, endIndex));
|
||||
|
||||
if (cfg.heatmap.enabled) {
|
||||
Annotations.applyHeatmap(hover, commit.date, now);
|
||||
}
|
||||
|
||||
decorations.push(hover);
|
||||
}
|
||||
|
||||
if (decorations.length) {
|
||||
this.editor.setDecorations(this.decoration, decorations);
|
||||
}
|
||||
|
||||
this.selection(shaOrLine, blame);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
150
src/annotations/whitespaceController.ts
Normal file
150
src/annotations/whitespaceController.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
'use strict';
|
||||
import { Disposable, workspace } from 'vscode';
|
||||
import { Logger } from '../logger';
|
||||
|
||||
interface ConfigurationInspection {
|
||||
key: string;
|
||||
defaultValue?: string;
|
||||
globalValue?: string;
|
||||
workspaceValue?: string;
|
||||
}
|
||||
|
||||
enum SettingLocation {
|
||||
workspace,
|
||||
global,
|
||||
default
|
||||
}
|
||||
|
||||
class RenderWhitespaceConfiguration {
|
||||
|
||||
constructor(public inspection: ConfigurationInspection) { }
|
||||
|
||||
get location(): SettingLocation {
|
||||
if (this.inspection.workspaceValue) return SettingLocation.workspace;
|
||||
if (this.inspection.globalValue) return SettingLocation.global;
|
||||
return SettingLocation.default;
|
||||
}
|
||||
|
||||
get overrideRequired() {
|
||||
return this.value != null && this.value !== 'none';
|
||||
}
|
||||
|
||||
get value(): string | undefined {
|
||||
return this.inspection.workspaceValue || this.inspection.globalValue || this.inspection.defaultValue;
|
||||
}
|
||||
|
||||
update(replacement: ConfigurationInspection): boolean {
|
||||
let override = false;
|
||||
|
||||
switch (this.location) {
|
||||
case SettingLocation.workspace:
|
||||
this.inspection.defaultValue = replacement.defaultValue;
|
||||
this.inspection.globalValue = replacement.globalValue;
|
||||
if (replacement.workspaceValue !== 'none') {
|
||||
this.inspection.workspaceValue = replacement.workspaceValue;
|
||||
override = true;
|
||||
}
|
||||
break;
|
||||
case SettingLocation.global:
|
||||
this.inspection.defaultValue = replacement.defaultValue;
|
||||
this.inspection.workspaceValue = replacement.workspaceValue;
|
||||
if (replacement.globalValue !== 'none') {
|
||||
this.inspection.globalValue = replacement.globalValue;
|
||||
override = true;
|
||||
}
|
||||
break;
|
||||
case SettingLocation.default:
|
||||
this.inspection.globalValue = replacement.globalValue;
|
||||
this.inspection.workspaceValue = replacement.workspaceValue;
|
||||
if (replacement.defaultValue !== 'none') {
|
||||
this.inspection.defaultValue = replacement.defaultValue;
|
||||
override = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return override;
|
||||
}
|
||||
}
|
||||
|
||||
export class WhitespaceController extends Disposable {
|
||||
|
||||
private _configuration: RenderWhitespaceConfiguration;
|
||||
private _count: number = 0;
|
||||
private _disposable: Disposable;
|
||||
private _disposed: boolean = false;
|
||||
|
||||
constructor() {
|
||||
super(() => this.dispose());
|
||||
|
||||
const subscriptions: Disposable[] = [];
|
||||
|
||||
subscriptions.push(workspace.onDidChangeConfiguration(this._onConfigurationChanged, this));
|
||||
|
||||
this._disposable = Disposable.from(...subscriptions);
|
||||
|
||||
this._onConfigurationChanged();
|
||||
}
|
||||
|
||||
async dispose() {
|
||||
this._disposed = true;
|
||||
if (this._count !== 0) {
|
||||
await this._restoreWhitespace();
|
||||
this._count = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private _onConfigurationChanged() {
|
||||
if (this._disposed) return;
|
||||
|
||||
const inspection = workspace.getConfiguration('editor').inspect<string>('renderWhitespace')!;
|
||||
|
||||
if (!this._count) {
|
||||
this._configuration = new RenderWhitespaceConfiguration(inspection);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._configuration.update(inspection)) {
|
||||
// Since we were currently overriding whitespace, re-override
|
||||
setTimeout(() => this._overrideWhitespace(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
async override() {
|
||||
if (this._disposed) return;
|
||||
|
||||
Logger.log(`Request whitespace override; count=${this._count}`);
|
||||
this._count++;
|
||||
if (this._count === 1 && this._configuration.overrideRequired) {
|
||||
// Override whitespace (turn off)
|
||||
await this._overrideWhitespace();
|
||||
}
|
||||
}
|
||||
|
||||
private async _overrideWhitespace() {
|
||||
Logger.log(`Override whitespace`);
|
||||
const cfg = workspace.getConfiguration('editor');
|
||||
return cfg.update('renderWhitespace', 'none', this._configuration.location === SettingLocation.global);
|
||||
}
|
||||
|
||||
async restore() {
|
||||
if (this._disposed || this._count === 0) return;
|
||||
|
||||
Logger.log(`Request whitespace restore; count=${this._count}`);
|
||||
this._count--;
|
||||
if (this._count === 0 && this._configuration.overrideRequired) {
|
||||
// restore whitespace
|
||||
await this._restoreWhitespace();
|
||||
}
|
||||
}
|
||||
|
||||
private async _restoreWhitespace() {
|
||||
Logger.log(`Restore whitespace`);
|
||||
const cfg = workspace.getConfiguration('editor');
|
||||
return cfg.update('renderWhitespace',
|
||||
this._configuration.location === SettingLocation.default
|
||||
? undefined
|
||||
: this._configuration.value,
|
||||
this._configuration.location === SettingLocation.global);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user