Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: fix
packages:
- "@typespec/playground"
---

Preserve a sample's preferred emitter and compiler options when loading it in the playground.
21 changes: 11 additions & 10 deletions packages/playground/src/react/hooks/use-monaco-sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,17 @@ export function useMonacoSync({
// to avoid the sync effect resetting the model during typing
const isModelDrivenChangeRef = useRef(false);

// Keep the listener's view of React state current before syncing an external
// content change into Monaco. `setValue` fires `onDidChangeContent`
// synchronously, so updating these refs after the sync effect would let that
// event call a stale callback with stale content.
const contentRef = useRef(content);
const onContentChangeRef = useRef(onContentChange);
useEffect(() => {
contentRef.current = content;
onContentChangeRef.current = onContentChange;
}, [content, onContentChange]);

// Sync external content changes → Monaco model
useEffect(() => {
if (isModelDrivenChangeRef.current) {
Expand All @@ -34,16 +45,6 @@ export function useMonacoSync({
}
}, [content, typespecModel]);

// Use refs to avoid re-subscribing to onDidChangeContent on every keystroke
const contentRef = useRef(content);
const onContentChangeRef = useRef(onContentChange);
useEffect(() => {
contentRef.current = content;
}, [content]);
useEffect(() => {
onContentChangeRef.current = onContentChange;
}, [onContentChange]);

// Sync Monaco model changes → React state
useEffect(() => {
const disposable = typespecModel.onDidChangeContent(() => {
Expand Down
31 changes: 31 additions & 0 deletions packages/playground/test/use-monaco-sync.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ function createMockModel(initialValue = "") {
getValue: vi.fn(() => value),
setValue: vi.fn((v: string) => {
value = v;
for (const cb of listeners) cb();
}),
onDidChangeContent: vi.fn((cb: () => void) => {
listeners.push(cb);
Expand Down Expand Up @@ -121,3 +122,33 @@ it("does not reset model after model-driven content change", () => {
// Should NOT call setValue again since it was model-driven
expect(model.setValue).not.toHaveBeenCalled();
});

it("does not report external content changes through a stale callback", () => {
const model = createMockModel("initial");
const initialOnContentChange = vi.fn();
const updatedOnContentChange = vi.fn();

const { rerender } = renderHook(
({ content, onContentChange }) =>
useMonacoSync({
typespecModel: model as any,
content,
onContentChange,
}),
{
initialProps: {
content: "initial",
onContentChange: initialOnContentChange,
},
},
);

rerender({
content: "sample content",
onContentChange: updatedOnContentChange,
});

expect(model.setValue).toHaveBeenCalledWith("sample content");
expect(initialOnContentChange).not.toHaveBeenCalled();
expect(updatedOnContentChange).not.toHaveBeenCalled();
});