autofixDiff.tsx 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. import {Fragment, useMemo} from 'react';
  2. import styled from '@emotion/styled';
  3. import {type Change, diffWords} from 'diff';
  4. import {
  5. type AutofixResult,
  6. type DiffLine,
  7. DiffLineType,
  8. } from 'sentry/components/events/autofix/types';
  9. import {space} from 'sentry/styles/space';
  10. type AutofixDiffProps = {
  11. fix: AutofixResult;
  12. };
  13. interface DiffLineWithChanges extends DiffLine {
  14. changes?: Change[];
  15. }
  16. function makeTestIdFromLineType(lineType: DiffLineType) {
  17. switch (lineType) {
  18. case DiffLineType.ADDED:
  19. return 'line-added';
  20. case DiffLineType.REMOVED:
  21. return 'line-removed';
  22. default:
  23. return 'line-context';
  24. }
  25. }
  26. function addChangesToDiffLines(lines: DiffLineWithChanges[]): DiffLineWithChanges[] {
  27. for (let i = 0; i < lines.length; i++) {
  28. const line = lines[i];
  29. if (line.line_type === DiffLineType.CONTEXT) {
  30. continue;
  31. }
  32. if (line.line_type === DiffLineType.REMOVED) {
  33. const prevLine = lines[i - 1];
  34. const nextLine = lines[i + 1];
  35. const nextNextLine = lines[i + 2];
  36. if (
  37. nextLine?.line_type === DiffLineType.ADDED &&
  38. prevLine?.line_type !== DiffLineType.REMOVED &&
  39. nextNextLine?.line_type !== DiffLineType.ADDED
  40. ) {
  41. const changes = diffWords(line.value, nextLine.value);
  42. lines[i] = {...line, changes: changes.filter(change => !change.added)};
  43. lines[i + 1] = {...nextLine, changes: changes.filter(change => !change.removed)};
  44. }
  45. }
  46. }
  47. return lines;
  48. }
  49. function DiffLineCode({line}: {line: DiffLineWithChanges}) {
  50. if (!line.changes) {
  51. return <Fragment>{line.value}</Fragment>;
  52. }
  53. return (
  54. <Fragment>
  55. {line.changes.map((change, i) => (
  56. <CodeDiff key={i} added={change.added} removed={change.removed}>
  57. {change.value}
  58. </CodeDiff>
  59. ))}
  60. </Fragment>
  61. );
  62. }
  63. function HunkHeader({lines, sectionHeader}: {lines: DiffLine[]; sectionHeader: string}) {
  64. const {sourceStart, sourceLength, targetStart, targetLength} = useMemo(
  65. () => ({
  66. sourceStart: lines.at(0)?.source_line_no ?? 0,
  67. sourceLength: lines.filter(line => line.line_type !== DiffLineType.ADDED).length,
  68. targetStart: lines.at(0)?.target_line_no ?? 0,
  69. targetLength: lines.filter(line => line.line_type !== DiffLineType.REMOVED).length,
  70. }),
  71. [lines]
  72. );
  73. return (
  74. <HunkHeaderContent>{`@@ -${sourceStart},${sourceLength} +${targetStart},${targetLength} @@ ${sectionHeader ? ' ' + sectionHeader : ''}`}</HunkHeaderContent>
  75. );
  76. }
  77. function DiffHunkContent({lines, header}: {header: string; lines: DiffLine[]}) {
  78. const linesWithChanges = useMemo(() => {
  79. return addChangesToDiffLines(lines);
  80. }, [lines]);
  81. return (
  82. <Fragment>
  83. <HunkHeaderEmptySpace />
  84. <HunkHeader lines={lines} sectionHeader={header} />
  85. {linesWithChanges.map(line => (
  86. <Fragment key={line.diff_line_no}>
  87. <LineNumber lineType={line.line_type}>{line.source_line_no}</LineNumber>
  88. <LineNumber lineType={line.line_type}>{line.target_line_no}</LineNumber>
  89. <DiffContent
  90. lineType={line.line_type}
  91. data-test-id={makeTestIdFromLineType(line.line_type)}
  92. >
  93. <DiffLineCode line={line} />
  94. </DiffContent>
  95. </Fragment>
  96. ))}
  97. </Fragment>
  98. );
  99. }
  100. export function AutofixDiff({fix}: AutofixDiffProps) {
  101. if (!fix.diff) {
  102. return null;
  103. }
  104. return (
  105. <DiffsColumn>
  106. {fix.diff.map((file, i) => (
  107. <FileDiffWrapper key={i}>
  108. <FileName>{file.path}</FileName>
  109. <DiffContainer>
  110. {file.hunks.map(({section_header, source_start, lines}) => {
  111. return (
  112. <DiffHunkContent
  113. key={source_start}
  114. lines={lines}
  115. header={section_header}
  116. />
  117. );
  118. })}
  119. </DiffContainer>
  120. </FileDiffWrapper>
  121. ))}
  122. </DiffsColumn>
  123. );
  124. }
  125. const DiffsColumn = styled('div')`
  126. display: flex;
  127. flex-direction: column;
  128. gap: ${space(1)};
  129. `;
  130. const FileDiffWrapper = styled('div')`
  131. margin: 0 -${space(2)};
  132. font-family: ${p => p.theme.text.familyMono};
  133. font-size: ${p => p.theme.fontSizeSmall};
  134. line-height: 20px;
  135. vertical-align: middle;
  136. `;
  137. const FileName = styled('div')`
  138. padding: 0 ${space(2)} ${space(1)} ${space(2)};
  139. `;
  140. const DiffContainer = styled('div')`
  141. border-top: 1px solid ${p => p.theme.border};
  142. border-bottom: 1px solid ${p => p.theme.border};
  143. display: grid;
  144. grid-template-columns: auto auto 1fr;
  145. `;
  146. const HunkHeaderEmptySpace = styled('div')`
  147. grid-column: 1 / 3;
  148. background-color: ${p => p.theme.backgroundSecondary};
  149. `;
  150. const HunkHeaderContent = styled('div')`
  151. grid-column: 3 / -1;
  152. background-color: ${p => p.theme.backgroundSecondary};
  153. color: ${p => p.theme.subText};
  154. padding: ${space(0.75)} ${space(1)} ${space(0.75)} ${space(4)};
  155. white-space: pre-wrap;
  156. `;
  157. const LineNumber = styled('div')<{lineType: DiffLineType}>`
  158. display: flex;
  159. padding: ${space(0.25)} ${space(2)};
  160. user-select: none;
  161. background-color: ${p => p.theme.backgroundSecondary};
  162. color: ${p => p.theme.subText};
  163. ${p =>
  164. p.lineType === DiffLineType.ADDED &&
  165. `background-color: ${p.theme.diff.added}; color: ${p.theme.textColor}`};
  166. ${p =>
  167. p.lineType === DiffLineType.REMOVED &&
  168. `background-color: ${p.theme.diff.removed}; color: ${p.theme.textColor}`};
  169. & + & {
  170. padding-left: 0;
  171. }
  172. `;
  173. const DiffContent = styled('div')<{lineType: DiffLineType}>`
  174. position: relative;
  175. padding-left: ${space(4)};
  176. white-space: pre-wrap;
  177. ${p =>
  178. p.lineType === DiffLineType.ADDED &&
  179. `background-color: ${p.theme.diff.addedRow}; color: ${p.theme.textColor}`};
  180. ${p =>
  181. p.lineType === DiffLineType.REMOVED &&
  182. `background-color: ${p.theme.diff.removedRow}; color: ${p.theme.textColor}`};
  183. &::before {
  184. content: ${p =>
  185. p.lineType === DiffLineType.ADDED
  186. ? "'+'"
  187. : p.lineType === DiffLineType.REMOVED
  188. ? "'-'"
  189. : "''"};
  190. position: absolute;
  191. top: 1px;
  192. left: ${space(1)};
  193. }
  194. `;
  195. const CodeDiff = styled('span')<{added?: boolean; removed?: boolean}>`
  196. vertical-align: middle;
  197. ${p => p.added && `background-color: ${p.theme.diff.added};`};
  198. ${p => p.removed && `background-color: ${p.theme.diff.removed};`};
  199. `;