選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

283 行
7.9 KiB

  1. 'use strict';
  2. import {
  3. CancellationToken,
  4. DecorationOptions,
  5. Disposable,
  6. Hover,
  7. languages,
  8. Position,
  9. Range,
  10. Selection,
  11. TextDocument,
  12. TextEditor,
  13. TextEditorDecorationType,
  14. TextEditorRevealType,
  15. } from 'vscode';
  16. import { AnnotationProviderBase } from './annotationProvider';
  17. import { FileAnnotationType } from '../configuration';
  18. import { Container } from '../container';
  19. import { Decorations } from './fileAnnotationController';
  20. import { GitDiff, GitLogCommit } from '../git/git';
  21. import { Hovers } from '../hovers/hovers';
  22. import { Logger } from '../logger';
  23. import { log, Strings } from '../system';
  24. import { GitDocumentState, TrackedDocument } from '../trackers/gitDocumentTracker';
  25. export class GutterChangesAnnotationProvider extends AnnotationProviderBase {
  26. private state: { commit: GitLogCommit | undefined; diffs: GitDiff[] } | undefined;
  27. private hoverProviderDisposable: Disposable | undefined;
  28. constructor(editor: TextEditor, trackedDocument: TrackedDocument<GitDocumentState>) {
  29. super(editor, trackedDocument);
  30. }
  31. clear() {
  32. this.state = undefined;
  33. if (this.hoverProviderDisposable != null) {
  34. this.hoverProviderDisposable.dispose();
  35. this.hoverProviderDisposable = undefined;
  36. }
  37. super.clear();
  38. }
  39. selection(_shaOrLine?: string | number): Promise<void> {
  40. return Promise.resolve();
  41. }
  42. validate(): Promise<boolean> {
  43. return Promise.resolve(true);
  44. }
  45. @log()
  46. async onProvideAnnotation(shaOrLine?: string | number): Promise<boolean> {
  47. const cc = Logger.getCorrelationContext();
  48. this.annotationType = FileAnnotationType.Changes;
  49. let ref1 = this.trackedDocument.uri.sha;
  50. let ref2;
  51. if (typeof shaOrLine === 'string') {
  52. if (shaOrLine !== this.trackedDocument.uri.sha) {
  53. ref2 = `${shaOrLine}^`;
  54. }
  55. }
  56. let commit: GitLogCommit | undefined;
  57. let localChanges = ref1 == null && ref2 == null;
  58. if (localChanges) {
  59. let ref = await Container.git.getOldestUnpushedRefForFile(
  60. this.trackedDocument.uri.repoPath!,
  61. this.trackedDocument.uri.fsPath,
  62. );
  63. if (ref != null) {
  64. ref = `${ref}^`;
  65. commit = await Container.git.getCommitForFile(
  66. this.trackedDocument.uri.repoPath,
  67. this.trackedDocument.uri.fsPath,
  68. { ref: ref },
  69. );
  70. if (commit != null) {
  71. if (ref2 != null) {
  72. ref2 = ref;
  73. } else {
  74. ref1 = ref;
  75. ref2 = '';
  76. }
  77. } else {
  78. localChanges = false;
  79. }
  80. } else {
  81. const status = await Container.git.getStatusForFile(
  82. this.trackedDocument.uri.repoPath!,
  83. this.trackedDocument.uri.fsPath,
  84. );
  85. const commits = await status?.toPsuedoCommits();
  86. if (commits?.length) {
  87. commit = await Container.git.getCommitForFile(
  88. this.trackedDocument.uri.repoPath,
  89. this.trackedDocument.uri.fsPath,
  90. );
  91. ref1 = 'HEAD';
  92. } else if (this.trackedDocument.dirty) {
  93. ref1 = 'HEAD';
  94. } else {
  95. localChanges = false;
  96. }
  97. }
  98. }
  99. if (!localChanges) {
  100. commit = await Container.git.getCommitForFile(
  101. this.trackedDocument.uri.repoPath,
  102. this.trackedDocument.uri.fsPath,
  103. {
  104. ref: ref2 ?? ref1,
  105. },
  106. );
  107. if (commit == null) return false;
  108. if (ref2 != null) {
  109. ref2 = commit.ref;
  110. } else {
  111. ref1 = `${commit.ref}^`;
  112. ref2 = commit.ref;
  113. }
  114. }
  115. const diffs = (
  116. await Promise.all(
  117. ref2 == null && this.editor.document.isDirty
  118. ? [
  119. Container.git.getDiffForFileContents(
  120. this.trackedDocument.uri,
  121. ref1!,
  122. this.editor.document.getText(),
  123. ),
  124. Container.git.getDiffForFile(this.trackedDocument.uri, ref1, ref2),
  125. ]
  126. : [Container.git.getDiffForFile(this.trackedDocument.uri, ref1, ref2)],
  127. )
  128. ).filter(<T>(d?: T): d is T => Boolean(d));
  129. if (!diffs?.length) return false;
  130. let start = process.hrtime();
  131. const decorationsMap = new Map<
  132. string,
  133. { decorationType: TextEditorDecorationType; rangesOrOptions: DecorationOptions[] }
  134. >();
  135. let selection: Selection | undefined;
  136. for (const diff of diffs) {
  137. for (const hunk of diff.hunks) {
  138. // Subtract 2 because editor lines are 0-based and we will be adding 1 in the first iteration of the loop
  139. let count = Math.max(hunk.current.position.start - 2, -1);
  140. let index = -1;
  141. for (const hunkLine of hunk.lines) {
  142. index++;
  143. count++;
  144. if (hunkLine.current?.state === 'unchanged') continue;
  145. const range = this.editor.document.validateRange(
  146. new Range(new Position(count, 0), new Position(count, Number.MAX_SAFE_INTEGER)),
  147. );
  148. if (selection == null) {
  149. selection = new Selection(range.start, range.end);
  150. }
  151. let state;
  152. if (hunkLine.current == null) {
  153. const previous = hunk.lines[index - 1];
  154. if (hunkLine.previous != null && (previous == null || previous.current != null)) {
  155. // Check if there are more deleted lines than added lines show a deleted indicator
  156. if (hunk.previous.count > hunk.current.count) {
  157. state = 'removed';
  158. } else {
  159. continue;
  160. }
  161. } else {
  162. continue;
  163. }
  164. } else if (hunkLine.current?.state === 'added') {
  165. if (hunkLine.previous?.state === 'removed') {
  166. state = 'changed';
  167. } else {
  168. state = 'added';
  169. }
  170. } else if (hunkLine?.current.state === 'removed') {
  171. // Check if there are more deleted lines than added lines show a deleted indicator
  172. if (hunk.previous.count > hunk.current.count) {
  173. state = 'removed';
  174. } else {
  175. continue;
  176. }
  177. } else {
  178. state = 'changed';
  179. }
  180. let decoration = decorationsMap.get(state);
  181. if (decoration == null) {
  182. decoration = {
  183. decorationType: (state === 'added'
  184. ? Decorations.changesLineAddedAnnotation
  185. : state === 'removed'
  186. ? Decorations.changesLineDeletedAnnotation
  187. : Decorations.changesLineChangedAnnotation)!,
  188. rangesOrOptions: [{ range: range }],
  189. };
  190. decorationsMap.set(state, decoration);
  191. } else {
  192. decoration.rangesOrOptions.push({ range: range });
  193. }
  194. }
  195. }
  196. }
  197. Logger.log(cc, `${Strings.getDurationMilliseconds(start)} ms to compute recent changes annotations`);
  198. if (decorationsMap.size) {
  199. start = process.hrtime();
  200. this.setDecorations([...decorationsMap.values()]);
  201. Logger.log(cc, `${Strings.getDurationMilliseconds(start)} ms to apply recent changes annotations`);
  202. if (selection != null) {
  203. this.editor.selection = selection;
  204. this.editor.revealRange(selection, TextEditorRevealType.InCenterIfOutsideViewport);
  205. }
  206. }
  207. this.state = { commit: commit, diffs: diffs };
  208. this.registerHoverProvider();
  209. return true;
  210. }
  211. registerHoverProvider() {
  212. if (!Container.config.hovers.enabled || !Container.config.hovers.annotations.enabled) {
  213. return;
  214. }
  215. this.hoverProviderDisposable = languages.registerHoverProvider(
  216. { pattern: this.document.uri.fsPath },
  217. {
  218. provideHover: (document, position, token) => this.provideHover(document, position, token),
  219. },
  220. );
  221. }
  222. provideHover(document: TextDocument, position: Position, _token: CancellationToken): Hover | undefined {
  223. if (this.state == null) return undefined;
  224. if (Container.config.hovers.annotations.over !== 'line' && position.character !== 0) return undefined;
  225. const { commit, diffs } = this.state;
  226. for (const diff of diffs) {
  227. for (const hunk of diff.hunks) {
  228. // If we have a "mixed" diff hunk, check if we have more deleted lines than added, to include a trailing line for the deleted indicator
  229. const hasMoreDeletedLines = hunk.state === 'changed' && hunk.previous.count > hunk.current.count;
  230. if (
  231. position.line >= hunk.current.position.start - 1 &&
  232. position.line <= hunk.current.position.end - (hasMoreDeletedLines ? 0 : 1)
  233. ) {
  234. return new Hover(
  235. Hovers.localChangesMessage(commit, this.trackedDocument.uri, position.line, hunk),
  236. document.validateRange(
  237. new Range(
  238. hunk.current.position.start - 1,
  239. 0,
  240. hunk.current.position.end - (hasMoreDeletedLines ? 0 : 1),
  241. Number.MAX_SAFE_INTEGER,
  242. ),
  243. ),
  244. );
  245. }
  246. }
  247. }
  248. return undefined;
  249. }
  250. }