@particle-academy/fancy-code 0.6.0 → 0.8.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
@@ -1,5 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import { RefObject, ReactNode, CSSProperties } from 'react';
3
+ import { DiffAnnotations, LineChangeType } from '@particle-academy/fancy-file-commons';
4
+ export { languageFromFilename } from '@particle-academy/fancy-file-commons';
3
5
  import { MediaKind, MediaViewerProps, InputTagProps, InputTagTriggers, InputTagAdapter } from '@particle-academy/react-fancy';
4
6
 
5
7
  interface ThemeColors {
@@ -11,6 +13,9 @@ interface ThemeColors {
11
13
  activeLineBackground: string;
12
14
  selectionBackground: string;
13
15
  cursorColor: string;
16
+ diffAdded?: string;
17
+ diffModified?: string;
18
+ diffRemoved?: string;
14
19
  keyword: string;
15
20
  string: string;
16
21
  comment: string;
@@ -43,6 +48,13 @@ interface UseEditorEngineReturn {
43
48
  handleInput: () => void;
44
49
  handleScroll: () => void;
45
50
  handleSelect: () => void;
51
+ /**
52
+ * Place the caret on a 1-based `line` / `column` and scroll the editor so the
53
+ * line is centered in view. Out-of-range positions clamp to document bounds.
54
+ */
55
+ revealLine: (line: number, column?: number, opts?: {
56
+ focus?: boolean;
57
+ }) => void;
46
58
  scrollTop: number;
47
59
  scrollLeft: number;
48
60
  }
@@ -76,6 +88,25 @@ interface CodeEditorProps {
76
88
  minHeight?: number;
77
89
  /** Maximum height in px (scrolls beyond this) */
78
90
  maxHeight?: number;
91
+ /**
92
+ * 1-based line to reveal (scroll to + place the caret on). Applied on mount
93
+ * and whenever it (or `cursorColumn`) changes — so opening a file *at* a line,
94
+ * or re-targeting an already-open file, scrolls the line into view. Does not
95
+ * steal focus; call the imperative `revealLine`/`setCursor` (or `focus`) for
96
+ * that. Out-of-range values clamp to document bounds.
97
+ */
98
+ cursorLine?: number;
99
+ /** 1-based column for {@link cursorLine} (default 1). */
100
+ cursorColumn?: number;
101
+ /**
102
+ * Baseline document for the diff gutter. When set, the line-number column
103
+ * shows per-line change marks of the CURRENT value against this baseline —
104
+ * VS Code convention: a green bar for added lines, a blue bar for modified
105
+ * lines, and a red wedge where lines were deleted. Recomputed live as the
106
+ * user types (line-level only — cheap). `null`/omitted disables the gutter
107
+ * marks. Requires `lineNumbers`.
108
+ */
109
+ diffBase?: string | null;
79
110
  }
80
111
  interface CodeEditorContextValue {
81
112
  /** Get the current document text */
@@ -88,6 +119,16 @@ interface CodeEditorContextValue {
88
119
  replaceSelection: (text: string) => void;
89
120
  /** Focus the editor */
90
121
  focus: () => void;
122
+ /**
123
+ * Scroll to + place the caret on a 1-based `line` (optional 1-based `column`),
124
+ * focusing the editor. Out-of-range positions clamp to document bounds.
125
+ */
126
+ revealLine: (line: number, column?: number) => void;
127
+ /** Place the caret at a 1-based `{ line, column }` and reveal it (focuses). */
128
+ setCursor: (pos: {
129
+ line: number;
130
+ column?: number;
131
+ }) => void;
91
132
  /** Current language name */
92
133
  language: string;
93
134
  /** Change the active language */
@@ -124,6 +165,18 @@ interface CodeEditorContextValue {
124
165
  textareaRef: RefObject<HTMLTextAreaElement | null>;
125
166
  /** Placeholder text */
126
167
  placeholder?: string;
168
+ /**
169
+ * Totals of the live diff against {@link CodeEditorProps.diffBase} —
170
+ * `{ added, modified, removed }` — or `null` when no baseline is set.
171
+ * Handy for a status-bar chip or a host's save indicator.
172
+ */
173
+ diffStats: {
174
+ added: number;
175
+ modified: number;
176
+ removed: number;
177
+ } | null;
178
+ /** @internal — per-line gutter annotations for the diff marks. */
179
+ _diffAnnotations: DiffAnnotations | null;
127
180
  /** @internal */
128
181
  _engineReturn: UseEditorEngineReturn | null;
129
182
  /** @internal */
@@ -163,7 +216,7 @@ declare namespace CodeEditorStatusBar {
163
216
  var displayName: string;
164
217
  }
165
218
 
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;
219
+ 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, diffBase, }: CodeEditorProps): react.JSX.Element;
167
220
  declare namespace CodeEditorRoot {
168
221
  var displayName: string;
169
222
  }
@@ -243,11 +296,7 @@ interface ResolveFileKindInput {
243
296
  /** MIME type — preferred over the filename when known. */
244
297
  mime?: string;
245
298
  }
246
- /**
247
- * Resolve the fancy-code editor language for a filename. Returns `"plaintext"`
248
- * (no syntax highlighting) for unknown or extension-less files.
249
- */
250
- declare function languageFromFilename(filename?: string): string;
299
+
251
300
  /**
252
301
  * Decide whether a file is previewable media (image / video / audio / PDF) or
253
302
  * text, from its MIME type (preferred) and/or filename. Media wins when the
@@ -325,6 +374,29 @@ declare namespace FileViewer {
325
374
  var displayName: string;
326
375
  }
327
376
 
377
+ /**
378
+ * Diff-gutter model — the pure mapping between fancy-file-commons line
379
+ * annotations and what the CodeEditor gutter renders per line-number row.
380
+ * Kept DOM-free (like engine/position.ts) so it's unit-testable; the panel
381
+ * only paints what this resolves.
382
+ */
383
+
384
+ /** What one line-number row shows: its own change bar and/or a deletion wedge above. */
385
+ interface GutterDiffMark {
386
+ /** The line's own change (colored bar), if any. */
387
+ type?: LineChangeType;
388
+ /** True when one or more lines were deleted immediately above this line (wedge). */
389
+ deletedAbove: boolean;
390
+ }
391
+ /** Resolve the mark for a 1-based line, or null when the line is untouched. */
392
+ declare function gutterDiffMark(annotations: DiffAnnotations | null, line: number): GutterDiffMark | null;
393
+ /** Whether the deleted-at-EOF wedge should render under the last line. */
394
+ declare function hasDeletedAtEnd(annotations: DiffAnnotations | null): boolean;
395
+ /** The bar color for a change type, honoring theme overrides. */
396
+ declare function diffMarkColor(type: LineChangeType, colors: ThemeColors): string;
397
+ /** The deletion-wedge color, honoring theme overrides. */
398
+ declare function diffRemovedColor(colors: ThemeColors): string;
399
+
328
400
  /**
329
401
  * Tiny, dependency-free Markdown → HTML renderer for the MarkdownEditor preview
330
402
  * pane. Covers the common subset — headings, bold/italic, inline + fenced code,
@@ -409,4 +481,4 @@ interface CodeEditorInputTagProps extends Omit<InputTagProps, "adapter"> {
409
481
  */
410
482
  declare function CodeEditorInputTag(props: CodeEditorInputTagProps): react.JSX.Element;
411
483
 
412
- export { CodeEditor, type CodeEditorContextValue, CodeEditorInputTag, type CodeEditorInputTagProps, type CodeEditorPanelProps, type CodeEditorProps, type CodeEditorStatusBarProps, type CodeEditorToolbarProps, type FileKind, FileViewer, type FileViewerProps, type LanguageDefinition, MarkdownEditor, type MarkdownEditorMode, type MarkdownEditorProps, type ResolveFileKindInput, type ThemeColors, type ThemeDefinition, type Token, type TokenType, type Tokenizer, getLanguage, getRegisteredLanguages, getRegisteredThemes, getTheme, languageFromFilename, registerLanguage, registerTheme, renderMarkdown, resolveFileKind, tokenizeMarkdown, useCodeEditor, useCodeEditorInputTagAdapter };
484
+ export { CodeEditor, type CodeEditorContextValue, CodeEditorInputTag, type CodeEditorInputTagProps, type CodeEditorPanelProps, type CodeEditorProps, type CodeEditorStatusBarProps, type CodeEditorToolbarProps, type FileKind, FileViewer, type FileViewerProps, type GutterDiffMark, type LanguageDefinition, MarkdownEditor, type MarkdownEditorMode, type MarkdownEditorProps, type ResolveFileKindInput, type ThemeColors, type ThemeDefinition, type Token, type TokenType, type Tokenizer, diffMarkColor, diffRemovedColor, getLanguage, getRegisteredLanguages, getRegisteredThemes, getTheme, gutterDiffMark, hasDeletedAtEnd, registerLanguage, registerTheme, renderMarkdown, resolveFileKind, tokenizeMarkdown, useCodeEditor, useCodeEditorInputTagAdapter };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import { RefObject, ReactNode, CSSProperties } from 'react';
3
+ import { DiffAnnotations, LineChangeType } from '@particle-academy/fancy-file-commons';
4
+ export { languageFromFilename } from '@particle-academy/fancy-file-commons';
3
5
  import { MediaKind, MediaViewerProps, InputTagProps, InputTagTriggers, InputTagAdapter } from '@particle-academy/react-fancy';
4
6
 
5
7
  interface ThemeColors {
@@ -11,6 +13,9 @@ interface ThemeColors {
11
13
  activeLineBackground: string;
12
14
  selectionBackground: string;
13
15
  cursorColor: string;
16
+ diffAdded?: string;
17
+ diffModified?: string;
18
+ diffRemoved?: string;
14
19
  keyword: string;
15
20
  string: string;
16
21
  comment: string;
@@ -43,6 +48,13 @@ interface UseEditorEngineReturn {
43
48
  handleInput: () => void;
44
49
  handleScroll: () => void;
45
50
  handleSelect: () => void;
51
+ /**
52
+ * Place the caret on a 1-based `line` / `column` and scroll the editor so the
53
+ * line is centered in view. Out-of-range positions clamp to document bounds.
54
+ */
55
+ revealLine: (line: number, column?: number, opts?: {
56
+ focus?: boolean;
57
+ }) => void;
46
58
  scrollTop: number;
47
59
  scrollLeft: number;
48
60
  }
@@ -76,6 +88,25 @@ interface CodeEditorProps {
76
88
  minHeight?: number;
77
89
  /** Maximum height in px (scrolls beyond this) */
78
90
  maxHeight?: number;
91
+ /**
92
+ * 1-based line to reveal (scroll to + place the caret on). Applied on mount
93
+ * and whenever it (or `cursorColumn`) changes — so opening a file *at* a line,
94
+ * or re-targeting an already-open file, scrolls the line into view. Does not
95
+ * steal focus; call the imperative `revealLine`/`setCursor` (or `focus`) for
96
+ * that. Out-of-range values clamp to document bounds.
97
+ */
98
+ cursorLine?: number;
99
+ /** 1-based column for {@link cursorLine} (default 1). */
100
+ cursorColumn?: number;
101
+ /**
102
+ * Baseline document for the diff gutter. When set, the line-number column
103
+ * shows per-line change marks of the CURRENT value against this baseline —
104
+ * VS Code convention: a green bar for added lines, a blue bar for modified
105
+ * lines, and a red wedge where lines were deleted. Recomputed live as the
106
+ * user types (line-level only — cheap). `null`/omitted disables the gutter
107
+ * marks. Requires `lineNumbers`.
108
+ */
109
+ diffBase?: string | null;
79
110
  }
80
111
  interface CodeEditorContextValue {
81
112
  /** Get the current document text */
@@ -88,6 +119,16 @@ interface CodeEditorContextValue {
88
119
  replaceSelection: (text: string) => void;
89
120
  /** Focus the editor */
90
121
  focus: () => void;
122
+ /**
123
+ * Scroll to + place the caret on a 1-based `line` (optional 1-based `column`),
124
+ * focusing the editor. Out-of-range positions clamp to document bounds.
125
+ */
126
+ revealLine: (line: number, column?: number) => void;
127
+ /** Place the caret at a 1-based `{ line, column }` and reveal it (focuses). */
128
+ setCursor: (pos: {
129
+ line: number;
130
+ column?: number;
131
+ }) => void;
91
132
  /** Current language name */
92
133
  language: string;
93
134
  /** Change the active language */
@@ -124,6 +165,18 @@ interface CodeEditorContextValue {
124
165
  textareaRef: RefObject<HTMLTextAreaElement | null>;
125
166
  /** Placeholder text */
126
167
  placeholder?: string;
168
+ /**
169
+ * Totals of the live diff against {@link CodeEditorProps.diffBase} —
170
+ * `{ added, modified, removed }` — or `null` when no baseline is set.
171
+ * Handy for a status-bar chip or a host's save indicator.
172
+ */
173
+ diffStats: {
174
+ added: number;
175
+ modified: number;
176
+ removed: number;
177
+ } | null;
178
+ /** @internal — per-line gutter annotations for the diff marks. */
179
+ _diffAnnotations: DiffAnnotations | null;
127
180
  /** @internal */
128
181
  _engineReturn: UseEditorEngineReturn | null;
129
182
  /** @internal */
@@ -163,7 +216,7 @@ declare namespace CodeEditorStatusBar {
163
216
  var displayName: string;
164
217
  }
165
218
 
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;
219
+ 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, diffBase, }: CodeEditorProps): react.JSX.Element;
167
220
  declare namespace CodeEditorRoot {
168
221
  var displayName: string;
169
222
  }
@@ -243,11 +296,7 @@ interface ResolveFileKindInput {
243
296
  /** MIME type — preferred over the filename when known. */
244
297
  mime?: string;
245
298
  }
246
- /**
247
- * Resolve the fancy-code editor language for a filename. Returns `"plaintext"`
248
- * (no syntax highlighting) for unknown or extension-less files.
249
- */
250
- declare function languageFromFilename(filename?: string): string;
299
+
251
300
  /**
252
301
  * Decide whether a file is previewable media (image / video / audio / PDF) or
253
302
  * text, from its MIME type (preferred) and/or filename. Media wins when the
@@ -325,6 +374,29 @@ declare namespace FileViewer {
325
374
  var displayName: string;
326
375
  }
327
376
 
377
+ /**
378
+ * Diff-gutter model — the pure mapping between fancy-file-commons line
379
+ * annotations and what the CodeEditor gutter renders per line-number row.
380
+ * Kept DOM-free (like engine/position.ts) so it's unit-testable; the panel
381
+ * only paints what this resolves.
382
+ */
383
+
384
+ /** What one line-number row shows: its own change bar and/or a deletion wedge above. */
385
+ interface GutterDiffMark {
386
+ /** The line's own change (colored bar), if any. */
387
+ type?: LineChangeType;
388
+ /** True when one or more lines were deleted immediately above this line (wedge). */
389
+ deletedAbove: boolean;
390
+ }
391
+ /** Resolve the mark for a 1-based line, or null when the line is untouched. */
392
+ declare function gutterDiffMark(annotations: DiffAnnotations | null, line: number): GutterDiffMark | null;
393
+ /** Whether the deleted-at-EOF wedge should render under the last line. */
394
+ declare function hasDeletedAtEnd(annotations: DiffAnnotations | null): boolean;
395
+ /** The bar color for a change type, honoring theme overrides. */
396
+ declare function diffMarkColor(type: LineChangeType, colors: ThemeColors): string;
397
+ /** The deletion-wedge color, honoring theme overrides. */
398
+ declare function diffRemovedColor(colors: ThemeColors): string;
399
+
328
400
  /**
329
401
  * Tiny, dependency-free Markdown → HTML renderer for the MarkdownEditor preview
330
402
  * pane. Covers the common subset — headings, bold/italic, inline + fenced code,
@@ -409,4 +481,4 @@ interface CodeEditorInputTagProps extends Omit<InputTagProps, "adapter"> {
409
481
  */
410
482
  declare function CodeEditorInputTag(props: CodeEditorInputTagProps): react.JSX.Element;
411
483
 
412
- export { CodeEditor, type CodeEditorContextValue, CodeEditorInputTag, type CodeEditorInputTagProps, type CodeEditorPanelProps, type CodeEditorProps, type CodeEditorStatusBarProps, type CodeEditorToolbarProps, type FileKind, FileViewer, type FileViewerProps, type LanguageDefinition, MarkdownEditor, type MarkdownEditorMode, type MarkdownEditorProps, type ResolveFileKindInput, type ThemeColors, type ThemeDefinition, type Token, type TokenType, type Tokenizer, getLanguage, getRegisteredLanguages, getRegisteredThemes, getTheme, languageFromFilename, registerLanguage, registerTheme, renderMarkdown, resolveFileKind, tokenizeMarkdown, useCodeEditor, useCodeEditorInputTagAdapter };
484
+ export { CodeEditor, type CodeEditorContextValue, CodeEditorInputTag, type CodeEditorInputTagProps, type CodeEditorPanelProps, type CodeEditorProps, type CodeEditorStatusBarProps, type CodeEditorToolbarProps, type FileKind, FileViewer, type FileViewerProps, type GutterDiffMark, type LanguageDefinition, MarkdownEditor, type MarkdownEditorMode, type MarkdownEditorProps, type ResolveFileKindInput, type ThemeColors, type ThemeDefinition, type Token, type TokenType, type Tokenizer, diffMarkColor, diffRemovedColor, getLanguage, getRegisteredLanguages, getRegisteredThemes, getTheme, gutterDiffMark, hasDeletedAtEnd, registerLanguage, registerTheme, renderMarkdown, resolveFileKind, tokenizeMarkdown, useCodeEditor, useCodeEditorInputTagAdapter };
package/dist/index.js CHANGED
@@ -1,5 +1,7 @@
1
- import { createContext, useState, useCallback, useRef, useMemo, useSyncExternalStore, useEffect, useContext } from 'react';
1
+ import { createContext, useMemo, useState, useCallback, useRef, useEffect, useSyncExternalStore, useContext } from 'react';
2
2
  import { cn, useControllableState, resolveMediaType, MediaViewer, textareaAdapter, InputTag } from '@particle-academy/react-fancy';
3
+ import { annotateLines, languageFromFilename } from '@particle-academy/fancy-file-commons';
4
+ export { languageFromFilename } from '@particle-academy/fancy-file-commons';
3
5
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
4
6
 
5
7
  // src/components/CodeEditor/CodeEditor.tsx
@@ -11,6 +13,24 @@ function useCodeEditor() {
11
13
  }
12
14
  return ctx;
13
15
  }
16
+
17
+ // src/components/CodeEditor/diff-gutter.ts
18
+ function gutterDiffMark(annotations, line) {
19
+ if (!annotations) return null;
20
+ const a = annotations.byLine[line];
21
+ if (!a) return null;
22
+ return { type: a.type, deletedAbove: (a.deletedAbove ?? 0) > 0 };
23
+ }
24
+ function hasDeletedAtEnd(annotations) {
25
+ return (annotations?.deletedAtEnd ?? 0) > 0;
26
+ }
27
+ var FALLBACK = { added: "#3fb950", modified: "#4184e4", removed: "#f85149" };
28
+ function diffMarkColor(type, colors) {
29
+ return type === "added" ? colors.diffAdded ?? FALLBACK.added : colors.diffModified ?? FALLBACK.modified;
30
+ }
31
+ function diffRemovedColor(colors) {
32
+ return colors.diffRemoved ?? FALLBACK.removed;
33
+ }
14
34
  function CodeEditorPanel({ className }) {
15
35
  const {
16
36
  _engineReturn: engine,
@@ -18,6 +38,7 @@ function CodeEditorPanel({ className }) {
18
38
  wordWrap,
19
39
  readOnly,
20
40
  placeholder,
41
+ _diffAnnotations: diffAnnotations,
21
42
  _maxHeight,
22
43
  _minHeight
23
44
  } = useCodeEditor();
@@ -38,25 +59,80 @@ function CodeEditorPanel({ className }) {
38
59
  const gutterWidth = lineNumbers ? `${Math.max(String(lineCount).length, 2) * 0.75 + 1.5}em` : "0";
39
60
  const lineNumberElements = useMemo(() => {
40
61
  if (!lineNumbers) return null;
62
+ const removed = diffRemovedColor(themeColors);
41
63
  const lines = [];
42
64
  for (let i = 1; i <= lineCount; i++) {
65
+ const mark = gutterDiffMark(diffAnnotations, i);
43
66
  lines.push(
44
- /* @__PURE__ */ jsx(
67
+ /* @__PURE__ */ jsxs(
45
68
  "div",
46
69
  {
70
+ "data-diff": mark?.type,
71
+ "data-diff-deleted-above": mark?.deletedAbove ? "" : void 0,
47
72
  style: {
73
+ position: "relative",
48
74
  color: i === activeLine ? themeColors.foreground : themeColors.gutterForeground,
49
75
  backgroundColor: i === activeLine ? themeColors.activeLineBackground : void 0
50
76
  },
51
77
  className: "pr-3 text-right text-[13px] leading-[1.5] select-none",
52
- children: i
78
+ children: [
79
+ mark?.type && /* @__PURE__ */ jsx(
80
+ "span",
81
+ {
82
+ "aria-hidden": "true",
83
+ style: {
84
+ position: "absolute",
85
+ left: 0,
86
+ top: 0,
87
+ bottom: 0,
88
+ width: 3,
89
+ borderRadius: 2,
90
+ backgroundColor: diffMarkColor(mark.type, themeColors)
91
+ }
92
+ }
93
+ ),
94
+ mark?.deletedAbove && /* @__PURE__ */ jsx(
95
+ "span",
96
+ {
97
+ "aria-hidden": "true",
98
+ style: {
99
+ position: "absolute",
100
+ left: 0,
101
+ top: -4,
102
+ width: 0,
103
+ height: 0,
104
+ borderLeft: `8px solid ${removed}`,
105
+ borderTop: "4px solid transparent",
106
+ borderBottom: "4px solid transparent"
107
+ }
108
+ }
109
+ ),
110
+ i,
111
+ i === lineCount && hasDeletedAtEnd(diffAnnotations) && /* @__PURE__ */ jsx(
112
+ "span",
113
+ {
114
+ "aria-hidden": "true",
115
+ "data-diff-deleted-at-end": "",
116
+ style: {
117
+ position: "absolute",
118
+ left: 0,
119
+ bottom: -4,
120
+ width: 0,
121
+ height: 0,
122
+ borderLeft: `8px solid ${removed}`,
123
+ borderTop: "4px solid transparent",
124
+ borderBottom: "4px solid transparent"
125
+ }
126
+ }
127
+ )
128
+ ]
53
129
  },
54
130
  i
55
131
  )
56
132
  );
57
133
  }
58
134
  return lines;
59
- }, [lineNumbers, lineCount, activeLine, themeColors]);
135
+ }, [lineNumbers, lineCount, activeLine, themeColors, diffAnnotations]);
60
136
  const containerStyle = {
61
137
  backgroundColor: themeColors.background,
62
138
  color: themeColors.foreground,
@@ -1258,6 +1334,9 @@ registerTheme({
1258
1334
  activeLineBackground: "#f8fafc",
1259
1335
  selectionBackground: "#dbeafe",
1260
1336
  cursorColor: "#3b82f6",
1337
+ diffAdded: "#1a7f37",
1338
+ diffModified: "#0969da",
1339
+ diffRemoved: "#cf222e",
1261
1340
  keyword: "#8b5cf6",
1262
1341
  string: "#059669",
1263
1342
  comment: "#94a3b8",
@@ -1286,6 +1365,9 @@ registerTheme({
1286
1365
  activeLineBackground: "#27272a",
1287
1366
  selectionBackground: "#1e3a5f",
1288
1367
  cursorColor: "#60a5fa",
1368
+ diffAdded: "#3fb950",
1369
+ diffModified: "#4184e4",
1370
+ diffRemoved: "#f85149",
1289
1371
  keyword: "#c084fc",
1290
1372
  string: "#34d399",
1291
1373
  comment: "#71717a",
@@ -1344,6 +1426,19 @@ function highlightCode(source, tokens, colors) {
1344
1426
  return parts.join("");
1345
1427
  }
1346
1428
 
1429
+ // src/engine/position.ts
1430
+ function lineColumnToOffset(text, line, column = 1) {
1431
+ const lines = text.split("\n");
1432
+ const targetLine = Math.max(1, Math.min(Math.trunc(line) || 1, lines.length));
1433
+ let offset = 0;
1434
+ for (let i = 0; i < targetLine - 1; i++) {
1435
+ offset += lines[i].length + 1;
1436
+ }
1437
+ const lineText = lines[targetLine - 1] ?? "";
1438
+ const col = Math.max(1, Math.min(Math.trunc(column) || 1, lineText.length + 1));
1439
+ return offset + (col - 1);
1440
+ }
1441
+
1347
1442
  // src/hooks/use-editor-engine.ts
1348
1443
  var DEFAULT_COLORS = {
1349
1444
  background: "#18181b",
@@ -1458,6 +1553,28 @@ function useEditorEngine({
1458
1553
  setScrollTop(ta.scrollTop);
1459
1554
  setScrollLeft(ta.scrollLeft);
1460
1555
  }, []);
1556
+ const revealLine = useCallback(
1557
+ (line, column = 1, opts) => {
1558
+ const ta = textareaRef.current;
1559
+ if (!ta) return;
1560
+ const offset = lineColumnToOffset(ta.value, line, column);
1561
+ if (opts?.focus) ta.focus();
1562
+ ta.selectionStart = ta.selectionEnd = offset;
1563
+ updateCursorInfo();
1564
+ const panel = ta.closest("[data-fancy-code-panel]");
1565
+ if (!panel) return;
1566
+ const cs = getComputedStyle(ta);
1567
+ const lineHeight = parseFloat(cs.lineHeight) || 19.5;
1568
+ const paddingTop = parseFloat(cs.paddingTop) || 0;
1569
+ const lineIndex = (ta.value.slice(0, offset).match(/\n/g) || []).length;
1570
+ const textareaTopInPanel = ta.getBoundingClientRect().top - panel.getBoundingClientRect().top + panel.scrollTop;
1571
+ const lineTop = textareaTopInPanel + paddingTop + lineIndex * lineHeight;
1572
+ const target = lineTop - panel.clientHeight / 2 + lineHeight / 2;
1573
+ const maxScroll = panel.scrollHeight - panel.clientHeight;
1574
+ panel.scrollTop = Math.max(0, Math.min(target, maxScroll));
1575
+ },
1576
+ [updateCursorInfo]
1577
+ );
1461
1578
  const handleKeyDown = useCallback(
1462
1579
  (e) => {
1463
1580
  if (readOnly) {
@@ -1544,6 +1661,7 @@ function useEditorEngine({
1544
1661
  handleInput,
1545
1662
  handleScroll,
1546
1663
  handleSelect,
1664
+ revealLine,
1547
1665
  scrollTop,
1548
1666
  scrollLeft
1549
1667
  };
@@ -1577,9 +1695,16 @@ function CodeEditorRoot({
1577
1695
  tabSize: tabSizeProp = 2,
1578
1696
  placeholder,
1579
1697
  minHeight,
1580
- maxHeight
1698
+ maxHeight,
1699
+ cursorLine,
1700
+ cursorColumn,
1701
+ diffBase
1581
1702
  }) {
1582
1703
  const [currentValue, setCurrentValue] = useControllableState(valueProp, defaultValue, onChange);
1704
+ const diffAnnotations = useMemo(
1705
+ () => diffBase == null ? null : annotateLines(diffBase, currentValue),
1706
+ [diffBase, currentValue]
1707
+ );
1583
1708
  const isDark = useDarkMode();
1584
1709
  const resolvedTheme = theme === "auto" ? isDark ? "dark" : "light" : theme;
1585
1710
  const [currentLanguage, setCurrentLanguageState] = useState(() => {
@@ -1613,6 +1738,11 @@ function CodeEditorRoot({
1613
1738
  setSelectionLength(sel);
1614
1739
  }
1615
1740
  });
1741
+ const { revealLine: revealLineImpl } = engineReturn;
1742
+ useEffect(() => {
1743
+ if (cursorLine == null) return;
1744
+ revealLineImpl(cursorLine, cursorColumn ?? 1, { focus: false });
1745
+ }, [cursorLine, cursorColumn, revealLineImpl]);
1616
1746
  const contextValue = useMemo(
1617
1747
  () => ({
1618
1748
  getValue: () => engineReturn.textareaRef.current?.value ?? currentValue,
@@ -1634,6 +1764,8 @@ function CodeEditorRoot({
1634
1764
  setCurrentValue(ta.value);
1635
1765
  },
1636
1766
  focus: () => engineReturn.textareaRef.current?.focus(),
1767
+ revealLine: (line, column) => engineReturn.revealLine(line, column ?? 1, { focus: true }),
1768
+ setCursor: ({ line, column }) => engineReturn.revealLine(line, column ?? 1, { focus: true }),
1637
1769
  language: currentLanguage,
1638
1770
  setLanguage,
1639
1771
  theme: resolvedTheme,
@@ -1651,11 +1783,17 @@ function CodeEditorRoot({
1651
1783
  selectionLength,
1652
1784
  textareaRef: engineReturn.textareaRef,
1653
1785
  placeholder,
1786
+ diffStats: diffAnnotations ? {
1787
+ added: diffAnnotations.added,
1788
+ modified: diffAnnotations.modified,
1789
+ removed: diffAnnotations.removed
1790
+ } : null,
1791
+ _diffAnnotations: diffAnnotations,
1654
1792
  _engineReturn: engineReturn,
1655
1793
  _minHeight: minHeight,
1656
1794
  _maxHeight: maxHeight
1657
1795
  }),
1658
- [engineReturn, currentValue, currentLanguage, setLanguage, resolvedTheme, readOnly, showLineNumbers, isWordWrap, tabSizeProp, cursorPosition, selectionLength, setCurrentValue, placeholder, minHeight, maxHeight]
1796
+ [engineReturn, currentValue, currentLanguage, setLanguage, resolvedTheme, readOnly, showLineNumbers, isWordWrap, tabSizeProp, cursorPosition, selectionLength, setCurrentValue, placeholder, diffAnnotations, minHeight, maxHeight]
1659
1797
  );
1660
1798
  return /* @__PURE__ */ jsx(CodeEditorContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx(
1661
1799
  "div",
@@ -1831,44 +1969,6 @@ function MarkdownEditor({
1831
1969
  );
1832
1970
  }
1833
1971
  MarkdownEditor.displayName = "MarkdownEditor";
1834
- var EXT_LANGUAGE = {
1835
- js: "javascript",
1836
- mjs: "javascript",
1837
- cjs: "javascript",
1838
- jsx: "javascript",
1839
- json: "javascript",
1840
- // the JS tokenizer highlights JSON acceptably
1841
- jsonc: "javascript",
1842
- ts: "typescript",
1843
- mts: "typescript",
1844
- cts: "typescript",
1845
- tsx: "typescript",
1846
- html: "html",
1847
- htm: "html",
1848
- xml: "html",
1849
- xhtml: "html",
1850
- vue: "html",
1851
- php: "php",
1852
- phtml: "php",
1853
- py: "python",
1854
- pyw: "python",
1855
- go: "go",
1856
- md: "markdown",
1857
- markdown: "markdown",
1858
- mkd: "markdown",
1859
- mdx: "markdown"
1860
- };
1861
- function basename(filename) {
1862
- return filename.split(/[?#]/, 1)[0].split(/[\\/]/).pop() ?? "";
1863
- }
1864
- function languageFromFilename(filename) {
1865
- if (!filename) return "plaintext";
1866
- const seg = basename(filename);
1867
- const dot = seg.lastIndexOf(".");
1868
- if (dot <= 0) return "plaintext";
1869
- const ext = seg.slice(dot + 1).toLowerCase();
1870
- return EXT_LANGUAGE[ext] ?? "plaintext";
1871
- }
1872
1972
  function resolveFileKind({ filename, mime }) {
1873
1973
  const media = resolveMediaType({ mime, src: filename });
1874
1974
  if (media !== "unknown") return { kind: "media", mediaKind: media };
@@ -1952,6 +2052,6 @@ function CodeEditorInputTag(props) {
1952
2052
  return /* @__PURE__ */ jsx(InputTag, { adapter, ...props });
1953
2053
  }
1954
2054
 
1955
- export { CodeEditor, CodeEditorInputTag, FileViewer, MarkdownEditor, getLanguage, getRegisteredLanguages, getRegisteredThemes, getTheme, languageFromFilename, registerLanguage, registerTheme, renderMarkdown, resolveFileKind, tokenizeMarkdown, useCodeEditor, useCodeEditorInputTagAdapter };
2055
+ export { CodeEditor, CodeEditorInputTag, FileViewer, MarkdownEditor, diffMarkColor, diffRemovedColor, getLanguage, getRegisteredLanguages, getRegisteredThemes, getTheme, gutterDiffMark, hasDeletedAtEnd, registerLanguage, registerTheme, renderMarkdown, resolveFileKind, tokenizeMarkdown, useCodeEditor, useCodeEditorInputTagAdapter };
1956
2056
  //# sourceMappingURL=index.js.map
1957
2057
  //# sourceMappingURL=index.js.map