mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-20 16:57:46 +00:00
Fix flicker issues by ensuring all actively changing content fits in the viewport (#1217)
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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('');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user