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