temp_index.tsx.bak 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406
  1. import {
  2. useCallback,
  3. useEffect,
  4. useState,
  5. useMemo,
  6. useRef
  7. } from "react";
  8. import {
  9. type TextInputKeyPressEvent,
  10. Text as NativeText,
  11. ScrollView,
  12. TextInput,
  13. Platform,
  14. View
  15. } from "react-native";
  16. import type ICodeEditorProps from "./type";
  17. import stylesheet, {
  18. useStyles
  19. } from "./stylesheet";
  20. import {
  21. NCoreUIKitTheme
  22. } from "../../core/hooks";
  23. import type {
  24. TokenType
  25. } from "../codeViewer/type";
  26. import {
  27. splitTokensIntoLines,
  28. parseCode
  29. } from "../codeViewer/parser";
  30. const CodeEditor = ({
  31. isLineNumberVisible = true,
  32. onChangeText,
  33. containerStyle,
  34. language,
  35. value,
  36. style,
  37. ...props
  38. }: ICodeEditorProps) => {
  39. const {
  40. typography,
  41. colors,
  42. spaces
  43. } = NCoreUIKitTheme.useContext();
  44. const inputRef = useRef<TextInput>(null);
  45. const [
  46. selection,
  47. setSelection
  48. ] = useState<{ end: number; start: number }>({
  49. end: 0,
  50. start: 0
  51. });
  52. const tokens = useMemo(() => parseCode(value, language), [
  53. language,
  54. value
  55. ]);
  56. const lines = useMemo(() => {
  57. return splitTokensIntoLines(tokens);
  58. }, [tokens]);
  59. const {
  60. languageText: languageTextDynamicStyle,
  61. editorInput: editorInputDynamicStyle,
  62. container: containerDynamicStyle,
  63. lineRow: lineRowDynamicStyle,
  64. gutter: gutterDynamicStyle,
  65. text: textDynamicStyle
  66. } = useStyles({
  67. isLineNumberVisible,
  68. hasLanguage: !!language,
  69. typography,
  70. colors,
  71. spaces
  72. });
  73. const getTokenColor = (type: TokenType) => {
  74. return colors.code[type as keyof typeof colors.code] || colors.code.default;
  75. };
  76. const handleKeyPress = (e: TextInputKeyPressEvent) => {
  77. if (e.nativeEvent.key === "Tab") {
  78. e.preventDefault();
  79. const before = value.substring(0, selection.start);
  80. const after = value.substring(selection.end);
  81. const newText = before + " " + after;
  82. onChangeText?.(newText);
  83. }
  84. };
  85. useEffect(() => {
  86. if (Platform.OS === "web" && inputRef.current) {
  87. const node = inputRef.current as unknown as HTMLTextAreaElement;
  88. const handleKeyDown = (e: KeyboardEvent) => {
  89. const target = e.target as HTMLTextAreaElement;
  90. const start = target.selectionStart;
  91. const end = target.selectionEnd;
  92. const before = value.substring(0, start);
  93. const after = value.substring(end);
  94. const updateText = (newText: string, newCursorPos: number) => {
  95. e.preventDefault();
  96. onChangeText?.(newText);
  97. setTimeout(() => {
  98. if (inputRef.current) {
  99. const currentTarget = inputRef.current as unknown as HTMLTextAreaElement;
  100. currentTarget.selectionStart = newCursorPos;
  101. currentTarget.selectionEnd = newCursorPos;
  102. setSelection({
  103. start: newCursorPos,
  104. end: newCursorPos
  105. });
  106. }
  107. }, 0);
  108. };
  109. if (e.key === "Tab") {
  110. updateText(before + " " + after, start + 4);
  111. return;
  112. }
  113. const pairs: Record<string, string> = {
  114. "{": "}",
  115. "[": "]",
  116. "(": ")",
  117. "\"": "\"",
  118. "'": "'",
  119. "`": "`"
  120. };
  121. const prevChar = before.charAt(before.length - 1);
  122. const nextChar = after.charAt(0);
  123. if (pairs[e.key]) {
  124. if (start !== end) {
  125. const selectedText = value.substring(start, end);
  126. updateText(before + e.key + selectedText + pairs[e.key] + after, end + 1);
  127. return;
  128. }
  129. if ([
  130. "\"",
  131. "'",
  132. "`"
  133. ].includes(e.key) && nextChar === e.key) {
  134. updateText(value, start + 1);
  135. return;
  136. }
  137. if ([
  138. "\"",
  139. "'",
  140. "`"
  141. ].includes(e.key) && /[a-zA-Z0-9]/.test(prevChar)) {
  142. return;
  143. }
  144. if (nextChar === "" || /[\s\]})]/ .test(nextChar)) {
  145. updateText(before + e.key + pairs[e.key] + after, start + 1);
  146. return;
  147. }
  148. }
  149. if ([
  150. ")",
  151. "}",
  152. "]"
  153. ].includes(e.key) && start === end && nextChar === e.key) {
  154. updateText(value, start + 1);
  155. return;
  156. }
  157. if (e.key === "Backspace" && start === end && start > 0) {
  158. if (
  159. (prevChar === "{" && nextChar === "}") ||
  160. (prevChar === "[" && nextChar === "]") ||
  161. (prevChar === "(" && nextChar === ")") ||
  162. (prevChar === "\"" && nextChar === "\"") ||
  163. (prevChar === "'" && nextChar === "'") ||
  164. (prevChar === "`" && nextChar === "`")
  165. ) {
  166. updateText(before.slice(0, -1) + after.slice(1), start - 1);
  167. return;
  168. }
  169. }
  170. if (e.key === "Enter" && start === end) {
  171. const currentLineText = before.split("\n").pop() || "";
  172. const match = currentLineText.match(/^(\s*)/);
  173. const currentIndentation = match?.[1] || "";
  174. if (prevChar === "{" && nextChar === "}") {
  175. updateText(
  176. before + "\n" + currentIndentation + " \n" + currentIndentation + after,
  177. start + 1 + currentIndentation.length + 4
  178. );
  179. return;
  180. } else if (currentIndentation.length > 0) {
  181. updateText(
  182. before + "\n" + currentIndentation + after,
  183. start + 1 + currentIndentation.length
  184. );
  185. return;
  186. }
  187. }
  188. };
  189. node.addEventListener?.("keydown", handleKeyDown);
  190. return () => {
  191. node.removeEventListener?.("keydown", handleKeyDown);
  192. };
  193. }
  194. }, [
  195. value,
  196. onChangeText
  197. ]);
  198. const renderBackground = useCallback(() => {
  199. return <View>
  200. {
  201. language ?
  202. <NativeText
  203. style={[
  204. stylesheet.content,
  205. textDynamicStyle,
  206. languageTextDynamicStyle
  207. ]}
  208. selectable={false}
  209. >
  210. // {language}
  211. </NativeText>
  212. :
  213. null
  214. }
  215. {lines.map((lineTokens, index) => {
  216. return <View
  217. style={[
  218. stylesheet.lineRow,
  219. lineRowDynamicStyle
  220. ]}
  221. key={`line-${index}`}
  222. >
  223. {
  224. isLineNumberVisible ?
  225. <View
  226. style={[
  227. stylesheet.gutter,
  228. gutterDynamicStyle,
  229. {
  230. borderRightColor: colors.content.border.subtle
  231. }
  232. ]}
  233. >
  234. <NativeText
  235. style={[
  236. stylesheet.lineNumber,
  237. {
  238. fontFamily: Platform.OS === "web" ? "monospace" : "System",
  239. color: colors.content.text.low
  240. }
  241. ]}
  242. selectable={false}
  243. >
  244. {index + 1}
  245. </NativeText>
  246. </View>
  247. :
  248. null
  249. }
  250. <NativeText
  251. style={[
  252. stylesheet.content,
  253. textDynamicStyle,
  254. {
  255. color: colors.content.text.high
  256. }
  257. ]}
  258. >
  259. {lineTokens.map((token, tIndex) => {
  260. return <NativeText
  261. style={{
  262. color: getTokenColor(token.type)
  263. }}
  264. key={`token-${index}-${tIndex}`}
  265. >
  266. {token.value}
  267. </NativeText>;
  268. })}
  269. </NativeText>
  270. </View>;
  271. })}
  272. {
  273. lines.length === 0 ?
  274. <View
  275. style={[
  276. stylesheet.lineRow,
  277. lineRowDynamicStyle
  278. ]}
  279. >
  280. {
  281. isLineNumberVisible ?
  282. <View
  283. style={[
  284. stylesheet.gutter,
  285. gutterDynamicStyle,
  286. {
  287. borderRightColor: colors.content.border.subtle
  288. }
  289. ]}
  290. >
  291. <NativeText
  292. style={[
  293. stylesheet.lineNumber,
  294. {
  295. fontFamily: Platform.OS === "web" ? "monospace" : "System",
  296. color: colors.content.text.low
  297. }
  298. ]}
  299. selectable={false}
  300. >
  301. 1
  302. </NativeText>
  303. </View>
  304. :
  305. null
  306. }
  307. <NativeText
  308. style={[
  309. stylesheet.content,
  310. textDynamicStyle,
  311. {
  312. color: colors.content.text.high
  313. }
  314. ]}
  315. >
  316. </NativeText>
  317. </View>
  318. :
  319. null
  320. }
  321. </View>;
  322. }, [
  323. languageTextDynamicStyle,
  324. isLineNumberVisible,
  325. lineRowDynamicStyle,
  326. gutterDynamicStyle,
  327. textDynamicStyle,
  328. language,
  329. colors,
  330. lines
  331. ]);
  332. return <View
  333. style={[
  334. stylesheet.container,
  335. containerDynamicStyle,
  336. containerStyle
  337. ]}
  338. >
  339. <ScrollView
  340. contentContainerStyle={[
  341. stylesheet.scrollContent
  342. ]}
  343. showsHorizontalScrollIndicator={true}
  344. style={[
  345. stylesheet.scrollView
  346. ]}
  347. horizontal={true}
  348. >
  349. <View
  350. style={[
  351. stylesheet.scrollContent
  352. ]}
  353. >
  354. {renderBackground()}
  355. <TextInput
  356. {...props}
  357. onSelectionChange={(e) => {
  358. setSelection(e.nativeEvent.selection);
  359. props.onSelectionChange?.(e);
  360. }}
  361. style={[
  362. stylesheet.editorInput,
  363. editorInputDynamicStyle,
  364. style
  365. ]}
  366. onChangeText={onChangeText}
  367. onKeyPress={handleKeyPress}
  368. autoCapitalize="none"
  369. autoCorrect={false}
  370. spellCheck={false}
  371. multiline={true}
  372. ref={inputRef}
  373. value={value}
  374. />
  375. </View>
  376. </ScrollView>
  377. </View>;
  378. };
  379. export default CodeEditor;