autofixDiff.tsx 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792
  1. import {Fragment, useEffect, useMemo, useRef, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import {type Change, diffWords} from 'diff';
  4. import {addErrorMessage} from 'sentry/actionCreators/indicator';
  5. import {Button} from 'sentry/components/button';
  6. import {
  7. type DiffLine,
  8. DiffLineType,
  9. type FilePatch,
  10. } from 'sentry/components/events/autofix/types';
  11. import {makeAutofixQueryKey} from 'sentry/components/events/autofix/useAutofix';
  12. import TextArea from 'sentry/components/forms/controls/textarea';
  13. import InteractionStateLayer from 'sentry/components/interactionStateLayer';
  14. import {IconChevron, IconClose, IconDelete, IconEdit} from 'sentry/icons';
  15. import {t} from 'sentry/locale';
  16. import {space} from 'sentry/styles/space';
  17. import {useMutation, useQueryClient} from 'sentry/utils/queryClient';
  18. import useApi from 'sentry/utils/useApi';
  19. type AutofixDiffProps = {
  20. diff: FilePatch[];
  21. editable: boolean;
  22. groupId: string;
  23. runId: string;
  24. repoId?: string;
  25. };
  26. interface DiffLineWithChanges extends DiffLine {
  27. changes?: Change[];
  28. }
  29. function makeTestIdFromLineType(lineType: DiffLineType) {
  30. switch (lineType) {
  31. case DiffLineType.ADDED:
  32. return 'line-added';
  33. case DiffLineType.REMOVED:
  34. return 'line-removed';
  35. default:
  36. return 'line-context';
  37. }
  38. }
  39. function addChangesToDiffLines(lines: DiffLineWithChanges[]): DiffLineWithChanges[] {
  40. for (let i = 0; i < lines.length; i++) {
  41. const line = lines[i]!;
  42. if (line.line_type === DiffLineType.CONTEXT) {
  43. continue;
  44. }
  45. if (line.line_type === DiffLineType.REMOVED) {
  46. const prevLine = lines[i - 1];
  47. const nextLine = lines[i + 1];
  48. const nextNextLine = lines[i + 2];
  49. if (
  50. nextLine?.line_type === DiffLineType.ADDED &&
  51. prevLine?.line_type !== DiffLineType.REMOVED &&
  52. nextNextLine?.line_type !== DiffLineType.ADDED
  53. ) {
  54. const changes = diffWords(line.value, nextLine.value);
  55. lines[i] = {...line, changes: changes.filter(change => !change.added)};
  56. lines[i + 1] = {...nextLine, changes: changes.filter(change => !change.removed)};
  57. }
  58. }
  59. }
  60. return lines;
  61. }
  62. function DiffLineCode({line}: {line: DiffLineWithChanges}) {
  63. if (!line.changes) {
  64. return <Fragment>{line.value}</Fragment>;
  65. }
  66. return (
  67. <Fragment>
  68. {line.changes.map((change, i) => (
  69. <CodeDiff key={i} added={change.added} removed={change.removed}>
  70. {change.value}
  71. </CodeDiff>
  72. ))}
  73. </Fragment>
  74. );
  75. }
  76. function HunkHeader({lines, sectionHeader}: {lines: DiffLine[]; sectionHeader: string}) {
  77. const {sourceStart, sourceLength, targetStart, targetLength} = useMemo(
  78. () => ({
  79. sourceStart: lines.at(0)?.source_line_no ?? 0,
  80. sourceLength: lines.filter(line => line.line_type !== DiffLineType.ADDED).length,
  81. targetStart: lines.at(0)?.target_line_no ?? 0,
  82. targetLength: lines.filter(line => line.line_type !== DiffLineType.REMOVED).length,
  83. }),
  84. [lines]
  85. );
  86. return (
  87. <HunkHeaderContent>{`@@ -${sourceStart},${sourceLength} +${targetStart},${targetLength} @@ ${sectionHeader ? ' ' + sectionHeader : ''}`}</HunkHeaderContent>
  88. );
  89. }
  90. function useUpdateHunk({groupId, runId}: {groupId: string; runId: string}) {
  91. const api = useApi({persistInFlight: true});
  92. const queryClient = useQueryClient();
  93. return useMutation({
  94. mutationFn: (params: {
  95. fileName: string;
  96. hunkIndex: number;
  97. lines: DiffLine[];
  98. repoId?: string;
  99. }) => {
  100. return api.requestPromise(`/issues/${groupId}/autofix/update/`, {
  101. method: 'POST',
  102. data: {
  103. run_id: runId,
  104. payload: {
  105. type: 'update_code_change',
  106. repo_id: params.repoId ?? null,
  107. hunk_index: params.hunkIndex,
  108. lines: params.lines,
  109. file_path: params.fileName,
  110. },
  111. },
  112. });
  113. },
  114. onSuccess: _ => {
  115. queryClient.invalidateQueries({queryKey: makeAutofixQueryKey(groupId)});
  116. },
  117. onError: () => {
  118. addErrorMessage(t('Something went wrong when updating changes.'));
  119. },
  120. });
  121. }
  122. function DiffHunkContent({
  123. groupId,
  124. runId,
  125. repoId,
  126. hunkIndex,
  127. lines,
  128. header,
  129. fileName,
  130. editable,
  131. }: {
  132. editable: boolean;
  133. fileName: string;
  134. groupId: string;
  135. header: string;
  136. hunkIndex: number;
  137. lines: DiffLine[];
  138. runId: string;
  139. repoId?: string;
  140. }) {
  141. const [linesWithChanges, setLinesWithChanges] = useState<DiffLineWithChanges[]>([]);
  142. useEffect(() => {
  143. setLinesWithChanges(addChangesToDiffLines(lines));
  144. }, [lines]);
  145. const [editingGroup, setEditingGroup] = useState<number | null>(null);
  146. const [editedContent, setEditedContent] = useState<string>('');
  147. const [editedLines, setEditedLines] = useState<string[]>([]);
  148. const overlayRef = useRef<HTMLDivElement>(null);
  149. const [hoveredGroup, setHoveredGroup] = useState<number | null>(null);
  150. useEffect(() => {
  151. function handleClickOutside(event: MouseEvent) {
  152. if (overlayRef.current && !overlayRef.current.contains(event.target as Node)) {
  153. setEditingGroup(null);
  154. setEditedContent('');
  155. }
  156. }
  157. document.addEventListener('mousedown', handleClickOutside);
  158. return () => {
  159. document.removeEventListener('mousedown', handleClickOutside);
  160. };
  161. }, []);
  162. const lineGroups = useMemo(() => {
  163. const groups: Array<{end: number; start: number; type: 'change' | DiffLineType}> = [];
  164. let currentGroup: (typeof groups)[number] | null = null;
  165. linesWithChanges.forEach((line, index) => {
  166. if (line.line_type !== DiffLineType.CONTEXT) {
  167. if (!currentGroup) {
  168. currentGroup = {start: index, end: index, type: 'change'};
  169. } else if (currentGroup.type === 'change') {
  170. currentGroup.end = index;
  171. } else {
  172. groups.push(currentGroup);
  173. currentGroup = {start: index, end: index, type: 'change'};
  174. }
  175. } else if (currentGroup) {
  176. groups.push(currentGroup);
  177. currentGroup = null;
  178. }
  179. });
  180. if (currentGroup) {
  181. groups.push(currentGroup);
  182. }
  183. return groups;
  184. }, [linesWithChanges]);
  185. const handleEditClick = (index: number) => {
  186. const group = lineGroups.find(g => g.start === index);
  187. if (group) {
  188. const content = linesWithChanges
  189. .slice(group.start, group.end + 1)
  190. .filter(line => line.line_type === DiffLineType.ADDED)
  191. .map(line => line.value)
  192. .join('');
  193. const splitLines = content.split('\n');
  194. if (splitLines[splitLines.length - 1] === '') {
  195. splitLines.pop();
  196. }
  197. setEditedLines(splitLines);
  198. if (content === '\n') {
  199. setEditedContent('');
  200. } else {
  201. setEditedContent(content.endsWith('\n') ? content.slice(0, -1) : content);
  202. }
  203. setEditingGroup(index);
  204. }
  205. };
  206. const handleTextAreaChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
  207. const newContent = e.target.value;
  208. setEditedContent(newContent);
  209. setEditedLines(newContent.split('\n'));
  210. };
  211. const updateHunk = useUpdateHunk({groupId, runId});
  212. const handleSaveEdit = () => {
  213. if (editingGroup === null) {
  214. return;
  215. }
  216. const group = lineGroups.find(g => g.start === editingGroup);
  217. if (!group) {
  218. return;
  219. }
  220. let lastSourceLineNo = 0;
  221. let lastTargetLineNo = 0;
  222. let lastDiffLineNo = 0;
  223. const updatedLines = linesWithChanges
  224. .map((line, index) => {
  225. if (index < group.start) {
  226. lastSourceLineNo = line.source_line_no ?? lastSourceLineNo;
  227. lastTargetLineNo = line.target_line_no ?? lastTargetLineNo;
  228. lastDiffLineNo = line.diff_line_no ?? lastDiffLineNo;
  229. }
  230. if (index >= group.start && index <= group.end) {
  231. if (line.line_type === DiffLineType.ADDED) {
  232. return null; // Remove existing added lines
  233. }
  234. if (line.line_type === DiffLineType.REMOVED) {
  235. lastSourceLineNo = line.source_line_no ?? lastSourceLineNo;
  236. }
  237. return line; // Keep other lines (removed and context) as is
  238. }
  239. return line;
  240. })
  241. .filter((line): line is DiffLine => line !== null);
  242. // Insert new added lines
  243. const newAddedLines: DiffLine[] = editedContent.split('\n').map((content, i) => {
  244. lastDiffLineNo++;
  245. lastTargetLineNo++;
  246. return {
  247. diff_line_no: lastDiffLineNo,
  248. source_line_no: null,
  249. target_line_no: lastTargetLineNo,
  250. line_type: DiffLineType.ADDED,
  251. value: content + (i === editedContent.split('\n').length - 1 ? '' : '\n'),
  252. };
  253. });
  254. // Find the insertion point (after the last removed line or at the start of the group)
  255. const insertionIndex = updatedLines.findIndex(
  256. (line, index) => index >= group.start && line.line_type !== DiffLineType.REMOVED
  257. );
  258. updatedLines.splice(
  259. insertionIndex === -1 ? group.start : insertionIndex,
  260. 0,
  261. ...newAddedLines
  262. );
  263. // Update diff_line_no for all lines after the insertion
  264. for (let i = insertionIndex + newAddedLines.length; i < updatedLines.length; i++) {
  265. updatedLines[i]!.diff_line_no = ++lastDiffLineNo;
  266. }
  267. updateHunk.mutate({hunkIndex, lines: updatedLines, repoId, fileName});
  268. setLinesWithChanges(addChangesToDiffLines(updatedLines));
  269. setEditingGroup(null);
  270. setEditedContent('');
  271. };
  272. const handleCancelEdit = () => {
  273. setEditingGroup(null);
  274. setEditedContent('');
  275. };
  276. const rejectChanges = (index: number) => {
  277. const group = lineGroups.find(g => g.start === index);
  278. if (!group) {
  279. return;
  280. }
  281. const updatedLines = linesWithChanges
  282. .map((line, i) => {
  283. if (i >= group.start && i <= group.end) {
  284. if (line.line_type === DiffLineType.ADDED) {
  285. return null; // Remove added lines
  286. }
  287. if (line.line_type === DiffLineType.REMOVED) {
  288. return {...line, line_type: DiffLineType.CONTEXT}; // Convert removed lines to context
  289. }
  290. }
  291. return line;
  292. })
  293. .filter((line): line is DiffLine => line !== null);
  294. updateHunk.mutate({hunkIndex, lines: updatedLines, repoId, fileName});
  295. setLinesWithChanges(addChangesToDiffLines(updatedLines));
  296. };
  297. const getStartLineNumber = (index: number, lineType: DiffLineType) => {
  298. const line = linesWithChanges[index]!;
  299. if (lineType === DiffLineType.REMOVED) {
  300. return line.source_line_no;
  301. }
  302. if (lineType === DiffLineType.ADDED) {
  303. // Find the first non-null target_line_no
  304. for (let i = index; i < linesWithChanges.length; i++) {
  305. if (linesWithChanges[i]!.target_line_no !== null) {
  306. return linesWithChanges[i]!.target_line_no;
  307. }
  308. }
  309. }
  310. return null;
  311. };
  312. const handleClearChanges = () => {
  313. setEditedContent('');
  314. setEditedLines([]);
  315. };
  316. const getDeletedLineTitle = (index: number) => {
  317. return t(
  318. '%s deleted line%s%s',
  319. linesWithChanges
  320. .slice(index, lineGroups.find(g => g.start === index)?.end! + 1)
  321. .filter(l => l.line_type === DiffLineType.REMOVED).length,
  322. linesWithChanges
  323. .slice(index, lineGroups.find(g => g.start === index)?.end)
  324. .filter(l => l.line_type === DiffLineType.REMOVED).length === 1
  325. ? ''
  326. : 's',
  327. linesWithChanges
  328. .slice(index, lineGroups.find(g => g.start === index)?.end)
  329. .filter(l => l.line_type === DiffLineType.REMOVED).length > 0
  330. ? t(' from line %s', getStartLineNumber(index, DiffLineType.REMOVED))
  331. : ''
  332. );
  333. };
  334. const getNewLineTitle = (index: number) => {
  335. return t(
  336. '%s new line%s%s',
  337. editedLines.length,
  338. editedLines.length === 1 ? '' : 's',
  339. editedLines.length > 0
  340. ? t(' from line %s', getStartLineNumber(index, DiffLineType.ADDED))
  341. : ''
  342. );
  343. };
  344. return (
  345. <Fragment>
  346. <HunkHeaderEmptySpace />
  347. <HunkHeader lines={lines} sectionHeader={header} />
  348. {linesWithChanges.map((line, index) => (
  349. <Fragment key={index}>
  350. <LineNumber lineType={line.line_type}>{line.source_line_no}</LineNumber>
  351. <LineNumber lineType={line.line_type}>{line.target_line_no}</LineNumber>
  352. <DiffContent
  353. lineType={line.line_type}
  354. data-test-id={makeTestIdFromLineType(line.line_type)}
  355. onMouseEnter={() => {
  356. const group = lineGroups.find(g => index >= g.start && index <= g.end);
  357. if (group) {
  358. setHoveredGroup(group.start);
  359. }
  360. }}
  361. onMouseLeave={() => setHoveredGroup(null)}
  362. >
  363. <DiffLineCode line={line} />
  364. {editable && lineGroups.some(group => index === group.start) && (
  365. <ButtonGroup>
  366. <ActionButton
  367. size="xs"
  368. icon={<IconEdit size="xs" />}
  369. aria-label={t('Edit changes')}
  370. title={t('Edit')}
  371. onClick={() => handleEditClick(index)}
  372. isHovered={hoveredGroup === index}
  373. />
  374. <ActionButton
  375. size="xs"
  376. icon={<IconClose size="xs" />}
  377. aria-label={t('Reject changes')}
  378. title={t('Reject')}
  379. onClick={() => rejectChanges(index)}
  380. isHovered={hoveredGroup === index}
  381. />
  382. </ButtonGroup>
  383. )}
  384. {editingGroup === index && (
  385. <EditOverlay ref={overlayRef}>
  386. <OverlayHeader>
  387. <OverlayTitle>{t('Editing %s', fileName)}</OverlayTitle>
  388. </OverlayHeader>
  389. <OverlayContent>
  390. <SectionTitle>{getDeletedLineTitle(index)}</SectionTitle>
  391. {linesWithChanges
  392. .slice(index, lineGroups.find(g => g.start === index)?.end! + 1)
  393. .filter(l => l.line_type === DiffLineType.REMOVED).length > 0 ? (
  394. <RemovedLines>
  395. {linesWithChanges
  396. .slice(index, lineGroups.find(g => g.start === index)?.end! + 1)
  397. .filter(l => l.line_type === DiffLineType.REMOVED)
  398. .map((l, i) => (
  399. <RemovedLine key={i}>{l.value}</RemovedLine>
  400. ))}
  401. </RemovedLines>
  402. ) : (
  403. <NoChangesMessage>
  404. {t('No lines are being deleted.')}
  405. </NoChangesMessage>
  406. )}
  407. <SectionTitle>{getNewLineTitle(index)}</SectionTitle>
  408. <TextAreaWrapper>
  409. <StyledTextArea
  410. value={editedContent}
  411. onChange={handleTextAreaChange}
  412. rows={5}
  413. autosize
  414. placeholder={
  415. editedLines.length === 0 ? t('No lines are being added...') : ''
  416. }
  417. />
  418. <ClearButton
  419. size="xs"
  420. onClick={handleClearChanges}
  421. aria-label={t('Clear changes')}
  422. icon={<IconDelete size="xs" />}
  423. title={t('Clear all new lines')}
  424. />
  425. </TextAreaWrapper>
  426. </OverlayContent>
  427. <OverlayFooter>
  428. <OverlayButtonGroup>
  429. <Button size="xs" onClick={handleCancelEdit}>
  430. {t('Cancel')}
  431. </Button>
  432. <Button size="xs" priority="primary" onClick={handleSaveEdit}>
  433. {t('Save')}
  434. </Button>
  435. </OverlayButtonGroup>
  436. </OverlayFooter>
  437. </EditOverlay>
  438. )}
  439. </DiffContent>
  440. </Fragment>
  441. ))}
  442. </Fragment>
  443. );
  444. }
  445. function FileDiff({
  446. file,
  447. groupId,
  448. runId,
  449. repoId,
  450. editable,
  451. }: {
  452. editable: boolean;
  453. file: FilePatch;
  454. groupId: string;
  455. runId: string;
  456. repoId?: string;
  457. }) {
  458. const [isExpanded, setIsExpanded] = useState(true);
  459. return (
  460. <FileDiffWrapper>
  461. <FileHeader onClick={() => setIsExpanded(value => !value)}>
  462. <InteractionStateLayer />
  463. <FileAddedRemoved>
  464. <FileAdded>+{file.added}</FileAdded>
  465. <FileRemoved>-{file.removed}</FileRemoved>
  466. </FileAddedRemoved>
  467. <FileName>{file.path}</FileName>
  468. <Button
  469. icon={<IconChevron size="xs" direction={isExpanded ? 'down' : 'right'} />}
  470. aria-label={t('Toggle file diff')}
  471. aria-expanded={isExpanded}
  472. size="zero"
  473. borderless
  474. />
  475. </FileHeader>
  476. {isExpanded && (
  477. <DiffContainer>
  478. {file.hunks.map(({section_header, source_start, lines}, index) => {
  479. return (
  480. <DiffHunkContent
  481. key={source_start}
  482. repoId={repoId}
  483. groupId={groupId}
  484. runId={runId}
  485. hunkIndex={index}
  486. lines={lines}
  487. header={section_header}
  488. fileName={file.path}
  489. editable={editable}
  490. />
  491. );
  492. })}
  493. </DiffContainer>
  494. )}
  495. </FileDiffWrapper>
  496. );
  497. }
  498. export function AutofixDiff({diff, groupId, runId, repoId, editable}: AutofixDiffProps) {
  499. if (!diff || !diff.length) {
  500. return null;
  501. }
  502. return (
  503. <DiffsColumn>
  504. {diff.map(file => (
  505. <FileDiff
  506. key={file.path}
  507. file={file}
  508. groupId={groupId}
  509. runId={runId}
  510. repoId={repoId}
  511. editable={editable}
  512. />
  513. ))}
  514. </DiffsColumn>
  515. );
  516. }
  517. const DiffsColumn = styled('div')`
  518. display: flex;
  519. flex-direction: column;
  520. gap: ${space(1)};
  521. `;
  522. const FileDiffWrapper = styled('div')`
  523. font-family: ${p => p.theme.text.familyMono};
  524. font-size: ${p => p.theme.fontSizeSmall};
  525. line-height: 20px;
  526. vertical-align: middle;
  527. border: 1px solid ${p => p.theme.border};
  528. border-radius: ${p => p.theme.borderRadius};
  529. overflow: hidden;
  530. `;
  531. const FileHeader = styled('div')`
  532. position: relative;
  533. display: grid;
  534. align-items: center;
  535. grid-template-columns: minmax(60px, auto) 1fr auto;
  536. gap: ${space(2)};
  537. background-color: ${p => p.theme.backgroundSecondary};
  538. padding: ${space(1)} ${space(2)};
  539. cursor: pointer;
  540. `;
  541. const FileAddedRemoved = styled('div')`
  542. display: flex;
  543. gap: ${space(1)};
  544. align-items: center;
  545. `;
  546. const FileAdded = styled('div')`
  547. color: ${p => p.theme.successText};
  548. `;
  549. const FileRemoved = styled('div')`
  550. color: ${p => p.theme.errorText};
  551. `;
  552. const FileName = styled('div')``;
  553. const DiffContainer = styled('div')`
  554. border-top: 1px solid ${p => p.theme.innerBorder};
  555. display: grid;
  556. grid-template-columns: auto auto 1fr;
  557. `;
  558. const HunkHeaderEmptySpace = styled('div')`
  559. grid-column: 1 / 3;
  560. background-color: ${p => p.theme.backgroundSecondary};
  561. `;
  562. const HunkHeaderContent = styled('div')`
  563. grid-column: 3 / -1;
  564. background-color: ${p => p.theme.backgroundSecondary};
  565. color: ${p => p.theme.subText};
  566. padding: ${space(0.75)} ${space(1)} ${space(0.75)} ${space(4)};
  567. white-space: pre-wrap;
  568. `;
  569. const LineNumber = styled('div')<{lineType: DiffLineType}>`
  570. display: flex;
  571. padding: ${space(0.25)} ${space(1)};
  572. user-select: none;
  573. background-color: ${p => p.theme.backgroundSecondary};
  574. color: ${p => p.theme.subText};
  575. ${p =>
  576. p.lineType === DiffLineType.ADDED &&
  577. `background-color: ${p.theme.diff.added}; color: ${p.theme.textColor}`};
  578. ${p =>
  579. p.lineType === DiffLineType.REMOVED &&
  580. `background-color: ${p.theme.diff.removed}; color: ${p.theme.textColor}`};
  581. & + & {
  582. padding-left: 0;
  583. }
  584. `;
  585. const DiffContent = styled('div')<{lineType: DiffLineType}>`
  586. position: relative;
  587. padding-left: ${space(4)};
  588. padding-right: ${space(4)};
  589. white-space: pre-wrap;
  590. word-break: break-all;
  591. word-wrap: break-word;
  592. ${p =>
  593. p.lineType === DiffLineType.ADDED &&
  594. `background-color: ${p.theme.diff.addedRow}; color: ${p.theme.textColor}`};
  595. ${p =>
  596. p.lineType === DiffLineType.REMOVED &&
  597. `background-color: ${p.theme.diff.removedRow}; color: ${p.theme.textColor}`};
  598. &::before {
  599. content: ${p =>
  600. p.lineType === DiffLineType.ADDED
  601. ? "'+'"
  602. : p.lineType === DiffLineType.REMOVED
  603. ? "'-'"
  604. : "''"};
  605. position: absolute;
  606. top: 1px;
  607. left: ${space(1)};
  608. }
  609. `;
  610. const CodeDiff = styled('span')<{added?: boolean; removed?: boolean}>`
  611. vertical-align: middle;
  612. ${p => p.added && `background-color: ${p.theme.diff.added};`};
  613. ${p => p.removed && `background-color: ${p.theme.diff.removed};`};
  614. `;
  615. const ButtonGroup = styled('div')`
  616. position: absolute;
  617. top: 0;
  618. right: ${space(0.25)};
  619. display: flex;
  620. `;
  621. const ActionButton = styled(Button)<{isHovered: boolean}>`
  622. margin-left: ${space(0.5)};
  623. font-family: ${p => p.theme.text.family};
  624. background-color: ${p =>
  625. p.isHovered ? p.theme.button.default.background : p.theme.translucentGray100};
  626. color: ${p =>
  627. p.isHovered ? p.theme.button.default.color : p.theme.translucentGray200};
  628. transition:
  629. background-color 0.2s ease-in-out,
  630. color 0.2s ease-in-out;
  631. `;
  632. const EditOverlay = styled('div')`
  633. position: fixed;
  634. bottom: 11rem;
  635. right: ${space(2)};
  636. left: calc(50% + ${space(2)});
  637. background: ${p => p.theme.backgroundElevated};
  638. border: 1px solid ${p => p.theme.border};
  639. border-radius: ${p => p.theme.borderRadius};
  640. box-shadow: ${p => p.theme.dropShadowHeavy};
  641. z-index: 1;
  642. display: flex;
  643. flex-direction: column;
  644. max-height: calc(100vh - 18rem);
  645. `;
  646. const OverlayHeader = styled('div')`
  647. padding: ${space(2)} ${space(2)} 0;
  648. border-bottom: 1px solid ${p => p.theme.border};
  649. `;
  650. const OverlayContent = styled('div')`
  651. padding: 0 ${space(2)} ${space(2)} ${space(2)};
  652. overflow-y: auto;
  653. `;
  654. const OverlayFooter = styled('div')`
  655. padding: ${space(2)};
  656. border-top: 1px solid ${p => p.theme.border};
  657. `;
  658. const OverlayButtonGroup = styled('div')`
  659. display: flex;
  660. justify-content: flex-end;
  661. gap: ${space(1)};
  662. font-family: ${p => p.theme.text.family};
  663. `;
  664. const RemovedLines = styled('div')`
  665. margin-bottom: ${space(1)};
  666. font-family: ${p => p.theme.text.familyMono};
  667. border-radius: ${p => p.theme.borderRadius};
  668. overflow: hidden;
  669. `;
  670. const RemovedLine = styled('div')`
  671. background-color: ${p => p.theme.diff.removedRow};
  672. color: ${p => p.theme.textColor};
  673. padding: ${space(0.25)} ${space(0.5)};
  674. `;
  675. const StyledTextArea = styled(TextArea)`
  676. font-family: ${p => p.theme.text.familyMono};
  677. font-size: ${p => p.theme.fontSizeSmall};
  678. background-color: ${p => p.theme.diff.addedRow};
  679. border-color: ${p => p.theme.border};
  680. position: relative;
  681. &:focus {
  682. border-color: ${p => p.theme.focusBorder};
  683. box-shadow: inset 0 0 0 1px ${p => p.theme.focusBorder};
  684. }
  685. `;
  686. const ClearButton = styled(Button)`
  687. position: absolute;
  688. top: -${space(1)};
  689. right: -${space(1)};
  690. z-index: 1;
  691. `;
  692. const TextAreaWrapper = styled('div')`
  693. position: relative;
  694. `;
  695. const SectionTitle = styled('p')`
  696. margin: ${space(1)} 0;
  697. font-size: ${p => p.theme.fontSizeMedium};
  698. font-weight: bold;
  699. color: ${p => p.theme.textColor};
  700. font-family: ${p => p.theme.text.family};
  701. `;
  702. const NoChangesMessage = styled('p')`
  703. margin: ${space(1)} 0;
  704. color: ${p => p.theme.subText};
  705. font-family: ${p => p.theme.text.family};
  706. `;
  707. const OverlayTitle = styled('h3')`
  708. margin: 0 0 ${space(2)} 0;
  709. font-size: ${p => p.theme.fontSizeMedium};
  710. font-weight: bold;
  711. color: ${p => p.theme.textColor};
  712. font-family: ${p => p.theme.text.family};
  713. `;