Support ink scrolling final pr (#12567)

This commit is contained in:
Jacob Richman
2025-11-11 07:50:11 -08:00
committed by GitHub
parent 7bb13d1c41
commit cbbf565121
43 changed files with 2498 additions and 1568 deletions

View File

@@ -119,28 +119,38 @@ export function colorizeLine(
return highlightAndRenderLine(line, language, activeTheme);
}
export interface ColorizeCodeOptions {
code: string;
language?: string | null;
availableHeight?: number;
maxWidth: number;
theme?: Theme | null;
settings: LoadedSettings;
hideLineNumbers?: boolean;
}
/**
* Renders syntax-highlighted code for Ink applications using a selected theme.
*
* @param code The code string to highlight.
* @param language The language identifier (e.g., 'javascript', 'css', 'html')
* @param options The options for colorizing the code.
* @returns A React.ReactNode containing Ink <Text> elements for the highlighted code.
*/
export function colorizeCode(
code: string,
language: string | null,
availableHeight?: number,
maxWidth?: number,
theme?: Theme,
settings?: LoadedSettings,
hideLineNumbers?: boolean,
): React.ReactNode {
export function colorizeCode({
code,
language = null,
availableHeight,
maxWidth,
theme = null,
settings,
hideLineNumbers = false,
}: ColorizeCodeOptions): React.ReactNode {
const codeToHighlight = code.replace(/\n$/, '');
const activeTheme = theme || themeManager.getActiveTheme();
const showLineNumbers = hideLineNumbers
? false
: (settings?.merged.ui?.showLineNumbers ?? true);
const useMaxSizedBox = settings?.merged.ui?.useAlternateBuffer !== true;
try {
// Render the HAST tree using the adapted theme
// Apply the theme's default foreground color to the top-level Text element
@@ -150,7 +160,10 @@ export function colorizeCode(
let hiddenLinesCount = 0;
// Optimization to avoid highlighting lines that cannot possibly be displayed.
if (availableHeight !== undefined) {
if (
availableHeight !== undefined &&
settings?.merged.ui?.useAlternateBuffer === false
) {
availableHeight = Math.max(availableHeight, MINIMUM_MAX_HEIGHT);
if (lines.length > availableHeight) {
const sliceIndex = lines.length - availableHeight;
@@ -159,37 +172,61 @@ export function colorizeCode(
}
}
return (
<MaxSizedBox
maxHeight={availableHeight}
maxWidth={maxWidth}
additionalHiddenLinesCount={hiddenLinesCount}
overflowDirection="top"
>
{lines.map((line, index) => {
const contentToRender = highlightAndRenderLine(
line,
language,
activeTheme,
);
const renderedLines = lines.map((line, index) => {
const contentToRender = highlightAndRenderLine(
line,
language,
activeTheme,
);
return (
<Box key={index}>
{showLineNumbers && (
<Text color={activeTheme.colors.Gray}>
{`${String(index + 1 + hiddenLinesCount).padStart(
padWidth,
' ',
)} `}
</Text>
)}
<Text color={activeTheme.defaultColor} wrap="wrap">
{contentToRender}
return (
<Box key={index}>
{/* We have to render line numbers differently depending on whether we are using MaxSizeBox or not */}
{showLineNumbers && useMaxSizedBox && (
<Text color={activeTheme.colors.Gray}>
{`${String(index + 1 + hiddenLinesCount).padStart(
padWidth,
' ',
)} `}
</Text>
)}
{showLineNumbers && !useMaxSizedBox && (
<Box
minWidth={padWidth + 1}
flexShrink={0}
paddingRight={1}
alignItems="flex-start"
justifyContent="flex-end"
>
<Text color={activeTheme.colors.Gray}>
{`${index + 1 + hiddenLinesCount}`}
</Text>
</Box>
);
})}
</MaxSizedBox>
)}
<Text color={activeTheme.defaultColor} wrap="wrap">
{contentToRender}
</Text>
</Box>
);
});
if (useMaxSizedBox) {
return (
<MaxSizedBox
maxHeight={availableHeight}
maxWidth={maxWidth}
additionalHiddenLinesCount={hiddenLinesCount}
overflowDirection="top"
>
{renderedLines}
</MaxSizedBox>
);
}
return (
<Box flexDirection="column" width={maxWidth}>
{renderedLines}
</Box>
);
} catch (error) {
debugLogger.warn(
@@ -200,23 +237,45 @@ export function colorizeCode(
// Also display line numbers in fallback
const lines = codeToHighlight.split('\n');
const padWidth = String(lines.length).length; // Calculate padding width based on number of lines
return (
<MaxSizedBox
maxHeight={availableHeight}
maxWidth={maxWidth}
overflowDirection="top"
>
{lines.map((line, index) => (
<Box key={index}>
{showLineNumbers && (
<Text color={activeTheme.defaultColor}>
{`${String(index + 1).padStart(padWidth, ' ')} `}
</Text>
)}
<Text color={activeTheme.colors.Gray}>{line}</Text>
const fallbackLines = lines.map((line, index) => (
<Box key={index}>
{/* We have to render line numbers differently depending on whether we are using MaxSizeBox or not */}
{showLineNumbers && useMaxSizedBox && (
<Text color={activeTheme.defaultColor}>
{`${String(index + 1).padStart(padWidth, ' ')} `}
</Text>
)}
{showLineNumbers && !useMaxSizedBox && (
<Box
minWidth={padWidth + 1}
flexShrink={0}
paddingRight={1}
alignItems="flex-start"
justifyContent="flex-end"
>
<Text color={activeTheme.defaultColor}>{`${index + 1}`}</Text>
</Box>
))}
</MaxSizedBox>
)}
<Text color={activeTheme.colors.Gray}>{line}</Text>
</Box>
));
if (useMaxSizedBox) {
return (
<MaxSizedBox
maxHeight={availableHeight}
maxWidth={maxWidth}
overflowDirection="top"
>
{fallbackLines}
</MaxSizedBox>
);
}
return (
<Box flexDirection="column" width={maxWidth}>
{fallbackLines}
</Box>
);
}
}

View File

@@ -11,6 +11,7 @@ import { colorizeCode } from './CodeColorizer.js';
import { TableRenderer } from './TableRenderer.js';
import { RenderInline } from './InlineMarkdownRenderer.js';
import { useSettings } from '../contexts/SettingsContext.js';
import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js';
interface MarkdownDisplayProps {
text: string;
@@ -35,6 +36,7 @@ const MarkdownDisplayInternal: React.FC<MarkdownDisplayProps> = ({
renderMarkdown = true,
}) => {
const settings = useSettings();
const isAlternateBuffer = useAlternateBuffer();
const responseColor = theme.text.response ?? theme.text.primary;
if (!text) return <></>;
@@ -42,15 +44,14 @@ const MarkdownDisplayInternal: React.FC<MarkdownDisplayProps> = ({
// Raw markdown mode - display syntax-highlighted markdown without rendering
if (!renderMarkdown) {
// Hide line numbers in raw markdown mode as they are confusing due to chunked output
const colorizedMarkdown = colorizeCode(
text,
'markdown',
availableTerminalHeight,
terminalWidth - CODE_BLOCK_PREFIX_PADDING,
undefined,
const colorizedMarkdown = colorizeCode({
code: text,
language: 'markdown',
availableHeight: isAlternateBuffer ? undefined : availableTerminalHeight,
maxWidth: terminalWidth - CODE_BLOCK_PREFIX_PADDING,
settings,
true, // hideLineNumbers
);
hideLineNumbers: true,
});
return (
<Box paddingLeft={CODE_BLOCK_PREFIX_PADDING} flexDirection="column">
{colorizedMarkdown}
@@ -100,7 +101,9 @@ const MarkdownDisplayInternal: React.FC<MarkdownDisplayProps> = ({
content={codeBlockContent}
lang={codeBlockLang}
isPending={isPending}
availableTerminalHeight={availableTerminalHeight}
availableTerminalHeight={
isAlternateBuffer ? undefined : availableTerminalHeight
}
terminalWidth={terminalWidth}
/>,
);
@@ -288,7 +291,9 @@ const MarkdownDisplayInternal: React.FC<MarkdownDisplayProps> = ({
content={codeBlockContent}
lang={codeBlockLang}
isPending={isPending}
availableTerminalHeight={availableTerminalHeight}
availableTerminalHeight={
isAlternateBuffer ? undefined : availableTerminalHeight
}
terminalWidth={terminalWidth}
/>,
);
@@ -327,10 +332,17 @@ const RenderCodeBlockInternal: React.FC<RenderCodeBlockProps> = ({
terminalWidth,
}) => {
const settings = useSettings();
const isAlternateBuffer = useAlternateBuffer();
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
if (isPending && availableTerminalHeight !== undefined) {
// When not in alternate buffer mode we need to be careful that we don't
// trigger flicker when the pending code is to long to fit in the terminal
if (
!isAlternateBuffer &&
isPending &&
availableTerminalHeight !== undefined
) {
const MAX_CODE_LINES_WHEN_PENDING = Math.max(
0,
availableTerminalHeight - RESERVED_LINES,
@@ -348,14 +360,13 @@ const RenderCodeBlockInternal: React.FC<RenderCodeBlockProps> = ({
);
}
const truncatedContent = content.slice(0, MAX_CODE_LINES_WHEN_PENDING);
const colorizedTruncatedCode = colorizeCode(
truncatedContent.join('\n'),
lang,
availableTerminalHeight,
terminalWidth - CODE_BLOCK_PREFIX_PADDING,
undefined,
const colorizedTruncatedCode = colorizeCode({
code: truncatedContent.join('\n'),
language: lang,
availableHeight: availableTerminalHeight,
maxWidth: terminalWidth - CODE_BLOCK_PREFIX_PADDING,
settings,
);
});
return (
<Box paddingLeft={CODE_BLOCK_PREFIX_PADDING} flexDirection="column">
{colorizedTruncatedCode}
@@ -366,14 +377,13 @@ const RenderCodeBlockInternal: React.FC<RenderCodeBlockProps> = ({
}
const fullContent = content.join('\n');
const colorizedCode = colorizeCode(
fullContent,
lang,
availableTerminalHeight,
terminalWidth - CODE_BLOCK_PREFIX_PADDING,
undefined,
const colorizedCode = colorizeCode({
code: fullContent,
language: lang,
availableHeight: isAlternateBuffer ? undefined : availableTerminalHeight,
maxWidth: terminalWidth - CODE_BLOCK_PREFIX_PADDING,
settings,
);
});
return (
<Box

View File

@@ -25,7 +25,12 @@ const getMainAreaWidthInternal = (terminalWidth: number): number => {
export const calculateMainAreaWidth = (
terminalWidth: number,
settings: LoadedSettings,
): number =>
settings.merged.ui?.useFullWidth
? terminalWidth
: getMainAreaWidthInternal(terminalWidth);
): number => {
if (settings.merged.ui?.useFullWidth) {
if (settings.merged.ui?.useAlternateBuffer) {
return terminalWidth - 1;
}
return terminalWidth;
}
return getMainAreaWidthInternal(terminalWidth);
};