@assistant-ui/react-ink 0.0.6 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/primitives/composer/ComposerSend.js +1 -1
- package/dist/primitives/composer/ComposerSend.js.map +1 -1
- package/dist/primitives/diff/DiffContent.d.ts +22 -0
- package/dist/primitives/diff/DiffContent.d.ts.map +1 -0
- package/dist/primitives/diff/DiffContent.js +40 -0
- package/dist/primitives/diff/DiffContent.js.map +1 -0
- package/dist/primitives/diff/DiffContext.d.ts +8 -0
- package/dist/primitives/diff/DiffContext.d.ts.map +1 -0
- package/dist/primitives/diff/DiffContext.js +11 -0
- package/dist/primitives/diff/DiffContext.js.map +1 -0
- package/dist/primitives/diff/DiffHeader.d.ts +10 -0
- package/dist/primitives/diff/DiffHeader.d.ts.map +1 -0
- package/dist/primitives/diff/DiffHeader.js +18 -0
- package/dist/primitives/diff/DiffHeader.js.map +1 -0
- package/dist/primitives/diff/DiffLine.d.ts +13 -0
- package/dist/primitives/diff/DiffLine.d.ts.map +1 -0
- package/dist/primitives/diff/DiffLine.js +20 -0
- package/dist/primitives/diff/DiffLine.js.map +1 -0
- package/dist/primitives/diff/DiffRoot.d.ts +12 -0
- package/dist/primitives/diff/DiffRoot.d.ts.map +1 -0
- package/dist/primitives/diff/DiffRoot.js +8 -0
- package/dist/primitives/diff/DiffRoot.js.map +1 -0
- package/dist/primitives/diff/DiffStats.d.ts +10 -0
- package/dist/primitives/diff/DiffStats.d.ts.map +1 -0
- package/dist/primitives/diff/DiffStats.js +12 -0
- package/dist/primitives/diff/DiffStats.js.map +1 -0
- package/dist/primitives/diff/DiffView.d.ts +13 -0
- package/dist/primitives/diff/DiffView.d.ts.map +1 -0
- package/dist/primitives/diff/DiffView.js +76 -0
- package/dist/primitives/diff/DiffView.js.map +1 -0
- package/dist/primitives/diff/diff-utils.d.ts +9 -0
- package/dist/primitives/diff/diff-utils.d.ts.map +1 -0
- package/dist/primitives/diff/diff-utils.js +125 -0
- package/dist/primitives/diff/diff-utils.js.map +1 -0
- package/dist/primitives/diff/index.d.ts +7 -0
- package/dist/primitives/diff/index.d.ts.map +1 -0
- package/dist/primitives/diff/index.js +6 -0
- package/dist/primitives/diff/index.js.map +1 -0
- package/dist/primitives/diff/types.d.ts +24 -0
- package/dist/primitives/diff/types.d.ts.map +1 -0
- package/dist/primitives/diff/types.js +2 -0
- package/dist/primitives/diff/types.js.map +1 -0
- package/dist/primitives/diff.d.ts +2 -0
- package/dist/primitives/diff.d.ts.map +1 -0
- package/dist/primitives/diff.js +2 -0
- package/dist/primitives/diff.js.map +1 -0
- package/package.json +9 -7
- package/src/index.ts +2 -0
- package/src/primitives/composer/ComposerSend.tsx +1 -1
- package/src/primitives/diff/DiffContent.tsx +77 -0
- package/src/primitives/diff/DiffContext.tsx +18 -0
- package/src/primitives/diff/DiffHeader.tsx +38 -0
- package/src/primitives/diff/DiffLine.tsx +42 -0
- package/src/primitives/diff/DiffRoot.tsx +25 -0
- package/src/primitives/diff/DiffStats.tsx +22 -0
- package/src/primitives/diff/DiffView.test.tsx +340 -0
- package/src/primitives/diff/DiffView.tsx +204 -0
- package/src/primitives/diff/diff-utils.ts +149 -0
- package/src/primitives/diff/index.ts +25 -0
- package/src/primitives/diff/types.ts +28 -0
- package/src/primitives/diff.ts +1 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type DiffLineType = "add" | "del" | "normal";
|
|
2
|
+
export interface ParsedLine {
|
|
3
|
+
type: DiffLineType;
|
|
4
|
+
content: string;
|
|
5
|
+
oldLineNumber?: number;
|
|
6
|
+
newLineNumber?: number;
|
|
7
|
+
}
|
|
8
|
+
export interface ParsedFile {
|
|
9
|
+
oldName?: string | undefined;
|
|
10
|
+
newName?: string | undefined;
|
|
11
|
+
lines: ParsedLine[];
|
|
12
|
+
additions: number;
|
|
13
|
+
deletions: number;
|
|
14
|
+
}
|
|
15
|
+
export interface FoldedRegion {
|
|
16
|
+
type: "fold";
|
|
17
|
+
hiddenCount: number;
|
|
18
|
+
}
|
|
19
|
+
export type DisplayLine = ParsedLine | FoldedRegion;
|
|
20
|
+
export interface DiffFileInput {
|
|
21
|
+
content: string;
|
|
22
|
+
name?: string | undefined;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/primitives/diff/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,YAAY,GAAG,KAAK,GAAG,KAAK,GAAG,QAAQ,CAAC;AAEpD,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,YAAY,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG,YAAY,CAAC;AAEpD,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC3B"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/primitives/diff/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diff.d.ts","sourceRoot":"","sources":["../../src/primitives/diff.ts"],"names":[],"mappings":"AAAA,gCAA6B"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diff.js","sourceRoot":"","sources":["../../src/primitives/diff.ts"],"names":[],"mappings":"AAAA,gCAA6B"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@assistant-ui/react-ink",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"description": "React Ink (terminal UI) bindings for assistant-ui",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"assistant",
|
|
@@ -34,12 +34,14 @@
|
|
|
34
34
|
],
|
|
35
35
|
"sideEffects": false,
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@assistant-ui/core": "^0.1.
|
|
38
|
-
"@assistant-ui/store": "^0.2.
|
|
39
|
-
"@assistant-ui/tap": "^0.5.
|
|
40
|
-
"assistant-stream": "^0.3.
|
|
37
|
+
"@assistant-ui/core": "^0.1.8",
|
|
38
|
+
"@assistant-ui/store": "^0.2.4",
|
|
39
|
+
"@assistant-ui/tap": "^0.5.4",
|
|
40
|
+
"assistant-stream": "^0.3.7",
|
|
41
|
+
"diff": "^8.0.3",
|
|
41
42
|
"ink-spinner": "^5.0.0",
|
|
42
|
-
"
|
|
43
|
+
"parse-diff": "^0.11.1",
|
|
44
|
+
"zustand": "^5.0.12"
|
|
43
45
|
},
|
|
44
46
|
"peerDependencies": {
|
|
45
47
|
"@types/react": "*",
|
|
@@ -55,7 +57,7 @@
|
|
|
55
57
|
"@types/react": "^19.2.14",
|
|
56
58
|
"ink": "^6.8.0",
|
|
57
59
|
"ink-testing-library": "^4.0.0",
|
|
58
|
-
"react": "^19.
|
|
60
|
+
"react": "^19.2.4",
|
|
59
61
|
"vitest": "^4.1.0",
|
|
60
62
|
"@assistant-ui/x-buildutils": "0.0.3"
|
|
61
63
|
},
|
package/src/index.ts
CHANGED
|
@@ -114,6 +114,8 @@ export * as ChainOfThoughtPrimitive from "./primitives/chainOfThought";
|
|
|
114
114
|
export * as SuggestionPrimitive from "./primitives/suggestion";
|
|
115
115
|
export * as ToolCallPrimitive from "./primitives/toolCall";
|
|
116
116
|
export * as ErrorPrimitive from "./primitives/error";
|
|
117
|
+
export * as DiffPrimitive from "./primitives/diff";
|
|
118
|
+
export { DiffView, type DiffViewProps } from "./primitives/diff/DiffView";
|
|
117
119
|
|
|
118
120
|
// Re-export shared providers from core/react
|
|
119
121
|
export {
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { type ComponentProps, type ReactNode, Fragment } from "react";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
import { useDiffContext } from "./DiffContext";
|
|
5
|
+
import { DiffLine } from "./DiffLine";
|
|
6
|
+
import { foldContext } from "./diff-utils";
|
|
7
|
+
import type { ParsedLine, DisplayLine, FoldedRegion } from "./types";
|
|
8
|
+
|
|
9
|
+
export type DiffContentProps = ComponentProps<typeof Box> & {
|
|
10
|
+
fileIndex?: number | undefined;
|
|
11
|
+
showLineNumbers?: boolean | undefined;
|
|
12
|
+
contextLines?: number | undefined;
|
|
13
|
+
maxLines?: number | undefined;
|
|
14
|
+
renderLine?:
|
|
15
|
+
| ((props: { line: ParsedLine; index: number }) => ReactNode)
|
|
16
|
+
| undefined;
|
|
17
|
+
renderFold?:
|
|
18
|
+
| ((props: { region: FoldedRegion; index: number }) => ReactNode)
|
|
19
|
+
| undefined;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const DiffContent = ({
|
|
23
|
+
fileIndex = 0,
|
|
24
|
+
showLineNumbers = true,
|
|
25
|
+
contextLines,
|
|
26
|
+
maxLines,
|
|
27
|
+
renderLine,
|
|
28
|
+
renderFold,
|
|
29
|
+
...boxProps
|
|
30
|
+
}: DiffContentProps) => {
|
|
31
|
+
const { files } = useDiffContext();
|
|
32
|
+
const file = files[fileIndex];
|
|
33
|
+
|
|
34
|
+
const displayLines: DisplayLine[] = useMemo(() => {
|
|
35
|
+
if (!file) return [];
|
|
36
|
+
if (contextLines !== undefined) {
|
|
37
|
+
return foldContext(file.lines, contextLines);
|
|
38
|
+
}
|
|
39
|
+
return file.lines;
|
|
40
|
+
}, [file, contextLines]);
|
|
41
|
+
|
|
42
|
+
if (!file) return null;
|
|
43
|
+
|
|
44
|
+
const truncated = maxLines !== undefined && displayLines.length > maxLines;
|
|
45
|
+
const visibleLines = truncated
|
|
46
|
+
? displayLines.slice(0, maxLines)
|
|
47
|
+
: displayLines;
|
|
48
|
+
const remainingCount = truncated ? displayLines.length - maxLines! : 0;
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<Box flexDirection="column" {...boxProps}>
|
|
52
|
+
{visibleLines.map((line, i) => {
|
|
53
|
+
if (line.type === "fold") {
|
|
54
|
+
if (renderFold) {
|
|
55
|
+
return (
|
|
56
|
+
<Fragment key={i}>
|
|
57
|
+
{renderFold({ region: line, index: i })}
|
|
58
|
+
</Fragment>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
return (
|
|
62
|
+
<Text key={i}>{` --- ${line.hiddenCount} lines hidden ---`}</Text>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
if (renderLine) {
|
|
66
|
+
return <Fragment key={i}>{renderLine({ line, index: i })}</Fragment>;
|
|
67
|
+
}
|
|
68
|
+
return (
|
|
69
|
+
<DiffLine key={i} line={line} showLineNumbers={showLineNumbers} />
|
|
70
|
+
);
|
|
71
|
+
})}
|
|
72
|
+
{truncated && <Text>{`... (${remainingCount} more lines)`}</Text>}
|
|
73
|
+
</Box>
|
|
74
|
+
);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
DiffContent.displayName = "DiffPrimitive.Content";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createContext, useContext } from "react";
|
|
2
|
+
import type { ParsedFile } from "./types";
|
|
3
|
+
|
|
4
|
+
interface DiffContextValue {
|
|
5
|
+
files: ParsedFile[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const DiffContext = createContext<DiffContextValue | null>(null);
|
|
9
|
+
|
|
10
|
+
export const useDiffContext = (): DiffContextValue => {
|
|
11
|
+
const ctx = useContext(DiffContext);
|
|
12
|
+
if (!ctx) {
|
|
13
|
+
throw new Error("useDiffContext must be used within a DiffRoot");
|
|
14
|
+
}
|
|
15
|
+
return ctx;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const DiffContextProvider = DiffContext.Provider;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ComponentProps } from "react";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { useDiffContext } from "./DiffContext";
|
|
4
|
+
import { DiffStats } from "./DiffStats";
|
|
5
|
+
|
|
6
|
+
export type DiffHeaderProps = ComponentProps<typeof Box> & {
|
|
7
|
+
fileIndex?: number;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const DiffHeader = ({ fileIndex = 0, ...boxProps }: DiffHeaderProps) => {
|
|
11
|
+
const { files } = useDiffContext();
|
|
12
|
+
const file = files[fileIndex];
|
|
13
|
+
if (!file) return null;
|
|
14
|
+
|
|
15
|
+
const isDevNull = (n: string | undefined) => !n || n === "/dev/null";
|
|
16
|
+
const renamed =
|
|
17
|
+
!isDevNull(file.oldName) &&
|
|
18
|
+
!isDevNull(file.newName) &&
|
|
19
|
+
file.oldName !== file.newName;
|
|
20
|
+
const displayName = isDevNull(file.newName) ? file.oldName : file.newName;
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<Box gap={1} {...boxProps}>
|
|
24
|
+
{renamed ? (
|
|
25
|
+
<>
|
|
26
|
+
<Text>{file.oldName}</Text>
|
|
27
|
+
<Text>{"->"}</Text>
|
|
28
|
+
<Text>{file.newName}</Text>
|
|
29
|
+
</>
|
|
30
|
+
) : (
|
|
31
|
+
<Text>{displayName}</Text>
|
|
32
|
+
)}
|
|
33
|
+
<DiffStats fileIndex={fileIndex} />
|
|
34
|
+
</Box>
|
|
35
|
+
);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
DiffHeader.displayName = "DiffPrimitive.Header";
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ComponentProps } from "react";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import type { ParsedLine } from "./types";
|
|
4
|
+
|
|
5
|
+
const INDICATOR: Record<ParsedLine["type"], string> = {
|
|
6
|
+
add: "+",
|
|
7
|
+
del: "-",
|
|
8
|
+
normal: " ",
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type DiffLineProps = ComponentProps<typeof Box> & {
|
|
12
|
+
line: ParsedLine;
|
|
13
|
+
showLineNumbers?: boolean;
|
|
14
|
+
lineNumberWidth?: number;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const DiffLine = ({
|
|
18
|
+
line,
|
|
19
|
+
showLineNumbers = true,
|
|
20
|
+
lineNumberWidth = 4,
|
|
21
|
+
...boxProps
|
|
22
|
+
}: DiffLineProps) => {
|
|
23
|
+
const lineNum =
|
|
24
|
+
line.type === "del"
|
|
25
|
+
? line.oldLineNumber
|
|
26
|
+
: line.type === "add"
|
|
27
|
+
? line.newLineNumber
|
|
28
|
+
: line.oldLineNumber;
|
|
29
|
+
|
|
30
|
+
const numStr = lineNum !== undefined ? String(lineNum) : "";
|
|
31
|
+
const padded = numStr.padStart(lineNumberWidth);
|
|
32
|
+
const content = `${INDICATOR[line.type]} ${line.content}`;
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<Box {...boxProps}>
|
|
36
|
+
{showLineNumbers && <Text>{padded} </Text>}
|
|
37
|
+
<Text>{content}</Text>
|
|
38
|
+
</Box>
|
|
39
|
+
);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
DiffLine.displayName = "DiffPrimitive.Line";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ComponentProps, ReactNode } from "react";
|
|
2
|
+
import { Box } from "ink";
|
|
3
|
+
import { DiffContextProvider } from "./DiffContext";
|
|
4
|
+
import type { ParsedFile } from "./types";
|
|
5
|
+
|
|
6
|
+
export type DiffRootProps = ComponentProps<typeof Box> & {
|
|
7
|
+
files?: ParsedFile[] | undefined;
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const DiffRoot = ({
|
|
12
|
+
files = [],
|
|
13
|
+
children,
|
|
14
|
+
...boxProps
|
|
15
|
+
}: DiffRootProps) => {
|
|
16
|
+
return (
|
|
17
|
+
<DiffContextProvider value={{ files }}>
|
|
18
|
+
<Box flexDirection="column" {...boxProps}>
|
|
19
|
+
{children}
|
|
20
|
+
</Box>
|
|
21
|
+
</DiffContextProvider>
|
|
22
|
+
);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
DiffRoot.displayName = "DiffPrimitive.Root";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ComponentProps } from "react";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { useDiffContext } from "./DiffContext";
|
|
4
|
+
|
|
5
|
+
export type DiffStatsProps = ComponentProps<typeof Box> & {
|
|
6
|
+
fileIndex?: number;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const DiffStats = ({ fileIndex = 0, ...boxProps }: DiffStatsProps) => {
|
|
10
|
+
const { files } = useDiffContext();
|
|
11
|
+
const file = files[fileIndex];
|
|
12
|
+
if (!file) return null;
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<Box gap={1} {...boxProps}>
|
|
16
|
+
<Text>+{file.additions}</Text>
|
|
17
|
+
<Text>-{file.deletions}</Text>
|
|
18
|
+
</Box>
|
|
19
|
+
);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
DiffStats.displayName = "DiffPrimitive.Stats";
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
3
|
+
import { cleanup, render } from "ink-testing-library";
|
|
4
|
+
import { parsePatch, computeDiff, foldContext } from "./diff-utils";
|
|
5
|
+
import { DiffContent } from "./DiffContent";
|
|
6
|
+
import { DiffHeader } from "./DiffHeader";
|
|
7
|
+
import { DiffRoot } from "./DiffRoot";
|
|
8
|
+
import { DiffView } from "./DiffView";
|
|
9
|
+
|
|
10
|
+
const renderFrame = async (node: ReactElement) => {
|
|
11
|
+
const instance = render(node);
|
|
12
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
13
|
+
return instance.lastFrame() ?? "";
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
cleanup();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const SAMPLE_PATCH = `diff --git a/hello.txt b/hello.txt
|
|
21
|
+
index 1234567..abcdefg 100644
|
|
22
|
+
--- a/hello.txt
|
|
23
|
+
+++ b/hello.txt
|
|
24
|
+
@@ -1,3 +1,4 @@
|
|
25
|
+
line 1
|
|
26
|
+
-line 2
|
|
27
|
+
+line 2 modified
|
|
28
|
+
+line 2.5 added
|
|
29
|
+
line 3
|
|
30
|
+
`;
|
|
31
|
+
|
|
32
|
+
describe("parsePatch", () => {
|
|
33
|
+
it("parses a unified diff string", () => {
|
|
34
|
+
const files = parsePatch(SAMPLE_PATCH);
|
|
35
|
+
expect(files).toHaveLength(1);
|
|
36
|
+
const file = files[0]!;
|
|
37
|
+
expect(file.oldName).toBe("hello.txt");
|
|
38
|
+
expect(file.newName).toBe("hello.txt");
|
|
39
|
+
expect(file.additions).toBe(2);
|
|
40
|
+
expect(file.deletions).toBe(1);
|
|
41
|
+
|
|
42
|
+
const types = file.lines.map((l) => l.type);
|
|
43
|
+
expect(types).toContain("add");
|
|
44
|
+
expect(types).toContain("del");
|
|
45
|
+
expect(types).toContain("normal");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("strips CRLF line endings from parsed patch lines", () => {
|
|
49
|
+
const patch = `diff --git a/x.txt b/x.txt
|
|
50
|
+
--- a/x.txt
|
|
51
|
+
+++ b/x.txt
|
|
52
|
+
@@ -1,2 +1,2 @@
|
|
53
|
+
a\r
|
|
54
|
+
-b\r
|
|
55
|
+
+c\r
|
|
56
|
+
`;
|
|
57
|
+
|
|
58
|
+
expect(parsePatch(patch)).toEqual([
|
|
59
|
+
{
|
|
60
|
+
oldName: "x.txt",
|
|
61
|
+
newName: "x.txt",
|
|
62
|
+
additions: 1,
|
|
63
|
+
deletions: 1,
|
|
64
|
+
lines: [
|
|
65
|
+
{
|
|
66
|
+
type: "normal",
|
|
67
|
+
content: "a",
|
|
68
|
+
oldLineNumber: 1,
|
|
69
|
+
newLineNumber: 1,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
type: "del",
|
|
73
|
+
content: "b",
|
|
74
|
+
oldLineNumber: 2,
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
type: "add",
|
|
78
|
+
content: "c",
|
|
79
|
+
newLineNumber: 2,
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
]);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("ignores no-newline markers in unified diff patches", () => {
|
|
87
|
+
const patch = `diff --git a/a.txt b/a.txt
|
|
88
|
+
--- a/a.txt
|
|
89
|
+
+++ b/a.txt
|
|
90
|
+
@@ -1 +1 @@
|
|
91
|
+
-old
|
|
92
|
+
\
|
|
93
|
+
+new
|
|
94
|
+
\
|
|
95
|
+
`;
|
|
96
|
+
|
|
97
|
+
expect(parsePatch(patch)).toEqual([
|
|
98
|
+
{
|
|
99
|
+
oldName: "a.txt",
|
|
100
|
+
newName: "a.txt",
|
|
101
|
+
additions: 1,
|
|
102
|
+
deletions: 1,
|
|
103
|
+
lines: [
|
|
104
|
+
{
|
|
105
|
+
type: "del",
|
|
106
|
+
content: "old",
|
|
107
|
+
oldLineNumber: 1,
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
type: "add",
|
|
111
|
+
content: "new",
|
|
112
|
+
newLineNumber: 1,
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
},
|
|
116
|
+
]);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe("computeDiff", () => {
|
|
121
|
+
it("diffs two strings", () => {
|
|
122
|
+
const result = computeDiff("alpha\nbeta\n", "alpha\ngamma\n");
|
|
123
|
+
expect(result.additions).toBeGreaterThan(0);
|
|
124
|
+
expect(result.deletions).toBeGreaterThan(0);
|
|
125
|
+
const types = result.lines.map((l) => l.type);
|
|
126
|
+
expect(types).toContain("add");
|
|
127
|
+
expect(types).toContain("del");
|
|
128
|
+
expect(types).toContain("normal");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("preserves blank-line additions and deletions", () => {
|
|
132
|
+
expect(computeDiff("a\n", "a\n\n")).toEqual({
|
|
133
|
+
additions: 1,
|
|
134
|
+
deletions: 0,
|
|
135
|
+
lines: [
|
|
136
|
+
{
|
|
137
|
+
type: "normal",
|
|
138
|
+
content: "a",
|
|
139
|
+
oldLineNumber: 1,
|
|
140
|
+
newLineNumber: 1,
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
type: "add",
|
|
144
|
+
content: "",
|
|
145
|
+
newLineNumber: 2,
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
expect(computeDiff("a\n\n", "a\n")).toEqual({
|
|
151
|
+
additions: 0,
|
|
152
|
+
deletions: 1,
|
|
153
|
+
lines: [
|
|
154
|
+
{
|
|
155
|
+
type: "normal",
|
|
156
|
+
content: "a",
|
|
157
|
+
oldLineNumber: 1,
|
|
158
|
+
newLineNumber: 1,
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
type: "del",
|
|
162
|
+
content: "",
|
|
163
|
+
oldLineNumber: 2,
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("strips CRLF line endings from computed diffs", () => {
|
|
170
|
+
expect(computeDiff("a\r\nb\r\n", "a\r\nc\r\n")).toEqual({
|
|
171
|
+
additions: 1,
|
|
172
|
+
deletions: 1,
|
|
173
|
+
lines: [
|
|
174
|
+
{
|
|
175
|
+
type: "normal",
|
|
176
|
+
content: "a",
|
|
177
|
+
oldLineNumber: 1,
|
|
178
|
+
newLineNumber: 1,
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
type: "del",
|
|
182
|
+
content: "b",
|
|
183
|
+
oldLineNumber: 2,
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
type: "add",
|
|
187
|
+
content: "c",
|
|
188
|
+
newLineNumber: 2,
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe("foldContext", () => {
|
|
196
|
+
it("folds unchanged regions beyond contextLines", () => {
|
|
197
|
+
const lines = [
|
|
198
|
+
...Array.from({ length: 10 }, (_, i) => ({
|
|
199
|
+
type: "normal" as const,
|
|
200
|
+
content: `line ${i}`,
|
|
201
|
+
oldLineNumber: i + 1,
|
|
202
|
+
newLineNumber: i + 1,
|
|
203
|
+
})),
|
|
204
|
+
{ type: "add" as const, content: "new line", newLineNumber: 11 },
|
|
205
|
+
...Array.from({ length: 10 }, (_, i) => ({
|
|
206
|
+
type: "normal" as const,
|
|
207
|
+
content: `line ${i + 11}`,
|
|
208
|
+
oldLineNumber: i + 11,
|
|
209
|
+
newLineNumber: i + 12,
|
|
210
|
+
})),
|
|
211
|
+
];
|
|
212
|
+
|
|
213
|
+
const result = foldContext(lines, 2);
|
|
214
|
+
const folds = result.filter((l) => l.type === "fold");
|
|
215
|
+
expect(folds.length).toBeGreaterThan(0);
|
|
216
|
+
const totalHidden = folds.reduce(
|
|
217
|
+
(sum, f) => sum + (f.type === "fold" ? f.hiddenCount : 0),
|
|
218
|
+
0,
|
|
219
|
+
);
|
|
220
|
+
expect(totalHidden).toBe(16);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe("DiffView", () => {
|
|
225
|
+
it("supports composing primitives from prepared files", async () => {
|
|
226
|
+
const frame = await renderFrame(
|
|
227
|
+
<DiffRoot
|
|
228
|
+
files={[
|
|
229
|
+
{
|
|
230
|
+
oldName: "before.txt",
|
|
231
|
+
newName: "after.txt",
|
|
232
|
+
additions: 1,
|
|
233
|
+
deletions: 1,
|
|
234
|
+
lines: [
|
|
235
|
+
{
|
|
236
|
+
type: "del",
|
|
237
|
+
content: "before",
|
|
238
|
+
oldLineNumber: 1,
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
type: "add",
|
|
242
|
+
content: "after",
|
|
243
|
+
newLineNumber: 1,
|
|
244
|
+
},
|
|
245
|
+
],
|
|
246
|
+
},
|
|
247
|
+
]}
|
|
248
|
+
>
|
|
249
|
+
<DiffHeader />
|
|
250
|
+
<DiffContent />
|
|
251
|
+
</DiffRoot>,
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
expect(frame).toContain("before.txt");
|
|
255
|
+
expect(frame).toContain("after.txt");
|
|
256
|
+
expect(frame).toContain("+1");
|
|
257
|
+
expect(frame).toContain("-1");
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("renders a basic patch", async () => {
|
|
261
|
+
const frame = await renderFrame(<DiffView patch={SAMPLE_PATCH} />);
|
|
262
|
+
expect(frame).toContain("hello.txt");
|
|
263
|
+
expect(frame).toContain("+");
|
|
264
|
+
expect(frame).toContain("-");
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("renders from oldFile/newFile", async () => {
|
|
268
|
+
const frame = await renderFrame(
|
|
269
|
+
<DiffView
|
|
270
|
+
oldFile={{ content: "hello\nworld\n", name: "test.txt" }}
|
|
271
|
+
newFile={{ content: "hello\nearth\n", name: "test.txt" }}
|
|
272
|
+
/>,
|
|
273
|
+
);
|
|
274
|
+
expect(frame).toContain("test.txt");
|
|
275
|
+
expect(frame).toContain("+");
|
|
276
|
+
expect(frame).toContain("-");
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it("hides line numbers when showLineNumbers=false", async () => {
|
|
280
|
+
const withNumbers = await renderFrame(<DiffView patch={SAMPLE_PATCH} />);
|
|
281
|
+
const withoutNumbers = await renderFrame(
|
|
282
|
+
<DiffView patch={SAMPLE_PATCH} showLineNumbers={false} />,
|
|
283
|
+
);
|
|
284
|
+
expect(withNumbers.length).toBeGreaterThan(withoutNumbers.length);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("truncates with maxLines", async () => {
|
|
288
|
+
const manyLines = Array.from({ length: 50 }, (_, i) => `+line${i}`).join(
|
|
289
|
+
"\n",
|
|
290
|
+
);
|
|
291
|
+
const patch = `diff --git a/big.txt b/big.txt
|
|
292
|
+
--- a/big.txt
|
|
293
|
+
+++ b/big.txt
|
|
294
|
+
@@ -0,0 +1,50 @@
|
|
295
|
+
${manyLines}
|
|
296
|
+
`;
|
|
297
|
+
const frame = await renderFrame(<DiffView patch={patch} maxLines={5} />);
|
|
298
|
+
expect(frame).toContain("more lines");
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it("folds context lines", async () => {
|
|
302
|
+
const normalBefore = Array.from({ length: 10 }, (_, i) => ` line${i}`).join(
|
|
303
|
+
"\n",
|
|
304
|
+
);
|
|
305
|
+
const normalAfter = Array.from({ length: 10 }, (_, i) => ` after${i}`).join(
|
|
306
|
+
"\n",
|
|
307
|
+
);
|
|
308
|
+
const patch = `diff --git a/ctx.txt b/ctx.txt
|
|
309
|
+
--- a/ctx.txt
|
|
310
|
+
+++ b/ctx.txt
|
|
311
|
+
@@ -1,21 +1,22 @@
|
|
312
|
+
${normalBefore}
|
|
313
|
+
+inserted
|
|
314
|
+
${normalAfter}
|
|
315
|
+
`;
|
|
316
|
+
const frame = await renderFrame(
|
|
317
|
+
<DiffView patch={patch} contextLines={2} />,
|
|
318
|
+
);
|
|
319
|
+
expect(frame).toContain("lines hidden");
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("renders multi-file patches", async () => {
|
|
323
|
+
const patch = `diff --git a/a.txt b/a.txt
|
|
324
|
+
--- a/a.txt
|
|
325
|
+
+++ b/a.txt
|
|
326
|
+
@@ -1 +1 @@
|
|
327
|
+
-old a
|
|
328
|
+
+new a
|
|
329
|
+
diff --git a/b.txt b/b.txt
|
|
330
|
+
--- a/b.txt
|
|
331
|
+
+++ b/b.txt
|
|
332
|
+
@@ -1 +1 @@
|
|
333
|
+
-old b
|
|
334
|
+
+new b
|
|
335
|
+
`;
|
|
336
|
+
const frame = await renderFrame(<DiffView patch={patch} />);
|
|
337
|
+
expect(frame).toContain("a.txt");
|
|
338
|
+
expect(frame).toContain("b.txt");
|
|
339
|
+
});
|
|
340
|
+
});
|