Skip to content

Commit d7d7090

Browse files
committed
feat: enhance Editor component with customizable token styles and improve json formatting
1 parent 42944b3 commit d7d7090

1 file changed

Lines changed: 65 additions & 73 deletions

File tree

src/components/editor/Editor.tsx

Lines changed: 65 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import CodeMirror, {
1313
WidgetType
1414
} from "@uiw/react-codemirror"
1515
import {json, jsonParseLinter} from "@codemirror/lang-json"
16-
import {StreamLanguage, syntaxTree} from "@codemirror/language"
16+
import {StreamLanguage, syntaxTree, type TagStyle} from "@codemirror/language"
1717
import {Diagnostic, linter} from "@codemirror/lint"
1818
import prettier from "prettier/standalone"
1919
import parserBabel from "prettier/plugins/babel"
@@ -68,6 +68,7 @@ export interface EditorInputProps extends Omit<Code0Component<HTMLDivElement>, '
6868
showValidation?: boolean,
6969
formatter?: (value: string) => Promise<string>
7070
basicSetup?: BasicSetupOptions
71+
tokenStyles?: TagStyle[]
7172
}
7273

7374
class ReactAnchorWidget extends WidgetType {
@@ -98,31 +99,6 @@ class ReactAnchorWidget extends WidgetType {
9899
}
99100
}
100101

101-
const myTheme = createTheme({
102-
theme: 'light',
103-
settings: {
104-
background: 'transparent',
105-
backgroundImage: '',
106-
foreground: 'rgba(255,255,255, 0.75)',
107-
caret: 'gray',
108-
selection: 'rgba(112,179,255,0.25)',
109-
selectionMatch: 'rgba(112,179,255,0.1)',
110-
fontSize: "0.8rem",
111-
gutterBackground: 'transparent',
112-
gutterForeground: 'rgba(255,255,255, 0.5)',
113-
gutterBorder: 'transparent',
114-
gutterActiveForeground: 'rgba(255,255,255, 1)',
115-
lineHighlight: 'rgba(255,255,255, 0.1)',
116-
},
117-
styles: [
118-
{tag: t.squareBracket, color: hashToColor("squareBracket")},
119-
{tag: t.bracket, color: hashToColor("bracket")},
120-
{tag: t.string, color: hashToColor("Text")},
121-
{tag: t.bool, color: hashToColor("Boolean")},
122-
{tag: t.number, color: hashToColor("Number")},
123-
]
124-
})
125-
126102

127103
// Helper function to check if value is a ReactNode
128104
const isReactNode = (value: any): value is React.ReactNode => {
@@ -134,6 +110,7 @@ export const Editor: React.FC<EditorInputProps> = (props) => {
134110
language,
135111
tokenizer,
136112
tokenHighlights,
113+
tokenStyles = [],
137114
suggestions,
138115
onChange,
139116
extensions = [],
@@ -149,34 +126,31 @@ export const Editor: React.FC<EditorInputProps> = (props) => {
149126
...rest
150127
} = props
151128

152-
const [formatted, setFormatted] = React.useState("")
129+
const [formatted, setFormatted] = React.useState<string>()
153130
const ref = React.useRef<HTMLDivElement>(null)
154131
const [anchors, setAnchors] = React.useState<Map<HTMLElement, { type: string, value: string }>>(new Map())
155132
const [diagnostics, setDiagnostics] = React.useState<readonly Diagnostic[]>([])
133+
const [selection, setSelection] = React.useState<{ from: number, to: number } | null>(null)
156134
const [customSuggestion, setCustomSuggestion] = React.useState<{
157135
component: React.ReactNode;
158136
position: { top: number; left: number };
159137
} | null>(null)
160138
const containerRef = React.useRef<HTMLDivElement>(null)
161139

162-
const jsonFormatter = (value: string) => {
163-
return prettier.format(JSON.stringify(value), {
164-
parser: "json",
165-
plugins: [parserBabel, parserEstree],
166-
printWidth: 1
167-
})
168-
}
169-
170140
React.useEffect(() => {
171141
(async () => {
172142
try {
173-
const pretty = formatter ? await formatter(initialValue) : language === "json" ? await jsonFormatter(initialValue) : initialValue
143+
const pretty = formatter ? await formatter(initialValue) : language === "json" ? await prettier.format(JSON.stringify(initialValue), {
144+
parser: "json",
145+
plugins: [parserBabel, parserEstree],
146+
printWidth: 1
147+
}) : initialValue
174148
setFormatted(pretty)
175149
} catch (e) {
176-
setFormatted(JSON.stringify(initialValue) ?? "")
150+
setFormatted(language == "json" ? JSON.stringify(initialValue) : initialValue)
177151
}
178152
})()
179-
}, [initialValue])
153+
}, [])
180154

181155
const internalExtensions = React.useMemo(() => {
182156
const internExtensions: Extension[] = [...extensions]
@@ -287,10 +261,13 @@ export const Editor: React.FC<EditorInputProps> = (props) => {
287261
})
288262

289263
if (foundKey && !userTag) {
290-
builder.add(node.from, node.to, Decoration.replace({
291-
widget: new ReactAnchorWidget(foundKey, rawText),
292-
point: true
293-
}))
264+
const renderFn = tokenHighlights?.[foundKey]({content: rawText})
265+
if (!!renderFn) {
266+
builder.add(node.from, node.to, Decoration.replace({
267+
widget: new ReactAnchorWidget(foundKey, rawText),
268+
point: true
269+
}))
270+
}
294271
} else if (userTag) {
295272
builder.add(node.from, node.to, Decoration.replace({
296273
widget: new ReactAnchorWidget(userTag, rawText),
@@ -310,9 +287,8 @@ export const Editor: React.FC<EditorInputProps> = (props) => {
310287
}))
311288

312289
return internExtensions
313-
}, [language, tokenizer, tokenHighlights, extensions, suggestions, ref.current])
290+
}, [ref])
314291

315-
const [selection, setSelection] = React.useState<{ from: number, to: number } | null>(null)
316292

317293
const handleUpdate = React.useCallback((viewUpdate: any) => {
318294
if (viewUpdate.docChanged || viewUpdate.viewportChanged || viewUpdate.selectionSet) {
@@ -350,34 +326,48 @@ export const Editor: React.FC<EditorInputProps> = (props) => {
350326
}
351327

352328

353-
window.requestAnimationFrame(() => {
354-
const foundNodes = containerRef.current?.querySelectorAll('.cm-react-anchor')
355-
const newAnchors = new Map()
329+
const foundNodes = containerRef.current?.querySelectorAll('.cm-react-anchor')
330+
const newAnchors = new Map()
356331

357-
foundNodes?.forEach((node: any) => {
358-
newAnchors.set(node, {
359-
type: node.dataset.type,
360-
value: node.dataset.value
361-
})
332+
foundNodes?.forEach((node: any) => {
333+
newAnchors.set(node, {
334+
type: node.dataset.type,
335+
value: node.dataset.value
362336
})
363-
364-
setAnchors(newAnchors)
365337
})
338+
339+
setAnchors(newAnchors)
366340
}
367341
}, [selection])
368342

369-
React.useEffect(() => {
370-
if (containerRef.current) {
371-
const timer = window.setTimeout(() => {
372-
const event = new CustomEvent('scroll');
373-
containerRef.current?.dispatchEvent(event);
374-
handleUpdate({docChanged: true, viewportChanged: true, selectionSet: false});
375-
}, 50);
376-
return () => clearTimeout(timer);
377-
}
378-
return () => {
379-
}
380-
}, [handleUpdate]);
343+
const myTheme = React.useMemo(
344+
() => createTheme({
345+
theme: 'light',
346+
settings: {
347+
background: 'transparent',
348+
backgroundImage: '',
349+
foreground: 'rgba(255,255,255, 0.75)',
350+
caret: 'gray',
351+
selection: 'rgba(112,179,255,0.25)',
352+
selectionMatch: 'rgba(112,179,255,0.1)',
353+
fontSize: "0.8rem",
354+
gutterBackground: 'transparent',
355+
gutterForeground: 'rgba(255,255,255, 0.5)',
356+
gutterBorder: 'transparent',
357+
gutterActiveForeground: 'rgba(255,255,255, 1)',
358+
lineHighlight: 'rgba(255,255,255, 0.1)',
359+
},
360+
styles: [
361+
{tag: t.squareBracket, color: hashToColor("squareBracket")},
362+
{tag: t.bracket, color: hashToColor("bracket")},
363+
{tag: t.string, color: hashToColor("Text")},
364+
{tag: t.bool, color: hashToColor("Boolean")},
365+
{tag: t.number, color: hashToColor("Number")},
366+
...tokenStyles
367+
]
368+
}),
369+
[tokenStyles]
370+
)
381371

382372
return (
383373
<ScrollArea h={"100%"} type={"scroll"}>
@@ -458,6 +448,7 @@ export const Editor: React.FC<EditorInputProps> = (props) => {
458448
) : null}
459449
</Flex>
460450
</div>
451+
461452
)}
462453
{showTooltips && (
463454
<div className={"editor__tools"}>
@@ -488,7 +479,7 @@ export const Editor: React.FC<EditorInputProps> = (props) => {
488479
<CodeMirror
489480
width="100%"
490481
height="100%"
491-
value={language === "json" ? formatted : initialValue}
482+
value={formatted}
492483
theme={myTheme}
493484
readOnly={disabled || readonly}
494485
editable={!disabled}
@@ -526,12 +517,13 @@ export const Editor: React.FC<EditorInputProps> = (props) => {
526517
})}
527518

528519
{customSuggestion && createPortal(
529-
<div key={customSuggestion.position.top + '-' + customSuggestion.position.left} ref={ref} style={{
530-
position: 'fixed',
531-
top: customSuggestion.position.top,
532-
left: customSuggestion.position.left,
533-
zIndex: 9999,
534-
}}
520+
<div key={customSuggestion.position.top + '-' + customSuggestion.position.left} ref={ref}
521+
style={{
522+
position: 'fixed',
523+
top: customSuggestion.position.top,
524+
left: customSuggestion.position.left,
525+
zIndex: 9999,
526+
}}
535527
>
536528
{customSuggestion.component}
537529
</div>,

0 commit comments

Comments
 (0)