@@ -11,9 +11,17 @@ import {
1111 ensureSyntaxTree ,
1212 foldAll ,
1313 foldGutter ,
14+ LanguageSupport ,
15+ type TagStyle ,
1416 unfoldAll ,
1517} from "@codemirror/language" ;
16- import { vscodeDarkInit , vscodeLightInit } from "@uiw/codemirror-theme-vscode" ;
18+ import { classHighlighter , highlightCode } from "@lezer/highlight" ;
19+ import {
20+ vscodeDarkInit ,
21+ vscodeDarkStyle ,
22+ vscodeLightInit ,
23+ vscodeLightStyle ,
24+ } from "@uiw/codemirror-theme-vscode" ;
1725import { EditorView } from "@codemirror/view" ;
1826import { SyntacticParseError } from "@mpeggroup/mpeg-sdl-parser" ;
1927export { ViewUpdate } from "@codemirror/view" ;
@@ -22,12 +30,17 @@ import { sdl } from "../sdl/sdlLanguage.ts";
2230import { sdlLinter } from "../sdl/sdlLinter.ts" ;
2331import { ruler } from "../codemirror/ruler.ts" ;
2432
33+ type TagWithNameAndModified = {
34+ name : string | undefined ;
35+ modified : Array < unknown > ;
36+ } ;
37+
2538const darkTheme = vscodeDarkInit ( { settings : { fontSize : "11px" } } ) ;
2639const lightTheme = vscodeLightInit ( { settings : { fontSize : "11px" } } ) ;
2740
2841interface EditorProps {
29- value : string ;
30- onCodeChange : ( value : string ) => void ;
42+ code : string ;
43+ onCodeChange : ( code : string ) => void ;
3144 onCursorChange : ( position : { line : number ; col : number } ) => void ;
3245 onParseErrorChange : ( syntacticParseErrors : SyntacticParseError [ ] ) => void ;
3346 theme : "light" | "dark" ;
@@ -36,55 +49,180 @@ interface EditorProps {
3649export interface EditorRef {
3750 expandAll : ( ) => void ;
3851 collapseAll : ( ) => void ;
52+ getStyledCode : ( ) => string ;
53+ }
54+
55+ function extractThemeStyleAttributes ( themeStyle : TagStyle [ ] ) {
56+ const styleAttributesByTagName = new Map < string , string > ( ) ;
57+
58+ themeStyle . forEach ( ( style : TagStyle ) => {
59+ let attributes = "" ;
60+
61+ Object . keys ( style ) . forEach ( ( key ) => {
62+ if ( ( key !== "tag" ) && ( key !== "class" ) ) {
63+ const value = style [ key as keyof typeof style ] ;
64+
65+ if ( typeof value === "string" ) {
66+ attributes += `${ key } : ${ value } ;` ;
67+ }
68+ }
69+ } ) ;
70+
71+ if ( attributes . length === 0 ) {
72+ return ;
73+ }
74+
75+ if ( style . tag instanceof Array ) {
76+ style . tag . forEach ( ( tag ) => {
77+ const actualTag = tag as unknown as TagWithNameAndModified ;
78+
79+ if ( actualTag . name && ( actualTag . modified . length === 0 ) ) {
80+ styleAttributesByTagName . set ( actualTag . name , attributes ) ;
81+ }
82+ } ) ;
83+ } else {
84+ const actualTag = style . tag as unknown as TagWithNameAndModified ;
85+
86+ if ( actualTag . name && ( actualTag . modified . length === 0 ) ) {
87+ styleAttributesByTagName . set ( actualTag . name , attributes ) ;
88+ }
89+ }
90+ } ) ;
91+
92+ return styleAttributesByTagName ;
93+ }
94+
95+ const lightStyleAttributesByTagName = extractThemeStyleAttributes (
96+ vscodeLightStyle ,
97+ ) ;
98+ const darkStyleAttributesByTagName = extractThemeStyleAttributes (
99+ vscodeDarkStyle ,
100+ ) ;
101+
102+ function getStyledCode (
103+ sdlLanguageSupport : LanguageSupport ,
104+ code : string ,
105+ theme : string ,
106+ ) : string {
107+ const styleAttributesByTagName = theme === "dark"
108+ ? darkStyleAttributesByTagName
109+ : lightStyleAttributesByTagName ;
110+
111+ let richText = "" ;
112+
113+ function emitSpan ( text : string , classes : string | undefined ) {
114+ let spanStyleAttributes ;
115+
116+ if ( classes ) {
117+ let tagName = classes ;
118+
119+ if ( classes ?. startsWith ( "tok-" ) ) {
120+ // If classes starts with "tok-" it is a class added by classHighlighter
121+ tagName = classes . substring ( 4 ) ;
122+ }
123+
124+ const styleAttributes = styleAttributesByTagName . get ( tagName ) ;
125+
126+ if ( styleAttributes ) {
127+ spanStyleAttributes = styleAttributes ;
128+ }
129+ }
130+
131+ const textNode = document . createTextNode ( text ) ;
132+ const p = document . createElement ( "p" ) ;
133+
134+ p . appendChild ( textNode ) ;
135+
136+ let span ;
137+ if ( spanStyleAttributes ) {
138+ span = "<span style='" + spanStyleAttributes + "'>" +
139+ p . innerHTML . replaceAll ( " " , " " ) +
140+ "</span>" ;
141+ } else {
142+ span = "<span>" + p . innerHTML . replaceAll ( " " , " " ) + "</span>" ;
143+ }
144+ richText += span ;
145+ }
146+
147+ function emitBreak ( ) {
148+ richText += "<br>" ;
149+ }
150+
151+ highlightCode (
152+ code ,
153+ sdlLanguageSupport . language . parser . parse ( code ) ,
154+ classHighlighter ,
155+ emitSpan ,
156+ emitBreak ,
157+ ) ;
158+
159+ return `<span style="font-family: monospace">${ richText } </span>` ;
39160}
40161
41162export const Editor = forwardRef < EditorRef , EditorProps > (
42- ( { value , onCodeChange, onCursorChange, onParseErrorChange, theme } , ref ) => {
163+ ( { code , onCodeChange, onCursorChange, onParseErrorChange, theme } , ref ) => {
43164 const lastCursorPosition = useRef ( { line : 1 , col : 1 } ) ;
44165 const editorViewRef = useRef < EditorView | null > ( null ) ;
45166
46- const extensions = useMemo ( ( ) => {
47- const ext = [
48- codeFolding ( ) ,
49- foldGutter ( ) ,
50- sdl ( ) ,
51- sdlLinter ( { onParseErrorChange } ) ,
52- lintGutter ( ) ,
53- ruler ( 80 ) ,
54- ] ;
55- return ext ;
56- } , [ onParseErrorChange ] ) ;
167+ const sdlLanguageSupport = useMemo ( ( ) => sdl ( ) , [ ] ) ;
57168
58- // Expose methods via ref
59- useImperativeHandle ( ref , ( ) => ( {
60- expandAll : ( ) => {
61- if ( editorViewRef . current ) {
62- const view = editorViewRef . current ;
63- const state = view . state ;
169+ // Memoize static extensions that never change
170+ const staticExtensions = useMemo ( ( ) => [
171+ codeFolding ( ) ,
172+ foldGutter ( ) ,
173+ lintGutter ( ) ,
174+ ruler ( 80 ) ,
175+ ] , [ ] ) ;
64176
65- view . dispatch ( { } ) ;
177+ // Memoize dynamic extensions that depend on props
178+ const dynamicExtensions = useMemo ( ( ) => [
179+ sdlLinter ( { onParseErrorChange } ) ,
180+ ] , [ onParseErrorChange ] ) ;
66181
67- ensureSyntaxTree ( view . state , state . doc . length , 5000 ) ;
182+ const extensions = useMemo ( ( ) => [
183+ ...staticExtensions ,
184+ sdlLanguageSupport ,
185+ ...dynamicExtensions ,
186+ ] , [ staticExtensions , sdlLanguageSupport , dynamicExtensions ] ) ;
68187
69- unfoldAll ( view ) ;
70- }
71- } ,
72- collapseAll : ( ) => {
73- if ( editorViewRef . current ) {
74- const view = editorViewRef . current ;
75- const state = view . state ;
188+ // Memoize imperative methods
189+ const expandAll = useCallback ( ( ) => {
190+ if ( editorViewRef . current ) {
191+ const view = editorViewRef . current ;
192+ const state = view . state ;
76193
77- view . dispatch ( { } ) ;
194+ view . dispatch ( { } ) ;
78195
79- ensureSyntaxTree ( view . state , state . doc . length , 5000 ) ;
196+ ensureSyntaxTree ( view . state , state . doc . length , 5000 ) ;
80197
81- foldAll ( view ) ;
82- }
198+ unfoldAll ( view ) ;
199+ }
200+ } , [ ] ) ;
201+
202+ const collapseAll = useCallback ( ( ) => {
203+ if ( editorViewRef . current ) {
204+ const view = editorViewRef . current ;
205+ const state = view . state ;
206+
207+ view . dispatch ( { } ) ;
208+
209+ ensureSyntaxTree ( view . state , state . doc . length , 5000 ) ;
210+
211+ foldAll ( view ) ;
212+ }
213+ } , [ ] ) ;
214+
215+ // Expose methods via ref
216+ useImperativeHandle ( ref , ( ) => ( {
217+ expandAll,
218+ collapseAll,
219+ getStyledCode : ( ) => {
220+ return getStyledCode ( sdlLanguageSupport , code , theme ) ;
83221 } ,
84- } ) , [ ] ) ;
222+ } ) , [ expandAll , collapseAll , sdlLanguageSupport , code , theme ] ) ;
85223
86- const onInternalCodeChange = useCallback ( ( val : string ) => {
87- onCodeChange ( val ) ;
224+ const onInternalCodeChange = useCallback ( ( code : string ) => {
225+ onCodeChange ( code ) ;
88226 } , [ onCodeChange ] ) ;
89227
90228 const onViewUpdate = useCallback ( ( viewUpdate : ViewUpdate ) => {
@@ -114,7 +252,7 @@ export const Editor = forwardRef<EditorRef, EditorProps>(
114252 return (
115253 < div className = "h-full w-full" >
116254 < CodeMirror
117- value = { value }
255+ value = { code }
118256 theme = { theme === "dark" ? darkTheme : lightTheme }
119257 width = "100%"
120258 height = "100%"
0 commit comments