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.

284 lines
10 KiB

8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
  1. 'use strict';
  2. import { Iterables } from './system';
  3. import { DecorationInstanceRenderOptions, DecorationOptions, Disposable, ExtensionContext, Range, TextDocument, TextEditor, TextEditorSelectionChangeEvent, window, workspace } from 'vscode';
  4. import BlameAnnotationFormatter, { BlameAnnotationFormat, cssIndent, defaultShaLength, defaultAuthorLength } from './blameAnnotationFormatter';
  5. import { blameDecoration, highlightDecoration } from './blameAnnotationController';
  6. import { TextDocumentComparer } from './comparers';
  7. import { BlameAnnotationStyle, IBlameConfig } from './configuration';
  8. import GitProvider, { GitUri, IGitBlame } from './gitProvider';
  9. import WhitespaceController from './whitespaceController';
  10. export class BlameAnnotationProvider extends Disposable {
  11. public document: TextDocument;
  12. private _blame: Promise<IGitBlame>;
  13. private _config: IBlameConfig;
  14. private _disposable: Disposable;
  15. private _uri: GitUri;
  16. constructor(context: ExtensionContext, private git: GitProvider, private whitespaceController: WhitespaceController | undefined, public editor: TextEditor) {
  17. super(() => this.dispose());
  18. this.document = this.editor.document;
  19. this._uri = GitUri.fromUri(this.document.uri, this.git);
  20. this._blame = this.git.getBlameForFile(this._uri.fsPath, this._uri.sha, this._uri.repoPath);
  21. this._config = workspace.getConfiguration('gitlens').get<IBlameConfig>('blame');
  22. const subscriptions: Disposable[] = [];
  23. subscriptions.push(window.onDidChangeTextEditorSelection(this._onActiveSelectionChanged, this));
  24. this._disposable = Disposable.from(...subscriptions);
  25. }
  26. async dispose() {
  27. if (this.editor) {
  28. this.editor.setDecorations(blameDecoration, []);
  29. highlightDecoration && this.editor.setDecorations(highlightDecoration, []);
  30. }
  31. // HACK: Until https://github.com/Microsoft/vscode/issues/11485 is fixed -- restore whitespace
  32. this.whitespaceController && await this.whitespaceController.restore();
  33. this._disposable && this._disposable.dispose();
  34. }
  35. private async _onActiveSelectionChanged(e: TextEditorSelectionChangeEvent) {
  36. if (!TextDocumentComparer.equals(this.document, e.textEditor && e.textEditor.document)) return;
  37. return this.setSelection(e.selections[0].active.line);
  38. }
  39. async supportsBlame(): Promise<boolean> {
  40. const blame = await this._blame;
  41. return !!(blame && blame.lines.length);
  42. }
  43. async provideBlameAnnotation(shaOrLine?: string | number): Promise<boolean> {
  44. const blame = await this._blame;
  45. if (!blame || !blame.lines.length) return false;
  46. // HACK: Until https://github.com/Microsoft/vscode/issues/11485 is fixed -- override whitespace (turn off)
  47. if (this._config.annotation.style !== BlameAnnotationStyle.Trailing) {
  48. this.whitespaceController && await this.whitespaceController.override();
  49. }
  50. let blameDecorationOptions: DecorationOptions[] | undefined;
  51. switch (this._config.annotation.style) {
  52. case BlameAnnotationStyle.Compact:
  53. blameDecorationOptions = this._getCompactGutterDecorations(blame);
  54. break;
  55. case BlameAnnotationStyle.Expanded:
  56. blameDecorationOptions = this._getExpandedGutterDecorations(blame, false);
  57. break;
  58. case BlameAnnotationStyle.Trailing:
  59. blameDecorationOptions = this._getExpandedGutterDecorations(blame, true);
  60. break;
  61. }
  62. if (blameDecorationOptions) {
  63. this.editor.setDecorations(blameDecoration, blameDecorationOptions);
  64. }
  65. this._setSelection(blame, shaOrLine);
  66. return true;
  67. }
  68. async setSelection(shaOrLine?: string | number) {
  69. const blame = await this._blame;
  70. if (!blame || !blame.lines.length) return;
  71. return this._setSelection(blame, shaOrLine);
  72. }
  73. private _setSelection(blame: IGitBlame, shaOrLine?: string | number) {
  74. if (!highlightDecoration) return;
  75. const offset = this._uri.offset;
  76. let sha: string;
  77. if (typeof shaOrLine === 'string') {
  78. sha = shaOrLine;
  79. }
  80. else if (typeof shaOrLine === 'number') {
  81. const line = shaOrLine - offset;
  82. if (line >= 0) {
  83. const commitLine = blame.lines[line];
  84. sha = commitLine && commitLine.sha;
  85. }
  86. }
  87. else {
  88. sha = Iterables.first(blame.commits.values()).sha;
  89. }
  90. if (!sha) {
  91. this.editor.setDecorations(highlightDecoration, []);
  92. return;
  93. }
  94. const highlightDecorationRanges = blame.lines
  95. .filter(l => l.sha === sha)
  96. .map(l => this.editor.document.validateRange(new Range(l.line + offset, 0, l.line + offset, 1000000)));
  97. this.editor.setDecorations(highlightDecoration, highlightDecorationRanges);
  98. }
  99. private _getCompactGutterDecorations(blame: IGitBlame): DecorationOptions[] {
  100. const offset = this._uri.offset;
  101. let count = 0;
  102. let lastSha: string;
  103. return blame.lines.map(l => {
  104. let commit = blame.commits.get(l.sha);
  105. let color: string;
  106. if (commit.isUncommitted) {
  107. color = 'rgba(0, 188, 242, 0.6)';
  108. }
  109. else {
  110. color = l.previousSha ? '#999999' : '#6b6b6b';
  111. }
  112. let gutter = '';
  113. if (lastSha !== l.sha) {
  114. count = -1;
  115. }
  116. const isEmptyOrWhitespace = this.document.lineAt(l.line).isEmptyOrWhitespace;
  117. if (!isEmptyOrWhitespace) {
  118. switch (++count) {
  119. case 0:
  120. gutter = commit.sha.substring(0, defaultShaLength);
  121. break;
  122. case 1:
  123. gutter = `${cssIndent} ${BlameAnnotationFormatter.getAuthor(this._config, commit, defaultAuthorLength, true)}`;
  124. break;
  125. case 2:
  126. gutter = `${cssIndent} ${BlameAnnotationFormatter.getDate(this._config, commit, 'MM/DD/YYYY', true, true)}`;
  127. break;
  128. default:
  129. gutter = `${cssIndent}`;
  130. break;
  131. }
  132. }
  133. const hoverMessage = BlameAnnotationFormatter.getAnnotationHover(this._config, l, commit);
  134. // Escape single quotes because for some reason that breaks the ::before or ::after element
  135. // https://github.com/Microsoft/vscode/issues/19922 remove once this is released
  136. gutter = gutter.replace(/\'/g, '\\\'');
  137. lastSha = l.sha;
  138. return {
  139. range: this.editor.document.validateRange(new Range(l.line + offset, 0, l.line + offset, 1000000)),
  140. hoverMessage: hoverMessage,
  141. renderOptions: {
  142. before: {
  143. color: color,
  144. contentText: gutter,
  145. width: '11em'
  146. }
  147. }
  148. } as DecorationOptions;
  149. });
  150. }
  151. private _getExpandedGutterDecorations(blame: IGitBlame, trailing: boolean = false): DecorationOptions[] {
  152. const offset = this._uri.offset;
  153. let width = 0;
  154. if (!trailing) {
  155. if (this._config.annotation.sha) {
  156. width += 5;
  157. }
  158. if (this._config.annotation.date && this._config.annotation.date !== 'off') {
  159. if (width > 0) {
  160. width += 7;
  161. }
  162. else {
  163. width += 6;
  164. }
  165. if (this._config.annotation.date === 'relative') {
  166. width += 2;
  167. }
  168. }
  169. if (this._config.annotation.author) {
  170. if (width > 5 + 6) {
  171. width += 12;
  172. }
  173. else if (width > 0) {
  174. width += 11;
  175. }
  176. else {
  177. width += 10;
  178. }
  179. }
  180. if (this._config.annotation.message) {
  181. if (width > 5 + 6 + 10) {
  182. width += 21;
  183. }
  184. else if (width > 5 + 6) {
  185. width += 21;
  186. }
  187. else if (width > 0) {
  188. width += 21;
  189. }
  190. else {
  191. width += 19;
  192. }
  193. }
  194. }
  195. return blame.lines.map(l => {
  196. let commit = blame.commits.get(l.sha);
  197. let color: string;
  198. if (commit.isUncommitted) {
  199. color = 'rgba(0, 188, 242, 0.6)';
  200. }
  201. else {
  202. if (trailing) {
  203. color = l.previousSha ? 'rgba(153, 153, 153, 0.5)' : 'rgba(107, 107, 107, 0.5)';
  204. }
  205. else {
  206. color = l.previousSha ? 'rgb(153, 153, 153)' : 'rgb(107, 107, 107)';
  207. }
  208. }
  209. const format = trailing ? BlameAnnotationFormat.Unconstrained : BlameAnnotationFormat.Constrained;
  210. // Escape single quotes because for some reason that breaks the ::before or ::after element
  211. // https://github.com/Microsoft/vscode/issues/19922 remove once this is released
  212. const gutter = BlameAnnotationFormatter.getAnnotation(this._config, commit, format).replace(/\'/g, '\\\'');
  213. const hoverMessage = BlameAnnotationFormatter.getAnnotationHover(this._config, l, commit);
  214. let renderOptions: DecorationInstanceRenderOptions;
  215. if (trailing) {
  216. renderOptions = {
  217. after: {
  218. color: color,
  219. contentText: gutter
  220. }
  221. } as DecorationInstanceRenderOptions;
  222. }
  223. else {
  224. renderOptions = {
  225. before: {
  226. color: color,
  227. contentText: gutter,
  228. width: `${width}em`
  229. }
  230. } as DecorationInstanceRenderOptions;
  231. }
  232. return {
  233. range: this.editor.document.validateRange(new Range(l.line + offset, 0, l.line + offset, 1000000)),
  234. hoverMessage: hoverMessage,
  235. renderOptions: renderOptions
  236. } as DecorationOptions;
  237. });
  238. }
  239. }