Skip to content

Commit cecd352

Browse files
committed
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`)
1 parent 5298511 commit cecd352

40 files changed

+2636
-1526
lines changed

README.md

Lines changed: 128 additions & 48 deletions
Large diffs are not rendered by default.

images/blame-dark.svg

Lines changed: 1 addition & 1 deletion
Loading

images/blame-light.svg

Lines changed: 1 addition & 1 deletion
Loading

package.json

Lines changed: 509 additions & 175 deletions
Large diffs are not rendered by default.
Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
'use strict';
2+
import { Functions, Objects } from '../system';
3+
import { DecorationRenderOptions, Disposable, Event, EventEmitter, ExtensionContext, OverviewRulerLane, TextDocument, TextDocumentChangeEvent, TextEditor, TextEditorDecorationType, TextEditorViewColumnChangeEvent, window, workspace } from 'vscode';
4+
import { AnnotationProviderBase } from './annotationProvider';
5+
import { TextDocumentComparer, TextEditorComparer } from '../comparers';
6+
import { BlameLineHighlightLocations, ExtensionKey, FileAnnotationType, IConfig, themeDefaults } from '../configuration';
7+
import { BlameabilityChangeEvent, GitContextTracker, GitService, GitUri } from '../gitService';
8+
import { GutterBlameAnnotationProvider } from './gutterBlameAnnotationProvider';
9+
import { HoverBlameAnnotationProvider } from './hoverBlameAnnotationProvider';
10+
import { Logger } from '../logger';
11+
import { WhitespaceController } from './whitespaceController';
12+
13+
export const Decorations = {
14+
annotation: window.createTextEditorDecorationType({
15+
isWholeLine: true
16+
} as DecorationRenderOptions),
17+
highlight: undefined as TextEditorDecorationType | undefined
18+
};
19+
20+
export class AnnotationController extends Disposable {
21+
22+
private _onDidToggleAnnotations = new EventEmitter<void>();
23+
get onDidToggleAnnotations(): Event<void> {
24+
return this._onDidToggleAnnotations.event;
25+
}
26+
27+
private _annotationsDisposable: Disposable | undefined;
28+
private _annotationProviders: Map<number, AnnotationProviderBase> = new Map();
29+
private _config: IConfig;
30+
private _disposable: Disposable;
31+
private _whitespaceController: WhitespaceController | undefined;
32+
33+
constructor(private context: ExtensionContext, private git: GitService, private gitContextTracker: GitContextTracker) {
34+
super(() => this.dispose());
35+
36+
this._onConfigurationChanged();
37+
38+
const subscriptions: Disposable[] = [];
39+
40+
subscriptions.push(workspace.onDidChangeConfiguration(this._onConfigurationChanged, this));
41+
42+
this._disposable = Disposable.from(...subscriptions);
43+
}
44+
45+
dispose() {
46+
this._annotationProviders.forEach(async (p, i) => await this.clear(i));
47+
48+
Decorations.annotation && Decorations.annotation.dispose();
49+
Decorations.highlight && Decorations.highlight.dispose();
50+
51+
this._annotationsDisposable && this._annotationsDisposable.dispose();
52+
this._whitespaceController && this._whitespaceController.dispose();
53+
this._disposable && this._disposable.dispose();
54+
}
55+
56+
private _onConfigurationChanged() {
57+
let toggleWhitespace = workspace.getConfiguration(`${ExtensionKey}.advanced.toggleWhitespace`).get<boolean>('enabled');
58+
if (!toggleWhitespace) {
59+
// Until https://github.com/Microsoft/vscode/issues/11485 is fixed we need to toggle whitespace for non-monospace fonts and ligatures
60+
// TODO: detect monospace font
61+
toggleWhitespace = workspace.getConfiguration('editor').get<boolean>('fontLigatures');
62+
}
63+
64+
if (toggleWhitespace && !this._whitespaceController) {
65+
this._whitespaceController = new WhitespaceController();
66+
}
67+
else if (!toggleWhitespace && this._whitespaceController) {
68+
this._whitespaceController.dispose();
69+
this._whitespaceController = undefined;
70+
}
71+
72+
const cfg = workspace.getConfiguration().get<IConfig>(ExtensionKey)!;
73+
const cfgHighlight = cfg.blame.file.lineHighlight;
74+
const cfgTheme = cfg.theme.lineHighlight;
75+
76+
let changed = false;
77+
78+
if (!Objects.areEquivalent(cfgHighlight, this._config && this._config.blame.file.lineHighlight) ||
79+
!Objects.areEquivalent(cfgTheme, this._config && this._config.theme.lineHighlight)) {
80+
changed = true;
81+
82+
Decorations.highlight && Decorations.highlight.dispose();
83+
84+
if (cfgHighlight.enabled) {
85+
Decorations.highlight = window.createTextEditorDecorationType({
86+
gutterIconSize: 'contain',
87+
isWholeLine: true,
88+
overviewRulerLane: OverviewRulerLane.Right,
89+
dark: {
90+
backgroundColor: cfgHighlight.locations.includes(BlameLineHighlightLocations.Line)
91+
? cfgTheme.dark.backgroundColor || themeDefaults.lineHighlight.dark.backgroundColor
92+
: undefined,
93+
gutterIconPath: cfgHighlight.locations.includes(BlameLineHighlightLocations.Gutter)
94+
? this.context.asAbsolutePath('images/blame-dark.svg')
95+
: undefined,
96+
overviewRulerColor: cfgHighlight.locations.includes(BlameLineHighlightLocations.OverviewRuler)
97+
? cfgTheme.dark.overviewRulerColor || themeDefaults.lineHighlight.dark.overviewRulerColor
98+
: undefined
99+
},
100+
light: {
101+
backgroundColor: cfgHighlight.locations.includes(BlameLineHighlightLocations.Line)
102+
? cfgTheme.light.backgroundColor || themeDefaults.lineHighlight.light.backgroundColor
103+
: undefined,
104+
gutterIconPath: cfgHighlight.locations.includes(BlameLineHighlightLocations.Gutter)
105+
? this.context.asAbsolutePath('images/blame-light.svg')
106+
: undefined,
107+
overviewRulerColor: cfgHighlight.locations.includes(BlameLineHighlightLocations.OverviewRuler)
108+
? cfgTheme.light.overviewRulerColor || themeDefaults.lineHighlight.light.overviewRulerColor
109+
: undefined
110+
}
111+
});
112+
}
113+
else {
114+
Decorations.highlight = undefined;
115+
}
116+
}
117+
118+
if (!Objects.areEquivalent(cfg.blame.file, this._config && this._config.blame.file) ||
119+
!Objects.areEquivalent(cfg.annotations, this._config && this._config.annotations) ||
120+
!Objects.areEquivalent(cfg.theme.annotations, this._config && this._config.theme.annotations)) {
121+
changed = true;
122+
}
123+
124+
this._config = cfg;
125+
126+
if (changed) {
127+
// Since the configuration has changed -- reset any visible annotations
128+
for (const provider of this._annotationProviders.values()) {
129+
if (provider === undefined) continue;
130+
131+
provider.reset();
132+
}
133+
}
134+
}
135+
136+
async clear(column: number) {
137+
const provider = this._annotationProviders.get(column);
138+
if (!provider) return;
139+
140+
this._annotationProviders.delete(column);
141+
await provider.dispose();
142+
143+
if (this._annotationProviders.size === 0) {
144+
Logger.log(`Remove listener registrations for annotations`);
145+
this._annotationsDisposable && this._annotationsDisposable.dispose();
146+
this._annotationsDisposable = undefined;
147+
}
148+
149+
this._onDidToggleAnnotations.fire();
150+
}
151+
152+
getAnnotationType(editor: TextEditor): FileAnnotationType | undefined {
153+
const provider = this.getProvider(editor);
154+
return provider === undefined ? undefined : provider.annotationType;
155+
}
156+
157+
getProvider(editor: TextEditor): AnnotationProviderBase | undefined {
158+
if (!editor || !editor.document || !this.git.isEditorBlameable(editor)) return undefined;
159+
160+
return this._annotationProviders.get(editor.viewColumn || -1);
161+
}
162+
163+
async showAnnotations(editor: TextEditor, type: FileAnnotationType, shaOrLine?: string | number): Promise<boolean> {
164+
if (!editor || !editor.document || !this.git.isEditorBlameable(editor)) return false;
165+
166+
const currentProvider = this._annotationProviders.get(editor.viewColumn || -1);
167+
if (currentProvider && TextEditorComparer.equals(currentProvider.editor, editor)) {
168+
await currentProvider.selection(shaOrLine);
169+
return true;
170+
}
171+
172+
const gitUri = await GitUri.fromUri(editor.document.uri, this.git);
173+
174+
let provider: AnnotationProviderBase | undefined = undefined;
175+
switch (type) {
176+
case FileAnnotationType.Gutter:
177+
provider = new GutterBlameAnnotationProvider(this.context, editor, Decorations.annotation, Decorations.highlight, this._whitespaceController, this.git, gitUri);
178+
break;
179+
case FileAnnotationType.Hover:
180+
provider = new HoverBlameAnnotationProvider(this.context, editor, Decorations.annotation, Decorations.highlight, this._whitespaceController, this.git, gitUri);
181+
break;
182+
}
183+
if (provider === undefined || !(await provider.validate())) return false;
184+
185+
if (currentProvider) {
186+
await this.clear(currentProvider.editor.viewColumn || -1);
187+
}
188+
189+
if (!this._annotationsDisposable && this._annotationProviders.size === 0) {
190+
Logger.log(`Add listener registrations for annotations`);
191+
192+
const subscriptions: Disposable[] = [];
193+
194+
subscriptions.push(window.onDidChangeVisibleTextEditors(Functions.debounce(this._onVisibleTextEditorsChanged, 100), this));
195+
subscriptions.push(window.onDidChangeTextEditorViewColumn(this._onTextEditorViewColumnChanged, this));
196+
subscriptions.push(workspace.onDidChangeTextDocument(this._onTextDocumentChanged, this));
197+
subscriptions.push(workspace.onDidCloseTextDocument(this._onTextDocumentClosed, this));
198+
subscriptions.push(this.gitContextTracker.onDidBlameabilityChange(this._onBlameabilityChanged, this));
199+
200+
this._annotationsDisposable = Disposable.from(...subscriptions);
201+
}
202+
203+
this._annotationProviders.set(editor.viewColumn || -1, provider);
204+
if (await provider.provideAnnotation(shaOrLine)) {
205+
this._onDidToggleAnnotations.fire();
206+
return true;
207+
}
208+
return false;
209+
}
210+
211+
async toggleAnnotations(editor: TextEditor, type: FileAnnotationType, shaOrLine?: string | number): Promise<boolean> {
212+
if (!editor || !editor.document || !this.git.isEditorBlameable(editor)) return false;
213+
214+
const provider = this._annotationProviders.get(editor.viewColumn || -1);
215+
if (provider === undefined) return this.showAnnotations(editor, type, shaOrLine);
216+
217+
await this.clear(provider.editor.viewColumn || -1);
218+
return false;
219+
}
220+
221+
private _onBlameabilityChanged(e: BlameabilityChangeEvent) {
222+
if (e.blameable || !e.editor) return;
223+
224+
for (const [key, p] of this._annotationProviders) {
225+
if (!TextDocumentComparer.equals(p.document, e.editor.document)) continue;
226+
227+
Logger.log('BlameabilityChanged:', `Clear annotations for column ${key}`);
228+
this.clear(key);
229+
}
230+
}
231+
232+
private _onTextDocumentChanged(e: TextDocumentChangeEvent) {
233+
for (const [key, p] of this._annotationProviders) {
234+
if (!TextDocumentComparer.equals(p.document, e.document)) continue;
235+
236+
// We have to defer because isDirty is not reliable inside this event
237+
setTimeout(() => {
238+
// If the document is dirty all is fine, just kick out since the GitContextTracker will handle it
239+
if (e.document.isDirty) return;
240+
241+
// If the document isn't dirty, it is very likely this event was triggered by an outside edit of this document
242+
// Which means the document has been reloaded and the annotations have been removed, so we need to update (clear) our state tracking
243+
Logger.log('TextDocumentChanged:', `Clear annotations for column ${key}`);
244+
this.clear(key);
245+
}, 1);
246+
}
247+
}
248+
249+
private _onTextDocumentClosed(e: TextDocument) {
250+
for (const [key, p] of this._annotationProviders) {
251+
if (!TextDocumentComparer.equals(p.document, e)) continue;
252+
253+
Logger.log('TextDocumentClosed:', `Clear annotations for column ${key}`);
254+
this.clear(key);
255+
}
256+
}
257+
258+
private async _onTextEditorViewColumnChanged(e: TextEditorViewColumnChangeEvent) {
259+
const viewColumn = e.viewColumn || -1;
260+
261+
Logger.log('TextEditorViewColumnChanged:', `Clear annotations for column ${viewColumn}`);
262+
await this.clear(viewColumn);
263+
264+
for (const [key, p] of this._annotationProviders) {
265+
if (!TextEditorComparer.equals(p.editor, e.textEditor)) continue;
266+
267+
Logger.log('TextEditorViewColumnChanged:', `Clear annotations for column ${key}`);
268+
await this.clear(key);
269+
}
270+
}
271+
272+
private async _onVisibleTextEditorsChanged(e: TextEditor[]) {
273+
if (e.every(_ => _.document.uri.scheme === 'inmemory')) return;
274+
275+
for (const [key, p] of this._annotationProviders) {
276+
if (e.some(_ => TextEditorComparer.equals(p.editor, _))) continue;
277+
278+
Logger.log('VisibleTextEditorsChanged:', `Clear annotations for column ${key}`);
279+
this.clear(key);
280+
}
281+
}
282+
}

src/annotations/annotationProvider.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
'use strict';
2+
import { Functions } from '../system';
3+
import { Disposable, ExtensionContext, TextDocument, TextEditor, TextEditorDecorationType, TextEditorSelectionChangeEvent, window, workspace } from 'vscode';
4+
import { TextDocumentComparer } from '../comparers';
5+
import { ExtensionKey, FileAnnotationType, IConfig } from '../configuration';
6+
import { WhitespaceController } from './whitespaceController';
7+
8+
export abstract class AnnotationProviderBase extends Disposable {
9+
10+
public annotationType: FileAnnotationType;
11+
public document: TextDocument;
12+
13+
protected _config: IConfig;
14+
protected _disposable: Disposable;
15+
16+
constructor(context: ExtensionContext, public editor: TextEditor, protected decoration: TextEditorDecorationType, protected highlightDecoration: TextEditorDecorationType | undefined, protected whitespaceController: WhitespaceController | undefined) {
17+
super(() => this.dispose());
18+
19+
this.document = this.editor.document;
20+
21+
this._config = workspace.getConfiguration().get<IConfig>(ExtensionKey)!;
22+
23+
const subscriptions: Disposable[] = [];
24+
25+
subscriptions.push(window.onDidChangeTextEditorSelection(this._onTextEditorSelectionChanged, this));
26+
27+
this._disposable = Disposable.from(...subscriptions);
28+
}
29+
30+
async dispose() {
31+
await this.clear();
32+
33+
this._disposable && this._disposable.dispose();
34+
}
35+
36+
private async _onTextEditorSelectionChanged(e: TextEditorSelectionChangeEvent) {
37+
if (!TextDocumentComparer.equals(this.document, e.textEditor && e.textEditor.document)) return;
38+
39+
return this.selection(e.selections[0].active.line);
40+
}
41+
42+
async clear() {
43+
if (this.editor !== undefined) {
44+
try {
45+
this.editor.setDecorations(this.decoration, []);
46+
this.highlightDecoration && this.editor.setDecorations(this.highlightDecoration, []);
47+
// I have no idea why the decorators sometimes don't get removed, but if they don't try again with a tiny delay
48+
if (this.highlightDecoration !== undefined) {
49+
await Functions.wait(1);
50+
51+
if (this.highlightDecoration === undefined) return;
52+
53+
this.editor.setDecorations(this.highlightDecoration, []);
54+
}
55+
}
56+
catch (ex) { }
57+
}
58+
59+
// HACK: Until https://github.com/Microsoft/vscode/issues/11485 is fixed -- restore whitespace
60+
this.whitespaceController && await this.whitespaceController.restore();
61+
}
62+
63+
async reset() {
64+
await this.clear();
65+
66+
this._config = workspace.getConfiguration().get<IConfig>(ExtensionKey)!;
67+
68+
await this.provideAnnotation(this.editor === undefined ? undefined : this.editor.selection.active.line);
69+
}
70+
71+
abstract async provideAnnotation(shaOrLine?: string | number): Promise<boolean>;
72+
abstract async selection(shaOrLine?: string | number): Promise<void>;
73+
abstract async validate(): Promise<boolean>;
74+
}

0 commit comments

Comments
 (0)