| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406 |
- 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<TextInput>(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<string, string> = {
- "{": "}",
- "[": "]",
- "(": ")",
- "\"": "\"",
- "'": "'",
- "`": "`"
- };
- 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 <View>
- {
- language ?
- <NativeText
- style={[
- stylesheet.content,
- textDynamicStyle,
- languageTextDynamicStyle
- ]}
- selectable={false}
- >
- // {language}
- </NativeText>
- :
- null
- }
- {lines.map((lineTokens, index) => {
- return <View
- style={[
- stylesheet.lineRow,
- lineRowDynamicStyle
- ]}
- key={`line-${index}`}
- >
- {
- isLineNumberVisible ?
- <View
- style={[
- stylesheet.gutter,
- gutterDynamicStyle,
- {
- borderRightColor: colors.content.border.subtle
- }
- ]}
- >
- <NativeText
- style={[
- stylesheet.lineNumber,
- {
- fontFamily: Platform.OS === "web" ? "monospace" : "System",
- color: colors.content.text.low
- }
- ]}
- selectable={false}
- >
- {index + 1}
- </NativeText>
- </View>
- :
- null
- }
- <NativeText
- style={[
- stylesheet.content,
- textDynamicStyle,
- {
- color: colors.content.text.high
- }
- ]}
- >
- {lineTokens.map((token, tIndex) => {
- return <NativeText
- style={{
- color: getTokenColor(token.type)
- }}
- key={`token-${index}-${tIndex}`}
- >
- {token.value}
- </NativeText>;
- })}
- </NativeText>
- </View>;
- })}
- {
- lines.length === 0 ?
- <View
- style={[
- stylesheet.lineRow,
- lineRowDynamicStyle
- ]}
- >
- {
- isLineNumberVisible ?
- <View
- style={[
- stylesheet.gutter,
- gutterDynamicStyle,
- {
- borderRightColor: colors.content.border.subtle
- }
- ]}
- >
- <NativeText
- style={[
- stylesheet.lineNumber,
- {
- fontFamily: Platform.OS === "web" ? "monospace" : "System",
- color: colors.content.text.low
- }
- ]}
- selectable={false}
- >
- 1
- </NativeText>
- </View>
- :
- null
- }
- <NativeText
- style={[
- stylesheet.content,
- textDynamicStyle,
- {
- color: colors.content.text.high
- }
- ]}
- >
- </NativeText>
- </View>
- :
- null
- }
- </View>;
- }, [
- languageTextDynamicStyle,
- isLineNumberVisible,
- lineRowDynamicStyle,
- gutterDynamicStyle,
- textDynamicStyle,
- language,
- colors,
- lines
- ]);
- return <View
- style={[
- stylesheet.container,
- containerDynamicStyle,
- containerStyle
- ]}
- >
- <ScrollView
- contentContainerStyle={[
- stylesheet.scrollContent
- ]}
- showsHorizontalScrollIndicator={true}
- style={[
- stylesheet.scrollView
- ]}
- horizontal={true}
- >
- <View
- style={[
- stylesheet.scrollContent
- ]}
- >
- {renderBackground()}
- <TextInput
- {...props}
- onSelectionChange={(e) => {
- 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}
- />
- </View>
- </ScrollView>
- </View>;
- };
- export default CodeEditor;
|