From c92d12da9dbde045f6b30185ce520d3d12a0474a Mon Sep 17 00:00:00 2001
From: MrWangJustToDo <2711470541@qq.com>
Date: Mon, 9 Mar 2026 20:17:28 +0800
Subject: [PATCH 1/5] support codeview component for cli pkg
---
packages/cli/package.json | 2 +-
packages/cli/src/components/CodeContent.tsx | 253 +++++++++++++
.../cli/src/components/CodeExtendLine.tsx | 48 +++
.../cli/src/components/CodeLineNumber.tsx | 48 +++
packages/cli/src/components/CodeView.tsx | 338 ++++++++++++++++++
.../cli/src/components/CodeViewContext.ts | 11 +
packages/cli/src/components/DiffContent.tsx | 52 ++-
.../src/components/DiffSplitContentLine.tsx | 6 +-
.../src/components/DiffUnifiedContentLine.tsx | 4 +-
packages/cli/src/components/DiffView.tsx | 10 +
packages/cli/src/components/codeTools.ts | 97 +++++
packages/cli/src/components/tools.ts | 8 +
packages/cli/src/hooks/useCodeTerminalSize.ts | 47 +++
packages/cli/src/index.ts | 2 +
packages/cli/test/code.mjs | 230 ++++++++++++
packages/cli/test/file.mjs | 9 +-
packages/core/index.d.ts | 2 +
packages/core/src/file.ts | 9 -
packages/file/index.d.ts | 2 +
packages/react/index.d.ts | 2 +
packages/solid/index.d.ts | 2 +
packages/vue/index.d.ts | 2 +
22 files changed, 1157 insertions(+), 27 deletions(-)
create mode 100644 packages/cli/src/components/CodeContent.tsx
create mode 100644 packages/cli/src/components/CodeExtendLine.tsx
create mode 100644 packages/cli/src/components/CodeLineNumber.tsx
create mode 100644 packages/cli/src/components/CodeView.tsx
create mode 100644 packages/cli/src/components/CodeViewContext.ts
create mode 100644 packages/cli/src/components/codeTools.ts
create mode 100644 packages/cli/src/hooks/useCodeTerminalSize.ts
create mode 100644 packages/cli/test/code.mjs
diff --git a/packages/cli/package.json b/packages/cli/package.json
index 5cf5f64..f44fd52 100644
--- a/packages/cli/package.json
+++ b/packages/cli/package.json
@@ -19,7 +19,7 @@
"directory": "packages/cli"
},
"scripts": {
- "dev": "DEV=true node ./test/index.mjs",
+ "dev": "DEV=true node ./test/file.mjs",
"gen:color": "node ./gen.mjs",
"gen:type": "dts-bundle-generator -o index.d.ts dist/types/index.d.ts"
},
diff --git a/packages/cli/src/components/CodeContent.tsx b/packages/cli/src/components/CodeContent.tsx
new file mode 100644
index 0000000..db15778
--- /dev/null
+++ b/packages/cli/src/components/CodeContent.tsx
@@ -0,0 +1,253 @@
+/**
+ * CodeContent component - renders code with optional syntax highlighting.
+ * Simplified version of DiffContent without diff-specific features.
+ */
+import { Box, Text } from "ink";
+import * as React from "react";
+
+import { buildAnsiStringWithLineBreaks, buildStyledBlock, type CharStyle } from "./ansiString";
+import { useCodeViewContext } from "./CodeViewContext";
+import { diffPlainContent } from "./color";
+import { getStyleObjectFromString, getStyleFromClassName } from "./DiffContent";
+
+import type { File } from "@git-diff-view/core";
+
+// Helper to get tab width value
+const getTabWidthValue = (tabWidth: "small" | "medium" | "large"): number => {
+ return tabWidth === "small" ? 1 : tabWidth === "medium" ? 2 : 4;
+};
+
+// Process a string into styled characters for ANSI output
+const processCharsForAnsi = (
+ str: string,
+ enableTabSpace: boolean,
+ tabWidth: "small" | "medium" | "large",
+ baseStyle: CharStyle
+): Array<{ char: string; style?: CharStyle }> => {
+ const result: Array<{ char: string; style?: CharStyle }> = [];
+ const tabWidthValue = getTabWidthValue(tabWidth);
+
+ for (const char of str) {
+ if (enableTabSpace && char === " ") {
+ // Show space as dimmed dot
+ result.push({ char: "\u00b7", style: { ...baseStyle, dim: true } });
+ } else if (char === "\t") {
+ if (enableTabSpace) {
+ // Show tab as arrow followed by spaces
+ result.push({ char: "\u2192", style: { ...baseStyle, dim: true } });
+ for (let i = 1; i < tabWidthValue; i++) {
+ result.push({ char: " ", style: baseStyle });
+ }
+ } else {
+ // Just show spaces for tab
+ for (let i = 0; i < tabWidthValue; i++) {
+ result.push({ char: " ", style: baseStyle });
+ }
+ }
+ } else {
+ result.push({ char, style: baseStyle });
+ }
+ }
+
+ return result;
+};
+
+/**
+ * CodeString component using ANSI escape codes for proper character-level wrapping.
+ */
+const CodeString = React.memo(({ bg, width, rawLine }: { bg: string; width: number; rawLine: string }) => {
+ const { useCodeContext } = useCodeViewContext();
+
+ const { enableTabSpace, tabWidth } = useCodeContext((s) => ({ enableTabSpace: s.tabSpace, tabWidth: s.tabWidth }));
+
+ // Memoize the ANSI content to avoid rebuilding on every render
+ const ansiContent = React.useMemo(() => {
+ const chars: Array<{ char: string; style?: CharStyle }> = [];
+ const baseStyle: CharStyle = { backgroundColor: bg };
+
+ // Process the whole line
+ chars.push(...processCharsForAnsi(rawLine, enableTabSpace, tabWidth, baseStyle));
+
+ return buildAnsiStringWithLineBreaks(chars, width);
+ }, [bg, width, rawLine, enableTabSpace, tabWidth]);
+
+ return (
+
+ {ansiContent}
+
+ );
+});
+
+CodeString.displayName = "CodeString";
+
+/**
+ * Helper function to process syntax-highlighted characters for ANSI output.
+ */
+const processSyntaxCharsForAnsi = (
+ str: string,
+ enableTabSpace: boolean,
+ tabWidth: "small" | "medium" | "large",
+ baseStyle: CharStyle,
+ syntaxColor?: string
+): Array<{ char: string; style?: CharStyle }> => {
+ const result: Array<{ char: string; style?: CharStyle }> = [];
+ const tabWidthValue = getTabWidthValue(tabWidth);
+
+ for (const char of str) {
+ const style: CharStyle = {
+ ...baseStyle,
+ color: syntaxColor || baseStyle.color,
+ };
+
+ if (enableTabSpace && char === " ") {
+ result.push({ char: "\u00b7", style: { ...style, dim: true } });
+ } else if (char === "\t") {
+ if (enableTabSpace) {
+ result.push({ char: "\u2192", style: { ...style, dim: true } });
+ for (let i = 1; i < tabWidthValue; i++) {
+ result.push({ char: " ", style });
+ }
+ } else {
+ for (let i = 0; i < tabWidthValue; i++) {
+ result.push({ char: " ", style });
+ }
+ }
+ } else {
+ result.push({ char, style });
+ }
+ }
+
+ return result;
+};
+
+/**
+ * CodeSyntax component using ANSI escape codes for proper character-level wrapping
+ * with syntax highlighting support.
+ */
+const CodeSyntax = React.memo(
+ ({
+ bg,
+ width,
+ theme,
+ rawLine,
+ syntaxLine,
+ }: {
+ bg: string;
+ width: number;
+ theme: "light" | "dark";
+ rawLine: string;
+ syntaxLine?: File["syntaxFile"][number];
+ }) => {
+ const { useCodeContext } = useCodeViewContext();
+
+ const { enableTabSpace, tabWidth } = useCodeContext((s) => ({ enableTabSpace: s.tabSpace, tabWidth: s.tabWidth }));
+
+ // Memoize the ANSI content with syntax highlighting
+ const ansiContent = React.useMemo(() => {
+ if (!syntaxLine) {
+ return null; // Will render CodeString instead
+ }
+
+ const chars: Array<{ char: string; style?: CharStyle }> = [];
+ const baseStyle: CharStyle = { backgroundColor: bg };
+
+ for (const { node, wrapper } of syntaxLine.nodeList || []) {
+ // Get syntax color from lowlight or shiki
+ const lowlightStyles = getStyleFromClassName(wrapper?.properties?.className?.join(" ") || "");
+ const lowlightStyle = theme === "dark" ? lowlightStyles.dark : lowlightStyles.light;
+ const shikiStyles = getStyleObjectFromString(wrapper?.properties?.style || "");
+ const shikiStyle = theme === "dark" ? shikiStyles.dark : shikiStyles.light;
+
+ // Determine the syntax color (shiki style takes precedence)
+ const syntaxColor = (shikiStyle as { color?: string })?.color || (lowlightStyle as { color?: string })?.color;
+
+ chars.push(
+ ...processSyntaxCharsForAnsi(node.value, enableTabSpace, tabWidth, { ...baseStyle, color: syntaxColor })
+ );
+ }
+
+ return buildAnsiStringWithLineBreaks(chars, width);
+ }, [bg, width, theme, rawLine, syntaxLine, enableTabSpace, tabWidth]);
+
+ // Fallback to CodeString if no syntax line
+ if (!syntaxLine) {
+ return ;
+ }
+
+ return (
+
+ {ansiContent}
+
+ );
+ }
+);
+
+CodeSyntax.displayName = "CodeSyntax";
+
+/**
+ * CodePadding component - Renders a 1-char padding column
+ * using chalk for proper multi-row support.
+ */
+const CodePadding = React.memo(({ height, backgroundColor }: { height: number; backgroundColor: string }) => {
+ const content = React.useMemo(() => {
+ const lines: string[] = [];
+ const style: CharStyle = { backgroundColor };
+
+ for (let row = 0; row < height; row++) {
+ lines.push(buildStyledBlock(" ", 1, 1, style, "left"));
+ }
+
+ return lines.join("\n");
+ }, [height, backgroundColor]);
+
+ return (
+
+ {content}
+
+ );
+});
+
+CodePadding.displayName = "CodePadding";
+
+export const CodeContent = React.memo(
+ ({
+ theme,
+ width,
+ height,
+ rawLine,
+ syntaxLine,
+ enableHighlight,
+ }: {
+ width: number;
+ height: number;
+ theme: "light" | "dark";
+ rawLine: string;
+ plainLine?: File["plainFile"][number];
+ syntaxLine?: File["syntaxFile"][number];
+ enableHighlight: boolean;
+ }) => {
+ const isMaxLineLengthToIgnoreSyntax = syntaxLine?.nodeList?.length > 150;
+
+ // Background color for normal code
+ const bg = React.useMemo(() => {
+ return theme === "light" ? diffPlainContent.light : diffPlainContent.dark;
+ }, [theme]);
+
+ // Content width is total width minus 2 char padding (1 on each side)
+ const contentWidth = width - 2;
+
+ return (
+
+
+ {enableHighlight && syntaxLine && !isMaxLineLengthToIgnoreSyntax ? (
+
+ ) : (
+
+ )}
+
+
+ );
+ }
+);
+
+CodeContent.displayName = "CodeContent";
diff --git a/packages/cli/src/components/CodeExtendLine.tsx b/packages/cli/src/components/CodeExtendLine.tsx
new file mode 100644
index 0000000..9eeb3d4
--- /dev/null
+++ b/packages/cli/src/components/CodeExtendLine.tsx
@@ -0,0 +1,48 @@
+import { Box, Text } from "ink";
+import * as React from "react";
+
+import { useCodeViewContext } from "./CodeViewContext";
+
+import type { File } from "@git-diff-view/core";
+
+const InternalCodeExtendLine = ({
+ columns,
+ lineNumber,
+ lineExtend,
+ file,
+}: {
+ columns: number;
+ lineNumber: number;
+ lineExtend: { data: any };
+ file: File;
+}) => {
+ const { useCodeContext } = useCodeViewContext();
+
+ const renderExtendLine = useCodeContext((s) => s.renderExtendLine);
+
+ if (!renderExtendLine) return null;
+
+ const extendRendered =
+ lineExtend?.data &&
+ renderExtendLine?.({
+ file,
+ lineNumber,
+ data: lineExtend.data,
+ });
+
+ return (
+
+ {React.isValidElement(extendRendered) ? extendRendered : {extendRendered}}
+
+ );
+};
+
+export const CodeExtendLine = ({ columns, lineNumber, file }: { columns: number; lineNumber: number; file: File }) => {
+ const { useCodeContext } = useCodeViewContext();
+
+ const lineExtend = useCodeContext(React.useCallback((s) => s.extendData?.[lineNumber], [lineNumber]));
+
+ if (!lineExtend?.data) return null;
+
+ return ;
+};
diff --git a/packages/cli/src/components/CodeLineNumber.tsx b/packages/cli/src/components/CodeLineNumber.tsx
new file mode 100644
index 0000000..30fd0ea
--- /dev/null
+++ b/packages/cli/src/components/CodeLineNumber.tsx
@@ -0,0 +1,48 @@
+/**
+ * CodeLineNumber component - Renders line numbers with proper multi-row support
+ * using chalk for ANSI styling.
+ *
+ * Simplified version of DiffLineNumber for code view.
+ */
+import { Box, Text } from "ink";
+import * as React from "react";
+
+import { buildStyledBlock, type CharStyle } from "./ansiString";
+
+/**
+ * Renders a single line number area for code view.
+ * Format: [ ][lineNum][ ]
+ */
+export const CodeLineNumberArea: React.FC<{
+ lineNumber: number;
+ lineNumWidth: number;
+ height: number;
+ backgroundColor: string;
+ color: string;
+ dim?: boolean;
+}> = React.memo(({ lineNumber, lineNumWidth, height, backgroundColor, color, dim = false }) => {
+ // Total width: leftPad + num + rightPad = 1 + lineNumWidth + 1
+ const totalWidth = lineNumWidth + 2;
+
+ const content = React.useMemo(() => {
+ const style: CharStyle = { backgroundColor, color, dim };
+ const lines: string[] = [];
+
+ for (let row = 0; row < height; row++) {
+ // Left padding + line number (right-aligned) + right padding
+ const numPart = row === 0 ? lineNumber.toString().padStart(lineNumWidth) : " ".repeat(lineNumWidth);
+ const lineText = ` ${numPart} `;
+ lines.push(buildStyledBlock(lineText, totalWidth, 1, style, "left"));
+ }
+
+ return lines.join("\n");
+ }, [lineNumber, lineNumWidth, height, backgroundColor, color, dim, totalWidth]);
+
+ return (
+
+ {content}
+
+ );
+});
+
+CodeLineNumberArea.displayName = "CodeLineNumberArea";
diff --git a/packages/cli/src/components/CodeView.tsx b/packages/cli/src/components/CodeView.tsx
new file mode 100644
index 0000000..a4194f9
--- /dev/null
+++ b/packages/cli/src/components/CodeView.tsx
@@ -0,0 +1,338 @@
+/**
+ * CodeView component - renders source code with syntax highlighting and line numbers.
+ * Simplified version of DiffView without diff-related features.
+ */
+import { _cacheMap, getFile } from "@git-diff-view/core";
+import { Box } from "ink";
+import React, { Fragment, forwardRef, memo, useEffect, useImperativeHandle, useMemo, useRef } from "react";
+
+import { useCodeTerminalSize } from "../hooks/useCodeTerminalSize";
+import { useIsMounted } from "../hooks/useIsMounted";
+
+import { CodeContent } from "./CodeContent";
+import { CodeExtendLine } from "./CodeExtendLine";
+import { CodeLineNumberArea } from "./CodeLineNumber";
+import { createCodeConfigStore, getCurrentLineRow } from "./codeTools";
+import { CodeViewContext, useCodeViewContext } from "./CodeViewContext";
+import { diffPlainLineNumber, diffPlainLineNumberColor } from "./color";
+
+import type { DiffHighlighter, DiffHighlighterLang, File } from "@git-diff-view/core";
+import type { DOMElement } from "ink";
+import type { ForwardedRef, ReactNode, RefObject } from "react";
+
+_cacheMap.name = "@git-diff-view/cli";
+
+export type CodeViewProps = {
+ data?: {
+ content: string;
+ fileName?: string | null;
+ fileLang?: DiffHighlighterLang | string | null;
+ };
+ extendData?: Record;
+ width?: number;
+ codeViewTheme?: "light" | "dark";
+ codeViewTabSpace?: boolean;
+ // tabWidth in the code view, small: 1, medium: 2, large: 4, default: medium
+ codeViewTabWidth?: "small" | "medium" | "large";
+ registerHighlighter?: Omit;
+ codeViewHighlight?: boolean;
+ renderExtendLine?: ({ file, data, lineNumber }: { file: File; lineNumber: number; data: T }) => ReactNode;
+};
+
+type CodeViewProps_1 = Omit, "data"> & {
+ data?: {
+ content: string;
+ fileName?: string | null;
+ fileLang?: DiffHighlighterLang | null;
+ };
+};
+
+type CodeViewProps_2 = Omit, "data"> & {
+ data?: {
+ content: string;
+ fileName?: string | null;
+ fileLang?: string | null;
+ };
+};
+
+/**
+ * Single line component for CodeView
+ */
+const CodeLine = memo(
+ ({
+ lineNumber,
+ theme,
+ columns,
+ file,
+ lineNumWidth,
+ enableHighlight,
+ }: {
+ lineNumber: number;
+ theme: "light" | "dark";
+ columns: number;
+ file: File;
+ lineNumWidth: number;
+ enableHighlight: boolean;
+ }) => {
+ const rawLine = file.rawFile[lineNumber] || "";
+ const syntaxLine = file.syntaxFile[lineNumber];
+ const plainLine = file.plainFile[lineNumber];
+
+ const contentWidth = columns - lineNumWidth - 2;
+
+ // Calculate row height based on actual text width (content width minus 2 char padding on both sides)
+ const row = getCurrentLineRow({ content: rawLine, width: contentWidth - 2 });
+
+ const bg = theme === "light" ? diffPlainLineNumber.light : diffPlainLineNumber.dark;
+ const color = theme === "light" ? diffPlainLineNumberColor.light : diffPlainLineNumberColor.dark;
+
+ return (
+
+
+
+
+ );
+ }
+);
+
+CodeLine.displayName = "CodeLine";
+
+/**
+ * CodeViewContent - renders all code lines using terminal size from context
+ */
+const CodeViewContent = memo(({ file, theme }: { file: File; theme: "light" | "dark" }) => {
+ const { useCodeContext } = useCodeViewContext();
+
+ const enableHighlight = useCodeContext((s) => s.enableHighlight);
+
+ const { columns } = useCodeTerminalSize();
+
+ // Calculate line number width based on max line number
+ const lineNumWidth = useMemo(() => {
+ const maxLineNumber = file.maxLineNumber || 1;
+ return Math.max(String(maxLineNumber).length, 2);
+ }, [file.maxLineNumber]);
+
+ // Generate line numbers
+ const lines = useMemo(() => {
+ const totalLines = file.rawLength || 0;
+ return Array.from({ length: totalLines }, (_, i) => i + 1);
+ }, [file.rawLength]);
+
+ if (!columns) return null;
+
+ return (
+ <>
+ {lines.map((lineNumber) => (
+
+
+
+
+ ))}
+ >
+ );
+});
+
+CodeViewContent.displayName = "CodeViewContent";
+
+/**
+ * Internal CodeView component that sets up context and renders content
+ */
+const InternalCodeView = (
+ props: Omit, "data"> & {
+ file: File;
+ isMounted: boolean;
+ wrapperRef?: RefObject;
+ }
+) => {
+ const {
+ file,
+ codeViewHighlight,
+ isMounted,
+ wrapperRef,
+ extendData,
+ renderExtendLine,
+ codeViewTabSpace,
+ codeViewTabWidth,
+ codeViewTheme,
+ } = props;
+
+ const fileId = useMemo(() => file.fileName || "code-file", [file.fileName]);
+
+ // Performance optimization using store
+ const useCodeContext = useMemo(() => createCodeConfigStore(props, fileId), []);
+
+ useEffect(() => {
+ const {
+ id,
+ setId,
+ mounted,
+ setMounted,
+ enableHighlight,
+ setEnableHighlight,
+ setExtendData,
+ renderExtendLine,
+ setRenderExtendLine,
+ tabSpace,
+ setTabSpace,
+ tabWidth,
+ setTabWidth,
+ } = useCodeContext.getReadonlyState();
+
+ if (fileId && fileId !== id) {
+ setId(fileId);
+ }
+
+ if (mounted !== isMounted) {
+ setMounted(isMounted);
+ }
+
+ if (codeViewHighlight !== enableHighlight) {
+ setEnableHighlight(codeViewHighlight);
+ }
+
+ if (props.extendData) {
+ setExtendData(props.extendData);
+ }
+
+ if (renderExtendLine !== props.renderExtendLine) {
+ setRenderExtendLine(props.renderExtendLine);
+ }
+
+ if (codeViewTabSpace !== tabSpace) {
+ setTabSpace(codeViewTabSpace);
+ }
+
+ if (codeViewTabWidth !== tabWidth) {
+ setTabWidth(codeViewTabWidth);
+ }
+ }, [
+ useCodeContext,
+ codeViewHighlight,
+ fileId,
+ isMounted,
+ extendData,
+ renderExtendLine,
+ codeViewTabSpace,
+ codeViewTabWidth,
+ ]);
+
+ useEffect(() => {
+ const { wrapper, setWrapper } = useCodeContext.getReadonlyState();
+ if (wrapperRef.current !== wrapper.current) {
+ setWrapper(wrapperRef.current);
+ }
+ });
+
+ const value = useMemo(() => ({ useCodeContext }), [useCodeContext]);
+
+ const theme = codeViewTheme || "light";
+
+ return (
+
+
+
+
+
+ );
+};
+
+const MemoedInternalCodeView = memo(InternalCodeView);
+
+const CodeViewContainerWithRef = (props: CodeViewProps, ref: ForwardedRef<{ getFileInstance: () => File }>) => {
+ const { registerHighlighter, data, codeViewTheme, width, ...restProps } = props;
+
+ const domRef = useRef(null);
+
+ const theme = codeViewTheme || "light";
+
+ const file = useMemo(() => {
+ if (data) {
+ return getFile(data.content || "", data.fileLang || "", theme, data.fileName || "");
+ }
+ return null;
+ }, [data, theme]);
+
+ const fileRef = useRef(file);
+
+ if (fileRef.current && fileRef.current !== file) {
+ fileRef.current = file;
+ }
+
+ const isMounted = useIsMounted();
+
+ useEffect(() => {
+ if (!file) return;
+ file.doRaw();
+ }, [file]);
+
+ useEffect(() => {
+ if (!file) return;
+ if (props.codeViewHighlight) {
+ file.doSyntax({ registerHighlighter: registerHighlighter, theme: codeViewTheme });
+ }
+ }, [file, props.codeViewHighlight, codeViewTheme, registerHighlighter]);
+
+ useImperativeHandle(ref, () => ({ getFileInstance: () => file }), [file]);
+
+ if (!file) return null;
+
+ return (
+
+
+
+ );
+};
+
+// type helper function
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+function ReactCodeView(
+ props: CodeViewProps_1 & { ref?: ForwardedRef<{ getFileInstance: () => File }> }
+): JSX.Element;
+function ReactCodeView(
+ props: CodeViewProps_2 & { ref?: ForwardedRef<{ getFileInstance: () => File }> }
+): JSX.Element;
+function ReactCodeView(_props: CodeViewProps & { ref?: ForwardedRef<{ getFileInstance: () => File }> }) {
+ return <>>;
+}
+
+const InnerCodeView = forwardRef(CodeViewContainerWithRef);
+
+InnerCodeView.displayName = "CodeView";
+
+export const CodeView = InnerCodeView as typeof ReactCodeView;
diff --git a/packages/cli/src/components/CodeViewContext.ts b/packages/cli/src/components/CodeViewContext.ts
new file mode 100644
index 0000000..a42c880
--- /dev/null
+++ b/packages/cli/src/components/CodeViewContext.ts
@@ -0,0 +1,11 @@
+import { createContext, useContext } from "react";
+
+import type { createCodeConfigStore } from "./codeTools";
+
+export const CodeViewContext = createContext<{
+ useCodeContext: ReturnType;
+}>(null);
+
+CodeViewContext.displayName = "CodeViewContext";
+
+export const useCodeViewContext = () => useContext(CodeViewContext);
diff --git a/packages/cli/src/components/DiffContent.tsx b/packages/cli/src/components/DiffContent.tsx
index dd930bd..6468102 100644
--- a/packages/cli/src/components/DiffContent.tsx
+++ b/packages/cli/src/components/DiffContent.tsx
@@ -124,7 +124,7 @@ const DiffString = React.memo(
const { useDiffContext } = useDiffViewContext();
- const { enableTabSpace, tabWidth } = useDiffContext((s) => ({ enableTabSpace: s.tabSpace, tabWidth: s.tabWidth }));
+ const { enableTabSpace, tabWidth } = useDiffContext.useShallowStableSelector((s) => ({ enableTabSpace: s.tabSpace, tabWidth: s.tabWidth }));
// Memoize the ANSI content to avoid rebuilding on every render
const ansiContent = React.useMemo(() => {
@@ -167,12 +167,12 @@ const DiffString = React.memo(
chars.push(...processCharsForAnsi(rawLine, enableTabSpace, tabWidth, baseStyle));
}
- // Use width - 1 because the operator column takes 1 character
- return buildAnsiStringWithLineBreaks(chars, width - 1);
+ // Use width - 2 because the operator column takes 1 character and end padding takes 1 character
+ return buildAnsiStringWithLineBreaks(chars, width - 2);
}, [bg, width, theme, rawLine, changes, operator, enableTabSpace, tabWidth]);
return (
-
+
{ansiContent}
);
@@ -250,7 +250,7 @@ const DiffSyntax = React.memo(
}) => {
const { useDiffContext } = useDiffViewContext();
- const { enableTabSpace, tabWidth } = useDiffContext((s) => ({ enableTabSpace: s.tabSpace, tabWidth: s.tabWidth }));
+ const { enableTabSpace, tabWidth } = useDiffContext.useShallowStableSelector((s) => ({ enableTabSpace: s.tabSpace, tabWidth: s.tabWidth }));
// Memoize the ANSI content with syntax highlighting
const ansiContent = React.useMemo(() => {
@@ -340,8 +340,8 @@ const DiffSyntax = React.memo(
}
}
- // Use width - 1 because the operator column takes 1 character
- return buildAnsiStringWithLineBreaks(chars, width - 1);
+ // Use width - 2 because the operator column takes 1 character and end padding takes 1 character
+ return buildAnsiStringWithLineBreaks(chars, width - 2);
}, [bg, width, theme, rawLine, diffLine, operator, syntaxLine, enableTabSpace, tabWidth]);
// Fallback to DiffString if no syntax line
@@ -360,7 +360,7 @@ const DiffSyntax = React.memo(
}
return (
-
+
{ansiContent}
);
@@ -398,6 +398,31 @@ const DiffOperator = React.memo(
DiffOperator.displayName = "DiffOperator";
+/**
+ * DiffPadding component - Renders a 1-char padding column
+ * using chalk for proper multi-row support.
+ */
+const DiffPadding = React.memo(({ height, backgroundColor }: { height: number; backgroundColor: string }) => {
+ const content = React.useMemo(() => {
+ const lines: string[] = [];
+ const style: CharStyle = { backgroundColor };
+
+ for (let row = 0; row < height; row++) {
+ lines.push(buildStyledBlock(" ", 1, 1, style, "left"));
+ }
+
+ return lines.join("\n");
+ }, [height, backgroundColor]);
+
+ return (
+
+ {content}
+
+ );
+});
+
+DiffPadding.displayName = "DiffPadding";
+
export const DiffContent = React.memo(
({
theme,
@@ -419,6 +444,10 @@ export const DiffContent = React.memo(
diffFile: DiffFile;
enableHighlight: boolean;
}) => {
+ const { useDiffContext } = useDiffViewContext();
+
+ const hideOperator = useDiffContext.useShallowStableSelector((s) => s.hideOperator);
+
const isAdded = diffLine?.type === DiffLineType.Add;
const isDelete = diffLine?.type === DiffLineType.Delete;
const isMaxLineLengthToIgnoreSyntax = syntaxLine?.nodeList?.length > 150;
@@ -443,7 +472,11 @@ export const DiffContent = React.memo(
return (
-
+ {hideOperator ? (
+
+ ) : (
+
+ )}
{enableHighlight && syntaxLine && !isMaxLineLengthToIgnoreSyntax ? (
)}
+
);
}
diff --git a/packages/cli/src/components/DiffSplitContentLine.tsx b/packages/cli/src/components/DiffSplitContentLine.tsx
index 019d864..862d971 100644
--- a/packages/cli/src/components/DiffSplitContentLine.tsx
+++ b/packages/cli/src/components/DiffSplitContentLine.tsx
@@ -55,9 +55,9 @@ const InternalDiffSplitLine = ({
const contentWidth = columns / 2 - lineNumWidth - 2;
// Calculate row heights - must match the actual wrap width used in DiffContent
- // DiffContent receives contentWidth and wraps at (contentWidth - 1) for the operator column
- let oldRow = getCurrentLineRow({ content: oldLine?.value || "", width: contentWidth - 1 });
- let newRow = getCurrentLineRow({ content: newLine?.value || "", width: contentWidth - 1 });
+ // DiffContent receives contentWidth and wraps at (contentWidth - 2) for the operator column and end padding
+ let oldRow = getCurrentLineRow({ content: oldLine?.value || "", width: contentWidth - 2 });
+ let newRow = getCurrentLineRow({ content: newLine?.value || "", width: contentWidth - 2 });
oldRow =
oldLine?.diff?.changes?.hasLineChange && oldLine?.diff?.changes.newLineSymbol === NewLineSymbol.NEWLINE
diff --git a/packages/cli/src/components/DiffUnifiedContentLine.tsx b/packages/cli/src/components/DiffUnifiedContentLine.tsx
index b701231..893ef6b 100644
--- a/packages/cli/src/components/DiffUnifiedContentLine.tsx
+++ b/packages/cli/src/components/DiffUnifiedContentLine.tsx
@@ -181,8 +181,8 @@ const InternalDiffUnifiedLine = ({
const contentWidth = columns - (lineNumWidth + 1) * 2 - 1;
- // Use contentWidth - 1 to match the actual wrap width in DiffContent (operator column takes 1 char)
- let row = getCurrentLineRow({ content: rawLine, width: contentWidth - 1 });
+ // Use contentWidth - 2 to match the actual wrap width in DiffContent (operator column takes 1 char, end padding takes 1 char)
+ let row = getCurrentLineRow({ content: rawLine, width: contentWidth - 2 });
row = diffLine?.changes?.hasLineChange && diffLine.changes.newLineSymbol === NewLineSymbol.NEWLINE ? row + 1 : row;
diff --git a/packages/cli/src/components/DiffView.tsx b/packages/cli/src/components/DiffView.tsx
index 77f6c90..9138b31 100644
--- a/packages/cli/src/components/DiffView.tsx
+++ b/packages/cli/src/components/DiffView.tsx
@@ -36,6 +36,8 @@ export type DiffViewProps = {
diffViewTabWidth?: "small" | "medium" | "large";
registerHighlighter?: Omit;
diffViewHighlight?: boolean;
+ // hide the diff operator (+/-/space) and show padding instead
+ diffViewHideOperator?: boolean;
renderExtendLine?: ({
diffFile,
side,
@@ -83,6 +85,7 @@ const InternalDiffView = (
renderExtendLine,
diffViewTabSpace,
diffViewTabWidth,
+ diffViewHideOperator,
} = props;
const diffFileId = useMemo(() => diffFile.getId(), [diffFile]);
@@ -107,6 +110,8 @@ const InternalDiffView = (
setTabSpace,
tabWidth,
setTabWidth,
+ hideOperator,
+ setHideOperator,
} = useDiffContext.getReadonlyState();
if (diffFileId && diffFileId !== id) {
@@ -140,6 +145,10 @@ const InternalDiffView = (
if (diffViewTabWidth !== tabWidth) {
setTabWidth(diffViewTabWidth);
}
+
+ if (diffViewHideOperator !== hideOperator) {
+ setHideOperator(diffViewHideOperator);
+ }
}, [
useDiffContext,
diffViewHighlight,
@@ -150,6 +159,7 @@ const InternalDiffView = (
renderExtendLine,
diffViewTabSpace,
diffViewTabWidth,
+ diffViewHideOperator,
]);
useEffect(() => {
diff --git a/packages/cli/src/components/codeTools.ts b/packages/cli/src/components/codeTools.ts
new file mode 100644
index 0000000..3df408a
--- /dev/null
+++ b/packages/cli/src/components/codeTools.ts
@@ -0,0 +1,97 @@
+import { createStore, markRaw, ref } from "reactivity-store";
+import stringWidth from "string-width";
+
+import type { CodeViewProps } from "./CodeView";
+import type { DOMElement } from "ink";
+import type { Ref, UseSelectorWithStore } from "reactivity-store";
+
+export const createCodeConfigStore = (props: CodeViewProps & { isMounted: boolean }, fileId: string) => {
+ return createStore(() => {
+ const id = ref(fileId);
+
+ const setId = (_id: string) => (id.value = _id);
+
+ const tabSpace = ref(props.codeViewTabSpace);
+
+ const setTabSpace = (_tabSpace: boolean) => (tabSpace.value = _tabSpace);
+
+ const tabWidth = ref(props.codeViewTabWidth || "medium");
+
+ const setTabWidth = (_tabWidth: "small" | "medium" | "large") => (tabWidth.value = _tabWidth);
+
+ const wrapper = ref<{ current: DOMElement }>(markRaw({ current: null }));
+
+ const setWrapper = (_wrapper?: DOMElement) => (wrapper.value = markRaw({ current: _wrapper }));
+
+ const mounted = ref(props.isMounted);
+
+ const setMounted = (_mounted: boolean) => (mounted.value = _mounted);
+
+ const enableHighlight = ref(props.codeViewHighlight);
+
+ const setEnableHighlight = (_enableHighlight: boolean) => (enableHighlight.value = _enableHighlight);
+
+ const extendData = ref({
+ ...props.extendData,
+ });
+
+ const setExtendData = (_extendData: CodeViewProps["extendData"]) => {
+ const existKeys = Object.keys(extendData.value || {});
+ const inComingKeys = Object.keys(_extendData || {});
+ for (const key of existKeys) {
+ if (!inComingKeys.includes(key)) {
+ delete extendData.value[key];
+ }
+ }
+ for (const key of inComingKeys) {
+ extendData.value[key] = _extendData[key];
+ }
+ };
+
+ const renderExtendLine = ref(props.renderExtendLine);
+
+ const setRenderExtendLine = (_renderExtendLine: typeof renderExtendLine.value) =>
+ (renderExtendLine.value = _renderExtendLine);
+
+ return {
+ id,
+ setId,
+ wrapper,
+ setWrapper,
+ mounted,
+ setMounted,
+ tabSpace,
+ setTabSpace,
+ tabWidth,
+ setTabWidth,
+ enableHighlight,
+ setEnableHighlight,
+ extendData,
+ setExtendData,
+ renderExtendLine,
+ setRenderExtendLine,
+ };
+ // fix rollup type error
+ }) as UseSelectorWithStore<{
+ id: Ref;
+ setId: (id: string) => void;
+ wrapper: Ref<{ current: DOMElement }>;
+ setWrapper: (wrapper?: DOMElement) => void;
+ mounted: Ref;
+ setMounted: (mounted: boolean) => void;
+ tabSpace: Ref;
+ setTabSpace: (tabSpace: boolean) => void;
+ tabWidth: Ref<"small" | "medium" | "large">;
+ setTabWidth: (tabWidth: "small" | "medium" | "large") => void;
+ enableHighlight: Ref;
+ setEnableHighlight: (enableHighlight: boolean) => void;
+ extendData: Ref>;
+ setExtendData: (extendData: CodeViewProps["extendData"]) => void;
+ renderExtendLine: Ref;
+ setRenderExtendLine: (renderExtendLine: typeof props.renderExtendLine) => void;
+ }>;
+};
+
+export const getCurrentLineRow = ({ content, width }: { content: string; width: number }) => {
+ return Math.ceil(stringWidth(content) / width);
+};
diff --git a/packages/cli/src/components/tools.ts b/packages/cli/src/components/tools.ts
index aa10109..a097994 100644
--- a/packages/cli/src/components/tools.ts
+++ b/packages/cli/src/components/tools.ts
@@ -72,6 +72,10 @@ export const createDiffConfigStore = (
const setRenderExtendLine = (_renderExtendLine: typeof renderExtendLine.value) =>
(renderExtendLine.value = _renderExtendLine);
+ const hideOperator = ref(props.diffViewHideOperator);
+
+ const setHideOperator = (_hideOperator: boolean) => (hideOperator.value = _hideOperator);
+
return {
id,
setId,
@@ -91,6 +95,8 @@ export const createDiffConfigStore = (
setExtendData,
renderExtendLine,
setRenderExtendLine,
+ hideOperator,
+ setHideOperator,
};
// fix rollup type error
}) as UseSelectorWithStore<{
@@ -115,6 +121,8 @@ export const createDiffConfigStore = (
setExtendData: (extendData: DiffViewProps["extendData"]) => void;
renderExtendLine: Ref;
setRenderExtendLine: (renderExtendLine: typeof props.renderExtendLine) => void;
+ hideOperator: Ref;
+ setHideOperator: (hideOperator: boolean) => void;
}>;
};
diff --git a/packages/cli/src/hooks/useCodeTerminalSize.ts b/packages/cli/src/hooks/useCodeTerminalSize.ts
new file mode 100644
index 0000000..a8def34
--- /dev/null
+++ b/packages/cli/src/hooks/useCodeTerminalSize.ts
@@ -0,0 +1,47 @@
+import { measureElement } from "ink";
+import { useLayoutEffect, useState } from "react";
+
+import { useCodeViewContext } from "../components/CodeViewContext";
+
+import type { DOMElement } from "ink";
+
+const TERMINAL_PADDING_X = 4;
+
+export function useCodeTerminalSize(): { columns: number; rows: number } {
+ const { useCodeContext } = useCodeViewContext();
+
+ const wrapper = useCodeContext((s) => s.wrapper);
+
+ const [size, setSize] = useState({
+ columns: 0,
+ rows: process.stdout.rows || 20,
+ });
+
+ useLayoutEffect(() => {
+ function updateSize() {
+ const terminalWidth = (process.stdout.columns || 60) - TERMINAL_PADDING_X;
+
+ let width = terminalWidth;
+
+ if (wrapper.current) {
+ width = measureElement(wrapper.current as DOMElement).width;
+ }
+
+ width = Math.min(width, terminalWidth);
+
+ setSize({
+ columns: width,
+ rows: process.stdout.rows || 20,
+ });
+ }
+
+ updateSize();
+
+ process.stdout.on("resize", updateSize);
+ return () => {
+ process.stdout.off("resize", updateSize);
+ };
+ }, [wrapper]);
+
+ return size;
+}
diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts
index 0afa4fd..c26799a 100644
--- a/packages/cli/src/index.ts
+++ b/packages/cli/src/index.ts
@@ -2,4 +2,6 @@
export * from "./components/DiffView";
+export * from "./components/CodeView";
+
export * from "@git-diff-view/core";
diff --git a/packages/cli/test/code.mjs b/packages/cli/test/code.mjs
new file mode 100644
index 0000000..e2780f6
--- /dev/null
+++ b/packages/cli/test/code.mjs
@@ -0,0 +1,230 @@
+import { getDiffViewHighlighter } from "@git-diff-view/shiki";
+// import { Box, render, Text } from "ink";
+// import { createElement } from "react";
+import { createElement } from '@my-react/react';
+import { Box, render, Text } from '@my-react/react-terminal'
+
+import { CodeView } from "@git-diff-view/cli";
+
+export const temp1 = `import { processAST, DiffHighlighter } from '@git-diff-view/core';
+import githubLight from 'shiki/themes/github-light.mjs';
+import githubDark from 'shiki/themes/github-dark.mjs';
+import getWasm from 'shiki/wasm';
+// import a from 'shiki/langs/angular-ts.mjs';
+// import b from 'shiki/langs/astro.mjs';
+import c from 'shiki/langs/bat.mjs';
+import d from 'shiki/langs/c.mjs';
+import e from 'shiki/langs/cmake.mjs';
+import f from 'shiki/langs/cpp.mjs';
+import g from 'shiki/langs/vue.mjs';
+import h from 'shiki/langs/css.mjs';
+// import i from 'shiki/langs/csv.mjs';
+// import j from 'shiki/langs/dart.mjs';
+ /*
+ import k from 'shiki/langs/diff.mjs';
+ import l from 'shiki/langs/docker.mjs';
+ */
+import m from 'shiki/langs/go.mjs';
+import n from 'shiki/langs/python.mjs';
+import o from 'shiki/langs/java.mjs';
+import p from 'shiki/langs/javascript.mjs';
+import q from 'shiki/langs/typescript.mjs';
+import r from 'shiki/langs/html.mjs';
+import s from 'shiki/langs/xml.mjs';
+// import t from 'shiki/langs/yaml.mjs';
+import u from 'shiki/langs/json.mjs';
+import v from 'shiki/langs/jsx.mjs';
+import w from 'shiki/langs/tsx.mjs';
+import x from 'shiki/langs/less.mjs';
+import y from 'shiki/langs/sass.mjs';
+import z from 'shiki/langs/scss.mjs';
+import z1 from 'shiki/langs/sql.mjs';
+// import z2 from '@shikijs/langs/swift';
+// import z3 from '@shikijs/langs/svelte';
+// import z4 from '@shikijs/langs/postcss';
+// import z5 from '@shikijs/langs/kotlin';
+// import z6 from '@shikijs/langs/make';
+import z7 from 'shiki/langs/markdown.mjs';
+// import z8 from 'shiki/langs/mdx.mjs';
+// import z9 from 'shiki/langs/php.mjs';
+// import za from 'shiki/langs/ruby.mjs';
+// import zb from 'shiki/langs/rust.mjs';
+// import zc from 'shiki/langs/nginx.mjs';
+// import zd from 'shiki/langs/objective-c.mjs';
+// import ze from 'shiki/langs/objective-cpp.mjs';
+import { createHighlighterCore, createJavaScriptRegexEngine } from 'shiki';
+import type { codeToHast } from 'shiki';
+
+// 更高质量的语法高亮引擎
+// SEE https://shiki.style/
+// SEE https://github.com/MrWangJustToDo/git-diff-view/tree/main/packages/shiki
+
+// 为了兼容现有的微应用架构,将所有需要的资源统一打包而不是按需加载
+const shikiHighlighter = createHighlighterCore({
+ themes: [githubLight, githubDark],
+ langs: [
+ // a,
+ // b,
+ c,
+ d,
+ e,
+ f,
+ g,
+ h,
+ // i,
+ // j,
+ // k,
+ // l,
+ m,
+ n,
+ o,
+ p,
+ q,
+ r,
+ s,
+ // t,
+ u,
+ v,
+ w,
+ x,
+ y,
+ z,
+ z1,
+ // z2,
+ // z3,
+ // z4,
+ // z5,
+ // z6,
+ z7,
+ // z8,
+ // z9,
+ // za,
+ // zb,
+ // zc,
+ // zd,
+ // ze,
+ ],
+ loadWasm: getWasm,
+});
+
+type DePromise = T extends Promise ? DePromise : T;
+
+export type DiffAST = DePromise>;
+
+let internal: DePromise> | null = null;
+
+const instance = { name: 'shiki' };
+
+let _maxLineToIgnoreSyntax = 2000;
+
+const _ignoreSyntaxHighlightList: (string | RegExp)[] = [];
+
+Object.defineProperty(instance, 'maxLineToIgnoreSyntax', {
+ get: () => _maxLineToIgnoreSyntax,
+});
+
+Object.defineProperty(instance, 'setMaxLineToIgnoreSyntax', {
+ value: (v: number) => {
+ _maxLineToIgnoreSyntax = v;
+ },
+});
+
+Object.defineProperty(instance, 'ignoreSyntaxHighlightList', {
+ get: () => _ignoreSyntaxHighlightList,
+});
+
+Object.defineProperty(instance, 'setIgnoreSyntaxHighlightList', {
+ value: (v: (string | RegExp)[]) => {
+ _ignoreSyntaxHighlightList.length = 0;
+ _ignoreSyntaxHighlightList.push(...v);
+ },
+});
+
+Object.defineProperty(instance, 'getAST', {
+ value: (raw: string, fileName?: string, lang?: string) => {
+ if (
+ fileName &&
+ highlighter.ignoreSyntaxHighlightList.some((item) => (item instanceof RegExp ? item.test(fileName) : fileName === item))
+ ) {
+ return;
+ }
+
+ try {
+ // @ts-ignore
+ return internal?.codeToHast(raw, {
+ lang: lang!,
+ themes: {
+ dark: githubDark,
+ light: githubLight,
+ },
+ defaultColor: false,
+ cssVariablePrefix: '--diff-view-',
+ mergeWhitespaces: false,
+ // TODO 提供额外的配置来控制加载插件
+ // transformers: [shikiColorizedBrackets()],
+ });
+ } catch (e) {
+ console.log((e as Error).message);
+ return;
+ }
+ },
+});
+
+Object.defineProperty(instance, 'processAST', {
+ value: (ast: DiffAST) => {
+ return processAST(ast);
+ },
+});
+
+Object.defineProperty(instance, 'hasRegisteredCurrentLang', {
+ value: (lang: string) => {
+ return internal?.getLanguage(lang) !== undefined;
+ },
+});
+
+Object.defineProperty(instance, 'getHighlighterEngine', {
+ value: () => {
+ return internal;
+ },
+});
+
+Object.defineProperty(instance, 'type', {
+ value: 'class',
+});
+
+const highlighter: DiffHighlighter = instance as DiffHighlighter;
+
+export const highlighterReady = new Promise((r) => {
+ if (internal) {
+ r(highlighter);
+ } else {
+ shikiHighlighter
+ .then((i) => {
+ internal = i;
+ })
+ .then(() => r(highlighter));
+ }
+});
+`;
+
+getDiffViewHighlighter().then((highlighter) => {
+ render(
+ createElement(CodeView, {
+ data: {content: temp1, fileLang: 'ts'},
+ // width: 80,
+ codeViewTheme: "dark",
+ extendData: {
+ newFile: { 107: { data: "test extend data" } },
+ },
+ renderExtendLine: ({ data }) => {
+ return createElement(
+ Box,
+ { backgroundColor: "red", width: "100%", padding: "1" },
+ createElement(Text, null, data)
+ );
+ },
+ codeViewHighlight: true,
+ registerHighlighter: highlighter,
+ })
+ );
+});
diff --git a/packages/cli/test/file.mjs b/packages/cli/test/file.mjs
index 6757f66..866d039 100644
--- a/packages/cli/test/file.mjs
+++ b/packages/cli/test/file.mjs
@@ -1,7 +1,9 @@
import { generateDiffFile } from "@git-diff-view/file";
import { getDiffViewHighlighter } from "@git-diff-view/shiki";
-import { Box, render, Text } from "ink";
-import { createElement } from "react";
+// import { Box, render, Text } from "ink";
+// import { createElement } from "react";
+import { createElement } from '@my-react/react';
+import { Box, render, Text } from '@my-react/react-terminal'
import { DiffView, DiffModeEnum } from "@git-diff-view/cli";
@@ -416,8 +418,9 @@ getDiffViewHighlighter().then((highlighter) => {
diffFile,
// width: 80,
diffViewTheme: "dark",
+ diffViewHideOperator: true,
// diffViewTabWidth: 'small',
- diffViewTabSpace: true,
+ // diffViewTabSpace: true,
extendData: {
newFile: { 107: { data: "test extend data" } },
},
diff --git a/packages/core/index.d.ts b/packages/core/index.d.ts
index 3edaab8..f409700 100644
--- a/packages/core/index.d.ts
+++ b/packages/core/index.d.ts
@@ -639,6 +639,8 @@ export declare function diffChanges(addition: DiffLine, deletion: DiffLine): {
delRange: DiffRange;
};
export declare function escapeHtml(string: unknown): string;
+export declare function getFile(raw: string, lang: DiffHighlighterLang, theme: "light" | "dark", fileName?: string, uuid?: string): File$1;
+export declare function getFile(raw: string, lang: string, theme: "light" | "dark", fileName?: string, uuid?: string): File$1;
/** Get the changed ranges in the strings, relative to each other. */
export declare function relativeChanges(addition: DiffLine, deletion: DiffLine): {
addRange: IRange;
diff --git a/packages/core/src/file.ts b/packages/core/src/file.ts
index 2e763b3..945eb20 100644
--- a/packages/core/src/file.ts
+++ b/packages/core/src/file.ts
@@ -216,9 +216,6 @@ export class File {
}
// TODO add highlight engine key to cache key
-/**
- * @internal
- */
export function getFile(
raw: string,
lang: DiffHighlighterLang,
@@ -226,13 +223,7 @@ export function getFile(
fileName?: string,
uuid?: string
): File;
-/**
- * @internal
- */
export function getFile(raw: string, lang: string, theme: "light" | "dark", fileName?: string, uuid?: string): File;
-/**
- * @internal
- */
export function getFile(
raw: string,
lang: DiffHighlighterLang | string,
diff --git a/packages/file/index.d.ts b/packages/file/index.d.ts
index 6aaee86..00fa0e1 100644
--- a/packages/file/index.d.ts
+++ b/packages/file/index.d.ts
@@ -637,6 +637,8 @@ export declare function diffChanges(addition: DiffLine, deletion: DiffLine): {
delRange: DiffRange;
};
export declare function escapeHtml(string: unknown): string;
+export declare function getFile(raw: string, lang: DiffHighlighterLang, theme: "light" | "dark", fileName?: string, uuid?: string): File$1;
+export declare function getFile(raw: string, lang: string, theme: "light" | "dark", fileName?: string, uuid?: string): File$1;
/** Get the changed ranges in the strings, relative to each other. */
export declare function relativeChanges(addition: DiffLine, deletion: DiffLine): {
addRange: IRange;
diff --git a/packages/react/index.d.ts b/packages/react/index.d.ts
index abf37c6..5b1189e 100644
--- a/packages/react/index.d.ts
+++ b/packages/react/index.d.ts
@@ -637,6 +637,8 @@ export declare function diffChanges(addition: DiffLine, deletion: DiffLine): {
delRange: DiffRange;
};
export declare function escapeHtml(string: unknown): string;
+export declare function getFile(raw: string, lang: DiffHighlighterLang, theme: "light" | "dark", fileName?: string, uuid?: string): File$1;
+export declare function getFile(raw: string, lang: string, theme: "light" | "dark", fileName?: string, uuid?: string): File$1;
/** Get the changed ranges in the strings, relative to each other. */
export declare function relativeChanges(addition: DiffLine, deletion: DiffLine): {
addRange: IRange;
diff --git a/packages/solid/index.d.ts b/packages/solid/index.d.ts
index b8f3b5e..da6b87f 100644
--- a/packages/solid/index.d.ts
+++ b/packages/solid/index.d.ts
@@ -637,6 +637,8 @@ export declare function diffChanges(addition: DiffLine, deletion: DiffLine): {
delRange: DiffRange;
};
export declare function escapeHtml(string: unknown): string;
+export declare function getFile(raw: string, lang: DiffHighlighterLang, theme: "light" | "dark", fileName?: string, uuid?: string): File$1;
+export declare function getFile(raw: string, lang: string, theme: "light" | "dark", fileName?: string, uuid?: string): File$1;
/** Get the changed ranges in the strings, relative to each other. */
export declare function relativeChanges(addition: DiffLine, deletion: DiffLine): {
addRange: IRange;
diff --git a/packages/vue/index.d.ts b/packages/vue/index.d.ts
index 72e3c04..956d0d7 100644
--- a/packages/vue/index.d.ts
+++ b/packages/vue/index.d.ts
@@ -637,6 +637,8 @@ export declare function diffChanges(addition: DiffLine, deletion: DiffLine): {
delRange: DiffRange;
};
export declare function escapeHtml(string: unknown): string;
+export declare function getFile(raw: string, lang: DiffHighlighterLang, theme: "light" | "dark", fileName?: string, uuid?: string): File$1;
+export declare function getFile(raw: string, lang: string, theme: "light" | "dark", fileName?: string, uuid?: string): File$1;
/** Get the changed ranges in the strings, relative to each other. */
export declare function relativeChanges(addition: DiffLine, deletion: DiffLine): {
addRange: IRange;
From 7d8bac0a35d58dab0d3a2f88a644b31189fabe24 Mon Sep 17 00:00:00 2001
From: MrWangJustToDo <2711470541@qq.com>
Date: Mon, 9 Mar 2026 23:15:06 +0800
Subject: [PATCH 2/5] fix: ensure minimum row height for empty lines and
correct 'No newline' row calculation
- Fix getCurrentLineRow to return minimum 1 row for empty lines (both CodeView and DiffView)
- Fix 'No newline at end of file' row height calculation to include actual text length instead of blindly adding +1
---
.../src/components/DiffSplitContentLine.tsx | 19 +++++++++++--------
.../src/components/DiffUnifiedContentLine.tsx | 11 ++++++++---
packages/cli/src/components/codeTools.ts | 3 ++-
packages/cli/src/components/tools.ts | 3 ++-
packages/cli/test/code.mjs | 4 ++--
packages/cli/test/file.mjs | 2 +-
6 files changed, 26 insertions(+), 16 deletions(-)
diff --git a/packages/cli/src/components/DiffSplitContentLine.tsx b/packages/cli/src/components/DiffSplitContentLine.tsx
index 862d971..4c40d08 100644
--- a/packages/cli/src/components/DiffSplitContentLine.tsx
+++ b/packages/cli/src/components/DiffSplitContentLine.tsx
@@ -56,18 +56,21 @@ const InternalDiffSplitLine = ({
// Calculate row heights - must match the actual wrap width used in DiffContent
// DiffContent receives contentWidth and wraps at (contentWidth - 2) for the operator column and end padding
- let oldRow = getCurrentLineRow({ content: oldLine?.value || "", width: contentWidth - 2 });
- let newRow = getCurrentLineRow({ content: newLine?.value || "", width: contentWidth - 2 });
+ const noNewlineText = "\\ No newline at end of file";
- oldRow =
+ // Include "No newline" text in row calculation if present
+ const oldContent =
oldLine?.diff?.changes?.hasLineChange && oldLine?.diff?.changes.newLineSymbol === NewLineSymbol.NEWLINE
- ? oldRow + 1
- : oldRow;
+ ? (oldLine?.value || "") + noNewlineText
+ : oldLine?.value || "";
- newRow =
+ const newContent =
newLine?.diff?.changes?.hasLineChange && newLine?.diff?.changes.newLineSymbol === NewLineSymbol.NEWLINE
- ? newRow + 1
- : newRow;
+ ? (newLine?.value || "") + noNewlineText
+ : newLine?.value || "";
+
+ const oldRow = getCurrentLineRow({ content: oldContent, width: contentWidth - 2 });
+ const newRow = getCurrentLineRow({ content: newContent, width: contentWidth - 2 });
const row = Math.max(oldRow, newRow);
diff --git a/packages/cli/src/components/DiffUnifiedContentLine.tsx b/packages/cli/src/components/DiffUnifiedContentLine.tsx
index 893ef6b..68f8d72 100644
--- a/packages/cli/src/components/DiffUnifiedContentLine.tsx
+++ b/packages/cli/src/components/DiffUnifiedContentLine.tsx
@@ -181,10 +181,15 @@ const InternalDiffUnifiedLine = ({
const contentWidth = columns - (lineNumWidth + 1) * 2 - 1;
- // Use contentWidth - 2 to match the actual wrap width in DiffContent (operator column takes 1 char, end padding takes 1 char)
- let row = getCurrentLineRow({ content: rawLine, width: contentWidth - 2 });
+ // Include "No newline" text in row calculation if present
+ const noNewlineText = "\\ No newline at end of file";
+ const contentWithSymbol =
+ diffLine?.changes?.hasLineChange && diffLine.changes.newLineSymbol === NewLineSymbol.NEWLINE
+ ? rawLine + noNewlineText
+ : rawLine;
- row = diffLine?.changes?.hasLineChange && diffLine.changes.newLineSymbol === NewLineSymbol.NEWLINE ? row + 1 : row;
+ // Use contentWidth - 2 to match the actual wrap width in DiffContent (operator column takes 1 char, end padding takes 1 char)
+ const row = getCurrentLineRow({ content: contentWithSymbol, width: contentWidth - 2 });
const color = hasDiff
? theme === "light"
diff --git a/packages/cli/src/components/codeTools.ts b/packages/cli/src/components/codeTools.ts
index 3df408a..a7135e8 100644
--- a/packages/cli/src/components/codeTools.ts
+++ b/packages/cli/src/components/codeTools.ts
@@ -93,5 +93,6 @@ export const createCodeConfigStore = (props: CodeViewProps & { isMou
};
export const getCurrentLineRow = ({ content, width }: { content: string; width: number }) => {
- return Math.ceil(stringWidth(content) / width);
+ // Ensure minimum of 1 row for empty lines
+ return Math.max(1, Math.ceil(stringWidth(content) / width));
};
diff --git a/packages/cli/src/components/tools.ts b/packages/cli/src/components/tools.ts
index a097994..248c7d5 100644
--- a/packages/cli/src/components/tools.ts
+++ b/packages/cli/src/components/tools.ts
@@ -127,7 +127,8 @@ export const createDiffConfigStore = (
};
export const getCurrentLineRow = ({ content, width }: { content: string; width: number }) => {
- return Math.ceil(stringWidth(content) / width);
+ // Ensure minimum of 1 row for empty lines
+ return Math.max(1, Math.ceil(stringWidth(content) / width));
};
export const getStringContentWithFixedWidth = ({
diff --git a/packages/cli/test/code.mjs b/packages/cli/test/code.mjs
index e2780f6..20daa00 100644
--- a/packages/cli/test/code.mjs
+++ b/packages/cli/test/code.mjs
@@ -210,11 +210,11 @@ export const highlighterReady = new Promise((r) => {
getDiffViewHighlighter().then((highlighter) => {
render(
createElement(CodeView, {
- data: {content: temp1, fileLang: 'ts'},
+ data: { content: temp1, fileLang: "ts" },
// width: 80,
codeViewTheme: "dark",
extendData: {
- newFile: { 107: { data: "test extend data" } },
+ 107: { data: "test extend data" },
},
renderExtendLine: ({ data }) => {
return createElement(
diff --git a/packages/cli/test/file.mjs b/packages/cli/test/file.mjs
index 866d039..7e96e0e 100644
--- a/packages/cli/test/file.mjs
+++ b/packages/cli/test/file.mjs
@@ -422,7 +422,7 @@ getDiffViewHighlighter().then((highlighter) => {
// diffViewTabWidth: 'small',
// diffViewTabSpace: true,
extendData: {
- newFile: { 107: { data: "test extend data" } },
+ newFile: { 97: { data: "test extend data" } },
},
renderExtendLine: ({ data }) => {
return createElement(
From c9101b71ab567f4bb59aff0ab66f2348c27d75d4 Mon Sep 17 00:00:00 2001
From: MrWangJustToDo <2711470541@qq.com>
Date: Mon, 9 Mar 2026 23:16:13 +0800
Subject: [PATCH 3/5] chore: update test files to use ink/react instead of
my-react
---
packages/cli/test/code.mjs | 8 ++++----
packages/cli/test/file.mjs | 10 +++++-----
2 files changed, 9 insertions(+), 9 deletions(-)
diff --git a/packages/cli/test/code.mjs b/packages/cli/test/code.mjs
index 20daa00..33fff05 100644
--- a/packages/cli/test/code.mjs
+++ b/packages/cli/test/code.mjs
@@ -1,8 +1,8 @@
import { getDiffViewHighlighter } from "@git-diff-view/shiki";
-// import { Box, render, Text } from "ink";
-// import { createElement } from "react";
-import { createElement } from '@my-react/react';
-import { Box, render, Text } from '@my-react/react-terminal'
+import { Box, render, Text } from "ink";
+import { createElement } from "react";
+// import { createElement } from '@my-react/react';
+// import { Box, render, Text } from '@my-react/react-terminal'
import { CodeView } from "@git-diff-view/cli";
diff --git a/packages/cli/test/file.mjs b/packages/cli/test/file.mjs
index 7e96e0e..2703e8e 100644
--- a/packages/cli/test/file.mjs
+++ b/packages/cli/test/file.mjs
@@ -1,9 +1,9 @@
import { generateDiffFile } from "@git-diff-view/file";
import { getDiffViewHighlighter } from "@git-diff-view/shiki";
-// import { Box, render, Text } from "ink";
-// import { createElement } from "react";
-import { createElement } from '@my-react/react';
-import { Box, render, Text } from '@my-react/react-terminal'
+import { Box, render, Text } from "ink";
+import { createElement } from "react";
+// import { createElement } from '@my-react/react';
+// import { Box, render, Text } from '@my-react/react-terminal'
import { DiffView, DiffModeEnum } from "@git-diff-view/cli";
@@ -433,7 +433,7 @@ getDiffViewHighlighter().then((highlighter) => {
},
diffViewHighlight: true,
registerHighlighter: highlighter,
- diffViewMode: DiffModeEnum.Split,
+ diffViewMode: DiffModeEnum.SplitGitLab,
})
);
});
From 1b5bb85d7c30885c56836ad4375b008371b90cbb Mon Sep 17 00:00:00 2001
From: MrWangJustToDo <2711470541@qq.com>
Date: Thu, 12 Mar 2026 01:29:50 +0800
Subject: [PATCH 4/5] update
---
packages/cli/package.json | 2 +-
packages/cli/src/components/tools.ts | 4 ++-
packages/react/package.json | 2 +-
packages/solid/package.json | 2 +-
pnpm-lock.yaml | 38 ++++++++++++++++++----------
ui/react-example/package.json | 2 +-
6 files changed, 32 insertions(+), 18 deletions(-)
diff --git a/packages/cli/package.json b/packages/cli/package.json
index f44fd52..61118ec 100644
--- a/packages/cli/package.json
+++ b/packages/cli/package.json
@@ -56,7 +56,7 @@
"highlight.js": "^11.11.0",
"lowlight": "^3.3.0",
"fast-diff": "^1.3.0",
- "reactivity-store": "^0.3.12",
+ "reactivity-store": "^0.4.0",
"string-width": "^7.0.0",
"use-sync-external-store": "^1.6.0"
},
diff --git a/packages/cli/src/components/tools.ts b/packages/cli/src/components/tools.ts
index 248c7d5..ebf5c03 100644
--- a/packages/cli/src/components/tools.ts
+++ b/packages/cli/src/components/tools.ts
@@ -1,4 +1,4 @@
-import { createStore, markRaw, ref } from "reactivity-store";
+import { createStore, markRaw, ref, configureEnv } from "reactivity-store";
import stringWidth from "string-width";
import type { DiffModeEnum, DiffViewProps } from "./DiffView";
@@ -6,6 +6,8 @@ import type { DiffLine } from "@git-diff-view/core";
import type { DOMElement } from "ink";
import type { Ref, UseSelectorWithStore } from "reactivity-store";
+configureEnv({ allowNonBrowserUpdates: true });
+
export const createDiffConfigStore = (
props: DiffViewProps & { isMounted: boolean },
diffFileId: string
diff --git a/packages/react/package.json b/packages/react/package.json
index cc8c24a..04e8f17 100644
--- a/packages/react/package.json
+++ b/packages/react/package.json
@@ -72,7 +72,7 @@
"fast-diff": "^1.3.0",
"highlight.js": "^11.11.0",
"lowlight": "^3.3.0",
- "reactivity-store": "^0.3.12",
+ "reactivity-store": "^0.4.0",
"use-sync-external-store": "^1.6.0"
},
"devDependencies": {
diff --git a/packages/solid/package.json b/packages/solid/package.json
index 0d3f1c5..8f96771 100644
--- a/packages/solid/package.json
+++ b/packages/solid/package.json
@@ -46,7 +46,7 @@
"highlight.js": "^11.11.0",
"lowlight": "^3.3.0",
"fast-diff": "^1.3.0",
- "reactivity-store": "^0.3.12"
+ "reactivity-store": "^0.4.0"
},
"devDependencies": {
"solid-js": "^1.9.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 1cc9dd5..e3566b5 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -131,8 +131,8 @@ importers:
specifier: ^19.2.0
version: 19.2.0
reactivity-store:
- specifier: ^0.3.12
- version: 0.3.12(react@19.2.0)
+ specifier: ^0.4.0
+ version: 0.4.0(react@19.2.0)
string-width:
specifier: ^7.0.0
version: 7.2.0
@@ -229,8 +229,8 @@ importers:
specifier: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
version: 19.2.0(react@19.2.0)
reactivity-store:
- specifier: ^0.3.12
- version: 0.3.12(react@19.2.0)
+ specifier: ^0.4.0
+ version: 0.4.0(react@19.2.0)
use-sync-external-store:
specifier: ^1.6.0
version: 1.6.0(react@19.2.0)
@@ -278,8 +278,8 @@ importers:
specifier: ^3.3.0
version: 3.3.0
reactivity-store:
- specifier: ^0.3.12
- version: 0.3.12(react@19.2.0)
+ specifier: ^0.4.0
+ version: 0.4.0(react@19.2.0)
devDependencies:
autoprefixer:
specifier: ^10.4.21
@@ -565,8 +565,8 @@ importers:
specifier: ^19.2.0
version: 19.2.0(react@19.2.0)
reactivity-store:
- specifier: ^0.3.12
- version: 0.3.12(react@19.2.0)
+ specifier: ^0.4.0
+ version: 0.4.0(react@19.2.0)
devDependencies:
'@my-react/react-refresh':
specifier: ^0.3.22
@@ -2419,6 +2419,9 @@ packages:
'@vue/reactivity@3.5.22':
resolution: {integrity: sha512-f2Wux4v/Z2pqc9+4SmgZC1p73Z53fyD90NFWXiX9AKVnVBEvLFOWCEgJD3GdGnlxPZt01PSlfmLqbLYzY/Fw4A==}
+ '@vue/reactivity@3.5.30':
+ resolution: {integrity: sha512-179YNgKATuwj9gB+66snskRDOitDiuOZqkYia7mHKJaidOMo/WJxHKF8DuGc4V4XbYTJANlfEKb0yxTQotnx4Q==}
+
'@vue/runtime-core@3.5.22':
resolution: {integrity: sha512-EHo4W/eiYeAzRTN5PCextDUZ0dMs9I8mQ2Fy+OkzvRPUYQEyK9yAjbasrMCXbLNhF7P0OUyivLjIy0yc6VrLJQ==}
@@ -2433,6 +2436,9 @@ packages:
'@vue/shared@3.5.22':
resolution: {integrity: sha512-F4yc6palwq3TT0u+FYf0Ns4Tfl9GRFURDN2gWG7L1ecIaS/4fCIuFOjMTnCyjsu/OK6vaDKLCrGAa+KvvH+h4w==}
+ '@vue/shared@3.5.30':
+ resolution: {integrity: sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ==}
+
accepts@2.0.0:
resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
engines: {node: '>= 0.6'}
@@ -5089,8 +5095,8 @@ packages:
resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==}
engines: {node: '>=0.10.0'}
- reactivity-store@0.3.12:
- resolution: {integrity: sha512-Idz9EL4dFUtQbHySZQzckWOTUfqjdYpUtNW0iOysC32mG7IjiUGB77QrsyR5eAWBkRiS9JscF6A3fuQAIy+LrQ==}
+ reactivity-store@0.4.0:
+ resolution: {integrity: sha512-uL9uoREOBg2o4zUa8vMU0AbvAOk0osPloizscmyZqMvJzcuuKX3ELFYYr1DX8gAcfvlhPduz4QuLZn1eChCu4Q==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
@@ -7956,6 +7962,10 @@ snapshots:
dependencies:
'@vue/shared': 3.5.22
+ '@vue/reactivity@3.5.30':
+ dependencies:
+ '@vue/shared': 3.5.30
+
'@vue/runtime-core@3.5.22':
dependencies:
'@vue/reactivity': 3.5.22
@@ -7976,6 +7986,8 @@ snapshots:
'@vue/shared@3.5.22': {}
+ '@vue/shared@3.5.30': {}
+
accepts@2.0.0:
dependencies:
mime-types: 3.0.1
@@ -10832,10 +10844,10 @@ snapshots:
react@19.2.0: {}
- reactivity-store@0.3.12(react@19.2.0):
+ reactivity-store@0.4.0(react@19.2.0):
dependencies:
- '@vue/reactivity': 3.5.22
- '@vue/shared': 3.5.22
+ '@vue/reactivity': 3.5.30
+ '@vue/shared': 3.5.30
react: 19.2.0
use-sync-external-store: 1.6.0(react@19.2.0)
diff --git a/ui/react-example/package.json b/ui/react-example/package.json
index 840254e..67ff04d 100644
--- a/ui/react-example/package.json
+++ b/ui/react-example/package.json
@@ -25,7 +25,7 @@
"overlayscrollbars": "^2.12.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
- "reactivity-store": "^0.3.12"
+ "reactivity-store": "^0.4.0"
},
"devDependencies": {
"@my-react/react-refresh": "^0.3.22",
From 0592a107afd65e11c42819733e736ed6586525ea Mon Sep 17 00:00:00 2001
From: MrWangJustToDo <2711470541@qq.com>
Date: Thu, 12 Mar 2026 13:04:03 +0800
Subject: [PATCH 5/5] add agent.md
---
AGENTS.md | 164 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 164 insertions(+)
create mode 100644 AGENTS.md
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..ae1df42
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,164 @@
+# AGENTS.md - Guidelines for AI Coding Agents
+
+This document provides essential information for AI coding agents working on the `git-diff-view` codebase.
+
+## Project Overview
+
+A high-performance Git diff view component library supporting React, Vue, Solid, and Svelte.
+
+- **Language:** TypeScript (strict mode, ESNext target)
+- **Package Manager:** pnpm (v9.12.0+) with workspace monorepo
+- **Build Tool:** Custom rollup-based tool (`project-tool`) with SWC
+- **Styling:** TailwindCSS with PostCSS
+
+## Build/Lint/Test Commands
+
+```bash
+pnpm install # Install dependencies
+pnpm run build:packages # Build all packages
+pnpm run dev:packages # Development mode for packages
+pnpm run lint # Run ESLint: eslint --cache --ext ts,tsx .
+pnpm run lint:fix # Fix lint issues automatically
+pnpm run prettier # Format code with Prettier
+pnpm run clean # Remove dist/dev/.cache directories
+pnpm run release # Full release: lint + prettier + clean + build + release
+```
+
+### Development Servers
+
+```bash
+pnpm run dev:react # React example (Vite)
+pnpm run dev:vue # Vue example (Vite)
+pnpm run dev:solid # Solid example
+pnpm run dev:svelte # Svelte package dev
+pnpm run dev:cli # CLI dev mode
+```
+
+### Running Tests
+
+**Note:** No test framework is currently configured. There are no test files or test commands.
+
+## Code Style Guidelines
+
+### Formatting (from .prettierrc)
+
+- **Semicolons:** Always required
+- **Quotes:** Double quotes (`"`) - not single quotes
+- **Indentation:** 2 spaces (no tabs)
+- **Line width:** 120 characters maximum
+- **Trailing commas:** ES5 style (arrays, objects)
+
+### Import Organization
+
+Order imports with blank lines between groups:
+1. External dependencies (npm packages)
+2. Internal monorepo packages (`@git-diff-view/*`)
+3. Local relative imports
+4. Type-only imports (at the end, using `import type`)
+
+```typescript
+import { memo, useEffect } from "react";
+import { DiffFile, SplitSide } from "@git-diff-view/core";
+import { useIsMounted } from "../hooks/useIsMounted";
+import type { DiffHighlighter } from "@git-diff-view/core";
+```
+
+### Naming Conventions
+
+| Element | Convention | Examples |
+|---------|------------|----------|
+| Variables/Functions | camelCase | `diffFile`, `getFile`, `parseInstance` |
+| Classes/Interfaces/Types/Enums | PascalCase | `DiffFile`, `SplitLineItem`, `DiffLineType` |
+| React Components | PascalCase | `DiffView`, `DiffSplitView` |
+| Custom Hooks | `use` prefix | `useIsMounted`, `useDomWidth` |
+| Component files | PascalCase | `DiffView.tsx` |
+| Utility files | camelCase | `utils.ts`, `diff-file.ts` |
+| Private class fields | `#` prefix (ES2022) | `#oldFileResult`, `#listeners` |
+| Internal properties | `_` prefix | `_oldFileName`, `_isHidden` |
+| CSS variable names | camelCase + `Name` suffix | `diffFontSizeName`, `emptyBGName` |
+
+### TypeScript Patterns
+
+- Use **interfaces** for object shapes/data structures
+- Use **types** for unions, intersections, and utility types
+- **Avoid `any`:** Use `unknown` with generics: ``
+- **Optional chaining:** Use extensively: `diffFile?.clear?.()`
+
+### Export Patterns
+
+**Named exports only - no default exports:**
+
+```typescript
+export const DiffView = InnerDiffView;
+export { SplitSide, DiffModeEnum };
+export class DiffFile { ... }
+```
+
+**Barrel exports in `index.ts`:**
+
+```typescript
+export * from "./components/DiffView";
+export * from "@git-diff-view/core";
+```
+
+### Error Handling
+
+Use the `__DEV__` global for development-only warnings/errors:
+
+```typescript
+if (__DEV__) {
+ console.warn('[@git-diff-view/core] The composed files are identical...');
+}
+```
+
+- Prefix messages with package name: `[@git-diff-view/core]`
+- Use defensive early returns rather than throwing exceptions
+- `__DEV__` blocks are stripped in production builds
+
+### React-Specific Patterns
+
+- **Memo pattern:** `const MemoedComponent = memo(InternalComponent);`
+- **forwardRef:** Add `displayName` after: `InnerDiffView.displayName = "DiffView";`
+- **Context:** Use `DiffViewContext.Provider` for state management
+
+## Project Structure
+
+```
+packages/
+ core/ # Core diff parsing engine (@git-diff-view/core)
+ react/ # React components (@git-diff-view/react)
+ vue/ # Vue components (@git-diff-view/vue)
+ solid/ # Solid components (@git-diff-view/solid)
+ svelte/ # Svelte components (@git-diff-view/svelte)
+ cli/ # CLI tool (@git-diff-view/cli)
+ file/ # File comparison engine (@git-diff-view/file)
+ utils/ # Shared utilities (@git-diff-view/utils)
+ lowlight/ # Lowlight syntax highlighter
+ shiki/ # Shiki syntax highlighter
+ui/
+ react-example/ # React demo app (Vite)
+ vue-example/ # Vue demo app (Vite)
+ solid-example/ # Solid demo app
+ svelte-example/ # Svelte demo
+scripts/ # Build scripts (ts-node)
+```
+
+## Package Dependencies
+
+- `@git-diff-view/core` is the foundation (all framework packages depend on it)
+- Framework packages (react, vue, solid, svelte) wrap the core
+- `@git-diff-view/file` provides file-to-file comparison using the `diff` library
+- Syntax highlighting via `lowlight` (highlight.js) or `shiki`
+
+## ESLint Configuration
+
+- Base config: `project-tool/baseLint`
+- Ignored: `dist`, `dev`, `scripts`, `node_modules`, `next-*-example`, `packages/solid`, `packages/svelte`
+
+## Important Notes
+
+1. **No test infrastructure** - be cautious when modifying core logic
+2. **Multi-framework support** - changes to core affect all framework bindings
+3. **Performance critical** - this library handles large diffs; avoid unnecessary re-renders
+4. **CSS variables** - styling uses CSS custom properties for theming
+5. **Private fields** - use ES2022 `#` syntax for true private fields