Skip to content

Commit c92d12d

Browse files
support codeview component for cli pkg
1 parent 10aaa1b commit c92d12d

22 files changed

+1157
-27
lines changed

packages/cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
"directory": "packages/cli"
2020
},
2121
"scripts": {
22-
"dev": "DEV=true node ./test/index.mjs",
22+
"dev": "DEV=true node ./test/file.mjs",
2323
"gen:color": "node ./gen.mjs",
2424
"gen:type": "dts-bundle-generator -o index.d.ts dist/types/index.d.ts"
2525
},
Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
/**
2+
* CodeContent component - renders code with optional syntax highlighting.
3+
* Simplified version of DiffContent without diff-specific features.
4+
*/
5+
import { Box, Text } from "ink";
6+
import * as React from "react";
7+
8+
import { buildAnsiStringWithLineBreaks, buildStyledBlock, type CharStyle } from "./ansiString";
9+
import { useCodeViewContext } from "./CodeViewContext";
10+
import { diffPlainContent } from "./color";
11+
import { getStyleObjectFromString, getStyleFromClassName } from "./DiffContent";
12+
13+
import type { File } from "@git-diff-view/core";
14+
15+
// Helper to get tab width value
16+
const getTabWidthValue = (tabWidth: "small" | "medium" | "large"): number => {
17+
return tabWidth === "small" ? 1 : tabWidth === "medium" ? 2 : 4;
18+
};
19+
20+
// Process a string into styled characters for ANSI output
21+
const processCharsForAnsi = (
22+
str: string,
23+
enableTabSpace: boolean,
24+
tabWidth: "small" | "medium" | "large",
25+
baseStyle: CharStyle
26+
): Array<{ char: string; style?: CharStyle }> => {
27+
const result: Array<{ char: string; style?: CharStyle }> = [];
28+
const tabWidthValue = getTabWidthValue(tabWidth);
29+
30+
for (const char of str) {
31+
if (enableTabSpace && char === " ") {
32+
// Show space as dimmed dot
33+
result.push({ char: "\u00b7", style: { ...baseStyle, dim: true } });
34+
} else if (char === "\t") {
35+
if (enableTabSpace) {
36+
// Show tab as arrow followed by spaces
37+
result.push({ char: "\u2192", style: { ...baseStyle, dim: true } });
38+
for (let i = 1; i < tabWidthValue; i++) {
39+
result.push({ char: " ", style: baseStyle });
40+
}
41+
} else {
42+
// Just show spaces for tab
43+
for (let i = 0; i < tabWidthValue; i++) {
44+
result.push({ char: " ", style: baseStyle });
45+
}
46+
}
47+
} else {
48+
result.push({ char, style: baseStyle });
49+
}
50+
}
51+
52+
return result;
53+
};
54+
55+
/**
56+
* CodeString component using ANSI escape codes for proper character-level wrapping.
57+
*/
58+
const CodeString = React.memo(({ bg, width, rawLine }: { bg: string; width: number; rawLine: string }) => {
59+
const { useCodeContext } = useCodeViewContext();
60+
61+
const { enableTabSpace, tabWidth } = useCodeContext((s) => ({ enableTabSpace: s.tabSpace, tabWidth: s.tabWidth }));
62+
63+
// Memoize the ANSI content to avoid rebuilding on every render
64+
const ansiContent = React.useMemo(() => {
65+
const chars: Array<{ char: string; style?: CharStyle }> = [];
66+
const baseStyle: CharStyle = { backgroundColor: bg };
67+
68+
// Process the whole line
69+
chars.push(...processCharsForAnsi(rawLine, enableTabSpace, tabWidth, baseStyle));
70+
71+
return buildAnsiStringWithLineBreaks(chars, width);
72+
}, [bg, width, rawLine, enableTabSpace, tabWidth]);
73+
74+
return (
75+
<Box width={width} backgroundColor={bg}>
76+
<Text wrap="truncate">{ansiContent}</Text>
77+
</Box>
78+
);
79+
});
80+
81+
CodeString.displayName = "CodeString";
82+
83+
/**
84+
* Helper function to process syntax-highlighted characters for ANSI output.
85+
*/
86+
const processSyntaxCharsForAnsi = (
87+
str: string,
88+
enableTabSpace: boolean,
89+
tabWidth: "small" | "medium" | "large",
90+
baseStyle: CharStyle,
91+
syntaxColor?: string
92+
): Array<{ char: string; style?: CharStyle }> => {
93+
const result: Array<{ char: string; style?: CharStyle }> = [];
94+
const tabWidthValue = getTabWidthValue(tabWidth);
95+
96+
for (const char of str) {
97+
const style: CharStyle = {
98+
...baseStyle,
99+
color: syntaxColor || baseStyle.color,
100+
};
101+
102+
if (enableTabSpace && char === " ") {
103+
result.push({ char: "\u00b7", style: { ...style, dim: true } });
104+
} else if (char === "\t") {
105+
if (enableTabSpace) {
106+
result.push({ char: "\u2192", style: { ...style, dim: true } });
107+
for (let i = 1; i < tabWidthValue; i++) {
108+
result.push({ char: " ", style });
109+
}
110+
} else {
111+
for (let i = 0; i < tabWidthValue; i++) {
112+
result.push({ char: " ", style });
113+
}
114+
}
115+
} else {
116+
result.push({ char, style });
117+
}
118+
}
119+
120+
return result;
121+
};
122+
123+
/**
124+
* CodeSyntax component using ANSI escape codes for proper character-level wrapping
125+
* with syntax highlighting support.
126+
*/
127+
const CodeSyntax = React.memo(
128+
({
129+
bg,
130+
width,
131+
theme,
132+
rawLine,
133+
syntaxLine,
134+
}: {
135+
bg: string;
136+
width: number;
137+
theme: "light" | "dark";
138+
rawLine: string;
139+
syntaxLine?: File["syntaxFile"][number];
140+
}) => {
141+
const { useCodeContext } = useCodeViewContext();
142+
143+
const { enableTabSpace, tabWidth } = useCodeContext((s) => ({ enableTabSpace: s.tabSpace, tabWidth: s.tabWidth }));
144+
145+
// Memoize the ANSI content with syntax highlighting
146+
const ansiContent = React.useMemo(() => {
147+
if (!syntaxLine) {
148+
return null; // Will render CodeString instead
149+
}
150+
151+
const chars: Array<{ char: string; style?: CharStyle }> = [];
152+
const baseStyle: CharStyle = { backgroundColor: bg };
153+
154+
for (const { node, wrapper } of syntaxLine.nodeList || []) {
155+
// Get syntax color from lowlight or shiki
156+
const lowlightStyles = getStyleFromClassName(wrapper?.properties?.className?.join(" ") || "");
157+
const lowlightStyle = theme === "dark" ? lowlightStyles.dark : lowlightStyles.light;
158+
const shikiStyles = getStyleObjectFromString(wrapper?.properties?.style || "");
159+
const shikiStyle = theme === "dark" ? shikiStyles.dark : shikiStyles.light;
160+
161+
// Determine the syntax color (shiki style takes precedence)
162+
const syntaxColor = (shikiStyle as { color?: string })?.color || (lowlightStyle as { color?: string })?.color;
163+
164+
chars.push(
165+
...processSyntaxCharsForAnsi(node.value, enableTabSpace, tabWidth, { ...baseStyle, color: syntaxColor })
166+
);
167+
}
168+
169+
return buildAnsiStringWithLineBreaks(chars, width);
170+
}, [bg, width, theme, rawLine, syntaxLine, enableTabSpace, tabWidth]);
171+
172+
// Fallback to CodeString if no syntax line
173+
if (!syntaxLine) {
174+
return <CodeString bg={bg} width={width} rawLine={rawLine} />;
175+
}
176+
177+
return (
178+
<Box width={width} backgroundColor={bg}>
179+
<Text wrap="truncate">{ansiContent}</Text>
180+
</Box>
181+
);
182+
}
183+
);
184+
185+
CodeSyntax.displayName = "CodeSyntax";
186+
187+
/**
188+
* CodePadding component - Renders a 1-char padding column
189+
* using chalk for proper multi-row support.
190+
*/
191+
const CodePadding = React.memo(({ height, backgroundColor }: { height: number; backgroundColor: string }) => {
192+
const content = React.useMemo(() => {
193+
const lines: string[] = [];
194+
const style: CharStyle = { backgroundColor };
195+
196+
for (let row = 0; row < height; row++) {
197+
lines.push(buildStyledBlock(" ", 1, 1, style, "left"));
198+
}
199+
200+
return lines.join("\n");
201+
}, [height, backgroundColor]);
202+
203+
return (
204+
<Box width={1} flexShrink={0}>
205+
<Text wrap="truncate">{content}</Text>
206+
</Box>
207+
);
208+
});
209+
210+
CodePadding.displayName = "CodePadding";
211+
212+
export const CodeContent = React.memo(
213+
({
214+
theme,
215+
width,
216+
height,
217+
rawLine,
218+
syntaxLine,
219+
enableHighlight,
220+
}: {
221+
width: number;
222+
height: number;
223+
theme: "light" | "dark";
224+
rawLine: string;
225+
plainLine?: File["plainFile"][number];
226+
syntaxLine?: File["syntaxFile"][number];
227+
enableHighlight: boolean;
228+
}) => {
229+
const isMaxLineLengthToIgnoreSyntax = syntaxLine?.nodeList?.length > 150;
230+
231+
// Background color for normal code
232+
const bg = React.useMemo(() => {
233+
return theme === "light" ? diffPlainContent.light : diffPlainContent.dark;
234+
}, [theme]);
235+
236+
// Content width is total width minus 2 char padding (1 on each side)
237+
const contentWidth = width - 2;
238+
239+
return (
240+
<Box height={height} width={width}>
241+
<CodePadding height={height} backgroundColor={bg} />
242+
{enableHighlight && syntaxLine && !isMaxLineLengthToIgnoreSyntax ? (
243+
<CodeSyntax bg={bg} theme={theme} width={contentWidth} rawLine={rawLine} syntaxLine={syntaxLine} />
244+
) : (
245+
<CodeString bg={bg} width={contentWidth} rawLine={rawLine} />
246+
)}
247+
<CodePadding height={height} backgroundColor={bg} />
248+
</Box>
249+
);
250+
}
251+
);
252+
253+
CodeContent.displayName = "CodeContent";
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { Box, Text } from "ink";
2+
import * as React from "react";
3+
4+
import { useCodeViewContext } from "./CodeViewContext";
5+
6+
import type { File } from "@git-diff-view/core";
7+
8+
const InternalCodeExtendLine = ({
9+
columns,
10+
lineNumber,
11+
lineExtend,
12+
file,
13+
}: {
14+
columns: number;
15+
lineNumber: number;
16+
lineExtend: { data: any };
17+
file: File;
18+
}) => {
19+
const { useCodeContext } = useCodeViewContext();
20+
21+
const renderExtendLine = useCodeContext((s) => s.renderExtendLine);
22+
23+
if (!renderExtendLine) return null;
24+
25+
const extendRendered =
26+
lineExtend?.data &&
27+
renderExtendLine?.({
28+
file,
29+
lineNumber,
30+
data: lineExtend.data,
31+
});
32+
33+
return (
34+
<Box data-line={`${lineNumber}-extend`} data-state="extend" width={columns}>
35+
{React.isValidElement(extendRendered) ? extendRendered : <Text>{extendRendered}</Text>}
36+
</Box>
37+
);
38+
};
39+
40+
export const CodeExtendLine = ({ columns, lineNumber, file }: { columns: number; lineNumber: number; file: File }) => {
41+
const { useCodeContext } = useCodeViewContext();
42+
43+
const lineExtend = useCodeContext(React.useCallback((s) => s.extendData?.[lineNumber], [lineNumber]));
44+
45+
if (!lineExtend?.data) return null;
46+
47+
return <InternalCodeExtendLine columns={columns} lineNumber={lineNumber} lineExtend={lineExtend} file={file} />;
48+
};
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/**
2+
* CodeLineNumber component - Renders line numbers with proper multi-row support
3+
* using chalk for ANSI styling.
4+
*
5+
* Simplified version of DiffLineNumber for code view.
6+
*/
7+
import { Box, Text } from "ink";
8+
import * as React from "react";
9+
10+
import { buildStyledBlock, type CharStyle } from "./ansiString";
11+
12+
/**
13+
* Renders a single line number area for code view.
14+
* Format: [ ][lineNum][ ]
15+
*/
16+
export const CodeLineNumberArea: React.FC<{
17+
lineNumber: number;
18+
lineNumWidth: number;
19+
height: number;
20+
backgroundColor: string;
21+
color: string;
22+
dim?: boolean;
23+
}> = React.memo(({ lineNumber, lineNumWidth, height, backgroundColor, color, dim = false }) => {
24+
// Total width: leftPad + num + rightPad = 1 + lineNumWidth + 1
25+
const totalWidth = lineNumWidth + 2;
26+
27+
const content = React.useMemo(() => {
28+
const style: CharStyle = { backgroundColor, color, dim };
29+
const lines: string[] = [];
30+
31+
for (let row = 0; row < height; row++) {
32+
// Left padding + line number (right-aligned) + right padding
33+
const numPart = row === 0 ? lineNumber.toString().padStart(lineNumWidth) : " ".repeat(lineNumWidth);
34+
const lineText = ` ${numPart} `;
35+
lines.push(buildStyledBlock(lineText, totalWidth, 1, style, "left"));
36+
}
37+
38+
return lines.join("\n");
39+
}, [lineNumber, lineNumWidth, height, backgroundColor, color, dim, totalWidth]);
40+
41+
return (
42+
<Box width={totalWidth} flexShrink={0}>
43+
<Text wrap="truncate">{content}</Text>
44+
</Box>
45+
);
46+
});
47+
48+
CodeLineNumberArea.displayName = "CodeLineNumberArea";

0 commit comments

Comments
 (0)