You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

270 lines
12 KiB

8 years ago
8 years ago
8 years ago
  1. 'use strict';
  2. import { Functions } from './system';
  3. import { DecorationRenderOptions, Disposable, Event, EventEmitter, ExtensionContext, OverviewRulerLane, TextDocument, TextDocumentChangeEvent, TextEditor, TextEditorDecorationType, TextEditorViewColumnChangeEvent, window, workspace } from 'vscode';
  4. import { BlameAnnotationProvider } from './blameAnnotationProvider';
  5. import { TextDocumentComparer, TextEditorComparer } from './comparers';
  6. import { IBlameConfig } from './configuration';
  7. import { ExtensionKey } from './constants';
  8. import { BlameabilityChangeEvent, GitContextTracker, GitService, GitUri } from './gitService';
  9. import { Logger } from './logger';
  10. import { WhitespaceController } from './whitespaceController';
  11. export const BlameDecorations = {
  12. annotation: window.createTextEditorDecorationType({
  13. before: {
  14. margin: '0 1.75em 0 0'
  15. },
  16. after: {
  17. margin: '0 0 0 4em'
  18. }
  19. } as DecorationRenderOptions),
  20. highlight: undefined as TextEditorDecorationType | undefined
  21. };
  22. export class BlameAnnotationController extends Disposable {
  23. private _onDidToggleBlameAnnotations = new EventEmitter<void>();
  24. get onDidToggleBlameAnnotations(): Event<void> {
  25. return this._onDidToggleBlameAnnotations.event;
  26. }
  27. private _annotationProviders: Map<number, BlameAnnotationProvider> = new Map();
  28. private _blameAnnotationsDisposable: Disposable | undefined;
  29. private _config: IBlameConfig;
  30. private _disposable: Disposable;
  31. private _whitespaceController: WhitespaceController | undefined;
  32. constructor(private context: ExtensionContext, private git: GitService, private gitContextTracker: GitContextTracker) {
  33. super(() => this.dispose());
  34. this._onConfigurationChanged();
  35. const subscriptions: Disposable[] = [];
  36. subscriptions.push(workspace.onDidChangeConfiguration(this._onConfigurationChanged, this));
  37. this._disposable = Disposable.from(...subscriptions);
  38. }
  39. dispose() {
  40. this._annotationProviders.forEach(async (p, i) => await this.clear(i));
  41. BlameDecorations.annotation && BlameDecorations.annotation.dispose();
  42. BlameDecorations.highlight && BlameDecorations.highlight.dispose();
  43. this._blameAnnotationsDisposable && this._blameAnnotationsDisposable.dispose();
  44. this._whitespaceController && this._whitespaceController.dispose();
  45. this._disposable && this._disposable.dispose();
  46. }
  47. private _onConfigurationChanged() {
  48. let toggleWhitespace = workspace.getConfiguration(`${ExtensionKey}.advanced.toggleWhitespace`).get<boolean>('enabled');
  49. if (!toggleWhitespace) {
  50. // Until https://github.com/Microsoft/vscode/issues/11485 is fixed we need to toggle whitespace for non-monospace fonts and ligatures
  51. // TODO: detect monospace font
  52. toggleWhitespace = workspace.getConfiguration('editor').get<boolean>('fontLigatures');
  53. }
  54. if (toggleWhitespace && !this._whitespaceController) {
  55. this._whitespaceController = new WhitespaceController();
  56. }
  57. else if (!toggleWhitespace && this._whitespaceController) {
  58. this._whitespaceController.dispose();
  59. this._whitespaceController = undefined;
  60. }
  61. const cfg = workspace.getConfiguration(ExtensionKey).get<IBlameConfig>('blame')!;
  62. if (cfg.annotation.highlight !== (this._config && this._config.annotation.highlight)) {
  63. BlameDecorations.highlight && BlameDecorations.highlight.dispose();
  64. switch (cfg.annotation.highlight) {
  65. case 'gutter':
  66. BlameDecorations.highlight = window.createTextEditorDecorationType({
  67. dark: {
  68. gutterIconPath: this.context.asAbsolutePath('images/blame-dark.svg'),
  69. overviewRulerColor: 'rgba(255, 255, 255, 0.75)'
  70. },
  71. light: {
  72. gutterIconPath: this.context.asAbsolutePath('images/blame-light.svg'),
  73. overviewRulerColor: 'rgba(0, 0, 0, 0.75)'
  74. },
  75. gutterIconSize: 'contain',
  76. overviewRulerLane: OverviewRulerLane.Right
  77. });
  78. break;
  79. case 'line':
  80. BlameDecorations.highlight = window.createTextEditorDecorationType({
  81. dark: {
  82. backgroundColor: 'rgba(255, 255, 255, 0.15)',
  83. overviewRulerColor: 'rgba(255, 255, 255, 0.75)'
  84. },
  85. light: {
  86. backgroundColor: 'rgba(0, 0, 0, 0.15)',
  87. overviewRulerColor: 'rgba(0, 0, 0, 0.75)'
  88. },
  89. overviewRulerLane: OverviewRulerLane.Right,
  90. isWholeLine: true
  91. });
  92. break;
  93. case 'both':
  94. BlameDecorations.highlight = window.createTextEditorDecorationType({
  95. dark: {
  96. backgroundColor: 'rgba(255, 255, 255, 0.15)',
  97. gutterIconPath: this.context.asAbsolutePath('images/blame-dark.svg'),
  98. overviewRulerColor: 'rgba(255, 255, 255, 0.75)'
  99. },
  100. light: {
  101. backgroundColor: 'rgba(0, 0, 0, 0.15)',
  102. gutterIconPath: this.context.asAbsolutePath('images/blame-light.svg'),
  103. overviewRulerColor: 'rgba(0, 0, 0, 0.75)'
  104. },
  105. gutterIconSize: 'contain',
  106. overviewRulerLane: OverviewRulerLane.Right,
  107. isWholeLine: true
  108. });
  109. break;
  110. default:
  111. BlameDecorations.highlight = undefined;
  112. break;
  113. }
  114. }
  115. this._config = cfg;
  116. }
  117. async clear(column: number) {
  118. const provider = this._annotationProviders.get(column);
  119. if (!provider) return;
  120. this._annotationProviders.delete(column);
  121. await provider.dispose();
  122. if (this._annotationProviders.size === 0) {
  123. Logger.log(`Remove listener registrations for blame annotations`);
  124. this._blameAnnotationsDisposable && this._blameAnnotationsDisposable.dispose();
  125. this._blameAnnotationsDisposable = undefined;
  126. }
  127. this._onDidToggleBlameAnnotations.fire();
  128. }
  129. async showBlameAnnotation(editor: TextEditor, shaOrLine?: string | number): Promise<boolean> {
  130. if (!editor || !editor.document || !this.git.isEditorBlameable(editor)) return false;
  131. const currentProvider = this._annotationProviders.get(editor.viewColumn || -1);
  132. if (currentProvider && TextEditorComparer.equals(currentProvider.editor, editor)) {
  133. await currentProvider.setSelection(shaOrLine);
  134. return true;
  135. }
  136. const gitUri = await GitUri.fromUri(editor.document.uri, this.git);
  137. const provider = new BlameAnnotationProvider(this.context, this.git, this._whitespaceController, editor, gitUri);
  138. if (!await provider.supportsBlame()) return false;
  139. if (currentProvider) {
  140. await this.clear(currentProvider.editor.viewColumn || -1);
  141. }
  142. if (!this._blameAnnotationsDisposable && this._annotationProviders.size === 0) {
  143. Logger.log(`Add listener registrations for blame annotations`);
  144. const subscriptions: Disposable[] = [];
  145. subscriptions.push(window.onDidChangeVisibleTextEditors(Functions.debounce(this._onVisibleTextEditorsChanged, 100), this));
  146. subscriptions.push(window.onDidChangeTextEditorViewColumn(this._onTextEditorViewColumnChanged, this));
  147. subscriptions.push(workspace.onDidChangeTextDocument(this._onTextDocumentChanged, this));
  148. subscriptions.push(workspace.onDidCloseTextDocument(this._onTextDocumentClosed, this));
  149. subscriptions.push(this.gitContextTracker.onDidBlameabilityChange(this._onBlameabilityChanged, this));
  150. this._blameAnnotationsDisposable = Disposable.from(...subscriptions);
  151. }
  152. this._annotationProviders.set(editor.viewColumn || -1, provider);
  153. if (await provider.provideBlameAnnotation(shaOrLine)) {
  154. this._onDidToggleBlameAnnotations.fire();
  155. return true;
  156. }
  157. return false;
  158. }
  159. isAnnotating(editor: TextEditor): boolean {
  160. if (!editor || !editor.document || !this.git.isEditorBlameable(editor)) return false;
  161. return !!this._annotationProviders.get(editor.viewColumn || -1);
  162. }
  163. async toggleBlameAnnotation(editor: TextEditor, shaOrLine?: string | number): Promise<boolean> {
  164. if (!editor || !editor.document || !this.git.isEditorBlameable(editor)) return false;
  165. const provider = this._annotationProviders.get(editor.viewColumn || -1);
  166. if (!provider) return this.showBlameAnnotation(editor, shaOrLine);
  167. await this.clear(provider.editor.viewColumn || -1);
  168. return false;
  169. }
  170. private _onBlameabilityChanged(e: BlameabilityChangeEvent) {
  171. if (e.blameable || !e.editor) return;
  172. for (const [key, p] of this._annotationProviders) {
  173. if (!TextDocumentComparer.equals(p.document, e.editor.document)) continue;
  174. Logger.log('BlameabilityChanged:', `Clear blame annotations for column ${key}`);
  175. this.clear(key);
  176. }
  177. }
  178. private _onTextDocumentChanged(e: TextDocumentChangeEvent) {
  179. for (const [key, p] of this._annotationProviders) {
  180. if (!TextDocumentComparer.equals(p.document, e.document)) continue;
  181. // We have to defer because isDirty is not reliable inside this event
  182. setTimeout(() => {
  183. // If the document is dirty all is fine, just kick out since the GitContextTracker will handle it
  184. if (e.document.isDirty) return;
  185. // If the document isn't dirty, it is very likely this event was triggered by an outside edit of this document
  186. // Which means the document has been reloaded and the blame annotations have been removed, so we need to update (clear) our state tracking
  187. Logger.log('TextDocumentChanged:', `Clear blame annotations for column ${key}`);
  188. this.clear(key);
  189. }, 1);
  190. }
  191. }
  192. private _onTextDocumentClosed(e: TextDocument) {
  193. for (const [key, p] of this._annotationProviders) {
  194. if (!TextDocumentComparer.equals(p.document, e)) continue;
  195. Logger.log('TextDocumentClosed:', `Clear blame annotations for column ${key}`);
  196. this.clear(key);
  197. }
  198. }
  199. private async _onTextEditorViewColumnChanged(e: TextEditorViewColumnChangeEvent) {
  200. const viewColumn = e.viewColumn || -1;
  201. Logger.log('TextEditorViewColumnChanged:', `Clear blame annotations for column ${viewColumn}`);
  202. await this.clear(viewColumn);
  203. for (const [key, p] of this._annotationProviders) {
  204. if (!TextEditorComparer.equals(p.editor, e.textEditor)) continue;
  205. Logger.log('TextEditorViewColumnChanged:', `Clear blame annotations for column ${key}`);
  206. await this.clear(key);
  207. }
  208. }
  209. private async _onVisibleTextEditorsChanged(e: TextEditor[]) {
  210. if (e.every(_ => _.document.uri.scheme === 'inmemory')) return;
  211. for (const [key, p] of this._annotationProviders) {
  212. if (e.some(_ => TextEditorComparer.equals(p.editor, _))) continue;
  213. Logger.log('VisibleTextEditorsChanged:', `Clear blame annotations for column ${key}`);
  214. this.clear(key);
  215. }
  216. }
  217. }