Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions src/components/monaco-editor/monaco-editor.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
73 changes: 73 additions & 0 deletions tests/jsdom/components/monaco-editor/render.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<MonacoEditor
aria-label="Dark editor"
monaco={fake.monaco}
theme="vs-dark"
value="dark"
/>
);
mountExtra(
<MonacoEditor
aria-label="Light editor"
monaco={fake.monaco}
theme="vs-light"
value="light"
/>
);
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 (
<MonacoEditor
aria-label="Path editor"
monaco={fake.monaco}
path={path()}
value="value"
/>
);
}
container = mount(<Harness />);
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();

Expand Down Expand Up @@ -454,6 +514,19 @@ describe('MonacoEditor - jsdom', () => {
expect(hostRef.current).toBeNull();
expect(editorRef.current).toBeNull();
expect(monacoRef.current).toBeNull();

const remounted = mountExtra(
<MonacoEditor
aria-label="Remounted editor"
monaco={fake.monaco}
value="ready"
/>
);
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 () => {
Expand Down