diff --git a/packages/core/src/parse/diff-parse.ts b/packages/core/src/parse/diff-parse.ts
index efe7e03..4270d58 100644
--- a/packages/core/src/parse/diff-parse.ts
+++ b/packages/core/src/parse/diff-parse.ts
@@ -1,6 +1,7 @@
/* eslint-disable max-lines */
// !NOTE: ALL of the diff parse logic copy from desktop, SEE https://github.com/desktop/desktop
+// With mirror change
// https://en.wikipedia.org/wiki/Diff_utility
//
diff --git a/packages/react/src/components/v2/DiffSplitExtendLineNormal_v2.tsx b/packages/react/src/components/v2/DiffSplitExtendLineNormal_v2.tsx
new file mode 100644
index 0000000..e4d6141
--- /dev/null
+++ b/packages/react/src/components/v2/DiffSplitExtendLineNormal_v2.tsx
@@ -0,0 +1,132 @@
+import * as React from "react";
+
+import { useDomWidth } from "../../hooks/useDomWidth";
+import { useSyncHeight } from "../../hooks/useSyncHeight";
+import { emptyBGName } from "../color";
+import { SplitSide } from "../DiffView";
+import { useDiffViewContext } from "../DiffViewContext";
+
+import type { DiffFile } from "@git-diff-view/core";
+
+const _DiffSplitExtendLine = ({
+ index,
+ diffFile,
+ oldLineExtend,
+ newLineExtend,
+ side,
+ lineNumber,
+}: {
+ index: number;
+ side: SplitSide;
+ oldLineExtend: { data: any };
+ newLineExtend: { data: any };
+ diffFile: DiffFile;
+ lineNumber: number;
+}) => {
+ const { useDiffContext } = useDiffViewContext();
+
+ const oldLine = diffFile.getSplitLeftLine(index);
+
+ const newLine = diffFile.getSplitRightLine(index);
+
+ const renderExtendLine = useDiffContext(React.useCallback((s) => s.renderExtendLine, []));
+
+ const currentExtend = side === SplitSide.old ? oldLineExtend : newLineExtend;
+
+ const currentLineNumber = side === SplitSide.old ? oldLine.lineNumber : newLine.lineNumber;
+
+ const otherSide = side === SplitSide.old ? SplitSide.new : SplitSide.old;
+
+ useSyncHeight({
+ selector: `div[data-state="extend"][data-line="${lineNumber}-extend"]`,
+ side: currentExtend ? SplitSide[side] : SplitSide[otherSide],
+ enable: side === SplitSide.new && typeof renderExtendLine === "function",
+ });
+
+ const width = useDomWidth({
+ selector: side === SplitSide.old ? ".old-diff-table-wrapper" : ".new-diff-table-wrapper",
+ enable: !!currentExtend && typeof renderExtendLine === "function",
+ });
+
+ if (!renderExtendLine) return null;
+
+ return (
+
+ {currentExtend ? (
+
+
+ {width > 0 &&
+ currentExtend?.data &&
+ renderExtendLine?.({
+ diffFile,
+ side,
+ lineNumber: currentLineNumber,
+ data: currentExtend.data,
+ onUpdate: diffFile.notifyAll,
+ })}
+
+
+ ) : (
+
+ )}
+
+ );
+};
+
+export const DiffSplitExtendLine = ({
+ index,
+ diffFile,
+ side,
+ lineNumber,
+}: {
+ index: number;
+ side: SplitSide;
+ diffFile: DiffFile;
+ lineNumber: number;
+}) => {
+ const { useDiffContext } = useDiffViewContext();
+
+ const oldLine = diffFile.getSplitLeftLine(index);
+
+ const newLine = diffFile.getSplitRightLine(index);
+
+ const { oldLineExtend, newLineExtend } = useDiffContext(
+ React.useCallback(
+ (s) => ({
+ oldLineExtend: s.extendData?.oldFile?.[oldLine?.lineNumber],
+ newLineExtend: s.extendData?.newFile?.[newLine?.lineNumber],
+ }),
+ [oldLine?.lineNumber, newLine?.lineNumber]
+ )
+ );
+
+ const hasExtend = oldLineExtend?.data || newLineExtend?.data;
+
+ // if the expand action not enabled, the `isHidden` property will never change
+ const enableExpand = diffFile.getExpandEnabled();
+
+ const currentLine = side === SplitSide.old ? oldLine : newLine;
+
+ const currentIsShow = hasExtend && (!currentLine.isHidden || !enableExpand);
+
+ if (!currentIsShow) return null;
+
+ return (
+ <_DiffSplitExtendLine
+ side={side}
+ index={index}
+ diffFile={diffFile}
+ lineNumber={lineNumber}
+ oldLineExtend={oldLineExtend}
+ newLineExtend={newLineExtend}
+ />
+ );
+};
diff --git a/packages/react/src/components/v2/DiffSplitExtendLineWrap_v2.tsx b/packages/react/src/components/v2/DiffSplitExtendLineWrap_v2.tsx
new file mode 100644
index 0000000..762b80b
--- /dev/null
+++ b/packages/react/src/components/v2/DiffSplitExtendLineWrap_v2.tsx
@@ -0,0 +1,125 @@
+import * as React from "react";
+
+import { emptyBGName } from "../color";
+import { SplitSide } from "../DiffView";
+import { useDiffViewContext } from "../DiffViewContext";
+
+import type { DiffFile } from "@git-diff-view/core";
+
+const _DiffSplitExtendLine = ({
+ index,
+ diffFile,
+ lineNumber,
+ oldLineExtend,
+ newLineExtend,
+}: {
+ index: number;
+ diffFile: DiffFile;
+ lineNumber: number;
+ oldLineExtend: { data: any };
+ newLineExtend: { data: any };
+}) => {
+ const { useDiffContext } = useDiffViewContext();
+
+ const oldLine = diffFile.getSplitLeftLine(index);
+
+ const newLine = diffFile.getSplitRightLine(index);
+
+ // 需要显示的时候才进行方法订阅,可以大幅度提高性能
+ const renderExtendLine = useDiffContext(React.useCallback((s) => s.renderExtendLine, []));
+
+ if (!renderExtendLine) return null;
+
+ return (
+
+ {oldLineExtend ? (
+
+
+ {oldLineExtend?.data &&
+ renderExtendLine?.({
+ diffFile,
+ side: SplitSide.old,
+ lineNumber: oldLine.lineNumber,
+ data: oldLineExtend.data,
+ onUpdate: diffFile.notifyAll,
+ })}
+
+
+ ) : (
+
+
+
+ )}
+
+ {newLineExtend ? (
+
+
+ {newLineExtend?.data &&
+ renderExtendLine?.({
+ diffFile,
+ side: SplitSide.new,
+ lineNumber: newLine.lineNumber,
+ data: newLineExtend.data,
+ onUpdate: diffFile.notifyAll,
+ })}
+
+
+ ) : (
+
+
+
+ )}
+
+ );
+};
+
+export const DiffSplitExtendLine = ({
+ index,
+ diffFile,
+ lineNumber,
+}: {
+ index: number;
+ diffFile: DiffFile;
+ lineNumber: number;
+}) => {
+ const { useDiffContext } = useDiffViewContext();
+
+ const oldLine = diffFile.getSplitLeftLine(index);
+
+ const newLine = diffFile.getSplitRightLine(index);
+
+ const { oldLineExtend, newLineExtend } = useDiffContext(
+ React.useCallback(
+ (s) => ({
+ oldLineExtend: s.extendData?.oldFile?.[oldLine?.lineNumber],
+ newLineExtend: s.extendData?.newFile?.[newLine?.lineNumber],
+ }),
+ [oldLine?.lineNumber, newLine?.lineNumber]
+ )
+ );
+
+ const hasExtend = oldLineExtend?.data || newLineExtend?.data;
+
+ // if the expand action not enabled, the `isHidden` property will never change
+ const enableExpand = diffFile.getExpandEnabled();
+
+ const currentIsShow = hasExtend && ((!oldLine?.isHidden && !newLine?.isHidden) || !enableExpand);
+
+ if (!currentIsShow) return null;
+
+ return (
+ <_DiffSplitExtendLine
+ index={index}
+ diffFile={diffFile}
+ lineNumber={lineNumber}
+ oldLineExtend={oldLineExtend}
+ newLineExtend={newLineExtend}
+ />
+ );
+};
diff --git a/packages/react/src/components/v2/DiffSplitHunkLineNormal_v2.tsx b/packages/react/src/components/v2/DiffSplitHunkLineNormal_v2.tsx
new file mode 100644
index 0000000..d7b66d9
--- /dev/null
+++ b/packages/react/src/components/v2/DiffSplitHunkLineNormal_v2.tsx
@@ -0,0 +1,297 @@
+import { composeLen, type DiffFile } from "@git-diff-view/core";
+import * as React from "react";
+
+import { useSyncHeight } from "../../hooks/useSyncHeight";
+import { hunkLineNumberBGName, plainLineNumberColorName, hunkContentBGName, hunkContentColorName } from "../color";
+import { ExpandUp, ExpandDown, ExpandAll } from "../DiffExpand";
+import { SplitSide } from "../DiffView";
+import { useDiffViewContext, DiffModeEnum } from "../DiffViewContext";
+import { diffAsideWidthName } from "../tools";
+
+const _DiffSplitHunkLineGitHub = ({
+ index,
+ diffFile,
+ side,
+ lineNumber,
+}: {
+ index: number;
+ side: SplitSide;
+ diffFile: DiffFile;
+ lineNumber: number;
+}) => {
+ const currentHunk = diffFile.getSplitHunkLine(index);
+
+ const expandEnabled = diffFile.getExpandEnabled();
+
+ useSyncHeight({
+ selector: `div[data-state="hunk"][data-line="${lineNumber}-hunk"]`,
+ side: SplitSide[SplitSide.old],
+ enable: side === SplitSide.new,
+ });
+
+ const enableHunkAction = side === SplitSide.old;
+
+ const couldExpand = expandEnabled && currentHunk && currentHunk.splitInfo;
+
+ const isExpandAll =
+ currentHunk &&
+ currentHunk.splitInfo &&
+ currentHunk.splitInfo.endHiddenIndex - currentHunk.splitInfo.startHiddenIndex < composeLen;
+
+ const isFirstLine = currentHunk && currentHunk.isFirst;
+
+ const isLastLine = currentHunk && currentHunk.isLast;
+
+ return (
+
+ {enableHunkAction ? (
+ <>
+
+ {couldExpand ? (
+ isFirstLine ? (
+
+ ) : isLastLine ? (
+
+ ) : isExpandAll ? (
+
+ ) : (
+ <>
+
+
+ >
+ )
+ ) : (
+
+ )}
+
+
+ {currentHunk.splitInfo?.plainText || currentHunk.text}
+
+ >
+ ) : (
+
+ )}
+
+ );
+};
+
+const _DiffSplitHunkLineGitLab = ({
+ index,
+ diffFile,
+ side,
+ lineNumber,
+}: {
+ index: number;
+ side: SplitSide;
+ diffFile: DiffFile;
+ lineNumber: number;
+}) => {
+ const currentHunk = diffFile.getSplitHunkLine(index);
+
+ const expandEnabled = diffFile.getExpandEnabled();
+
+ useSyncHeight({
+ selector: `div[data-state="hunk"][data-line="${lineNumber}-hunk"]`,
+ side: SplitSide[SplitSide.old],
+ enable: side === SplitSide.new,
+ });
+
+ const couldExpand = expandEnabled && currentHunk && currentHunk.splitInfo;
+
+ const isExpandAll =
+ currentHunk &&
+ currentHunk.splitInfo &&
+ currentHunk.splitInfo.endHiddenIndex - currentHunk.splitInfo.startHiddenIndex < composeLen;
+
+ const isFirstLine = currentHunk && currentHunk.isFirst;
+
+ const isLastLine = currentHunk && currentHunk.isLast;
+
+ return (
+
+
+ {couldExpand ? (
+ isFirstLine ? (
+
+ ) : isLastLine ? (
+
+ ) : isExpandAll ? (
+
+ ) : (
+ <>
+
+
+ >
+ )
+ ) : (
+
+ )}
+
+
+ {currentHunk.splitInfo?.plainText || currentHunk.text}
+
+
+ );
+};
+
+const _DiffSplitHunkLine = ({
+ index,
+ diffFile,
+ side,
+ lineNumber,
+}: {
+ index: number;
+ side: SplitSide;
+ diffFile: DiffFile;
+ lineNumber: number;
+}) => {
+ const { useDiffContext } = useDiffViewContext();
+
+ const diffViewMode = useDiffContext(React.useCallback((s) => s.mode, []));
+
+ if (
+ diffViewMode === DiffModeEnum.SplitGitHub ||
+ diffViewMode === DiffModeEnum.Split ||
+ diffViewMode === DiffModeEnum.Unified
+ ) {
+ return <_DiffSplitHunkLineGitHub index={index} diffFile={diffFile} side={side} lineNumber={lineNumber} />;
+ } else {
+ return <_DiffSplitHunkLineGitLab index={index} diffFile={diffFile} side={side} lineNumber={lineNumber} />;
+ }
+};
+
+export const DiffSplitHunkLine = ({
+ index,
+ diffFile,
+ side,
+ lineNumber,
+}: {
+ index: number;
+ side: SplitSide;
+ diffFile: DiffFile;
+ lineNumber: number;
+}) => {
+ const currentHunk = diffFile.getSplitHunkLine(index);
+
+ const currentIsShow =
+ currentHunk &&
+ currentHunk.splitInfo &&
+ currentHunk.splitInfo.startHiddenIndex < currentHunk.splitInfo.endHiddenIndex;
+
+ const currentIsPureHunk = currentHunk && diffFile._getIsPureDiffRender() && !currentHunk.splitInfo;
+
+ if (!currentIsShow && !currentIsPureHunk) return null;
+
+ return <_DiffSplitHunkLine index={index} diffFile={diffFile} side={side} lineNumber={lineNumber} />;
+};
diff --git a/packages/react/src/components/v2/DiffSplitHunkLineWrap_v2.tsx b/packages/react/src/components/v2/DiffSplitHunkLineWrap_v2.tsx
new file mode 100644
index 0000000..8cc99a4
--- /dev/null
+++ b/packages/react/src/components/v2/DiffSplitHunkLineWrap_v2.tsx
@@ -0,0 +1,325 @@
+import { composeLen, type DiffFile } from "@git-diff-view/core";
+import * as React from "react";
+
+import { hunkLineNumberBGName, plainLineNumberColorName, hunkContentBGName, hunkContentColorName } from "../color";
+import { ExpandUp, ExpandDown, ExpandAll } from "../DiffExpand";
+import { useDiffViewContext, DiffModeEnum } from "../DiffViewContext";
+
+const DiffSplitHunkLineGitHub = ({
+ index,
+ diffFile,
+ lineNumber,
+}: {
+ index: number;
+ diffFile: DiffFile;
+ lineNumber: number;
+}) => {
+ const currentHunk = diffFile.getSplitHunkLine(index);
+
+ const expandEnabled = diffFile.getExpandEnabled();
+
+ const couldExpand = expandEnabled && currentHunk && currentHunk.splitInfo;
+
+ const isExpandAll =
+ currentHunk &&
+ currentHunk.splitInfo &&
+ currentHunk.splitInfo.endHiddenIndex - currentHunk.splitInfo.startHiddenIndex < composeLen;
+
+ const currentIsShow =
+ currentHunk &&
+ currentHunk.splitInfo &&
+ currentHunk.splitInfo.startHiddenIndex < currentHunk.splitInfo.endHiddenIndex;
+
+ const currentIsPureHunk = currentHunk && diffFile._getIsPureDiffRender() && !currentHunk.splitInfo;
+
+ const isFirstLine = currentHunk && currentHunk.isFirst;
+
+ const isLastLine = currentHunk && currentHunk.isLast;
+
+ if (!currentIsShow && !currentIsPureHunk) return null;
+
+ return (
+
+
+ {couldExpand ? (
+ isFirstLine ? (
+
+ ) : isLastLine ? (
+
+ ) : isExpandAll ? (
+
+ ) : (
+ <>
+
+
+ >
+ )
+ ) : (
+
+ )}
+
+
+
+ {currentHunk.splitInfo?.plainText || currentHunk.text}
+
+
+
+ );
+};
+
+const DiffSplitHunkLineGitLab = ({
+ index,
+ diffFile,
+ lineNumber,
+}: {
+ index: number;
+ diffFile: DiffFile;
+ lineNumber: number;
+}) => {
+ const currentHunk = diffFile.getSplitHunkLine(index);
+
+ const expandEnabled = diffFile.getExpandEnabled();
+
+ const couldExpand = expandEnabled && currentHunk && currentHunk.splitInfo;
+
+ const isExpandAll =
+ currentHunk &&
+ currentHunk.splitInfo &&
+ currentHunk.splitInfo.endHiddenIndex - currentHunk.splitInfo.startHiddenIndex < composeLen;
+
+ const currentIsShow =
+ currentHunk &&
+ currentHunk.splitInfo &&
+ currentHunk.splitInfo.startHiddenIndex < currentHunk.splitInfo.endHiddenIndex;
+
+ const currentIsPureHunk = currentHunk && diffFile._getIsPureDiffRender() && !currentHunk.splitInfo;
+
+ const isFirstLine = currentHunk && currentHunk.isFirst;
+
+ const isLastLine = currentHunk && currentHunk.isLast;
+
+ if (!currentIsShow && !currentIsPureHunk) return null;
+
+ return (
+
+
+ {couldExpand ? (
+ isFirstLine ? (
+
+ ) : isLastLine ? (
+
+ ) : isExpandAll ? (
+
+ ) : (
+ <>
+
+
+ >
+ )
+ ) : (
+
+ )}
+
+
+
+ {currentHunk.splitInfo?.plainText || currentHunk.text}
+
+
+
+
+ {couldExpand ? (
+ isFirstLine ? (
+
+ ) : isLastLine ? (
+
+ ) : isExpandAll ? (
+
+ ) : (
+ <>
+
+
+ >
+ )
+ ) : (
+
+ )}
+
+
+
+ {currentHunk.splitInfo?.plainText || currentHunk.text}
+
+
+
+ );
+};
+
+export const DiffSplitHunkLine = ({
+ index,
+ diffFile,
+ lineNumber,
+}: {
+ index: number;
+ diffFile: DiffFile;
+ lineNumber: number;
+}) => {
+ const { useDiffContext } = useDiffViewContext();
+
+ const diffViewMode = useDiffContext(React.useCallback((s) => s.mode, []));
+
+ if (
+ diffViewMode === DiffModeEnum.SplitGitHub ||
+ diffViewMode === DiffModeEnum.Split ||
+ diffViewMode === DiffModeEnum.Unified
+ ) {
+ return ;
+ } else {
+ return ;
+ }
+};
diff --git a/packages/react/src/components/v2/DiffSplitLineNormal_v2.tsx b/packages/react/src/components/v2/DiffSplitLineNormal_v2.tsx
new file mode 100644
index 0000000..1d2c4aa
--- /dev/null
+++ b/packages/react/src/components/v2/DiffSplitLineNormal_v2.tsx
@@ -0,0 +1,144 @@
+import { DiffLineType, type DiffFile, checkDiffLineIncludeChange } from "@git-diff-view/core";
+import * as React from "react";
+
+import { getContentBG, getLineNumberBG, plainLineNumberColorName, emptyBGName } from "../color";
+import { DiffSplitAddWidget } from "../DiffAddWidget";
+import { DiffContent } from "../DiffContent";
+import { SplitSide } from "../DiffView";
+import { useDiffViewContext } from "../DiffViewContext";
+import { useDiffWidgetContext } from "../DiffWidgetContext";
+import { diffAsideWidthName } from "../tools";
+
+const _DiffSplitLine = ({
+ index,
+ diffFile,
+ lineNumber,
+ side,
+}: {
+ index: number;
+ side: SplitSide;
+ diffFile: DiffFile;
+ lineNumber: number;
+}) => {
+ const getCurrentSyntaxLine = side === SplitSide.old ? diffFile.getOldSyntaxLine : diffFile.getNewSyntaxLine;
+
+ const oldLine = diffFile.getSplitLeftLine(index);
+
+ const newLine = diffFile.getSplitRightLine(index);
+
+ const currentLine = side === SplitSide.old ? oldLine : newLine;
+
+ const hasDiff = !!currentLine?.diff;
+
+ const hasContent = !!currentLine.lineNumber;
+
+ const hasChange = checkDiffLineIncludeChange(currentLine?.diff);
+
+ const isAdded = currentLine?.diff?.type === DiffLineType.Add;
+
+ const isDelete = currentLine?.diff?.type === DiffLineType.Delete;
+
+ const { useDiffContext } = useDiffViewContext();
+
+ const { enableHighlight, enableAddWidget, onAddWidgetClick } = useDiffContext(
+ React.useCallback(
+ (s) => ({
+ enableHighlight: s.enableHighlight,
+ enableAddWidget: s.enableAddWidget,
+ onAddWidgetClick: s.onAddWidgetClick,
+ }),
+ []
+ )
+ );
+
+ const { useWidget } = useDiffWidgetContext();
+
+ const setWidget = useWidget.getReadonlyState().setWidget;
+
+ const contentBG = getContentBG(isAdded, isDelete, hasDiff);
+
+ const lineNumberBG = getLineNumberBG(isAdded, isDelete, hasDiff);
+
+ const syntaxLine = getCurrentSyntaxLine(currentLine.lineNumber);
+
+ return (
+
+ {hasContent ? (
+ <>
+
+ {hasDiff && enableAddWidget && (
+ onAddWidgetClick.current?.(...props)}
+ className="absolute left-[100%] top-[50%] z-[1] translate-x-[-50%] translate-y-[-50%]"
+ onOpenAddWidget={(lineNumber, side) => setWidget({ lineNumber: lineNumber, side: side })}
+ />
+ )}
+
+ {currentLine.lineNumber}
+
+
+
+
+
+ >
+ ) : (
+
+ )}
+
+ );
+};
+
+export const DiffSplitLine = ({
+ index,
+ diffFile,
+ lineNumber,
+ side,
+}: {
+ index: number;
+ side: SplitSide;
+ diffFile: DiffFile;
+ lineNumber: number;
+}) => {
+ const getCurrentLine = side === SplitSide.old ? diffFile.getSplitLeftLine : diffFile.getSplitRightLine;
+
+ const currentLine = getCurrentLine(index);
+
+ if (currentLine?.isHidden) return null;
+
+ return <_DiffSplitLine index={index} diffFile={diffFile} lineNumber={lineNumber} side={side} />;
+};
diff --git a/packages/react/src/components/v2/DiffSplitLineWrap_v2.tsx b/packages/react/src/components/v2/DiffSplitLineWrap_v2.tsx
new file mode 100644
index 0000000..68a1b70
--- /dev/null
+++ b/packages/react/src/components/v2/DiffSplitLineWrap_v2.tsx
@@ -0,0 +1,194 @@
+import { DiffLineType, type DiffFile, checkDiffLineIncludeChange } from "@git-diff-view/core";
+import * as React from "react";
+
+import { getContentBG, getLineNumberBG, plainLineNumberColorName, emptyBGName } from "../color";
+import { DiffSplitAddWidget } from "../DiffAddWidget";
+import { DiffContent } from "../DiffContent";
+import { SplitSide } from "../DiffView";
+import { useDiffViewContext } from "../DiffViewContext";
+import { useDiffWidgetContext } from "../DiffWidgetContext";
+
+const _DiffSplitLine = ({ index, diffFile, lineNumber }: { index: number; diffFile: DiffFile; lineNumber: number }) => {
+ const oldLine = diffFile.getSplitLeftLine(index);
+
+ const newLine = diffFile.getSplitRightLine(index);
+
+ const oldSyntaxLine = diffFile.getOldSyntaxLine(oldLine?.lineNumber);
+
+ const newSyntaxLine = diffFile.getNewSyntaxLine(newLine?.lineNumber);
+
+ const hasDiff = !!oldLine?.diff || !!newLine?.diff;
+
+ const hasChange = checkDiffLineIncludeChange(oldLine?.diff) || checkDiffLineIncludeChange(newLine?.diff);
+
+ const oldLineIsDelete = oldLine?.diff?.type === DiffLineType.Delete;
+
+ const newLineIsAdded = newLine?.diff?.type === DiffLineType.Add;
+
+ const { useDiffContext } = useDiffViewContext();
+
+ const { enableHighlight, enableAddWidget, onAddWidgetClick } = useDiffContext(
+ React.useCallback(
+ (s) => ({
+ enableHighlight: s.enableHighlight,
+ enableAddWidget: s.enableAddWidget,
+ onAddWidgetClick: s.onAddWidgetClick,
+ }),
+ []
+ )
+ );
+
+ const { useWidget } = useDiffWidgetContext();
+
+ const setWidget = useWidget.getReadonlyState().setWidget;
+
+ const hasOldLine = !!oldLine.lineNumber;
+
+ const hasNewLine = !!newLine.lineNumber;
+
+ const oldLineContentBG = getContentBG(false, oldLineIsDelete, hasDiff);
+
+ const oldLineNumberBG = getLineNumberBG(false, oldLineIsDelete, hasDiff);
+
+ const newLineContentBG = getContentBG(newLineIsAdded, false, hasDiff);
+
+ const newLineNumberBG = getLineNumberBG(newLineIsAdded, false, hasDiff);
+
+ return (
+
+ {hasOldLine ? (
+ <>
+
+ {hasDiff && enableAddWidget && (
+ onAddWidgetClick.current?.(...props)}
+ className="absolute left-[100%] z-[1] translate-x-[-50%]"
+ onOpenAddWidget={(lineNumber, side) => setWidget({ lineNumber: lineNumber, side: side })}
+ />
+ )}
+
+ {oldLine.lineNumber}
+
+
+
+ {hasDiff && enableAddWidget && (
+ onAddWidgetClick.current?.(...props)}
+ className="absolute right-[100%] z-[1] translate-x-[50%]"
+ onOpenAddWidget={(lineNumber, side) => setWidget({ lineNumber: lineNumber, side: side })}
+ />
+ )}
+
+
+ >
+ ) : (
+
+
+
+ )}
+
+ {hasNewLine ? (
+ <>
+
+ {hasDiff && enableAddWidget && (
+ onAddWidgetClick.current?.(...props)}
+ className="absolute left-[100%] z-[1] translate-x-[-50%]"
+ onOpenAddWidget={(lineNumber, side) => setWidget({ lineNumber: lineNumber, side: side })}
+ />
+ )}
+
+ {newLine.lineNumber}
+
+
+
+ {hasDiff && enableAddWidget && (
+ onAddWidgetClick.current?.(...props)}
+ className="absolute right-[100%] z-[1] translate-x-[50%]"
+ onOpenAddWidget={(lineNumber, side) => setWidget({ lineNumber: lineNumber, side: side })}
+ />
+ )}
+
+
+ >
+ ) : (
+
+
+
+ )}
+
+ );
+};
+
+export const DiffSplitLine = ({
+ index,
+ diffFile,
+ lineNumber,
+}: {
+ index: number;
+ diffFile: DiffFile;
+ lineNumber: number;
+}) => {
+ const oldLine = diffFile.getSplitLeftLine(index);
+
+ const newLine = diffFile.getSplitRightLine(index);
+
+ if (oldLine?.isHidden && newLine?.isHidden) return null;
+
+ return <_DiffSplitLine index={index} diffFile={diffFile} lineNumber={lineNumber} />;
+};
diff --git a/packages/react/src/components/v2/DiffSplitViewLineNormal_v2.tsx b/packages/react/src/components/v2/DiffSplitViewLineNormal_v2.tsx
new file mode 100644
index 0000000..2d8321f
--- /dev/null
+++ b/packages/react/src/components/v2/DiffSplitViewLineNormal_v2.tsx
@@ -0,0 +1,35 @@
+import { DiffFileLineType } from "@git-diff-view/core";
+import * as React from "react";
+
+
+import { DiffSplitExtendLine } from "./DiffSplitExtendLineNormal_v2";
+import { DiffSplitHunkLine } from "./DiffSplitHunkLineNormal_v2";
+import { DiffSplitLine } from "./DiffSplitLineNormal_v2";
+import { DiffSplitWidgetLine } from "./DiffSplitWidgetLineNormal_v2";
+
+import type { SplitSide } from "../DiffView";
+import type { DiffFile, DiffSplitLineItem } from "@git-diff-view/core";
+
+// TODO
+export const DiffSplitViewLine = ({
+ line,
+ side,
+ diffFile,
+}: {
+ line: DiffSplitLineItem;
+ side: SplitSide;
+ diffFile: DiffFile;
+}) => {
+ switch (line.type) {
+ case DiffFileLineType.hunk:
+ return ;
+ case DiffFileLineType.content:
+ return ;
+ case DiffFileLineType.widget:
+ return ;
+ case DiffFileLineType.extend:
+ return ;
+ default:
+ return ;
+ }
+};
diff --git a/packages/react/src/components/v2/DiffSplitViewLineWrap_v2.tsx b/packages/react/src/components/v2/DiffSplitViewLineWrap_v2.tsx
new file mode 100644
index 0000000..aa0fc51
--- /dev/null
+++ b/packages/react/src/components/v2/DiffSplitViewLineWrap_v2.tsx
@@ -0,0 +1,25 @@
+import { DiffFileLineType } from "@git-diff-view/core";
+import * as React from "react";
+
+import { DiffSplitExtendLine } from "./DiffSplitExtendLineWrap_v2";
+import { DiffSplitHunkLine } from "./DiffSplitHunkLineWrap_v2";
+import { DiffSplitLine } from "./DiffSplitLineWrap_v2";
+import { DiffSplitWidgetLine } from "./DiffSplitWidgetLineWrap_v2";
+
+import type { DiffFile, DiffSplitLineItem } from "@git-diff-view/core";
+
+// TODO
+export const DiffSplitViewLine = ({ line, diffFile }: { line: DiffSplitLineItem; diffFile: DiffFile }) => {
+ switch (line.type) {
+ case DiffFileLineType.hunk:
+ return ;
+ case DiffFileLineType.content:
+ return ;
+ case DiffFileLineType.widget:
+ return ;
+ case DiffFileLineType.extend:
+ return ;
+ default:
+ return ;
+ }
+};
diff --git a/packages/react/src/components/v2/DiffSplitViewNormal_v2.tsx b/packages/react/src/components/v2/DiffSplitViewNormal_v2.tsx
new file mode 100644
index 0000000..fe016db
--- /dev/null
+++ b/packages/react/src/components/v2/DiffSplitViewNormal_v2.tsx
@@ -0,0 +1,113 @@
+/* eslint-disable @typescript-eslint/ban-ts-comment */
+import { DiffFileLineType, getSplitLines, type DiffFile } from "@git-diff-view/core";
+import { memo, useCallback, useEffect, useRef } from "react";
+import * as React from "react";
+import { useSyncExternalStore } from "use-sync-external-store/shim/index.js";
+
+
+import { useTextWidth } from "../../hooks/useTextWidth";
+import { SplitSide } from "../DiffView";
+import { useDiffViewContext } from "../DiffViewContext";
+import { removeAllSelection, syncScroll, diffFontSizeName } from "../tools";
+
+import { DiffSplitViewLine } from "./DiffSplitViewLineNormal_v2";
+
+import type { MouseEventHandler } from "react";
+
+const onMouseDown: MouseEventHandler = (e) => {
+ const ele = e.target;
+
+ // need remove all the selection
+ if (ele && ele instanceof HTMLElement && ele.nodeName === "BUTTON") {
+ removeAllSelection();
+ return;
+ }
+};
+
+export const DiffSplitViewTable = ({ side, diffFile }: { side: SplitSide; diffFile: DiffFile }) => {
+ const className = side === SplitSide.new ? "new-diff-table" : "old-diff-table";
+
+ const lines = getSplitLines(diffFile);
+
+ return (
+
+
+ {lines.map((line) => (
+
+ ))}
+
+
+ );
+};
+
+export const DiffSplitViewNormal = memo(({ diffFile }: { diffFile: DiffFile }) => {
+ const ref1 = useRef(null);
+
+ const ref2 = useRef(null);
+
+ const splitLineLength = Math.max(diffFile.splitLineLength, diffFile.fileLineLength);
+
+ const { useDiffContext } = useDiffViewContext();
+
+ const fontSize = useDiffContext(useCallback((s) => s.fontSize, []));
+
+ useSyncExternalStore(diffFile.subscribe, diffFile.getUpdateCount);
+
+ useEffect(() => {
+ const left = ref1.current;
+ const right = ref2.current;
+ if (!left || !right) return;
+ return syncScroll(left, right);
+ }, []);
+
+ const font = React.useMemo(
+ () => ({ fontSize: fontSize + "px", fontFamily: "Menlo, Consolas, monospace" }),
+ [fontSize]
+ );
+
+ const _width = useTextWidth({
+ text: splitLineLength.toString(),
+ font,
+ });
+
+ const width = Math.max(40, _width + 25);
+
+ return (
+
+ );
+});
+
+DiffSplitViewNormal.displayName = "DiffSplitViewNormal";
diff --git a/packages/react/src/components/v2/DiffSplitViewWrap_v2.tsx b/packages/react/src/components/v2/DiffSplitViewWrap_v2.tsx
new file mode 100644
index 0000000..8c673f7
--- /dev/null
+++ b/packages/react/src/components/v2/DiffSplitViewWrap_v2.tsx
@@ -0,0 +1,109 @@
+/* eslint-disable @typescript-eslint/ban-ts-comment */
+import { type DiffFile, getSplitLines } from "@git-diff-view/core";
+import { memo, useCallback, useMemo } from "react";
+import * as React from "react";
+// SEE https://github.com/facebook/react/pull/25231
+import { useSyncExternalStore } from "use-sync-external-store/shim/index.js";
+
+import { useTextWidth } from "../../hooks/useTextWidth";
+import { SplitSide } from "../DiffView";
+import { useDiffViewContext } from "../DiffViewContext";
+import { createDiffSplitConfigStore, removeAllSelection, diffFontSizeName } from "../tools";
+
+import { DiffSplitViewLine } from "./DiffSplitViewLineWrap_v2";
+
+import type { MouseEventHandler } from "react";
+import type { Ref, UseSelectorWithStore } from "reactivity-store";
+
+const Style = ({
+ useSelector,
+ id,
+}: {
+ useSelector: UseSelectorWithStore<{ splitRef: Ref }>;
+ id: string;
+}) => {
+ const splitRef = useSelector((s) => s.splitRef);
+
+ return (
+
+ );
+};
+
+export const DiffSplitViewWrap = memo(({ diffFile }: { diffFile: DiffFile }) => {
+ const splitLineLength = Math.max(diffFile.splitLineLength, diffFile.fileLineLength);
+
+ const { useDiffContext } = useDiffViewContext();
+
+ const useSplitConfig = useMemo(() => createDiffSplitConfigStore(), []);
+
+ const fontSize = useDiffContext(useCallback((s) => s.fontSize, []));
+
+ useSyncExternalStore(diffFile.subscribe, diffFile.getUpdateCount);
+
+ const onMouseDown = useCallback>((e) => {
+ let ele = e.target;
+
+ const setSelectSide = useSplitConfig.getReadonlyState().setSplit;
+
+ // need remove all the selection
+ if (ele && ele instanceof HTMLElement && ele.nodeName === "BUTTON") {
+ removeAllSelection();
+ return;
+ }
+
+ while (ele && ele instanceof HTMLElement && ele.nodeName !== "TD") {
+ ele = ele.parentElement;
+ }
+
+ if (ele instanceof HTMLElement) {
+ const side = ele.getAttribute("data-side");
+ if (side) {
+ setSelectSide(SplitSide[side]);
+ removeAllSelection();
+ }
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ const font = useMemo(() => ({ fontSize: fontSize + "px", fontFamily: "Menlo, Consolas, monospace" }), [fontSize]);
+
+ const _width = useTextWidth({
+ text: splitLineLength.toString(),
+ font,
+ });
+
+ const width = Math.max(40, _width + 25);
+
+ const lines = getSplitLines(diffFile);
+
+ return (
+
+
+
+
+
+ {lines.map((line, index) => (
+
+ ))}
+
+
+
+
+ );
+});
+
+DiffSplitViewWrap.displayName = "DiffSplitViewWrap";
diff --git a/packages/react/src/components/v2/DiffSplitView_v2.tsx b/packages/react/src/components/v2/DiffSplitView_v2.tsx
new file mode 100644
index 0000000..c556c75
--- /dev/null
+++ b/packages/react/src/components/v2/DiffSplitView_v2.tsx
@@ -0,0 +1,40 @@
+import { memo, useEffect, useMemo, useRef, useCallback } from "react";
+import * as React from "react";
+
+import { useDiffViewContext } from "../DiffViewContext";
+import { DiffWidgetContext } from "../DiffWidgetContext";
+import { createDiffWidgetStore } from "../tools";
+
+import { DiffSplitViewNormal } from "./DiffSplitViewNormal_v2";
+import { DiffSplitViewWrap } from "./DiffSplitViewWrap_v2";
+
+import type { DiffFile } from "@git-diff-view/core";
+
+export const DiffSplitView = memo(({ diffFile }: { diffFile: DiffFile }) => {
+ const { useDiffContext } = useDiffViewContext();
+
+ const useDiffContextRef = useRef(useDiffContext);
+
+ useDiffContextRef.current = useDiffContext;
+
+ const enableWrap = useDiffContext(useCallback((s) => s.enableWrap, []));
+
+ // performance optimization
+ const useWidget = useMemo(() => createDiffWidgetStore(useDiffContextRef), []);
+
+ const contextValue = useMemo(() => ({ useWidget }), [useWidget]);
+
+ useEffect(() => {
+ const { setWidget } = useWidget.getReadonlyState();
+
+ setWidget({});
+ }, [diffFile, useWidget]);
+
+ return (
+
+ {enableWrap ? : }
+
+ );
+});
+
+DiffSplitView.displayName = "DiffSplitView";
diff --git a/packages/react/src/components/v2/DiffSplitWidgetLineNormal_v2.tsx b/packages/react/src/components/v2/DiffSplitWidgetLineNormal_v2.tsx
new file mode 100644
index 0000000..eb74c81
--- /dev/null
+++ b/packages/react/src/components/v2/DiffSplitWidgetLineNormal_v2.tsx
@@ -0,0 +1,123 @@
+import * as React from "react";
+
+import { useDomWidth } from "../../hooks/useDomWidth";
+import { useSyncHeight } from "../../hooks/useSyncHeight";
+import { emptyBGName } from "../color";
+import { SplitSide } from "../DiffView";
+import { useDiffViewContext } from "../DiffViewContext";
+import { useDiffWidgetContext } from "../DiffWidgetContext";
+
+import type { DiffFile } from "@git-diff-view/core";
+
+const _DiffSplitWidgetLine = ({
+ diffFile,
+ side,
+ lineNumber,
+ currentLine,
+ setWidget,
+ currentWidget,
+}: {
+ index: number;
+ side: SplitSide;
+ diffFile: DiffFile;
+ lineNumber: number;
+ currentLine: ReturnType;
+ currentWidget: boolean;
+ setWidget: (props: { side?: SplitSide; lineNumber?: number }) => void;
+}) => {
+ const otherSide = side === SplitSide.old ? SplitSide.new : SplitSide.old;
+
+ const { useDiffContext } = useDiffViewContext();
+
+ const renderWidgetLine = useDiffContext(React.useCallback((s) => s.renderWidgetLine, []));
+
+ useSyncHeight({
+ selector: `div[data-state="widget"][data-line="${lineNumber}-widget"]`,
+ side: currentWidget ? SplitSide[side] : SplitSide[otherSide],
+ enable: side === SplitSide.new && typeof renderWidgetLine === "function",
+ });
+
+ const width = useDomWidth({
+ selector: side === SplitSide.old ? ".old-diff-table-wrapper" : ".new-diff-table-wrapper",
+ enable: !!currentWidget && typeof renderWidgetLine === "function",
+ });
+
+ if (!renderWidgetLine) return null;
+
+ return (
+
+ {currentWidget ? (
+
+
+ {width > 0 &&
+ renderWidgetLine?.({
+ diffFile,
+ side,
+ lineNumber: currentLine.lineNumber,
+ onClose: () => setWidget({}),
+ })}
+
+
+ ) : (
+
+ )}
+
+ );
+};
+
+export const DiffSplitWidgetLine = ({
+ index,
+ diffFile,
+ side,
+ lineNumber,
+}: {
+ index: number;
+ side: SplitSide;
+ diffFile: DiffFile;
+ lineNumber: number;
+}) => {
+ const { useWidget } = useDiffWidgetContext();
+
+ const { widgetLineNumber, widgetSide, setWidget } = useWidget(
+ React.useCallback(
+ (s) => ({ widgetLineNumber: s.widgetLineNumber, widgetSide: s.widgetSide, setWidget: s.setWidget }),
+ []
+ )
+ );
+
+ const oldLine = diffFile.getSplitLeftLine(index);
+
+ const newLine = diffFile.getSplitRightLine(index);
+
+ const oldLineWidget = oldLine.lineNumber && widgetSide === SplitSide.old && widgetLineNumber === oldLine.lineNumber;
+
+ const newLineWidget = newLine.lineNumber && widgetSide === SplitSide.new && widgetLineNumber === newLine.lineNumber;
+
+ const currentLine = side === SplitSide.old ? oldLine : newLine;
+
+ const currentWidget = side === SplitSide.old ? oldLineWidget : newLineWidget;
+
+ const currentIsShow = oldLineWidget || newLineWidget;
+
+ if (!currentIsShow) return null;
+
+ return (
+ <_DiffSplitWidgetLine
+ index={index}
+ diffFile={diffFile}
+ side={side}
+ lineNumber={lineNumber}
+ currentLine={currentLine}
+ setWidget={setWidget}
+ currentWidget={currentWidget}
+ />
+ );
+};
diff --git a/packages/react/src/components/v2/DiffSplitWidgetLineWrap_v2.tsx b/packages/react/src/components/v2/DiffSplitWidgetLineWrap_v2.tsx
new file mode 100644
index 0000000..f8be149
--- /dev/null
+++ b/packages/react/src/components/v2/DiffSplitWidgetLineWrap_v2.tsx
@@ -0,0 +1,118 @@
+import * as React from "react";
+
+import { emptyBGName } from "../color";
+import { SplitSide } from "../DiffView";
+import { useDiffViewContext } from "../DiffViewContext";
+import { useDiffWidgetContext } from "../DiffWidgetContext";
+
+import type { DiffFile } from "@git-diff-view/core";
+
+const _DiffSplitWidgetLine = ({
+ index,
+ diffFile,
+ lineNumber,
+ oldLineWidget,
+ newLineWidget,
+}: {
+ index: number;
+ diffFile: DiffFile;
+ lineNumber: number;
+ oldLineWidget: boolean;
+ newLineWidget: boolean;
+}) => {
+ const { useWidget } = useDiffWidgetContext();
+
+ const setWidget = useWidget.getReadonlyState().setWidget;
+
+ const { useDiffContext } = useDiffViewContext();
+
+ const renderWidgetLine = useDiffContext(React.useCallback((s) => s.renderWidgetLine, []));
+
+ const oldLine = diffFile.getSplitLeftLine(index);
+
+ const newLine = diffFile.getSplitRightLine(index);
+
+ if (!renderWidgetLine) return null;
+
+ return (
+
+ {oldLineWidget ? (
+
+
+ {renderWidgetLine?.({
+ diffFile,
+ side: SplitSide.old,
+ lineNumber: oldLine.lineNumber,
+ onClose: () => setWidget({}),
+ })}
+
+
+ ) : (
+
+
+
+ )}
+
+ {newLineWidget ? (
+
+
+ {renderWidgetLine?.({
+ diffFile,
+ side: SplitSide.new,
+ lineNumber: newLine.lineNumber,
+ onClose: () => setWidget({}),
+ })}
+
+
+ ) : (
+
+
+
+ )}
+
+ );
+};
+
+export const DiffSplitWidgetLine = ({
+ index,
+ diffFile,
+ lineNumber,
+}: {
+ index: number;
+ diffFile: DiffFile;
+ lineNumber: number;
+}) => {
+ const { useWidget } = useDiffWidgetContext();
+
+ const { widgetLineNumber, widgetSide } = useWidget(
+ React.useCallback((s) => ({ widgetLineNumber: s.widgetLineNumber, widgetSide: s.widgetSide }), [])
+ );
+
+ const oldLine = diffFile.getSplitLeftLine(index);
+
+ const newLine = diffFile.getSplitRightLine(index);
+
+ const oldLineWidget = oldLine.lineNumber && widgetSide === SplitSide.old && widgetLineNumber === oldLine.lineNumber;
+
+ const newLineWidget = newLine.lineNumber && widgetSide === SplitSide.new && widgetLineNumber === newLine.lineNumber;
+
+ const currentIsShow = oldLineWidget || newLineWidget;
+
+ if (!currentIsShow) return null;
+
+ return (
+ <_DiffSplitWidgetLine
+ index={index}
+ diffFile={diffFile}
+ lineNumber={lineNumber}
+ oldLineWidget={oldLineWidget}
+ newLineWidget={newLineWidget}
+ />
+ );
+};