diff options
Diffstat (limited to 'packages/cli/src/ui/utils')
| -rw-r--r-- | packages/cli/src/ui/utils/CodeColorizer.tsx | 55 | ||||
| -rw-r--r-- | packages/cli/src/ui/utils/MarkdownDisplay.tsx | 37 | ||||
| -rw-r--r-- | packages/cli/src/ui/utils/textUtils.ts | 22 |
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(''); +} |
