mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-15 12:55:51 +00:00
- 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>
129 lines
4.2 KiB
TypeScript
129 lines
4.2 KiB
TypeScript
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>
|
||
);
|
||
}
|