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 diff --git a/packages/cli/package.json b/packages/cli/package.json index 5cf5f64..61118ec 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" }, @@ -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/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..4c40d08 100644 --- a/packages/cli/src/components/DiffSplitContentLine.tsx +++ b/packages/cli/src/components/DiffSplitContentLine.tsx @@ -55,19 +55,22 @@ 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 + 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 b701231..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 - 1 to match the actual wrap width in DiffContent (operator column takes 1 char) - let row = getCurrentLineRow({ content: rawLine, width: contentWidth - 1 }); + // 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/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..a7135e8 --- /dev/null +++ b/packages/cli/src/components/codeTools.ts @@ -0,0 +1,98 @@ +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 }) => { + // 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 aa10109..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 @@ -72,6 +74,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 +97,8 @@ export const createDiffConfigStore = ( setExtendData, renderExtendLine, setRenderExtendLine, + hideOperator, + setHideOperator, }; // fix rollup type error }) as UseSelectorWithStore<{ @@ -115,11 +123,14 @@ export const createDiffConfigStore = ( setExtendData: (extendData: DiffViewProps["extendData"]) => void; renderExtendLine: Ref; setRenderExtendLine: (renderExtendLine: typeof props.renderExtendLine) => void; + hideOperator: Ref; + setHideOperator: (hideOperator: boolean) => void; }>; }; 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/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..33fff05 --- /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: { + 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..2703e8e 100644 --- a/packages/cli/test/file.mjs +++ b/packages/cli/test/file.mjs @@ -2,6 +2,8 @@ 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 { DiffView, DiffModeEnum } from "@git-diff-view/cli"; @@ -416,10 +418,11 @@ getDiffViewHighlighter().then((highlighter) => { diffFile, // width: 80, diffViewTheme: "dark", + diffViewHideOperator: true, // diffViewTabWidth: 'small', - diffViewTabSpace: true, + // diffViewTabSpace: true, extendData: { - newFile: { 107: { data: "test extend data" } }, + newFile: { 97: { data: "test extend data" } }, }, renderExtendLine: ({ data }) => { return createElement( @@ -430,7 +433,7 @@ getDiffViewHighlighter().then((highlighter) => { }, diffViewHighlight: true, registerHighlighter: highlighter, - diffViewMode: DiffModeEnum.Split, + diffViewMode: DiffModeEnum.SplitGitLab, }) ); }); 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/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/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/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/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; 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",