import { useCallback, useEffect, useState, useMemo, useRef } from "react"; import { type TextInputKeyPressEvent, Text as NativeText, ScrollView, TextInput, Platform, View } from "react-native"; import type ICodeEditorProps from "./type"; import stylesheet, { useStyles } from "./stylesheet"; import { NCoreUIKitTheme } from "../../core/hooks"; import type { TokenType } from "../codeViewer/type"; import { splitTokensIntoLines, parseCode } from "../codeViewer/parser"; const CodeEditor = ({ isLineNumberVisible = true, onChangeText, containerStyle, language, value, style, ...props }: ICodeEditorProps) => { const { typography, colors, spaces } = NCoreUIKitTheme.useContext(); const inputRef = useRef(null); const [ selection, setSelection ] = useState<{ end: number; start: number }>({ end: 0, start: 0 }); const tokens = useMemo(() => parseCode(value, language), [ language, value ]); const lines = useMemo(() => { return splitTokensIntoLines(tokens); }, [tokens]); const { languageText: languageTextDynamicStyle, editorInput: editorInputDynamicStyle, container: containerDynamicStyle, lineRow: lineRowDynamicStyle, gutter: gutterDynamicStyle, text: textDynamicStyle } = useStyles({ isLineNumberVisible, hasLanguage: !!language, typography, colors, spaces }); const getTokenColor = (type: TokenType) => { return colors.code[type as keyof typeof colors.code] || colors.code.default; }; const handleKeyPress = (e: TextInputKeyPressEvent) => { if (e.nativeEvent.key === "Tab") { e.preventDefault(); const before = value.substring(0, selection.start); const after = value.substring(selection.end); const newText = before + " " + after; onChangeText?.(newText); } }; useEffect(() => { if (Platform.OS === "web" && inputRef.current) { const node = inputRef.current as unknown as HTMLTextAreaElement; const handleKeyDown = (e: KeyboardEvent) => { const target = e.target as HTMLTextAreaElement; const start = target.selectionStart; const end = target.selectionEnd; const before = value.substring(0, start); const after = value.substring(end); const updateText = (newText: string, newCursorPos: number) => { e.preventDefault(); onChangeText?.(newText); setTimeout(() => { if (inputRef.current) { const currentTarget = inputRef.current as unknown as HTMLTextAreaElement; currentTarget.selectionStart = newCursorPos; currentTarget.selectionEnd = newCursorPos; setSelection({ start: newCursorPos, end: newCursorPos }); } }, 0); }; if (e.key === "Tab") { updateText(before + " " + after, start + 4); return; } const pairs: Record = { "{": "}", "[": "]", "(": ")", "\"": "\"", "'": "'", "`": "`" }; const prevChar = before.charAt(before.length - 1); const nextChar = after.charAt(0); if (pairs[e.key]) { if (start !== end) { const selectedText = value.substring(start, end); updateText(before + e.key + selectedText + pairs[e.key] + after, end + 1); return; } if ([ "\"", "'", "`" ].includes(e.key) && nextChar === e.key) { updateText(value, start + 1); return; } if ([ "\"", "'", "`" ].includes(e.key) && /[a-zA-Z0-9]/.test(prevChar)) { return; } if (nextChar === "" || /[\s\]})]/ .test(nextChar)) { updateText(before + e.key + pairs[e.key] + after, start + 1); return; } } if ([ ")", "}", "]" ].includes(e.key) && start === end && nextChar === e.key) { updateText(value, start + 1); return; } if (e.key === "Backspace" && start === end && start > 0) { if ( (prevChar === "{" && nextChar === "}") || (prevChar === "[" && nextChar === "]") || (prevChar === "(" && nextChar === ")") || (prevChar === "\"" && nextChar === "\"") || (prevChar === "'" && nextChar === "'") || (prevChar === "`" && nextChar === "`") ) { updateText(before.slice(0, -1) + after.slice(1), start - 1); return; } } if (e.key === "Enter" && start === end) { const currentLineText = before.split("\n").pop() || ""; const match = currentLineText.match(/^(\s*)/); const currentIndentation = match?.[1] || ""; if (prevChar === "{" && nextChar === "}") { updateText( before + "\n" + currentIndentation + " \n" + currentIndentation + after, start + 1 + currentIndentation.length + 4 ); return; } else if (currentIndentation.length > 0) { updateText( before + "\n" + currentIndentation + after, start + 1 + currentIndentation.length ); return; } } }; node.addEventListener?.("keydown", handleKeyDown); return () => { node.removeEventListener?.("keydown", handleKeyDown); }; } }, [ value, onChangeText ]); const renderBackground = useCallback(() => { return { language ? // {language} : null } {lines.map((lineTokens, index) => { return { isLineNumberVisible ? {index + 1} : null } {lineTokens.map((token, tIndex) => { return {token.value} ; })} ; })} { lines.length === 0 ? { isLineNumberVisible ? 1 : null } : null } ; }, [ languageTextDynamicStyle, isLineNumberVisible, lineRowDynamicStyle, gutterDynamicStyle, textDynamicStyle, language, colors, lines ]); return {renderBackground()} { setSelection(e.nativeEvent.selection); props.onSelectionChange?.(e); }} style={[ stylesheet.editorInput, editorInputDynamicStyle, style ]} onChangeText={onChangeText} onKeyPress={handleKeyPress} autoCapitalize="none" autoCorrect={false} spellCheck={false} multiline={true} ref={inputRef} value={value} /> ; }; export default CodeEditor;