25'ten fazla konu seçemezsiniz Konular bir harf veya rakamla başlamalı, kısa çizgiler ('-') içerebilir ve en fazla 35 karakter uzunluğunda olabilir.

336 satır
9.4 KiB

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