diff --git a/README.md b/README.md index 7a0e16a..dabd234 100644 --- a/README.md +++ b/README.md @@ -51,8 +51,13 @@ const model = monaco.editor.createModel( - Controlled parent rerenders retain the live editor host, so focus, selection, history, and completion state survive model-content callbacks. Actual component unmount remains the single disposal boundary. -- Wrapper-owned `path` values must be unique; pass an existing Monaco model - through `model={monaco.editor.getModel(uri)}` when sharing is intentional. +- Wrapper-owned `path` values (usage without a `model` prop) must be unique. + Externally-owned models are exempt from this wrapper check; pass an existing + Monaco model through `model={monaco.editor.getModel(uri)}` when sharing is + intentional, and retain responsibility for its identity and disposal. +- Monaco's `setTheme` API is process-global. The `theme` prop configures that + shared Monaco namespace, so simultaneous editors cannot display different + themes; the most recently applied theme wins. ## Layout diff --git a/src/components/monaco-editor/monaco-editor.types.ts b/src/components/monaco-editor/monaco-editor.types.ts index 8ac3d06..ee100ab 100644 --- a/src/components/monaco-editor/monaco-editor.types.ts +++ b/src/components/monaco-editor/monaco-editor.types.ts @@ -53,6 +53,7 @@ export type MonacoEditorProps = Omit< defaultValue?: string; language?: string; path?: string | MonacoUri; + /** Monaco theme name. Monaco applies themes process-wide, so simultaneous editors cannot use different themes. */ theme?: string; monaco?: MonacoNamespace; loadMonaco?: MonacoLoader; diff --git a/tests/jsdom/components/monaco-editor/render.test.tsx b/tests/jsdom/components/monaco-editor/render.test.tsx index de0eb38..1ffacc4 100644 --- a/tests/jsdom/components/monaco-editor/render.test.tsx +++ b/tests/jsdom/components/monaco-editor/render.test.tsx @@ -155,6 +155,66 @@ describe('MonacoEditor - jsdom', () => { expect(fake.editors[0].getModel()?.getLanguageId()).toBe('javascript'); }); + it('should expose Monaco theme changes as namespace-global across instances', async () => { + const fake = createFakeMonaco(); + container = mount( + + ); + mountExtra( + + ); + await flushUpdates(); + await flushUpdates(); + await flushUpdates(); + + expect(fake.editors).toHaveLength(2); + expect(fake.themeCalls).toEqual(['vs-dark', 'vs-light']); + }); + + it('should dispose each wrapper-owned model exactly once during rapid path replacement', async () => { + const fake = createFakeMonaco(); + let setPath!: (path: string) => void; + function Harness() { + const path = state('file:///src/0.ts'); + setPath = path.set; + return ( + + ); + } + container = mount(); + await flushUpdates(); + await flushUpdates(); + for (let index = 1; index <= 8; index += 1) { + setPath(`file:///src/${index}.ts`); + await flushUpdates(); + await flushUpdates(); + } + + expect(fake.createdModels).toHaveLength(9); + expect( + fake.createdModels.slice(0, -1).every((model) => model.disposeCalls === 1) + ).toBe(true); + expect( + fake.createdModels[fake.createdModels.length - 1]?.disposeCalls + ).toBe(0); + expect(fake.editors[0].disposed).toBe(false); + }); + it('should keep the editor mounted when its change event updates controlled parent state', async () => { const fake = createFakeMonaco(); @@ -454,6 +514,19 @@ describe('MonacoEditor - jsdom', () => { expect(hostRef.current).toBeNull(); expect(editorRef.current).toBeNull(); expect(monacoRef.current).toBeNull(); + + const remounted = mountExtra( + + ); + await flushUpdates(); + await flushUpdates(); + await flushUpdates(); + expect(remounted.querySelector('[data-askr-monaco-editor]')).not.toBeNull(); + expect(fake.createCalls).toHaveLength(1); }); it('should load a new Monaco namespace when a provided namespace is removed', async () => {