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