mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-17 05:45:51 +00:00
120 lines
3.3 KiB
TypeScript
120 lines
3.3 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'
|
||
|
||
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
|
||
}
|
||
return new ColorFile(code, filePath)
|
||
}, [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>
|
||
)
|
||
}
|