diff --git a/Cargo.lock b/Cargo.lock index f0afc0227e..6e3c70d13a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -496,6 +496,7 @@ dependencies = [ "comrak", "console-subscriber", "criterion", + "diffy", "directories", "either", "erased-serde", @@ -1511,6 +1512,15 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" +[[package]] +name = "diffy" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e616e59155c92257e84970156f506287853355f58cd4a6eb167385722c32b790" +dependencies = [ + "nu-ansi-term", +] + [[package]] name = "digest" version = "0.10.7" diff --git a/client/src/components/Breadcrumbs/index.tsx b/client/src/components/Breadcrumbs/index.tsx index 0036bc165b..f7e49f4cee 100644 --- a/client/src/components/Breadcrumbs/index.tsx +++ b/client/src/components/Breadcrumbs/index.tsx @@ -56,6 +56,7 @@ const Breadcrumbs = ({ useEffect(() => { if (!containerRef.current || allowOverflow) { + setFormattedPathParts(pathParts); return; } const parentWidth = containerRef.current.parentElement!.clientWidth; diff --git a/client/src/components/BreadcrumbsPath/index.tsx b/client/src/components/BreadcrumbsPath/index.tsx index 85dab694a3..d76677b8a7 100644 --- a/client/src/components/BreadcrumbsPath/index.tsx +++ b/client/src/components/BreadcrumbsPath/index.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useMemo } from 'react'; import Breadcrumbs, { PathParts } from '../Breadcrumbs'; import { breadcrumbsItemPath, @@ -30,7 +30,7 @@ const BreadcrumbsPath = ({ ...rest }: Props) => { const { navigateRepoPath, navigateFullResult } = useAppNavigation(); - const mapPath = useCallback(() => { + const pathParts: PathParts[] = useMemo(() => { return splitPathForBreadcrumbs(path, (e, item, index, pParts) => { if (onClick) { e.stopPropagation(); @@ -53,13 +53,7 @@ const BreadcrumbsPath = ({ navigateFullResult(path); } }); - }, [path, shouldGoToFile]); - - const [pathParts, setPathParts] = useState(mapPath()); - - useEffect(() => { - setPathParts(mapPath()); - }, [path]); + }, [path, shouldGoToFile, onClick, repo]); return (
{ + const lineNumbersAdd = useMemo(() => { + let curr = lineStart; + return tokensMap.map((line, i) => { + if ( + line.tokens[0]?.token?.content === '-' || + line.tokens[1]?.token?.content === '-' + ) { + return null; + } else { + curr++; + return curr; + } + }); + }, [tokensMap, lineStart]); + const lineNumbersRemove = useMemo(() => { + let curr = lineStart; + return tokensMap.map((line, i) => { + if ( + line.tokens[0]?.token?.content === '+' || + line.tokens[1]?.token?.content === '+' + ) { + return null; + } else { + curr++; + return curr; + } + }); + }, [tokensMap, lineStart]); const getSymbols = (lineNumber: number) => { if (symbols?.length) { return symbols @@ -50,6 +78,11 @@ const CodeContainer = ({ key={lineNumber} lineNumber={lineStart + lineNumber} lineNumberToShow={line.lineNumber} + lineNumbersDiff={ + isDiff + ? [lineNumbersRemove[lineNumber], lineNumbersAdd[lineNumber]] + : null + } showLineNumbers={showLines} symbols={getSymbols(lineStart + lineNumber)} lineHidden={ @@ -107,7 +140,7 @@ const CodeContainer = ({ canWrap && codeLines.length < 2 ? '!whitespace-pre-wrap' : '' }`} > -
{codeLines}
+
{codeLines}
); diff --git a/client/src/components/CodeBlock/Code/CodeLine.tsx b/client/src/components/CodeBlock/Code/CodeLine.tsx index 3c968f7f3f..5a96ccaf10 100644 --- a/client/src/components/CodeBlock/Code/CodeLine.tsx +++ b/client/src/components/CodeBlock/Code/CodeLine.tsx @@ -17,6 +17,7 @@ import { markNode, unmark } from '../../../utils/textSearch'; type Props = { lineNumber: number; lineNumberToShow?: number | null; + lineNumbersDiff?: [number | null, number | null] | null; children: ReactNode; showLineNumbers?: boolean; lineFoldable?: boolean; @@ -64,6 +65,7 @@ const CodeLine = ({ leftHighlight, removePaddings, hoveredBackground, + lineNumbersDiff, }: Props) => { const codeRef = useRef(null); @@ -162,11 +164,13 @@ const CodeLine = ({ return (
{ @@ -210,38 +214,44 @@ const CodeLine = ({ )}
- ) : ( -
- )} - {showLineNumbers && ( -
( +
+ )) + ) : ( +
- )} + /> + ))}
@@ -258,15 +268,8 @@ const CodeLine = ({
{children}
diff --git a/client/src/components/CodeBlock/CodeDiff/index.tsx b/client/src/components/CodeBlock/CodeDiff/index.tsx new file mode 100644 index 0000000000..9e32a1fac0 --- /dev/null +++ b/client/src/components/CodeBlock/CodeDiff/index.tsx @@ -0,0 +1,191 @@ +import React, { ChangeEvent, useCallback, useEffect, useState } from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import FileIcon from '../../FileIcon'; +import { getFileExtensionForLang, getPrettyLangName } from '../../../utils'; +import BreadcrumbsPath from '../../BreadcrumbsPath'; +import CopyButton from '../../MarkdownWithCode/CopyButton'; +import Code from '../Code'; +import { DiffChunkType } from '../../../types/general'; +import Button from '../../Button'; +import { Pen, TrashCanFilled } from '../../../icons'; + +type Props = DiffChunkType & { + onClick: (d: DiffChunkType) => void; + language: string; + filePath: string; + onDiffRemoved: (i: number) => void; + onDiffChanged: (i: number, p: string) => void; + i: number; +}; + +const CodeDiff = ({ + hunks, + language, + filePath, + onClick, + file, + repo, + branch, + raw_patch, + lang, + i, + onDiffChanged, + onDiffRemoved, +}: Props) => { + const [isEditMode, setIsEditMode] = useState(false); + const [editedValue, setEditedValue] = useState( + raw_patch.split('\n').slice(2, -1).join('\n'), + ); + const { t } = useTranslation(); + + useEffect(() => { + setEditedValue(raw_patch.split('\n').slice(2, -1).join('\n')); + }, [raw_patch]); + + const handleClick = useCallback(() => { + onClick({ hunks, repo, branch, file, lang, raw_patch }); + }, [hunks, repo, branch, file, lang, raw_patch]); + + const onEditClick = useCallback((e: React.MouseEvent) => { + e.stopPropagation(); + setIsEditMode(true); + }, []); + + const onCancelEdit = useCallback((e: React.MouseEvent) => { + e.stopPropagation(); + setIsEditMode(false); + }, []); + + const handleChange = useCallback((e: ChangeEvent) => { + setEditedValue(e.target.value); + }, []); + + const onSaveEdit = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation(); + onDiffChanged( + i, + `--- ${filePath} ++++ ${filePath} +${editedValue} +`, + ); + setIsEditMode(false); + }, + [i, editedValue, onDiffChanged], + ); + + const onRemove = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation(); + onDiffRemoved(i); + }, + [i, onDiffRemoved], + ); + + return ( +
+
+
+ + {filePath ? ( + + ) : ( + + {getPrettyLangName(language) || language} + + )} +
+
+ {!repo.startsWith('local//') ? ( + + ) : isEditMode ? ( + <> + + + + ) : ( + <> + + + + )} +
+
+
+ {isEditMode ? ( +