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