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.

237 lines
10 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, 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 GitProvider from './gitProvider';
  8. import { Logger } from './logger';
  9. import WhitespaceController from './whitespaceController';
  10. export const blameDecoration: TextEditorDecorationType = window.createTextEditorDecorationType({
  11. before: {
  12. margin: '0 1.75em 0 0'
  13. },
  14. after: {
  15. margin: '0 0 0 4em'
  16. }
  17. } as DecorationRenderOptions);
  18. export let highlightDecoration: TextEditorDecorationType;
  19. export default class BlameAnnotationController extends Disposable {
  20. private _onDidToggleBlameAnnotationsEmitter = new EventEmitter<void>();
  21. get onDidToggleBlameAnnotations(): Event<void> {
  22. return this._onDidToggleBlameAnnotationsEmitter.event;
  23. }
  24. private _annotationProviders: Map<number, BlameAnnotationProvider> = new Map();
  25. private _blameAnnotationsDisposable: Disposable;
  26. private _config: IBlameConfig;
  27. private _disposable: Disposable;
  28. private _whitespaceController: WhitespaceController | undefined;
  29. constructor(private context: ExtensionContext, private git: GitProvider) {
  30. super(() => this.dispose());
  31. this._onConfigurationChanged();
  32. const subscriptions: Disposable[] = [];
  33. subscriptions.push(workspace.onDidChangeConfiguration(this._onConfigurationChanged, this));
  34. this._disposable = Disposable.from(...subscriptions);
  35. }
  36. dispose() {
  37. this._annotationProviders.forEach(async (p, i) => await this.clear(i));
  38. this._blameAnnotationsDisposable && this._blameAnnotationsDisposable.dispose();
  39. this._whitespaceController && this._whitespaceController.dispose();
  40. this._disposable && this._disposable.dispose();
  41. }
  42. private _onConfigurationChanged() {
  43. let toggleWhitespace = workspace.getConfiguration('gitlens.advanced.toggleWhitespace').get<boolean>('enabled');
  44. if (!toggleWhitespace) {
  45. // Until https://github.com/Microsoft/vscode/issues/11485 is fixed we need to toggle whitespace for non-monospace fonts and ligatures
  46. // TODO: detect monospace font
  47. toggleWhitespace = workspace.getConfiguration('editor').get<boolean>('fontLigatures');
  48. }
  49. if (toggleWhitespace && !this._whitespaceController) {
  50. this._whitespaceController = new WhitespaceController();
  51. }
  52. else if (!toggleWhitespace && this._whitespaceController) {
  53. this._whitespaceController.dispose();
  54. this._whitespaceController = undefined;
  55. }
  56. const config = workspace.getConfiguration('gitlens').get<IBlameConfig>('blame');
  57. if (config.annotation.highlight !== (this._config && this._config.annotation.highlight)) {
  58. highlightDecoration && highlightDecoration.dispose();
  59. switch (config.annotation.highlight) {
  60. case 'none':
  61. highlightDecoration = undefined;
  62. break;
  63. case 'gutter':
  64. highlightDecoration = window.createTextEditorDecorationType({
  65. dark: {
  66. gutterIconPath: this.context.asAbsolutePath('images/blame-dark.svg'),
  67. overviewRulerColor: 'rgba(255, 255, 255, 0.75)'
  68. },
  69. light: {
  70. gutterIconPath: this.context.asAbsolutePath('images/blame-light.svg'),
  71. overviewRulerColor: 'rgba(0, 0, 0, 0.75)'
  72. },
  73. gutterIconSize: 'contain',
  74. overviewRulerLane: OverviewRulerLane.Right
  75. });
  76. break;
  77. case 'line':
  78. highlightDecoration = window.createTextEditorDecorationType({
  79. dark: {
  80. backgroundColor: 'rgba(255, 255, 255, 0.15)',
  81. overviewRulerColor: 'rgba(255, 255, 255, 0.75)'
  82. },
  83. light: {
  84. backgroundColor: 'rgba(0, 0, 0, 0.15)',
  85. overviewRulerColor: 'rgba(0, 0, 0, 0.75)'
  86. },
  87. overviewRulerLane: OverviewRulerLane.Right,
  88. isWholeLine: true
  89. });
  90. break;
  91. case 'both':
  92. highlightDecoration = window.createTextEditorDecorationType({
  93. dark: {
  94. backgroundColor: 'rgba(255, 255, 255, 0.15)',
  95. gutterIconPath: this.context.asAbsolutePath('images/blame-dark.svg'),
  96. overviewRulerColor: 'rgba(255, 255, 255, 0.75)'
  97. },
  98. light: {
  99. backgroundColor: 'rgba(0, 0, 0, 0.15)',
  100. gutterIconPath: this.context.asAbsolutePath('images/blame-light.svg'),
  101. overviewRulerColor: 'rgba(0, 0, 0, 0.75)'
  102. },
  103. gutterIconSize: 'contain',
  104. overviewRulerLane: OverviewRulerLane.Right,
  105. isWholeLine: true
  106. });
  107. break;
  108. }
  109. }
  110. this._config = config;
  111. }
  112. async clear(column: number) {
  113. const provider = this._annotationProviders.get(column);
  114. if (!provider) return;
  115. this._annotationProviders.delete(column);
  116. await provider.dispose();
  117. if (this._annotationProviders.size === 0) {
  118. Logger.log(`Remove listener registrations for blame annotations`);
  119. this._blameAnnotationsDisposable && this._blameAnnotationsDisposable.dispose();
  120. this._blameAnnotationsDisposable = undefined;
  121. }
  122. this._onDidToggleBlameAnnotationsEmitter.fire();
  123. }
  124. async showBlameAnnotation(editor: TextEditor, shaOrLine?: string | number): Promise<boolean> {
  125. if (!editor || !editor.document) return false;
  126. if (editor.viewColumn === undefined && !this.git.hasGitUriForFile(editor)) return false;
  127. const currentProvider = this._annotationProviders.get(editor.viewColumn || -1);
  128. if (currentProvider && TextEditorComparer.equals(currentProvider.editor, editor)) {
  129. await currentProvider.setSelection(shaOrLine);
  130. return true;
  131. }
  132. const provider = new BlameAnnotationProvider(this.context, this.git, this._whitespaceController, editor);
  133. if (!await provider.supportsBlame()) return false;
  134. if (currentProvider) {
  135. await this.clear(currentProvider.editor.viewColumn || -1);
  136. }
  137. if (!this._blameAnnotationsDisposable && this._annotationProviders.size === 0) {
  138. Logger.log(`Add listener registrations for blame annotations`);
  139. const subscriptions: Disposable[] = [];
  140. subscriptions.push(window.onDidChangeVisibleTextEditors(Functions.debounce(this._onVisibleTextEditorsChanged, 100), this));
  141. subscriptions.push(window.onDidChangeTextEditorViewColumn(this._onTextEditorViewColumnChanged, this));
  142. subscriptions.push(workspace.onDidCloseTextDocument(this._onTextDocumentClosed, this));
  143. this._blameAnnotationsDisposable = Disposable.from(...subscriptions);
  144. }
  145. this._annotationProviders.set(editor.viewColumn || -1, provider);
  146. if (await provider.provideBlameAnnotation(shaOrLine)) {
  147. this._onDidToggleBlameAnnotationsEmitter.fire();
  148. return true;
  149. }
  150. return false;
  151. }
  152. isAnnotating(editor: TextEditor): boolean {
  153. if (!editor || !editor.document) return false;
  154. if (editor.viewColumn === undefined && !this.git.hasGitUriForFile(editor)) return false;
  155. return !!this._annotationProviders.get(editor.viewColumn || -1);
  156. }
  157. async toggleBlameAnnotation(editor: TextEditor, shaOrLine?: string | number): Promise<boolean> {
  158. if (!editor || !editor.document) return false;
  159. if (editor.viewColumn === undefined && !this.git.hasGitUriForFile(editor)) return false;
  160. let provider = this._annotationProviders.get(editor.viewColumn || -1);
  161. if (!provider) return this.showBlameAnnotation(editor, shaOrLine);
  162. await this.clear(provider.editor.viewColumn || -1);
  163. return false;
  164. }
  165. private _onTextDocumentClosed(e: TextDocument) {
  166. for (const [key, p] of this._annotationProviders) {
  167. if (!TextDocumentComparer.equals(p.document, e)) continue;
  168. Logger.log('TextDocumentClosed:', `Clear blame annotations for column ${key}`);
  169. this.clear(key);
  170. }
  171. }
  172. private async _onTextEditorViewColumnChanged(e: TextEditorViewColumnChangeEvent) {
  173. const viewColumn = e.viewColumn || -1;
  174. Logger.log('TextEditorViewColumnChanged:', `Clear blame annotations for column ${viewColumn}`);
  175. await this.clear(viewColumn);
  176. for (const [key, p] of this._annotationProviders) {
  177. if (!TextEditorComparer.equals(p.editor, e.textEditor)) continue;
  178. Logger.log('TextEditorViewColumnChanged:', `Clear blame annotations for column ${key}`);
  179. await this.clear(key);
  180. }
  181. }
  182. private async _onVisibleTextEditorsChanged(e: TextEditor[]) {
  183. if (e.every(_ => _.document.uri.scheme === 'inmemory')) return;
  184. for (const [key, p] of this._annotationProviders) {
  185. if (e.some(_ => TextEditorComparer.equals(p.editor, _))) continue;
  186. Logger.log('VisibleTextEditorsChanged:', `Clear blame annotations for column ${key}`);
  187. this.clear(key);
  188. }
  189. }
  190. }