Files
claude-code/src/components/HighlightedCode.tsx
claude-code-best ef10ad2839 fix: 优化内存峰值与 CPU 性能,降低 100-300MB 内存占用
- claude.ts: 流式字符串拼接从 O(n²) += 改为数组累积 join,消除 4 处热点
- Messages.tsx: 合并 3 组独立遍历为单次 pass(thinking/bash 查找、3-filter 链、divider/selectedIdx)
- HighlightedCode.tsx: ColorFile 实例添加模块级 LRU 缓存(50 条),避免重复创建
- screen.ts: StylePool 衍生缓存添加 1000 条上限淘汰,防止无界增长
- CompanionSprite.tsx: TICK_MS 从 500ms 提升至 1000ms,减少 setState 频率
- connection.ts: MCP stderr 缓冲从 64MB 降至 8MB
- stringUtils.ts: MAX_STRING_LENGTH 从 32MB 降至 2MB
- sessionStorage.ts: Transcript 写入队列添加 1000 条上限
- query.ts: spread 改 concat 减少一次数组拷贝
- PromptInputFooterLeftSide.tsx: 显示进程 pid 便于调试

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 00:45:03 +08:00

129 lines
4.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import * as React from 'react';
import { memo, useEffect, useMemo, useRef, useState } from 'react';
import { useSettings } from '../hooks/useSettings.js';
import { Ansi, Box, type DOMElement, measureElement, NoSelect, Text, useTheme } from '@anthropic/ink';
import { isFullscreenEnvEnabled } from '../utils/fullscreen.js';
import sliceAnsi from '../utils/sliceAnsi.js';
import { countCharInString } from '../utils/stringUtils.js';
import { HighlightedCodeFallback } from './HighlightedCode/Fallback.js';
import { expectColorFile } from './StructuredDiff/colorDiff.js';
import type { ColorFile as ColorFileType } from 'color-diff-napi';
// Module-level LRU cache for ColorFile instances to avoid recreating
// them for the same (filePath, code) across component instances.
const colorFileCache = new Map<string, { colorFile: ColorFileType; code: string }>();
const COLOR_FILE_CACHE_MAX = 50;
type Props = {
code: string;
filePath: string;
width?: number;
dim?: boolean;
};
const DEFAULT_WIDTH = 80;
export const HighlightedCode = memo(function HighlightedCode({
code,
filePath,
width,
dim = false,
}: Props): React.ReactElement {
const ref = useRef<DOMElement>(null);
const [measuredWidth, setMeasuredWidth] = useState(width || DEFAULT_WIDTH);
const [theme] = useTheme();
const settings = useSettings();
const syntaxHighlightingDisabled = settings.syntaxHighlightingDisabled ?? false;
const colorFile = useMemo(() => {
if (syntaxHighlightingDisabled) {
return null;
}
const ColorFile = expectColorFile();
if (!ColorFile) {
return null;
}
const cacheKey = `${filePath}\0${code.length}`;
const cached = colorFileCache.get(cacheKey);
if (cached && cached.code === code) {
// Move to end (most recently used)
colorFileCache.delete(cacheKey);
colorFileCache.set(cacheKey, cached);
return cached.colorFile;
}
const instance = new ColorFile(code, filePath);
// Evict oldest entry if cache is full
if (colorFileCache.size >= COLOR_FILE_CACHE_MAX) {
const oldest = colorFileCache.keys().next().value;
if (oldest !== undefined) colorFileCache.delete(oldest);
}
colorFileCache.set(cacheKey, { colorFile: instance, code });
return instance;
}, [code, filePath, syntaxHighlightingDisabled]);
useEffect(() => {
if (!width && ref.current) {
const { width: elementWidth } = measureElement(ref.current);
if (elementWidth > 0) {
setMeasuredWidth(elementWidth - 2);
}
}
}, [width]);
const lines = useMemo(() => {
if (colorFile === null) {
return null;
}
return colorFile.render(theme, measuredWidth, dim);
}, [colorFile, theme, measuredWidth, dim]);
// Gutter width matches ColorFile's layout in lib.rs: space + right-aligned
// line number (max_digits = lineCount.toString().length) + space. No marker
// column like the diff path. Wrap in <NoSelect> so fullscreen selection
// yields clean code without line numbers. Only split in fullscreen mode
// (~4× DOM nodes + sliceAnsi cost); non-fullscreen uses terminal-native
// selection where noSelect is meaningless.
const gutterWidth = useMemo(() => {
if (!isFullscreenEnvEnabled()) return 0;
const lineCount = countCharInString(code, '\n') + 1;
return lineCount.toString().length + 2;
}, [code]);
return (
<Box ref={ref}>
{lines ? (
<Box flexDirection="column">
{lines.map((line, i) =>
gutterWidth > 0 ? (
<CodeLine key={i} line={line} gutterWidth={gutterWidth} />
) : (
<Text key={i}>
<Ansi>{line}</Ansi>
</Text>
),
)}
</Box>
) : (
<HighlightedCodeFallback code={code} filePath={filePath} dim={dim} skipColoring={syntaxHighlightingDisabled} />
)}
</Box>
);
});
function CodeLine({ line, gutterWidth }: { line: string; gutterWidth: number }): React.ReactNode {
const gutter = sliceAnsi(line, 0, gutterWidth);
const content = sliceAnsi(line, gutterWidth);
return (
<Box flexDirection="row">
<NoSelect fromLeftEdge>
<Text>
<Ansi>{gutter}</Ansi>
</Text>
</NoSelect>
<Text>
<Ansi>{content}</Ansi>
</Text>
</Box>
);
}