summaryrefslogtreecommitdiff
path: root/packages/cli/src/ui/utils
diff options
context:
space:
mode:
Diffstat (limited to 'packages/cli/src/ui/utils')
-rw-r--r--packages/cli/src/ui/utils/CodeColorizer.tsx55
-rw-r--r--packages/cli/src/ui/utils/MarkdownDisplay.tsx37
-rw-r--r--packages/cli/src/ui/utils/textUtils.ts22
3 files changed, 91 insertions, 23 deletions
diff --git a/packages/cli/src/ui/utils/CodeColorizer.tsx b/packages/cli/src/ui/utils/CodeColorizer.tsx
index f3e7e8eb..f96e6c9a 100644
--- a/packages/cli/src/ui/utils/CodeColorizer.tsx
+++ b/packages/cli/src/ui/utils/CodeColorizer.tsx
@@ -5,7 +5,7 @@
*/
import React from 'react';
-import { Text } from 'ink';
+import { Text, Box } from 'ink';
import { common, createLowlight } from 'lowlight';
import type {
Root,
@@ -16,6 +16,7 @@ import type {
} from 'hast';
import { themeManager } from '../themes/theme-manager.js';
import { Theme } from '../themes/theme.js';
+import { MaxSizedBox } from '../components/shared/MaxSizedBox.js';
// Configure themeing and parsing utilities.
const lowlight = createLowlight(common);
@@ -84,6 +85,8 @@ function renderHastNode(
return null;
}
+const RESERVED_LINES_FOR_TRUNCATION_MESSAGE = 2;
+
/**
* Renders syntax-highlighted code for Ink applications using a selected theme.
*
@@ -94,6 +97,8 @@ function renderHastNode(
export function colorizeCode(
code: string,
language: string | null,
+ availableHeight?: number,
+ maxWidth?: number,
): React.ReactNode {
const codeToHighlight = code.replace(/\n$/, '');
const activeTheme = themeManager.getActiveTheme();
@@ -101,15 +106,33 @@ export function colorizeCode(
try {
// Render the HAST tree using the adapted theme
// Apply the theme's default foreground color to the top-level Text element
- const lines = codeToHighlight.split('\n');
+ let lines = codeToHighlight.split('\n');
const padWidth = String(lines.length).length; // Calculate padding width based on number of lines
+
+ let hiddenLinesCount = 0;
+
+ // Optimizaiton to avoid highlighting lines that cannot possibly be displayed.
+ if (availableHeight && lines.length > availableHeight) {
+ const sliceIndex =
+ lines.length - availableHeight + RESERVED_LINES_FOR_TRUNCATION_MESSAGE;
+ if (sliceIndex > 0) {
+ hiddenLinesCount = sliceIndex;
+ lines = lines.slice(sliceIndex);
+ }
+ }
+
const getHighlightedLines = (line: string) =>
!language || !lowlight.registered(language)
? lowlight.highlightAuto(line)
: lowlight.highlight(language, line);
return (
- <Text>
+ <MaxSizedBox
+ maxHeight={availableHeight}
+ maxWidth={maxWidth}
+ additionalHiddenLinesCount={hiddenLinesCount}
+ overflowDirection="top"
+ >
{lines.map((line, index) => {
const renderedNode = renderHastNode(
getHighlightedLines(line),
@@ -119,16 +142,17 @@ export function colorizeCode(
const contentToRender = renderedNode !== null ? renderedNode : line;
return (
- <Text key={index}>
+ <Box key={index}>
<Text color={activeTheme.colors.Gray}>
- {`${String(index + 1).padStart(padWidth, ' ')} `}
+ {`${String(index + 1 + hiddenLinesCount).padStart(padWidth, ' ')} `}
</Text>
- <Text color={activeTheme.defaultColor}>{contentToRender}</Text>
- {index < lines.length - 1 && '\n'}
- </Text>
+ <Text color={activeTheme.defaultColor} wrap="wrap">
+ {contentToRender}
+ </Text>
+ </Box>
);
})}
- </Text>
+ </MaxSizedBox>
);
} catch (error) {
console.error(
@@ -140,17 +164,20 @@ export function colorizeCode(
const lines = codeToHighlight.split('\n');
const padWidth = String(lines.length).length; // Calculate padding width based on number of lines
return (
- <Text>
+ <MaxSizedBox
+ maxHeight={availableHeight}
+ maxWidth={maxWidth}
+ overflowDirection="top"
+ >
{lines.map((line, index) => (
- <Text key={index}>
+ <Box key={index}>
<Text color={activeTheme.defaultColor}>
{`${String(index + 1).padStart(padWidth, ' ')} `}
</Text>
<Text color={activeTheme.colors.Gray}>{line}</Text>
- {index < lines.length - 1 && '\n'}
- </Text>
+ </Box>
))}
- </Text>
+ </MaxSizedBox>
);
}
}
diff --git a/packages/cli/src/ui/utils/MarkdownDisplay.tsx b/packages/cli/src/ui/utils/MarkdownDisplay.tsx
index 1eda45d3..d78360b5 100644
--- a/packages/cli/src/ui/utils/MarkdownDisplay.tsx
+++ b/packages/cli/src/ui/utils/MarkdownDisplay.tsx
@@ -12,7 +12,8 @@ import { colorizeCode } from './CodeColorizer.js';
interface MarkdownDisplayProps {
text: string;
isPending: boolean;
- availableTerminalHeight: number;
+ availableTerminalHeight?: number;
+ terminalWidth: number;
}
// Constants for Markdown parsing and rendering
@@ -32,6 +33,7 @@ const MarkdownDisplayInternal: React.FC<MarkdownDisplayProps> = ({
text,
isPending,
availableTerminalHeight,
+ terminalWidth,
}) => {
if (!text) return <></>;
@@ -65,6 +67,7 @@ const MarkdownDisplayInternal: React.FC<MarkdownDisplayProps> = ({
lang={codeBlockLang}
isPending={isPending}
availableTerminalHeight={availableTerminalHeight}
+ terminalWidth={terminalWidth}
/>,
);
inCodeBlock = false;
@@ -186,6 +189,7 @@ const MarkdownDisplayInternal: React.FC<MarkdownDisplayProps> = ({
lang={codeBlockLang}
isPending={isPending}
availableTerminalHeight={availableTerminalHeight}
+ terminalWidth={terminalWidth}
/>,
);
}
@@ -336,7 +340,8 @@ interface RenderCodeBlockProps {
content: string[];
lang: string | null;
isPending: boolean;
- availableTerminalHeight: number;
+ availableTerminalHeight?: number;
+ terminalWidth: number;
}
const RenderCodeBlockInternal: React.FC<RenderCodeBlockProps> = ({
@@ -344,15 +349,17 @@ const RenderCodeBlockInternal: React.FC<RenderCodeBlockProps> = ({
lang,
isPending,
availableTerminalHeight,
+ terminalWidth,
}) => {
const MIN_LINES_FOR_MESSAGE = 1; // Minimum lines to show before the "generating more" message
const RESERVED_LINES = 2; // Lines reserved for the message itself and potential padding
- const MAX_CODE_LINES_WHEN_PENDING = Math.max(
- 0,
- availableTerminalHeight - CODE_BLOCK_PADDING * 2 - RESERVED_LINES,
- );
- if (isPending) {
+ if (isPending && availableTerminalHeight !== undefined) {
+ const MAX_CODE_LINES_WHEN_PENDING = Math.max(
+ 0,
+ availableTerminalHeight - CODE_BLOCK_PADDING * 2 - RESERVED_LINES,
+ );
+
if (content.length > MAX_CODE_LINES_WHEN_PENDING) {
if (MAX_CODE_LINES_WHEN_PENDING < MIN_LINES_FOR_MESSAGE) {
// Not enough space to even show the message meaningfully
@@ -366,6 +373,8 @@ const RenderCodeBlockInternal: React.FC<RenderCodeBlockProps> = ({
const colorizedTruncatedCode = colorizeCode(
truncatedContent.join('\n'),
lang,
+ availableTerminalHeight,
+ terminalWidth - CODE_BLOCK_PADDING * 2,
);
return (
<Box flexDirection="column" padding={CODE_BLOCK_PADDING}>
@@ -377,10 +386,20 @@ const RenderCodeBlockInternal: React.FC<RenderCodeBlockProps> = ({
}
const fullContent = content.join('\n');
- const colorizedCode = colorizeCode(fullContent, lang);
+ const colorizedCode = colorizeCode(
+ fullContent,
+ lang,
+ availableTerminalHeight,
+ terminalWidth - CODE_BLOCK_PADDING * 2,
+ );
return (
- <Box flexDirection="column" padding={CODE_BLOCK_PADDING}>
+ <Box
+ flexDirection="column"
+ padding={CODE_BLOCK_PADDING}
+ width={terminalWidth}
+ flexShrink={0}
+ >
{colorizedCode}
</Box>
);
diff --git a/packages/cli/src/ui/utils/textUtils.ts b/packages/cli/src/ui/utils/textUtils.ts
index 35e4c4a2..f7006047 100644
--- a/packages/cli/src/ui/utils/textUtils.ts
+++ b/packages/cli/src/ui/utils/textUtils.ts
@@ -45,3 +45,25 @@ export function isBinary(
// If no NULL bytes were found in the sample, we assume it's text.
return false;
}
+
+/*
+ * -------------------------------------------------------------------------
+ * Unicode‑aware helpers (work at the code‑point level rather than UTF‑16
+ * code units so that surrogate‑pair emoji count as one "column".)
+ * ---------------------------------------------------------------------- */
+
+export function toCodePoints(str: string): string[] {
+ // [...str] or Array.from both iterate by UTF‑32 code point, handling
+ // surrogate pairs correctly.
+ return Array.from(str);
+}
+
+export function cpLen(str: string): number {
+ return toCodePoints(str).length;
+}
+
+export function cpSlice(str: string, start: number, end?: number): string {
+ // Slice by code‑point indices and re‑join.
+ const arr = toCodePoints(str).slice(start, end);
+ return arr.join('');
+}