1- import { OnMount , OnChange , BeforeMount , EditorProps , Monaco as MonacoType } from '@monaco-editor/react'
1+ import { UsePanda } from '@/src/hooks/usePanda'
2+ import { TypingsSourceResolver } from '@/src/lib/typings-source-resolver'
3+ import { BeforeMount , EditorProps , Monaco as MonacoType , OnChange , OnMount } from '@monaco-editor/react'
24import * as Monaco from 'monaco-editor'
35import { AutoTypings , LocalStorageCache } from 'monaco-editor-auto-typings/custom-editor'
4-
6+ import { MonacoJsxSyntaxHighlight , getWorker } from 'monaco-jsx-syntax-highlight'
7+ import { useTheme } from 'next-themes'
8+ import { useSearchParams } from 'next/navigation'
59import { useCallback , useEffect , useMemo , useRef , useState } from 'react'
610import { useLocalStorage , useUpdateEffect } from 'usehooks-ts'
7-
8- import { State } from './usePlayground'
9-
11+ import { configureAutoImports } from '../lib/auto-import'
1012import { pandaTheme } from '../lib/gruvbox-theme'
11- import { useTheme } from 'next-themes'
12- import { MonacoJsxSyntaxHighlight , getWorker } from 'monaco-jsx-syntax-highlight'
13+ import { State } from './usePlayground'
1314
1415// @ts -ignore
1516import pandaDevDts from '../dts/@pandacss_dev.d.ts?raw'
1617// @ts -ignore
1718import pandaTypesDts from '../dts/@pandacss_types.d.ts?raw'
1819// @ts -ignore
1920import reactDts from '../dts/react.d.ts?raw'
20- import { useSearchParams } from 'next/navigation'
21- import { configureAutoImports } from '../lib/auto-import'
22- import { UsePanda } from '@/src/hooks/usePanda'
23- import { TypingsSourceResolver } from '@/src/lib/typings-source-resolver'
2421
2522export interface PandaEditorProps {
2623 value : State
@@ -49,27 +46,17 @@ export const defaultEditorOptions: EditorProps['options'] = {
4946}
5047
5148const activateAutoTypings = async ( monacoEditor : Monaco . editor . IStandaloneCodeEditor , monaco : MonacoType ) => {
52- const activate = async ( ) => {
53- const { dispose } = await AutoTypings . create ( monacoEditor , {
54- monaco,
55- sourceCache : new LocalStorageCache ( ) ,
56- fileRootPath : 'file:///' ,
57- debounceDuration : 500 ,
58- sourceResolver : new TypingsSourceResolver ( ) ,
59- } )
60-
61- return dispose
62- }
63-
64- activate ( )
65-
66- monacoEditor . onDidChangeModel ( ( ) => {
67- activate ( )
49+ // AutoTypings internally watches for content changes via debounceDuration
50+ // No need for additional event handlers - they cause duplicate instances
51+ const { dispose } = await AutoTypings . create ( monacoEditor , {
52+ monaco,
53+ sourceCache : new LocalStorageCache ( ) ,
54+ fileRootPath : 'file:///' ,
55+ debounceDuration : 500 ,
56+ sourceResolver : new TypingsSourceResolver ( ) ,
6857 } )
6958
70- monacoEditor . onDidChangeModelContent ( ( ) => {
71- activate ( )
72- } )
59+ return dispose
7360}
7461
7562const activateMonacoJSXHighlighter = async ( monacoEditor : Monaco . editor . IStandaloneCodeEditor , monaco : MonacoType ) => {
@@ -108,6 +95,11 @@ export function useEditor(props: PandaEditorProps) {
10895 const monacoEditorRef = useRef < Parameters < OnMount > [ 0 ] | undefined > ( undefined )
10996 const monacoRef = useRef < Parameters < OnMount > [ 1 ] | undefined > ( undefined )
11097
98+ // Track the last known value for external change detection
99+ // We use refs to avoid re-renders - Monaco manages its own state (uncontrolled)
100+ const lastValueRef = useRef ( value )
101+ const isUserEditRef = useRef ( false )
102+
111103 const [ wordWrap , setWordwrap ] = useLocalStorage < 'on' | 'off' > ( 'editor_wordWrap' , 'off' )
112104
113105 const onToggleWrap = useCallback ( ( ) => {
@@ -118,12 +110,15 @@ export function useEditor(props: PandaEditorProps) {
118110 monacoEditorRef . current ?. updateOptions ( { wordWrap } )
119111 } , [ wordWrap ] )
120112
113+ // Memoize based on actual data, not context reference, to prevent unnecessary re-renders
114+ const patterns = context . patterns . details
115+ const recipeKeys = context . recipes . keys
121116 const autoImportCtx = useMemo ( ( ) => {
122117 return {
123- patterns : context . patterns . details ,
124- recipes : Array . from ( context . recipes . keys ) ,
118+ patterns,
119+ recipes : Array . from ( recipeKeys ) ,
125120 }
126- } , [ context ] )
121+ } , [ patterns , recipeKeys ] )
127122
128123 const configureEditor : OnMount = useCallback (
129124 ( editor , monaco ) => {
@@ -255,22 +250,49 @@ export function jsxs(type: React.ElementType, props: unknown, key?: React.Key):
255250 [ configureEditor , setupLibs , getPandaTypes ] ,
256251 )
257252
258- const onCodeEditorChange = ( content : Parameters < OnChange > [ 0 ] ) => {
259- onChange ( {
260- ...value ,
261- [ activeTab ] : content ,
262- } )
263- }
253+ // Use ref to track current value for onChange - keeps callback stable
254+ const stateRef = useRef ( { value : lastValueRef . current , activeTab } )
255+ stateRef . current = { value : lastValueRef . current , activeTab }
256+
257+ const onCodeEditorChange = useCallback (
258+ ( content : Parameters < OnChange > [ 0 ] ) => {
259+ const { value : currentValue , activeTab : currentTab } = stateRef . current
260+ const newValue = { ...currentValue , [ currentTab ] : content }
261+ // Mark as user edit to skip sync effect
262+ isUserEditRef . current = true
263+ lastValueRef . current = newValue
264+ onChange ( newValue )
265+ } ,
266+ [ onChange ] ,
267+ )
264268
265269 const onCodeEditorFormat = ( ) => {
266270 monacoEditorRef . current ?. getAction ( 'editor.action.formatDocument' ) ?. run ( )
267271 }
268272
273+ // Track previous artifacts to avoid unnecessary lib updates that cause cursor jumps
274+ const prevArtifactsRef = useRef < string > ( '' )
275+ const libsDisposablesRef = useRef < ReturnType < typeof setupLibs > > ( [ ] )
276+
269277 useUpdateEffect ( ( ) => {
270- const libsSetup = setupLibs ( monacoRef . current ! )
278+ // Create a signature of the artifact type definitions to detect actual changes
279+ const dtsSignature = artifacts
280+ . flatMap ( ( a ) => a ?. files . filter ( ( f ) => f . file . endsWith ( '.d.ts' ) ) . map ( ( f ) => f . code ) ?? [ ] )
281+ . join ( '' )
282+
283+ // Skip if the type definitions haven't actually changed
284+ if ( prevArtifactsRef . current === dtsSignature ) return
285+ prevArtifactsRef . current = dtsSignature
286+
287+ // Dispose previous libs before setting up new ones
288+ for ( const lib of libsDisposablesRef . current ) {
289+ lib ?. dispose ( )
290+ }
291+
292+ libsDisposablesRef . current = setupLibs ( monacoRef . current ! )
271293
272294 return ( ) => {
273- for ( const lib of libsSetup ) {
295+ for ( const lib of libsDisposablesRef . current ) {
274296 lib ?. dispose ( )
275297 }
276298 }
@@ -286,7 +308,42 @@ export function jsxs(type: React.ElementType, props: unknown, key?: React.Key):
286308 return ( ) => {
287309 autoImports ?. dispose ( )
288310 }
289- } , [ context ] )
311+ } , [ autoImportCtx ] )
312+
313+ // Sync external changes (example selection) via editor.setValue()
314+ // This is the key to uncontrolled mode - we don't use value prop, we call setValue directly
315+ useEffect ( ( ) => {
316+ // Skip if this change came from user editing (our own onChange)
317+ if ( isUserEditRef . current ) {
318+ isUserEditRef . current = false
319+ return
320+ }
321+
322+ // Only update if content actually changed
323+ const editor = monacoEditorRef . current
324+ if ( ! editor ) return
325+
326+ const currentContent = value [ activeTab as keyof Pick < State , 'code' | 'config' | 'css' > ]
327+ const editorContent = editor . getValue ( )
328+
329+ if ( currentContent !== editorContent ) {
330+ // Save cursor position
331+ const position = editor . getPosition ( )
332+ const scrollTop = editor . getScrollTop ( )
333+
334+ // Update editor content directly
335+ editor . setValue ( currentContent ?? '' )
336+
337+ // Restore cursor position
338+ if ( position ) {
339+ editor . setPosition ( position )
340+ }
341+ editor . setScrollTop ( scrollTop )
342+ }
343+
344+ // Update ref
345+ lastValueRef . current = value
346+ } , [ value . code , value . config , value . css , activeTab ] )
290347
291348 return {
292349 activeTab,
@@ -297,5 +354,7 @@ export function jsxs(type: React.ElementType, props: unknown, key?: React.Key):
297354 onCodeEditorFormat,
298355 onToggleWrap,
299356 wordWrap,
357+ // Return initial value for defaultValue prop
358+ immediateValue : value ,
300359 }
301360}
0 commit comments