@particle-academy/fancy-code 0.6.0 → 0.7.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.d.cts CHANGED
@@ -43,6 +43,13 @@ interface UseEditorEngineReturn {
43
43
  handleInput: () => void;
44
44
  handleScroll: () => void;
45
45
  handleSelect: () => void;
46
+ /**
47
+ * Place the caret on a 1-based `line` / `column` and scroll the editor so the
48
+ * line is centered in view. Out-of-range positions clamp to document bounds.
49
+ */
50
+ revealLine: (line: number, column?: number, opts?: {
51
+ focus?: boolean;
52
+ }) => void;
46
53
  scrollTop: number;
47
54
  scrollLeft: number;
48
55
  }
@@ -76,6 +83,16 @@ interface CodeEditorProps {
76
83
  minHeight?: number;
77
84
  /** Maximum height in px (scrolls beyond this) */
78
85
  maxHeight?: number;
86
+ /**
87
+ * 1-based line to reveal (scroll to + place the caret on). Applied on mount
88
+ * and whenever it (or `cursorColumn`) changes — so opening a file *at* a line,
89
+ * or re-targeting an already-open file, scrolls the line into view. Does not
90
+ * steal focus; call the imperative `revealLine`/`setCursor` (or `focus`) for
91
+ * that. Out-of-range values clamp to document bounds.
92
+ */
93
+ cursorLine?: number;
94
+ /** 1-based column for {@link cursorLine} (default 1). */
95
+ cursorColumn?: number;
79
96
  }
80
97
  interface CodeEditorContextValue {
81
98
  /** Get the current document text */
@@ -88,6 +105,16 @@ interface CodeEditorContextValue {
88
105
  replaceSelection: (text: string) => void;
89
106
  /** Focus the editor */
90
107
  focus: () => void;
108
+ /**
109
+ * Scroll to + place the caret on a 1-based `line` (optional 1-based `column`),
110
+ * focusing the editor. Out-of-range positions clamp to document bounds.
111
+ */
112
+ revealLine: (line: number, column?: number) => void;
113
+ /** Place the caret at a 1-based `{ line, column }` and reveal it (focuses). */
114
+ setCursor: (pos: {
115
+ line: number;
116
+ column?: number;
117
+ }) => void;
91
118
  /** Current language name */
92
119
  language: string;
93
120
  /** Change the active language */
@@ -163,7 +190,7 @@ declare namespace CodeEditorStatusBar {
163
190
  var displayName: string;
164
191
  }
165
192
 
166
- declare function CodeEditorRoot({ children, className, value: valueProp, defaultValue, onChange, language: languageProp, onLanguageChange, theme, readOnly, lineNumbers: lineNumbersProp, wordWrap: wordWrapProp, tabSize: tabSizeProp, placeholder, minHeight, maxHeight, }: CodeEditorProps): react.JSX.Element;
193
+ declare function CodeEditorRoot({ children, className, value: valueProp, defaultValue, onChange, language: languageProp, onLanguageChange, theme, readOnly, lineNumbers: lineNumbersProp, wordWrap: wordWrapProp, tabSize: tabSizeProp, placeholder, minHeight, maxHeight, cursorLine, cursorColumn, }: CodeEditorProps): react.JSX.Element;
167
194
  declare namespace CodeEditorRoot {
168
195
  var displayName: string;
169
196
  }
package/dist/index.d.ts CHANGED
@@ -43,6 +43,13 @@ interface UseEditorEngineReturn {
43
43
  handleInput: () => void;
44
44
  handleScroll: () => void;
45
45
  handleSelect: () => void;
46
+ /**
47
+ * Place the caret on a 1-based `line` / `column` and scroll the editor so the
48
+ * line is centered in view. Out-of-range positions clamp to document bounds.
49
+ */
50
+ revealLine: (line: number, column?: number, opts?: {
51
+ focus?: boolean;
52
+ }) => void;
46
53
  scrollTop: number;
47
54
  scrollLeft: number;
48
55
  }
@@ -76,6 +83,16 @@ interface CodeEditorProps {
76
83
  minHeight?: number;
77
84
  /** Maximum height in px (scrolls beyond this) */
78
85
  maxHeight?: number;
86
+ /**
87
+ * 1-based line to reveal (scroll to + place the caret on). Applied on mount
88
+ * and whenever it (or `cursorColumn`) changes — so opening a file *at* a line,
89
+ * or re-targeting an already-open file, scrolls the line into view. Does not
90
+ * steal focus; call the imperative `revealLine`/`setCursor` (or `focus`) for
91
+ * that. Out-of-range values clamp to document bounds.
92
+ */
93
+ cursorLine?: number;
94
+ /** 1-based column for {@link cursorLine} (default 1). */
95
+ cursorColumn?: number;
79
96
  }
80
97
  interface CodeEditorContextValue {
81
98
  /** Get the current document text */
@@ -88,6 +105,16 @@ interface CodeEditorContextValue {
88
105
  replaceSelection: (text: string) => void;
89
106
  /** Focus the editor */
90
107
  focus: () => void;
108
+ /**
109
+ * Scroll to + place the caret on a 1-based `line` (optional 1-based `column`),
110
+ * focusing the editor. Out-of-range positions clamp to document bounds.
111
+ */
112
+ revealLine: (line: number, column?: number) => void;
113
+ /** Place the caret at a 1-based `{ line, column }` and reveal it (focuses). */
114
+ setCursor: (pos: {
115
+ line: number;
116
+ column?: number;
117
+ }) => void;
91
118
  /** Current language name */
92
119
  language: string;
93
120
  /** Change the active language */
@@ -163,7 +190,7 @@ declare namespace CodeEditorStatusBar {
163
190
  var displayName: string;
164
191
  }
165
192
 
166
- declare function CodeEditorRoot({ children, className, value: valueProp, defaultValue, onChange, language: languageProp, onLanguageChange, theme, readOnly, lineNumbers: lineNumbersProp, wordWrap: wordWrapProp, tabSize: tabSizeProp, placeholder, minHeight, maxHeight, }: CodeEditorProps): react.JSX.Element;
193
+ declare function CodeEditorRoot({ children, className, value: valueProp, defaultValue, onChange, language: languageProp, onLanguageChange, theme, readOnly, lineNumbers: lineNumbersProp, wordWrap: wordWrapProp, tabSize: tabSizeProp, placeholder, minHeight, maxHeight, cursorLine, cursorColumn, }: CodeEditorProps): react.JSX.Element;
167
194
  declare namespace CodeEditorRoot {
168
195
  var displayName: string;
169
196
  }
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { createContext, useState, useCallback, useRef, useMemo, useSyncExternalStore, useEffect, useContext } from 'react';
1
+ import { createContext, useState, useCallback, useRef, useEffect, useMemo, useSyncExternalStore, useContext } from 'react';
2
2
  import { cn, useControllableState, resolveMediaType, MediaViewer, textareaAdapter, InputTag } from '@particle-academy/react-fancy';
3
3
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
4
4
 
@@ -1344,6 +1344,19 @@ function highlightCode(source, tokens, colors) {
1344
1344
  return parts.join("");
1345
1345
  }
1346
1346
 
1347
+ // src/engine/position.ts
1348
+ function lineColumnToOffset(text, line, column = 1) {
1349
+ const lines = text.split("\n");
1350
+ const targetLine = Math.max(1, Math.min(Math.trunc(line) || 1, lines.length));
1351
+ let offset = 0;
1352
+ for (let i = 0; i < targetLine - 1; i++) {
1353
+ offset += lines[i].length + 1;
1354
+ }
1355
+ const lineText = lines[targetLine - 1] ?? "";
1356
+ const col = Math.max(1, Math.min(Math.trunc(column) || 1, lineText.length + 1));
1357
+ return offset + (col - 1);
1358
+ }
1359
+
1347
1360
  // src/hooks/use-editor-engine.ts
1348
1361
  var DEFAULT_COLORS = {
1349
1362
  background: "#18181b",
@@ -1458,6 +1471,28 @@ function useEditorEngine({
1458
1471
  setScrollTop(ta.scrollTop);
1459
1472
  setScrollLeft(ta.scrollLeft);
1460
1473
  }, []);
1474
+ const revealLine = useCallback(
1475
+ (line, column = 1, opts) => {
1476
+ const ta = textareaRef.current;
1477
+ if (!ta) return;
1478
+ const offset = lineColumnToOffset(ta.value, line, column);
1479
+ if (opts?.focus) ta.focus();
1480
+ ta.selectionStart = ta.selectionEnd = offset;
1481
+ updateCursorInfo();
1482
+ const panel = ta.closest("[data-fancy-code-panel]");
1483
+ if (!panel) return;
1484
+ const cs = getComputedStyle(ta);
1485
+ const lineHeight = parseFloat(cs.lineHeight) || 19.5;
1486
+ const paddingTop = parseFloat(cs.paddingTop) || 0;
1487
+ const lineIndex = (ta.value.slice(0, offset).match(/\n/g) || []).length;
1488
+ const textareaTopInPanel = ta.getBoundingClientRect().top - panel.getBoundingClientRect().top + panel.scrollTop;
1489
+ const lineTop = textareaTopInPanel + paddingTop + lineIndex * lineHeight;
1490
+ const target = lineTop - panel.clientHeight / 2 + lineHeight / 2;
1491
+ const maxScroll = panel.scrollHeight - panel.clientHeight;
1492
+ panel.scrollTop = Math.max(0, Math.min(target, maxScroll));
1493
+ },
1494
+ [updateCursorInfo]
1495
+ );
1461
1496
  const handleKeyDown = useCallback(
1462
1497
  (e) => {
1463
1498
  if (readOnly) {
@@ -1544,6 +1579,7 @@ function useEditorEngine({
1544
1579
  handleInput,
1545
1580
  handleScroll,
1546
1581
  handleSelect,
1582
+ revealLine,
1547
1583
  scrollTop,
1548
1584
  scrollLeft
1549
1585
  };
@@ -1577,7 +1613,9 @@ function CodeEditorRoot({
1577
1613
  tabSize: tabSizeProp = 2,
1578
1614
  placeholder,
1579
1615
  minHeight,
1580
- maxHeight
1616
+ maxHeight,
1617
+ cursorLine,
1618
+ cursorColumn
1581
1619
  }) {
1582
1620
  const [currentValue, setCurrentValue] = useControllableState(valueProp, defaultValue, onChange);
1583
1621
  const isDark = useDarkMode();
@@ -1613,6 +1651,11 @@ function CodeEditorRoot({
1613
1651
  setSelectionLength(sel);
1614
1652
  }
1615
1653
  });
1654
+ const { revealLine: revealLineImpl } = engineReturn;
1655
+ useEffect(() => {
1656
+ if (cursorLine == null) return;
1657
+ revealLineImpl(cursorLine, cursorColumn ?? 1, { focus: false });
1658
+ }, [cursorLine, cursorColumn, revealLineImpl]);
1616
1659
  const contextValue = useMemo(
1617
1660
  () => ({
1618
1661
  getValue: () => engineReturn.textareaRef.current?.value ?? currentValue,
@@ -1634,6 +1677,8 @@ function CodeEditorRoot({
1634
1677
  setCurrentValue(ta.value);
1635
1678
  },
1636
1679
  focus: () => engineReturn.textareaRef.current?.focus(),
1680
+ revealLine: (line, column) => engineReturn.revealLine(line, column ?? 1, { focus: true }),
1681
+ setCursor: ({ line, column }) => engineReturn.revealLine(line, column ?? 1, { focus: true }),
1637
1682
  language: currentLanguage,
1638
1683
  setLanguage,
1639
1684
  theme: resolvedTheme,