@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/README.md +20 -0
- package/dist/index.cjs +151 -45
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +79 -7
- package/dist/index.d.ts +79 -7
- package/dist/index.js +145 -45
- package/dist/index.js.map +1 -1
- package/package.json +5 -2
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,
|
|
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,
|
|
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,
|
|
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__ */
|
|
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:
|
|
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,
|
|
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
|