@particle-academy/fancy-code 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,16 +1,6 @@
1
1
  import { createContext, useState, useCallback, useRef, useMemo, useSyncExternalStore, useEffect, useContext } from 'react';
2
2
  import { cn, useControllableState } from '@particle-academy/react-fancy';
3
3
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
4
- import { javascript } from '@codemirror/lang-javascript';
5
- import { html } from '@codemirror/lang-html';
6
- import { php } from '@codemirror/lang-php';
7
- import { Compartment, EditorState } from '@codemirror/state';
8
- import { EditorView, lineNumbers, highlightActiveLineGutter, placeholder, drawSelection, dropCursor, highlightActiveLine, keymap } from '@codemirror/view';
9
- import { history, defaultKeymap, historyKeymap, indentWithTab } from '@codemirror/commands';
10
- import { HighlightStyle, foldGutter, indentOnInput, bracketMatching, foldKeymap, syntaxHighlighting } from '@codemirror/language';
11
- import { highlightSelectionMatches, searchKeymap } from '@codemirror/search';
12
- import { closeBrackets, autocompletion, closeBracketsKeymap, completionKeymap } from '@codemirror/autocomplete';
13
- import { tags } from '@lezer/highlight';
14
4
 
15
5
  // src/components/CodeEditor/CodeEditor.tsx
16
6
  var CodeEditorContext = createContext(null);
@@ -22,13 +12,116 @@ function useCodeEditor() {
22
12
  return ctx;
23
13
  }
24
14
  function CodeEditorPanel({ className }) {
25
- const { _containerRef } = useCodeEditor();
15
+ const {
16
+ _engineReturn: engine,
17
+ lineNumbers,
18
+ readOnly,
19
+ placeholder,
20
+ _maxHeight,
21
+ _minHeight
22
+ } = useCodeEditor();
23
+ if (!engine) return null;
24
+ const {
25
+ textareaRef,
26
+ highlightedHtml,
27
+ lineCount,
28
+ activeLine,
29
+ themeColors,
30
+ handleKeyDown,
31
+ handleInput,
32
+ handleScroll,
33
+ handleSelect,
34
+ scrollTop,
35
+ scrollLeft
36
+ } = engine;
37
+ const gutterWidth = lineNumbers ? `${Math.max(String(lineCount).length, 2) * 0.75 + 1.5}em` : "0";
38
+ const lineNumberElements = useMemo(() => {
39
+ if (!lineNumbers) return null;
40
+ const lines = [];
41
+ for (let i = 1; i <= lineCount; i++) {
42
+ lines.push(
43
+ /* @__PURE__ */ jsx(
44
+ "div",
45
+ {
46
+ style: {
47
+ color: i === activeLine ? themeColors.foreground : themeColors.gutterForeground,
48
+ backgroundColor: i === activeLine ? themeColors.activeLineBackground : void 0
49
+ },
50
+ className: "pr-3 text-right text-[13px] leading-[1.5] select-none",
51
+ children: i
52
+ },
53
+ i
54
+ )
55
+ );
56
+ }
57
+ return lines;
58
+ }, [lineNumbers, lineCount, activeLine, themeColors]);
59
+ const containerStyle = {
60
+ backgroundColor: themeColors.background,
61
+ color: themeColors.foreground,
62
+ minHeight: _minHeight,
63
+ maxHeight: _maxHeight
64
+ };
65
+ const isEmpty = textareaRef.current ? textareaRef.current.value.length === 0 : highlightedHtml.length === 0;
26
66
  return /* @__PURE__ */ jsx(
27
67
  "div",
28
68
  {
29
69
  "data-fancy-code-panel": "",
30
- ref: _containerRef,
31
- className: cn("text-sm", className)
70
+ className: cn("relative overflow-auto", className),
71
+ style: containerStyle,
72
+ children: /* @__PURE__ */ jsxs("div", { className: "flex min-h-full", children: [
73
+ lineNumbers && /* @__PURE__ */ jsx(
74
+ "div",
75
+ {
76
+ className: "sticky left-0 z-10 shrink-0 pt-2.5 pb-2.5 pl-2",
77
+ style: {
78
+ backgroundColor: themeColors.gutterBackground,
79
+ borderRight: `1px solid ${themeColors.gutterBorder}`,
80
+ width: gutterWidth
81
+ },
82
+ children: lineNumberElements
83
+ }
84
+ ),
85
+ /* @__PURE__ */ jsxs("div", { className: "relative min-w-0 flex-1", children: [
86
+ /* @__PURE__ */ jsx(
87
+ "pre",
88
+ {
89
+ className: "pointer-events-none absolute inset-0 m-0 overflow-hidden whitespace-pre border-none p-2.5 text-[13px] leading-[1.5]",
90
+ "aria-hidden": "true",
91
+ children: /* @__PURE__ */ jsx("code", { dangerouslySetInnerHTML: { __html: highlightedHtml + "\n" } })
92
+ }
93
+ ),
94
+ placeholder && isEmpty && /* @__PURE__ */ jsx(
95
+ "div",
96
+ {
97
+ className: "pointer-events-none absolute left-0 top-0 p-2.5 text-[13px] leading-[1.5] opacity-40",
98
+ style: { color: themeColors.foreground },
99
+ children: placeholder
100
+ }
101
+ ),
102
+ /* @__PURE__ */ jsx(
103
+ "textarea",
104
+ {
105
+ ref: textareaRef,
106
+ className: "relative m-0 block w-full resize-none border-none bg-transparent p-2.5 text-[13px] leading-[1.5] text-transparent outline-none",
107
+ style: {
108
+ caretColor: themeColors.cursorColor,
109
+ minHeight: _minHeight ? _minHeight - 40 : 80
110
+ },
111
+ spellCheck: false,
112
+ autoComplete: "off",
113
+ autoCorrect: "off",
114
+ autoCapitalize: "off",
115
+ readOnly,
116
+ onKeyDown: handleKeyDown,
117
+ onInput: handleInput,
118
+ onScroll: handleScroll,
119
+ onSelect: handleSelect,
120
+ onClick: handleSelect
121
+ }
122
+ )
123
+ ] })
124
+ ] })
32
125
  }
33
126
  );
34
127
  }
@@ -68,25 +161,519 @@ function getRegisteredLanguages() {
68
161
  }
69
162
  return names;
70
163
  }
164
+
165
+ // src/engine/tokenizers/javascript.ts
166
+ var KEYWORDS = /* @__PURE__ */ new Set([
167
+ "abstract",
168
+ "arguments",
169
+ "as",
170
+ "async",
171
+ "await",
172
+ "boolean",
173
+ "break",
174
+ "byte",
175
+ "case",
176
+ "catch",
177
+ "char",
178
+ "class",
179
+ "const",
180
+ "continue",
181
+ "debugger",
182
+ "default",
183
+ "delete",
184
+ "do",
185
+ "double",
186
+ "else",
187
+ "enum",
188
+ "export",
189
+ "extends",
190
+ "false",
191
+ "final",
192
+ "finally",
193
+ "float",
194
+ "for",
195
+ "from",
196
+ "function",
197
+ "get",
198
+ "goto",
199
+ "if",
200
+ "implements",
201
+ "import",
202
+ "in",
203
+ "instanceof",
204
+ "int",
205
+ "interface",
206
+ "keyof",
207
+ "let",
208
+ "long",
209
+ "native",
210
+ "new",
211
+ "null",
212
+ "of",
213
+ "package",
214
+ "private",
215
+ "protected",
216
+ "public",
217
+ "readonly",
218
+ "return",
219
+ "set",
220
+ "short",
221
+ "static",
222
+ "super",
223
+ "switch",
224
+ "synchronized",
225
+ "this",
226
+ "throw",
227
+ "throws",
228
+ "transient",
229
+ "true",
230
+ "try",
231
+ "type",
232
+ "typeof",
233
+ "undefined",
234
+ "var",
235
+ "void",
236
+ "volatile",
237
+ "while",
238
+ "with",
239
+ "yield"
240
+ ]);
241
+ var TYPE_KEYWORDS = /* @__PURE__ */ new Set([
242
+ "string",
243
+ "number",
244
+ "boolean",
245
+ "any",
246
+ "void",
247
+ "never",
248
+ "unknown",
249
+ "object",
250
+ "symbol",
251
+ "bigint",
252
+ "undefined",
253
+ "null"
254
+ ]);
255
+ var tokenizeJavaScript = (source) => {
256
+ const tokens = [];
257
+ const len = source.length;
258
+ let i = 0;
259
+ while (i < len) {
260
+ const ch = source[i];
261
+ if (ch === " " || ch === " " || ch === "\n" || ch === "\r") {
262
+ i++;
263
+ continue;
264
+ }
265
+ if (ch === "/" && source[i + 1] === "/") {
266
+ const pos = i;
267
+ i += 2;
268
+ while (i < len && source[i] !== "\n") i++;
269
+ tokens.push({ type: "comment", start: pos, end: i });
270
+ continue;
271
+ }
272
+ if (ch === "/" && source[i + 1] === "*") {
273
+ const pos = i;
274
+ i += 2;
275
+ while (i < len && !(source[i] === "*" && source[i + 1] === "/")) i++;
276
+ i += 2;
277
+ tokens.push({ type: "comment", start: pos, end: i });
278
+ continue;
279
+ }
280
+ if (ch === "`") {
281
+ const pos = i;
282
+ i++;
283
+ while (i < len && source[i] !== "`") {
284
+ if (source[i] === "\\") i++;
285
+ i++;
286
+ }
287
+ i++;
288
+ tokens.push({ type: "string", start: pos, end: i });
289
+ continue;
290
+ }
291
+ if (ch === '"' || ch === "'") {
292
+ const pos = i;
293
+ const quote = ch;
294
+ i++;
295
+ while (i < len && source[i] !== quote) {
296
+ if (source[i] === "\\") i++;
297
+ i++;
298
+ }
299
+ i++;
300
+ tokens.push({ type: "string", start: pos, end: i });
301
+ continue;
302
+ }
303
+ if (ch >= "0" && ch <= "9" || ch === "." && source[i + 1] >= "0" && source[i + 1] <= "9") {
304
+ const pos = i;
305
+ if (ch === "0" && (source[i + 1] === "x" || source[i + 1] === "X")) {
306
+ i += 2;
307
+ while (i < len && /[0-9a-fA-F_]/.test(source[i])) i++;
308
+ } else if (ch === "0" && (source[i + 1] === "b" || source[i + 1] === "B")) {
309
+ i += 2;
310
+ while (i < len && /[01_]/.test(source[i])) i++;
311
+ } else {
312
+ while (i < len && /[0-9_.]/.test(source[i])) i++;
313
+ if (i < len && (source[i] === "e" || source[i] === "E")) {
314
+ i++;
315
+ if (i < len && (source[i] === "+" || source[i] === "-")) i++;
316
+ while (i < len && source[i] >= "0" && source[i] <= "9") i++;
317
+ }
318
+ }
319
+ if (i < len && source[i] === "n") i++;
320
+ tokens.push({ type: "number", start: pos, end: i });
321
+ continue;
322
+ }
323
+ if (/[a-zA-Z_$]/.test(ch)) {
324
+ const pos = i;
325
+ i++;
326
+ while (i < len && /[a-zA-Z0-9_$]/.test(source[i])) i++;
327
+ const word = source.slice(pos, i);
328
+ let j = i;
329
+ while (j < len && (source[j] === " " || source[j] === " ")) j++;
330
+ if (TYPE_KEYWORDS.has(word)) {
331
+ tokens.push({ type: "type", start: pos, end: i });
332
+ } else if (KEYWORDS.has(word)) {
333
+ tokens.push({ type: "keyword", start: pos, end: i });
334
+ } else if (source[j] === "(") {
335
+ tokens.push({ type: "function", start: pos, end: i });
336
+ } else if (word[0] >= "A" && word[0] <= "Z") {
337
+ tokens.push({ type: "type", start: pos, end: i });
338
+ } else {
339
+ tokens.push({ type: "variable", start: pos, end: i });
340
+ }
341
+ continue;
342
+ }
343
+ if ("+-*/%=<>!&|^~?:".includes(ch)) {
344
+ const pos = i;
345
+ i++;
346
+ while (i < len && "+-*/%=<>!&|^~?:".includes(source[i])) i++;
347
+ tokens.push({ type: "operator", start: pos, end: i });
348
+ continue;
349
+ }
350
+ if ("(){}[];,.@#".includes(ch)) {
351
+ tokens.push({ type: "punctuation", start: i, end: i + 1 });
352
+ i++;
353
+ continue;
354
+ }
355
+ tokens.push({ type: "plain", start: i, end: i + 1 });
356
+ i++;
357
+ }
358
+ return tokens;
359
+ };
360
+
361
+ // src/engine/tokenizers/html.ts
362
+ var tokenizeHtml = (source) => {
363
+ const tokens = [];
364
+ const len = source.length;
365
+ let i = 0;
366
+ while (i < len) {
367
+ if (source[i] === "<" && source.slice(i, i + 4) === "<!--") {
368
+ const pos2 = i;
369
+ i += 4;
370
+ while (i < len && source.slice(i, i + 3) !== "-->") i++;
371
+ i += 3;
372
+ tokens.push({ type: "comment", start: pos2, end: i });
373
+ continue;
374
+ }
375
+ if (source[i] === "<" && source.slice(i, i + 9).toUpperCase() === "<!DOCTYPE") {
376
+ const pos2 = i;
377
+ while (i < len && source[i] !== ">") i++;
378
+ i++;
379
+ tokens.push({ type: "keyword", start: pos2, end: i });
380
+ continue;
381
+ }
382
+ if (source[i] === "<") {
383
+ tokens.push({ type: "punctuation", start: i, end: i + 1 });
384
+ i++;
385
+ if (i < len && source[i] === "/") {
386
+ tokens.push({ type: "punctuation", start: i, end: i + 1 });
387
+ i++;
388
+ }
389
+ if (i < len && /[a-zA-Z]/.test(source[i])) {
390
+ const pos2 = i;
391
+ while (i < len && /[a-zA-Z0-9-]/.test(source[i])) i++;
392
+ tokens.push({ type: "tag", start: pos2, end: i });
393
+ }
394
+ while (i < len && source[i] !== ">" && !(source[i] === "/" && source[i + 1] === ">")) {
395
+ if (source[i] === " " || source[i] === " " || source[i] === "\n" || source[i] === "\r") {
396
+ i++;
397
+ continue;
398
+ }
399
+ if (/[a-zA-Z_@:]/.test(source[i])) {
400
+ const pos2 = i;
401
+ while (i < len && /[a-zA-Z0-9_\-:.]/.test(source[i])) i++;
402
+ tokens.push({ type: "attribute", start: pos2, end: i });
403
+ while (i < len && (source[i] === " " || source[i] === " ")) i++;
404
+ if (i < len && source[i] === "=") {
405
+ tokens.push({ type: "operator", start: i, end: i + 1 });
406
+ i++;
407
+ while (i < len && (source[i] === " " || source[i] === " ")) i++;
408
+ if (i < len && (source[i] === '"' || source[i] === "'")) {
409
+ const quote = source[i];
410
+ const pos3 = i;
411
+ i++;
412
+ while (i < len && source[i] !== quote) i++;
413
+ i++;
414
+ tokens.push({ type: "attributeValue", start: pos3, end: i });
415
+ } else if (i < len && /[^\s>]/.test(source[i])) {
416
+ const pos3 = i;
417
+ while (i < len && /[^\s>]/.test(source[i])) i++;
418
+ tokens.push({ type: "attributeValue", start: pos3, end: i });
419
+ }
420
+ }
421
+ continue;
422
+ }
423
+ i++;
424
+ }
425
+ if (i < len && source[i] === "/") {
426
+ tokens.push({ type: "punctuation", start: i, end: i + 1 });
427
+ i++;
428
+ }
429
+ if (i < len && source[i] === ">") {
430
+ tokens.push({ type: "punctuation", start: i, end: i + 1 });
431
+ i++;
432
+ }
433
+ continue;
434
+ }
435
+ const pos = i;
436
+ while (i < len && source[i] !== "<") i++;
437
+ if (i > pos) {
438
+ tokens.push({ type: "plain", start: pos, end: i });
439
+ }
440
+ }
441
+ return tokens;
442
+ };
443
+
444
+ // src/engine/tokenizers/php.ts
445
+ var PHP_KEYWORDS = /* @__PURE__ */ new Set([
446
+ "abstract",
447
+ "and",
448
+ "array",
449
+ "as",
450
+ "break",
451
+ "callable",
452
+ "case",
453
+ "catch",
454
+ "class",
455
+ "clone",
456
+ "const",
457
+ "continue",
458
+ "declare",
459
+ "default",
460
+ "do",
461
+ "echo",
462
+ "else",
463
+ "elseif",
464
+ "empty",
465
+ "enddeclare",
466
+ "endfor",
467
+ "endforeach",
468
+ "endif",
469
+ "endswitch",
470
+ "endwhile",
471
+ "enum",
472
+ "eval",
473
+ "exit",
474
+ "extends",
475
+ "false",
476
+ "final",
477
+ "finally",
478
+ "fn",
479
+ "for",
480
+ "foreach",
481
+ "function",
482
+ "global",
483
+ "goto",
484
+ "if",
485
+ "implements",
486
+ "include",
487
+ "include_once",
488
+ "instanceof",
489
+ "insteadof",
490
+ "interface",
491
+ "isset",
492
+ "list",
493
+ "match",
494
+ "namespace",
495
+ "new",
496
+ "null",
497
+ "or",
498
+ "print",
499
+ "private",
500
+ "protected",
501
+ "public",
502
+ "readonly",
503
+ "require",
504
+ "require_once",
505
+ "return",
506
+ "self",
507
+ "static",
508
+ "switch",
509
+ "throw",
510
+ "trait",
511
+ "true",
512
+ "try",
513
+ "unset",
514
+ "use",
515
+ "var",
516
+ "while",
517
+ "xor",
518
+ "yield"
519
+ ]);
520
+ function tokenizePhpBlock(source, offset) {
521
+ const tokens = [];
522
+ const len = source.length;
523
+ let i = offset;
524
+ while (i < len) {
525
+ const ch = source[i];
526
+ if (ch === "?" && source[i + 1] === ">") {
527
+ return { tokens, end: i };
528
+ }
529
+ if (ch === " " || ch === " " || ch === "\n" || ch === "\r") {
530
+ i++;
531
+ continue;
532
+ }
533
+ if (ch === "/" && source[i + 1] === "/" || ch === "#") {
534
+ const pos = i;
535
+ while (i < len && source[i] !== "\n") i++;
536
+ tokens.push({ type: "comment", start: pos, end: i });
537
+ continue;
538
+ }
539
+ if (ch === "/" && source[i + 1] === "*") {
540
+ const pos = i;
541
+ i += 2;
542
+ while (i < len && !(source[i] === "*" && source[i + 1] === "/")) i++;
543
+ i += 2;
544
+ tokens.push({ type: "comment", start: pos, end: i });
545
+ continue;
546
+ }
547
+ if (ch === "$") {
548
+ const pos = i;
549
+ i++;
550
+ while (i < len && /[a-zA-Z0-9_]/.test(source[i])) i++;
551
+ tokens.push({ type: "variable", start: pos, end: i });
552
+ continue;
553
+ }
554
+ if (ch === '"' || ch === "'") {
555
+ const pos = i;
556
+ const quote = ch;
557
+ i++;
558
+ while (i < len && source[i] !== quote) {
559
+ if (source[i] === "\\") i++;
560
+ i++;
561
+ }
562
+ i++;
563
+ tokens.push({ type: "string", start: pos, end: i });
564
+ continue;
565
+ }
566
+ if (ch >= "0" && ch <= "9") {
567
+ const pos = i;
568
+ while (i < len && /[0-9_.]/.test(source[i])) i++;
569
+ if (i < len && (source[i] === "e" || source[i] === "E")) {
570
+ i++;
571
+ if (i < len && (source[i] === "+" || source[i] === "-")) i++;
572
+ while (i < len && source[i] >= "0" && source[i] <= "9") i++;
573
+ }
574
+ tokens.push({ type: "number", start: pos, end: i });
575
+ continue;
576
+ }
577
+ if (/[a-zA-Z_]/.test(ch)) {
578
+ const pos = i;
579
+ i++;
580
+ while (i < len && /[a-zA-Z0-9_]/.test(source[i])) i++;
581
+ const word = source.slice(pos, i);
582
+ let j = i;
583
+ while (j < len && (source[j] === " " || source[j] === " ")) j++;
584
+ if (PHP_KEYWORDS.has(word.toLowerCase())) {
585
+ tokens.push({ type: "keyword", start: pos, end: i });
586
+ } else if (source[j] === "(") {
587
+ tokens.push({ type: "function", start: pos, end: i });
588
+ } else if (word[0] >= "A" && word[0] <= "Z") {
589
+ tokens.push({ type: "type", start: pos, end: i });
590
+ } else {
591
+ tokens.push({ type: "variable", start: pos, end: i });
592
+ }
593
+ continue;
594
+ }
595
+ if ("+-*/%=<>!&|^~?:.".includes(ch)) {
596
+ const pos = i;
597
+ i++;
598
+ while (i < len && "+-*/%=<>!&|^~?:.".includes(source[i])) i++;
599
+ tokens.push({ type: "operator", start: pos, end: i });
600
+ continue;
601
+ }
602
+ if ("(){}[];,@\\".includes(ch)) {
603
+ tokens.push({ type: "punctuation", start: i, end: i + 1 });
604
+ i++;
605
+ continue;
606
+ }
607
+ tokens.push({ type: "plain", start: i, end: i + 1 });
608
+ i++;
609
+ }
610
+ return { tokens, end: i };
611
+ }
612
+ var tokenizePhp = (source) => {
613
+ const tokens = [];
614
+ const len = source.length;
615
+ let i = 0;
616
+ let htmlChunkStart = 0;
617
+ while (i < len) {
618
+ if (source[i] === "<" && source[i + 1] === "?") {
619
+ if (i > htmlChunkStart) {
620
+ const htmlChunk = source.slice(htmlChunkStart, i);
621
+ const htmlTokens = tokenizeHtml(htmlChunk);
622
+ for (const t of htmlTokens) {
623
+ tokens.push({ type: t.type, start: t.start + htmlChunkStart, end: t.end + htmlChunkStart });
624
+ }
625
+ }
626
+ const tagStart = i;
627
+ i += 2;
628
+ if (i < len && source.slice(i, i + 3).toLowerCase() === "php") {
629
+ i += 3;
630
+ } else if (i < len && source[i] === "=") {
631
+ i++;
632
+ }
633
+ tokens.push({ type: "keyword", start: tagStart, end: i });
634
+ const result = tokenizePhpBlock(source, i);
635
+ tokens.push(...result.tokens);
636
+ i = result.end;
637
+ if (i < len && source[i] === "?" && source[i + 1] === ">") {
638
+ tokens.push({ type: "keyword", start: i, end: i + 2 });
639
+ i += 2;
640
+ }
641
+ htmlChunkStart = i;
642
+ continue;
643
+ }
644
+ i++;
645
+ }
646
+ if (htmlChunkStart < len) {
647
+ const htmlChunk = source.slice(htmlChunkStart);
648
+ const htmlTokens = tokenizeHtml(htmlChunk);
649
+ for (const t of htmlTokens) {
650
+ tokens.push({ type: t.type, start: t.start + htmlChunkStart, end: t.end + htmlChunkStart });
651
+ }
652
+ }
653
+ return tokens;
654
+ };
655
+
656
+ // src/languages/builtin.ts
71
657
  registerLanguage({
72
658
  name: "JavaScript",
73
659
  aliases: ["js", "javascript", "jsx"],
74
- support: () => javascript({ jsx: true })
660
+ tokenize: tokenizeJavaScript
75
661
  });
76
662
  registerLanguage({
77
663
  name: "TypeScript",
78
664
  aliases: ["ts", "typescript", "tsx"],
79
- support: () => javascript({ typescript: true, jsx: true })
665
+ tokenize: tokenizeJavaScript
666
+ // Same tokenizer — JS/TS syntax is identical for highlighting
80
667
  });
81
668
  registerLanguage({
82
669
  name: "HTML",
83
670
  aliases: ["html", "htm"],
84
- support: () => html()
671
+ tokenize: tokenizeHtml
85
672
  });
86
673
  registerLanguage({
87
674
  name: "PHP",
88
675
  aliases: ["php"],
89
- support: () => php()
676
+ tokenize: tokenizePhp
90
677
  });
91
678
  var iconBtnClass = "inline-flex items-center justify-center rounded-md p-1 text-zinc-500 transition-colors hover:bg-zinc-100 hover:text-zinc-700 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-200";
92
679
  function LanguageSelector() {
@@ -192,396 +779,286 @@ function getTheme(name) {
192
779
  function getRegisteredThemes() {
193
780
  return Array.from(themeRegistry.keys());
194
781
  }
195
- var editorTheme = EditorView.theme(
196
- {
197
- "&": {
198
- backgroundColor: "#ffffff",
199
- color: "#1e1e2e"
200
- },
201
- ".cm-content": {
202
- caretColor: "#3b82f6"
203
- },
204
- ".cm-cursor, .cm-dropCursor": {
205
- borderLeftColor: "#3b82f6"
206
- },
207
- "&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection": {
208
- backgroundColor: "#dbeafe"
209
- },
210
- ".cm-activeLine": {
211
- backgroundColor: "#f8fafc"
212
- },
213
- ".cm-gutters": {
214
- backgroundColor: "#f8fafc",
215
- color: "#94a3b8",
216
- borderRight: "1px solid #e2e8f0"
217
- },
218
- ".cm-activeLineGutter": {
219
- backgroundColor: "#f1f5f9",
220
- color: "#475569"
221
- },
222
- ".cm-foldPlaceholder": {
223
- backgroundColor: "#e2e8f0",
224
- color: "#64748b",
225
- border: "none"
226
- },
227
- ".cm-tooltip": {
228
- backgroundColor: "#ffffff",
229
- border: "1px solid #e2e8f0"
230
- },
231
- ".cm-tooltip .cm-tooltip-arrow:before": {
232
- borderTopColor: "#e2e8f0",
233
- borderBottomColor: "#e2e8f0"
234
- },
235
- ".cm-tooltip .cm-tooltip-arrow:after": {
236
- borderTopColor: "#ffffff",
237
- borderBottomColor: "#ffffff"
238
- },
239
- ".cm-tooltip-autocomplete": {
240
- "& > ul > li[aria-selected]": {
241
- backgroundColor: "#dbeafe",
242
- color: "#1e40af"
243
- }
244
- },
245
- ".cm-searchMatch": {
246
- backgroundColor: "#fef08a",
247
- outline: "1px solid #facc15"
248
- },
249
- ".cm-searchMatch.cm-searchMatch-selected": {
250
- backgroundColor: "#bbf7d0",
251
- outline: "1px solid #22c55e"
252
- },
253
- ".cm-selectionMatch": {
254
- backgroundColor: "#e0f2fe"
255
- },
256
- ".cm-matchingBracket, .cm-nonmatchingBracket": {
257
- outline: "1px solid #94a3b8"
258
- },
259
- ".cm-matchingBracket": {
260
- backgroundColor: "#e0f2fe"
261
- }
262
- },
263
- { dark: false }
264
- );
265
- var highlightStyle = HighlightStyle.define([
266
- { tag: tags.keyword, color: "#8b5cf6" },
267
- { tag: [tags.name, tags.deleted, tags.character, tags.macroName], color: "#1e1e2e" },
268
- { tag: [tags.function(tags.variableName), tags.labelName], color: "#2563eb" },
269
- { tag: [tags.color, tags.constant(tags.name), tags.standard(tags.name)], color: "#d97706" },
270
- { tag: [tags.definition(tags.name), tags.separator], color: "#1e1e2e" },
271
- { tag: [tags.typeName, tags.className, tags.number, tags.changed, tags.annotation, tags.modifier, tags.self, tags.namespace], color: "#d97706" },
272
- { tag: [tags.operator, tags.operatorKeyword, tags.url, tags.escape, tags.regexp, tags.link, tags.special(tags.string)], color: "#0891b2" },
273
- { tag: [tags.meta, tags.comment], color: "#94a3b8", fontStyle: "italic" },
274
- { tag: tags.strong, fontWeight: "bold" },
275
- { tag: tags.emphasis, fontStyle: "italic" },
276
- { tag: tags.strikethrough, textDecoration: "line-through" },
277
- { tag: tags.link, color: "#2563eb", textDecoration: "underline" },
278
- { tag: tags.heading, fontWeight: "bold", color: "#8b5cf6" },
279
- { tag: [tags.atom, tags.bool, tags.special(tags.variableName)], color: "#d97706" },
280
- { tag: [tags.processingInstruction, tags.string, tags.inserted], color: "#059669" },
281
- { tag: tags.invalid, color: "#ef4444" },
282
- { tag: tags.tagName, color: "#dc2626" },
283
- { tag: tags.attributeName, color: "#d97706" },
284
- { tag: tags.attributeValue, color: "#059669" }
285
- ]);
782
+
783
+ // src/themes/light.ts
286
784
  registerTheme({
287
785
  name: "light",
288
786
  variant: "light",
289
- editorTheme,
290
- highlightStyle
787
+ colors: {
788
+ background: "#ffffff",
789
+ foreground: "#1e1e2e",
790
+ gutterBackground: "#f8fafc",
791
+ gutterForeground: "#94a3b8",
792
+ gutterBorder: "#e2e8f0",
793
+ activeLineBackground: "#f8fafc",
794
+ selectionBackground: "#dbeafe",
795
+ cursorColor: "#3b82f6",
796
+ keyword: "#8b5cf6",
797
+ string: "#059669",
798
+ comment: "#94a3b8",
799
+ number: "#d97706",
800
+ operator: "#0891b2",
801
+ function: "#2563eb",
802
+ type: "#d97706",
803
+ tag: "#dc2626",
804
+ attribute: "#d97706",
805
+ attributeValue: "#059669",
806
+ punctuation: "#64748b",
807
+ variable: "#1e1e2e"
808
+ }
291
809
  });
292
- var editorTheme2 = EditorView.theme(
293
- {
294
- "&": {
295
- backgroundColor: "#18181b",
296
- color: "#e4e4e7"
297
- },
298
- ".cm-content": {
299
- caretColor: "#60a5fa"
300
- },
301
- ".cm-cursor, .cm-dropCursor": {
302
- borderLeftColor: "#60a5fa"
303
- },
304
- "&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection": {
305
- backgroundColor: "#1e3a5f"
306
- },
307
- ".cm-activeLine": {
308
- backgroundColor: "#27272a"
309
- },
310
- ".cm-gutters": {
311
- backgroundColor: "#18181b",
312
- color: "#52525b",
313
- borderRight: "1px solid #27272a"
314
- },
315
- ".cm-activeLineGutter": {
316
- backgroundColor: "#27272a",
317
- color: "#a1a1aa"
318
- },
319
- ".cm-foldPlaceholder": {
320
- backgroundColor: "#3f3f46",
321
- color: "#a1a1aa",
322
- border: "none"
323
- },
324
- ".cm-tooltip": {
325
- backgroundColor: "#27272a",
326
- border: "1px solid #3f3f46",
327
- color: "#e4e4e7"
328
- },
329
- ".cm-tooltip .cm-tooltip-arrow:before": {
330
- borderTopColor: "#3f3f46",
331
- borderBottomColor: "#3f3f46"
332
- },
333
- ".cm-tooltip .cm-tooltip-arrow:after": {
334
- borderTopColor: "#27272a",
335
- borderBottomColor: "#27272a"
336
- },
337
- ".cm-tooltip-autocomplete": {
338
- "& > ul > li[aria-selected]": {
339
- backgroundColor: "#1e3a5f",
340
- color: "#93c5fd"
341
- }
342
- },
343
- ".cm-searchMatch": {
344
- backgroundColor: "#854d0e",
345
- outline: "1px solid #a16207"
346
- },
347
- ".cm-searchMatch.cm-searchMatch-selected": {
348
- backgroundColor: "#166534",
349
- outline: "1px solid #15803d"
350
- },
351
- ".cm-selectionMatch": {
352
- backgroundColor: "#1e3a5f"
353
- },
354
- ".cm-matchingBracket, .cm-nonmatchingBracket": {
355
- outline: "1px solid #71717a"
356
- },
357
- ".cm-matchingBracket": {
358
- backgroundColor: "#3f3f46"
359
- }
360
- },
361
- { dark: true }
362
- );
363
- var highlightStyle2 = HighlightStyle.define([
364
- { tag: tags.keyword, color: "#c084fc" },
365
- { tag: [tags.name, tags.deleted, tags.character, tags.macroName], color: "#e4e4e7" },
366
- { tag: [tags.function(tags.variableName), tags.labelName], color: "#60a5fa" },
367
- { tag: [tags.color, tags.constant(tags.name), tags.standard(tags.name)], color: "#fbbf24" },
368
- { tag: [tags.definition(tags.name), tags.separator], color: "#e4e4e7" },
369
- { tag: [tags.typeName, tags.className, tags.number, tags.changed, tags.annotation, tags.modifier, tags.self, tags.namespace], color: "#fbbf24" },
370
- { tag: [tags.operator, tags.operatorKeyword, tags.url, tags.escape, tags.regexp, tags.link, tags.special(tags.string)], color: "#22d3ee" },
371
- { tag: [tags.meta, tags.comment], color: "#71717a", fontStyle: "italic" },
372
- { tag: tags.strong, fontWeight: "bold" },
373
- { tag: tags.emphasis, fontStyle: "italic" },
374
- { tag: tags.strikethrough, textDecoration: "line-through" },
375
- { tag: tags.link, color: "#60a5fa", textDecoration: "underline" },
376
- { tag: tags.heading, fontWeight: "bold", color: "#c084fc" },
377
- { tag: [tags.atom, tags.bool, tags.special(tags.variableName)], color: "#fbbf24" },
378
- { tag: [tags.processingInstruction, tags.string, tags.inserted], color: "#34d399" },
379
- { tag: tags.invalid, color: "#f87171" },
380
- { tag: tags.tagName, color: "#f87171" },
381
- { tag: tags.attributeName, color: "#fbbf24" },
382
- { tag: tags.attributeValue, color: "#34d399" }
383
- ]);
810
+
811
+ // src/themes/dark.ts
384
812
  registerTheme({
385
813
  name: "dark",
386
814
  variant: "dark",
387
- editorTheme: editorTheme2,
388
- highlightStyle: highlightStyle2
815
+ colors: {
816
+ background: "#18181b",
817
+ foreground: "#e4e4e7",
818
+ gutterBackground: "#18181b",
819
+ gutterForeground: "#52525b",
820
+ gutterBorder: "#27272a",
821
+ activeLineBackground: "#27272a",
822
+ selectionBackground: "#1e3a5f",
823
+ cursorColor: "#60a5fa",
824
+ keyword: "#c084fc",
825
+ string: "#34d399",
826
+ comment: "#71717a",
827
+ number: "#fbbf24",
828
+ operator: "#22d3ee",
829
+ function: "#60a5fa",
830
+ type: "#fbbf24",
831
+ tag: "#f87171",
832
+ attribute: "#fbbf24",
833
+ attributeValue: "#34d399",
834
+ punctuation: "#a1a1aa",
835
+ variable: "#e4e4e7"
836
+ }
389
837
  });
390
838
 
391
- // src/hooks/use-codemirror.ts
392
- function resolveLanguageExtension(name) {
393
- const def = getLanguage(name);
394
- if (!def) return [];
395
- const result = def.support();
396
- if (result instanceof Promise) {
397
- return [];
398
- }
399
- return result;
839
+ // src/engine/highlight.ts
840
+ function escapeHtml(text) {
841
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
400
842
  }
401
- function resolveThemeExtensions(name) {
402
- const def = getTheme(name);
403
- if (!def) {
404
- const fallback = getTheme("dark");
405
- if (!fallback) return [];
406
- return [fallback.editorTheme, syntaxHighlighting(fallback.highlightStyle)];
843
+ function highlightCode(source, tokens, colors) {
844
+ if (tokens.length === 0) return escapeHtml(source);
845
+ const colorMap = {
846
+ keyword: colors.keyword,
847
+ string: colors.string,
848
+ comment: colors.comment,
849
+ number: colors.number,
850
+ operator: colors.operator,
851
+ function: colors.function,
852
+ type: colors.type,
853
+ tag: colors.tag,
854
+ attribute: colors.attribute,
855
+ attributeValue: colors.attributeValue,
856
+ punctuation: colors.punctuation,
857
+ variable: colors.variable,
858
+ plain: colors.foreground
859
+ };
860
+ const parts = [];
861
+ let pos = 0;
862
+ for (const token of tokens) {
863
+ if (token.start > pos) {
864
+ parts.push(escapeHtml(source.slice(pos, token.start)));
865
+ }
866
+ const text = escapeHtml(source.slice(token.start, token.end));
867
+ const color = colorMap[token.type];
868
+ const style = token.type === "comment" ? `color:${color};font-style:italic` : `color:${color}`;
869
+ parts.push(`<span style="${style}">${text}</span>`);
870
+ pos = token.end;
871
+ }
872
+ if (pos < source.length) {
873
+ parts.push(escapeHtml(source.slice(pos)));
407
874
  }
408
- return [def.editorTheme, syntaxHighlighting(def.highlightStyle)];
875
+ return parts.join("");
409
876
  }
410
- function useCodemirror({
411
- containerRef,
877
+
878
+ // src/hooks/use-editor-engine.ts
879
+ var DEFAULT_COLORS = {
880
+ background: "#18181b",
881
+ foreground: "#e4e4e7",
882
+ gutterBackground: "#18181b",
883
+ gutterForeground: "#52525b",
884
+ gutterBorder: "#27272a",
885
+ activeLineBackground: "#27272a",
886
+ selectionBackground: "#1e3a5f",
887
+ cursorColor: "#60a5fa",
888
+ keyword: "#c084fc",
889
+ string: "#34d399",
890
+ comment: "#71717a",
891
+ number: "#fbbf24",
892
+ operator: "#22d3ee",
893
+ function: "#60a5fa",
894
+ type: "#fbbf24",
895
+ tag: "#f87171",
896
+ attribute: "#fbbf24",
897
+ attributeValue: "#34d399",
898
+ punctuation: "#a1a1aa",
899
+ variable: "#e4e4e7"
900
+ };
901
+ function useEditorEngine({
412
902
  value,
413
903
  onChange,
414
904
  language,
415
905
  theme,
416
906
  readOnly,
417
- lineNumbers: lineNumbers$1,
418
- wordWrap,
419
907
  tabSize,
420
- placeholder: placeholder$1,
421
- minHeight,
422
- maxHeight,
423
- searchEnabled,
424
- additionalExtensions,
425
908
  onCursorChange
426
909
  }) {
427
- const viewRef = useRef(null);
428
- const isExternalUpdate = useRef(false);
910
+ const textareaRef = useRef(null);
429
911
  const onChangeRef = useRef(onChange);
430
912
  const onCursorChangeRef = useRef(onCursorChange);
431
913
  onChangeRef.current = onChange;
432
914
  onCursorChangeRef.current = onCursorChange;
433
- const languageComp = useRef(new Compartment());
434
- const themeComp = useRef(new Compartment());
435
- const lineNumbersComp = useRef(new Compartment());
436
- const wrapComp = useRef(new Compartment());
437
- const tabSizeComp = useRef(new Compartment());
438
- const readOnlyComp = useRef(new Compartment());
439
- const placeholderComp = useRef(new Compartment());
440
- const heightComp = useRef(new Compartment());
441
- function buildHeightExtension(min, max) {
442
- const styles = {};
443
- if (min) styles.minHeight = `${min}px`;
444
- if (max) styles.maxHeight = `${max}px`;
445
- if (Object.keys(styles).length === 0) return [];
446
- return EditorView.theme({
447
- "&": { ...max ? { maxHeight: `${max}px` } : {} },
448
- ".cm-scroller": { overflow: "auto", ...styles }
449
- });
450
- }
451
- useEffect(() => {
452
- const container = containerRef.current;
453
- if (!container) return;
454
- const updateListener = EditorView.updateListener.of((update) => {
455
- if (update.docChanged && !isExternalUpdate.current) {
456
- onChangeRef.current?.(update.state.doc.toString());
457
- }
458
- if (update.selectionSet || update.docChanged) {
459
- const pos = update.state.selection.main;
460
- const line = update.state.doc.lineAt(pos.head);
461
- onCursorChangeRef.current?.({
462
- line: line.number,
463
- col: pos.head - line.from + 1,
464
- selectionLength: Math.abs(pos.to - pos.from)
465
- });
466
- }
467
- });
468
- const state = EditorState.create({
469
- doc: value,
470
- extensions: [
471
- // Compartmentalized extensions
472
- languageComp.current.of(resolveLanguageExtension(language)),
473
- themeComp.current.of(resolveThemeExtensions(theme)),
474
- lineNumbersComp.current.of(lineNumbers$1 ? [lineNumbers(), highlightActiveLineGutter()] : []),
475
- wrapComp.current.of(wordWrap ? EditorView.lineWrapping : []),
476
- tabSizeComp.current.of(EditorState.tabSize.of(tabSize)),
477
- readOnlyComp.current.of(EditorState.readOnly.of(readOnly)),
478
- placeholderComp.current.of(placeholder$1 ? placeholder(placeholder$1) : []),
479
- heightComp.current.of(buildHeightExtension(minHeight, maxHeight)),
480
- // Static extensions
481
- history(),
482
- foldGutter(),
483
- drawSelection(),
484
- dropCursor(),
485
- indentOnInput(),
486
- bracketMatching(),
487
- closeBrackets(),
488
- autocompletion(),
489
- highlightActiveLine(),
490
- highlightSelectionMatches(),
491
- keymap.of([
492
- ...closeBracketsKeymap,
493
- ...defaultKeymap,
494
- ...searchKeymap,
495
- ...historyKeymap,
496
- ...foldKeymap,
497
- ...completionKeymap,
498
- indentWithTab
499
- ]),
500
- updateListener,
501
- ...additionalExtensions ?? []
502
- ]
503
- });
504
- const view = new EditorView({ state, parent: container });
505
- viewRef.current = view;
506
- return () => {
507
- view.destroy();
508
- viewRef.current = null;
509
- };
510
- }, [containerRef]);
511
- useEffect(() => {
512
- const view = viewRef.current;
513
- if (!view) return;
514
- const currentDoc = view.state.doc.toString();
515
- if (value !== currentDoc) {
516
- isExternalUpdate.current = true;
517
- view.dispatch({
518
- changes: { from: 0, to: currentDoc.length, insert: value }
519
- });
520
- isExternalUpdate.current = false;
915
+ const [scrollTop, setScrollTop] = useState(0);
916
+ const [scrollLeft, setScrollLeft] = useState(0);
917
+ const [activeLine, setActiveLine] = useState(1);
918
+ const themeColors = useMemo(() => {
919
+ const def = getTheme(theme);
920
+ return def?.colors ?? DEFAULT_COLORS;
921
+ }, [theme]);
922
+ const tokenizer = useMemo(() => {
923
+ const def = getLanguage(language);
924
+ return def?.tokenize ?? null;
925
+ }, [language]);
926
+ const highlightedHtml = useMemo(() => {
927
+ if (!tokenizer) {
928
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
521
929
  }
930
+ const tokens = tokenizer(value);
931
+ return highlightCode(value, tokens, themeColors);
932
+ }, [value, tokenizer, themeColors]);
933
+ const lineCount = useMemo(() => {
934
+ let count = 1;
935
+ for (let i = 0; i < value.length; i++) {
936
+ if (value[i] === "\n") count++;
937
+ }
938
+ return count;
522
939
  }, [value]);
523
940
  useEffect(() => {
524
- const view = viewRef.current;
525
- if (!view) return;
526
- const def = getLanguage(language);
527
- if (!def) {
528
- view.dispatch({ effects: languageComp.current.reconfigure([]) });
529
- return;
530
- }
531
- const result = def.support();
532
- if (result instanceof Promise) {
533
- result.then((ext) => {
534
- if (viewRef.current) {
535
- viewRef.current.dispatch({ effects: languageComp.current.reconfigure(ext) });
941
+ const ta = textareaRef.current;
942
+ if (!ta || ta.value === value) return;
943
+ const { selectionStart, selectionEnd } = ta;
944
+ ta.value = value;
945
+ ta.selectionStart = selectionStart;
946
+ ta.selectionEnd = selectionEnd;
947
+ }, [value]);
948
+ const updateCursorInfo = useCallback(() => {
949
+ const ta = textareaRef.current;
950
+ if (!ta) return;
951
+ const pos = ta.selectionStart;
952
+ const textBefore = ta.value.slice(0, pos);
953
+ const line = (textBefore.match(/\n/g) || []).length + 1;
954
+ const lastNewline = textBefore.lastIndexOf("\n");
955
+ const col = pos - lastNewline;
956
+ const selLen = Math.abs(ta.selectionEnd - ta.selectionStart);
957
+ setActiveLine(line);
958
+ onCursorChangeRef.current?.({ line, col, selectionLength: selLen });
959
+ }, []);
960
+ const handleInput = useCallback(() => {
961
+ const ta = textareaRef.current;
962
+ if (!ta) return;
963
+ onChangeRef.current?.(ta.value);
964
+ updateCursorInfo();
965
+ }, [updateCursorInfo]);
966
+ const handleSelect = useCallback(() => {
967
+ updateCursorInfo();
968
+ }, [updateCursorInfo]);
969
+ const handleScroll = useCallback(() => {
970
+ const ta = textareaRef.current;
971
+ if (!ta) return;
972
+ setScrollTop(ta.scrollTop);
973
+ setScrollLeft(ta.scrollLeft);
974
+ }, []);
975
+ const handleKeyDown = useCallback(
976
+ (e) => {
977
+ if (readOnly) {
978
+ if (e.key === "c" && (e.metaKey || e.ctrlKey)) return;
979
+ if (e.key === "a" && (e.metaKey || e.ctrlKey)) return;
980
+ e.preventDefault();
981
+ return;
982
+ }
983
+ if (e.key === "Tab") {
984
+ e.preventDefault();
985
+ const ta = e.currentTarget;
986
+ const start = ta.selectionStart;
987
+ const end = ta.selectionEnd;
988
+ const spaces = " ".repeat(tabSize);
989
+ if (e.shiftKey) {
990
+ const lines = ta.value.split("\n");
991
+ const startLine = ta.value.slice(0, start).split("\n").length - 1;
992
+ const endLine = ta.value.slice(0, end).split("\n").length - 1;
993
+ let removed = 0;
994
+ for (let i = startLine; i <= endLine; i++) {
995
+ const match = lines[i].match(new RegExp(`^ {1,${tabSize}}`));
996
+ if (match) {
997
+ removed += match[0].length;
998
+ lines[i] = lines[i].slice(match[0].length);
999
+ }
1000
+ }
1001
+ const newValue = lines.join("\n");
1002
+ ta.value = newValue;
1003
+ ta.selectionStart = Math.max(0, start - (startLine === endLine ? removed : tabSize));
1004
+ ta.selectionEnd = Math.max(0, end - removed);
1005
+ onChangeRef.current?.(newValue);
1006
+ } else if (start !== end) {
1007
+ const lines = ta.value.split("\n");
1008
+ const startLine = ta.value.slice(0, start).split("\n").length - 1;
1009
+ const endLine = ta.value.slice(0, end).split("\n").length - 1;
1010
+ for (let i = startLine; i <= endLine; i++) {
1011
+ lines[i] = spaces + lines[i];
1012
+ }
1013
+ const newValue = lines.join("\n");
1014
+ ta.value = newValue;
1015
+ ta.selectionStart = start + tabSize;
1016
+ ta.selectionEnd = end + (endLine - startLine + 1) * tabSize;
1017
+ onChangeRef.current?.(newValue);
1018
+ } else {
1019
+ const before = ta.value.slice(0, start);
1020
+ const after = ta.value.slice(end);
1021
+ ta.value = before + spaces + after;
1022
+ ta.selectionStart = ta.selectionEnd = start + tabSize;
1023
+ onChangeRef.current?.(ta.value);
536
1024
  }
537
- });
538
- } else {
539
- view.dispatch({ effects: languageComp.current.reconfigure(result) });
540
- }
541
- }, [language]);
542
- useEffect(() => {
543
- const view = viewRef.current;
544
- if (!view) return;
545
- view.dispatch({ effects: themeComp.current.reconfigure(resolveThemeExtensions(theme)) });
546
- }, [theme]);
547
- useEffect(() => {
548
- const view = viewRef.current;
549
- if (!view) return;
550
- view.dispatch({
551
- effects: lineNumbersComp.current.reconfigure(
552
- lineNumbers$1 ? [lineNumbers(), highlightActiveLineGutter()] : []
553
- )
554
- });
555
- }, [lineNumbers$1]);
556
- useEffect(() => {
557
- const view = viewRef.current;
558
- if (!view) return;
559
- view.dispatch({
560
- effects: wrapComp.current.reconfigure(wordWrap ? EditorView.lineWrapping : [])
561
- });
562
- }, [wordWrap]);
563
- useEffect(() => {
564
- const view = viewRef.current;
565
- if (!view) return;
566
- view.dispatch({
567
- effects: tabSizeComp.current.reconfigure(EditorState.tabSize.of(tabSize))
568
- });
569
- }, [tabSize]);
570
- useEffect(() => {
571
- const view = viewRef.current;
572
- if (!view) return;
573
- view.dispatch({
574
- effects: readOnlyComp.current.reconfigure(EditorState.readOnly.of(readOnly))
575
- });
576
- }, [readOnly]);
577
- useEffect(() => {
578
- const view = viewRef.current;
579
- if (!view) return;
580
- view.dispatch({
581
- effects: placeholderComp.current.reconfigure(placeholder$1 ? placeholder(placeholder$1) : [])
582
- });
583
- }, [placeholder$1]);
584
- return { view: viewRef.current };
1025
+ updateCursorInfo();
1026
+ return;
1027
+ }
1028
+ if (e.key === "Enter") {
1029
+ e.preventDefault();
1030
+ const ta = e.currentTarget;
1031
+ const start = ta.selectionStart;
1032
+ const end = ta.selectionEnd;
1033
+ const before = ta.value.slice(0, start);
1034
+ const after = ta.value.slice(end);
1035
+ const currentLine = before.split("\n").pop() || "";
1036
+ const indent = currentLine.match(/^(\s*)/)?.[1] || "";
1037
+ const lastChar = before.trimEnd().slice(-1);
1038
+ const extraIndent = lastChar === "{" || lastChar === "(" ? " ".repeat(tabSize) : "";
1039
+ const insertion = "\n" + indent + extraIndent;
1040
+ ta.value = before + insertion + after;
1041
+ ta.selectionStart = ta.selectionEnd = start + insertion.length;
1042
+ onChangeRef.current?.(ta.value);
1043
+ updateCursorInfo();
1044
+ return;
1045
+ }
1046
+ },
1047
+ [readOnly, tabSize, updateCursorInfo]
1048
+ );
1049
+ return {
1050
+ textareaRef,
1051
+ highlightedHtml,
1052
+ lineCount,
1053
+ activeLine,
1054
+ themeColors,
1055
+ handleKeyDown,
1056
+ handleInput,
1057
+ handleScroll,
1058
+ handleSelect,
1059
+ scrollTop,
1060
+ scrollLeft
1061
+ };
585
1062
  }
586
1063
  function subscribe(callback) {
587
1064
  const mq = window.matchMedia("(prefers-color-scheme: dark)");
@@ -612,8 +1089,7 @@ function CodeEditorRoot({
612
1089
  tabSize: tabSizeProp = 2,
613
1090
  placeholder,
614
1091
  minHeight,
615
- maxHeight,
616
- extensions: additionalExtensions
1092
+ maxHeight
617
1093
  }) {
618
1094
  const [currentValue, setCurrentValue] = useControllableState(valueProp, defaultValue, onChange);
619
1095
  const isDark = useDarkMode();
@@ -635,22 +1111,14 @@ function CodeEditorRoot({
635
1111
  const [isWordWrap, setIsWordWrap] = useState(wordWrapProp);
636
1112
  const [cursorPosition, setCursorPosition] = useState({ line: 1, col: 1 });
637
1113
  const [selectionLength, setSelectionLength] = useState(0);
638
- const containerRef = useRef(null);
639
- const { view } = useCodemirror({
640
- containerRef,
1114
+ useRef(null);
1115
+ const engineReturn = useEditorEngine({
641
1116
  value: currentValue,
642
1117
  onChange: setCurrentValue,
643
1118
  language: currentLanguage,
644
1119
  theme: resolvedTheme,
645
1120
  readOnly,
646
- lineNumbers: showLineNumbers,
647
- wordWrap: isWordWrap,
648
1121
  tabSize: tabSizeProp,
649
- placeholder,
650
- minHeight,
651
- maxHeight,
652
- searchEnabled: true,
653
- additionalExtensions,
654
1122
  onCursorChange: ({ line, col, selectionLength: sel }) => {
655
1123
  setCursorPosition({ line, col });
656
1124
  setSelectionLength(sel);
@@ -658,19 +1126,25 @@ function CodeEditorRoot({
658
1126
  });
659
1127
  const contextValue = useMemo(
660
1128
  () => ({
661
- view,
662
- getValue: () => view?.state.doc.toString() ?? currentValue,
1129
+ getValue: () => engineReturn.textareaRef.current?.value ?? currentValue,
663
1130
  getSelection: () => {
664
- if (!view) return "";
665
- const sel = view.state.selection.main;
666
- return view.state.sliceDoc(sel.from, sel.to);
1131
+ const ta = engineReturn.textareaRef.current;
1132
+ if (!ta) return "";
1133
+ return ta.value.slice(ta.selectionStart, ta.selectionEnd);
667
1134
  },
668
1135
  setValue: (v) => setCurrentValue(v),
669
1136
  replaceSelection: (text) => {
670
- if (!view) return;
671
- view.dispatch(view.state.replaceSelection(text));
1137
+ const ta = engineReturn.textareaRef.current;
1138
+ if (!ta) return;
1139
+ const start = ta.selectionStart;
1140
+ const end = ta.selectionEnd;
1141
+ const before = ta.value.slice(0, start);
1142
+ const after = ta.value.slice(end);
1143
+ ta.value = before + text + after;
1144
+ ta.selectionStart = ta.selectionEnd = start + text.length;
1145
+ setCurrentValue(ta.value);
672
1146
  },
673
- focus: () => view?.focus(),
1147
+ focus: () => engineReturn.textareaRef.current?.focus(),
674
1148
  language: currentLanguage,
675
1149
  setLanguage,
676
1150
  theme: resolvedTheme,
@@ -681,16 +1155,17 @@ function CodeEditorRoot({
681
1155
  toggleWordWrap: () => setIsWordWrap((w) => !w),
682
1156
  toggleLineNumbers: () => setShowLineNumbers((l) => !l),
683
1157
  copyToClipboard: async () => {
684
- const text = view?.state.doc.toString() ?? currentValue;
1158
+ const text = engineReturn.textareaRef.current?.value ?? currentValue;
685
1159
  await navigator.clipboard.writeText(text);
686
1160
  },
687
1161
  cursorPosition,
688
1162
  selectionLength,
689
- _containerRef: containerRef,
1163
+ placeholder,
1164
+ _engineReturn: engineReturn,
690
1165
  _minHeight: minHeight,
691
1166
  _maxHeight: maxHeight
692
1167
  }),
693
- [view, currentValue, currentLanguage, setLanguage, resolvedTheme, readOnly, showLineNumbers, isWordWrap, tabSizeProp, cursorPosition, selectionLength, setCurrentValue, minHeight, maxHeight]
1168
+ [engineReturn, currentValue, currentLanguage, setLanguage, resolvedTheme, readOnly, showLineNumbers, isWordWrap, tabSizeProp, cursorPosition, selectionLength, setCurrentValue, placeholder, minHeight, maxHeight]
694
1169
  );
695
1170
  return /* @__PURE__ */ jsx(CodeEditorContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx(
696
1171
  "div",