@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.cjs +46 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +28 -1
- package/dist/index.d.ts +28 -1
- package/dist/index.js +47 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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,
|
|
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,
|