Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
b966e9a
Playground file browser
adamziel Oct 21, 2025
d09b6f0
resizable site panel
adamziel Oct 21, 2025
75f935d
resizable site panel
adamziel Oct 22, 2025
5223de8
Merge
adamziel Oct 23, 2025
6f6bfb3
Don't move the focus back to the file tree too early
adamziel Oct 23, 2025
c9e8507
Double click = focus on the file. Single click = select file
adamziel Oct 23, 2025
6e9934d
Enter = focus on the code editor
adamziel Oct 23, 2025
d04a1f5
Immediately open newly created files
adamziel Oct 23, 2025
2c0686c
Focus on the first context menu item
adamziel Oct 23, 2025
7d69e57
E2E the file picker
adamziel Oct 23, 2025
cb22d52
Long editor
adamziel Oct 23, 2025
c4282d9
Retain cursor position in the code editor
adamziel Oct 23, 2025
2378604
Reopen the last active tab when Playground is reopened
adamziel Oct 23, 2025
bfa8f56
Lint, typecheck
adamziel Oct 23, 2025
41707a5
remove dev artifacts
adamziel Oct 23, 2025
6fa8eee
Merge branch 'trunk' into file-browser
adamziel Oct 23, 2025
43ddc83
Load non-php language extensions asynchronously
adamziel Oct 23, 2025
97c83a6
Simplify logic, improve comments
adamziel Oct 23, 2025
9712e4b
Simplify logic, remove constants file, reuse documentRoot, reuse WP i…
adamziel Oct 23, 2025
63afe2c
Display a clickable download link when editing binary files
adamziel Oct 23, 2025
cd5a154
Reuse php-wasm/util fs helpers instead of reimplementing them
adamziel Oct 23, 2025
402d7f5
Rename isProbablyTextBuffer to seemsLikeBinary
adamziel Oct 23, 2025
bb80aa7
Remove 300ms delay when opening a file
adamziel Oct 24, 2025
2e500e1
lint
adamziel Oct 24, 2025
ac748f3
Add Blueprint editor
adamziel Oct 24, 2025
56e1941
Don't lose focus when typing in the Blueprint editor
adamziel Oct 24, 2025
91d1331
Recreate Playground after clicking the button
adamziel Oct 24, 2025
31f9587
UI improvements
adamziel Oct 24, 2025
0c6aa13
UI improvements
adamziel Oct 24, 2025
29dec41
UI improvements
adamziel Oct 24, 2025
f8fa230
Merge branch 'trunk' into blueprint-editor
adamziel Oct 26, 2025
1182e53
Fix the test
adamziel Oct 27, 2025
3d7d5f2
Add e2e tests
adamziel Oct 28, 2025
9593220
Lint, typecheck
adamziel Oct 28, 2025
0134c12
Make E2E tests headless again
adamziel Nov 20, 2025
284cca9
Merge branch 'trunk' into blueprint-editor
adamziel Nov 20, 2025
f9f7074
Merge branch 'trunk' into blueprint-editor
adamziel Nov 20, 2025
62d50dd
Move schema– and autocompletion– related code to schema-utils.ts
adamziel Nov 20, 2025
0597624
Document schema-utils.ts
adamziel Nov 20, 2025
48331e6
Lint
adamziel Nov 20, 2025
3dd355e
Restore Playwright config
adamziel Nov 20, 2025
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
156 changes: 156 additions & 0 deletions packages/playground/website/playwright/e2e/website-ui.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -557,3 +557,159 @@ test('should keep query arguments when updating settings', async ({
await wordpress.locator('body').evaluate((body) => body.baseURI)
).toMatch('/wp-admin/');
});

test('should edit a file in the code editor and see changes in the viewport', async ({
website,
wordpress,
}) => {
await website.goto('./');

// Open site manager
await website.ensureSiteManagerIsOpen();

// Navigate to File Browser tab
await website.page.getByRole('tab', { name: 'File Browser' }).click();

// Wait for file tree to load
await website.page.locator('[data-path="/wordpress"]').waitFor();

// Expand /wordpress folder
const wordpressFolder = website.page.locator(
'button[data-path="/wordpress"]'
);
if ((await wordpressFolder.getAttribute('data-expanded')) !== 'true') {
await wordpressFolder.click();
}

// Double-click index.php to open it in the editor
await website.page
.locator('button[data-path="/wordpress/index.php"]')
.dblclick();

// Wait for CodeMirror editor to load
const editor = website.page.locator('[class*="file-browser"] .cm-editor');
await editor.waitFor({ timeout: 10000 });

// Click on the editor to focus it
await website.page.waitForTimeout(50);

await editor.click();

await website.page.waitForTimeout(250);

// Select all content in the editor (Cmd+A or Ctrl+A)
await website.page.keyboard.press(
process.platform === 'darwin' ? 'Meta+A' : 'Control+A'
);

await website.page.keyboard.press('Backspace');
await website.page.waitForTimeout(200);

// Type the new content with a delay between keystrokes
await website.page.keyboard.type('Edited file', { delay: 50 });

// Wait a moment for the change to be processed
await website.page.waitForTimeout(500);

// Save the file (Cmd+S or Ctrl+S)
await website.page.keyboard.press(
process.platform === 'darwin' ? 'Meta+S' : 'Control+S'
);

// Wait for save to complete (look for save indicator if there is one)
await website.page.waitForTimeout(1000);

// Close the site manager to see the viewport
await website.ensureSiteManagerIsClosed();

// Reload just the WordPress iframe to see the changes
const playgroundViewport = website.page.frameLocator(
'#playground-viewport:visible,.playground-viewport:visible'
);
await playgroundViewport
.locator('#wp')
.evaluate((iframe: HTMLIFrameElement) => {
iframe.contentWindow?.location.reload();
});

// Verify the page shows "Edited file"
await expect(wordpress.locator('body')).toContainText('Edited file', {
timeout: 10000,
});
});

test('should edit a blueprint in the blueprint editor and recreate the playground', async ({
website,
wordpress,
}) => {
await website.goto('./');

// Open site manager
await website.ensureSiteManagerIsOpen();

// Navigate to Blueprint tab
await website.page.getByRole('tab', { name: 'Blueprint' }).click();

// Wait for CodeMirror editor to load
const editor = website.page.locator(
'[class*="blueprint-editor"] .cm-editor'
);
await editor.waitFor({ timeout: 10000 });

await editor.click();

// Delete all content in the editor (Cmd+A or Ctrl+A)
await website.page.keyboard.press(
process.platform === 'darwin' ? 'Meta+A' : 'Control+A'
);

await website.page.keyboard.press('Backspace');
await website.page.waitForTimeout(200);

// Create a simple blueprint that writes "Blueprint test" to index.php
const blueprint = JSON.stringify(
{
landingPage: '/index.php',
steps: [
{
step: 'writeFile',
path: '/wordpress/index.php',
data: 'Blueprint test',
},
],
},
null,
2
);

// Type the new blueprint with a delay between keystrokes
await website.page.keyboard.type(blueprint, { delay: 50 });

// Remove the autoinserted brackets until the end of the Blueprint
await website.page.keyboard.down('Shift');
for (let i = 0; i < 4; i++) {
await website.page.keyboard.press('ArrowDown');
}

// Delete the selected lines
await website.page.keyboard.press('Backspace');

// Wait a moment for the change to be processed
await website.page.waitForTimeout(500);

// Click the "Recreate Playground from this Blueprint" button
await website.page
.getByRole('button', {
name: 'Recreate Playground from this Blueprint',
})
.click();

await website.page.waitForTimeout(1500);
// Wait for the playground to recreate
await website.waitForNestedIframes();

// Verify the page shows "Blueprint test"
await expect(wordpress.locator('body')).toContainText('Blueprint test', {
timeout: 10000,
});
});
180 changes: 180 additions & 0 deletions packages/playground/website/src/components/blueprint-editor/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import {
autocompletion,
closeBrackets,
completionKeymap,
closeBracketsKeymap,
} from '@codemirror/autocomplete';
import {
defaultKeymap,
history,
historyKeymap,
indentWithTab,
} from '@codemirror/commands';
import { json } from '@codemirror/lang-json';
import {
bracketMatching,
foldGutter,
foldKeymap,
indentOnInput,
indentUnit,
syntaxHighlighting,
defaultHighlightStyle,
} from '@codemirror/language';
import { searchKeymap, highlightSelectionMatches } from '@codemirror/search';
import { EditorState, type Extension } from '@codemirror/state';
import {
EditorView,
keymap,
type ViewUpdate,
lineNumbers,
highlightActiveLineGutter,
highlightActiveLine,
dropCursor,
rectangularSelection,
crosshairCursor,
} from '@codemirror/view';
import { useEffect, useRef } from 'react';
import type { JSONSchemaCompletionConfig } from './types';
import { formatEditor, jsonSchemaCompletion } from './schema-utils';
const SCHEMA_URL = 'https://playground.wordpress.net/blueprint-schema.json';
const DEFAULT_DOC = `{
"$schema": ${JSON.stringify(SCHEMA_URL)}
}`;

interface JSONSchemaEditorProps {
config?: JSONSchemaCompletionConfig;
className?: string;
}

export function JSONSchemaEditor({
config = {},
className = '',
}: JSONSchemaEditorProps) {
const editorRef = useRef<HTMLDivElement>(null);
const viewRef = useRef<EditorView | null>(null);

useEffect(() => {
if (!editorRef.current) return;

const initialDoc = config.initialDoc || DEFAULT_DOC;
const autofocus = config.autofocus ?? true;

const extensions: Extension[] = [
// Line numbers and highlighting
lineNumbers(),
highlightActiveLineGutter(),
highlightActiveLine(),
// Folding
foldGutter(),
// Selection features
dropCursor(),
rectangularSelection(),
crosshairCursor(),
// Language support
json(),
syntaxHighlighting(defaultHighlightStyle),
// Indentation
indentUnit.of('\t'),
indentOnInput(),
// Bracket features
bracketMatching(),
closeBrackets(),
// History
history(),
// Selection highlighting
highlightSelectionMatches(),
// Keymaps
keymap.of([
...defaultKeymap,
...historyKeymap,
...foldKeymap,
...searchKeymap,
...completionKeymap,
...closeBracketsKeymap,
indentWithTab,
]),
// Autocompletion with JSON schema
autocompletion({
override: [jsonSchemaCompletion],
activateOnTyping: true,
closeOnBlur: false,
}),
];

// Add readOnly extension if specified
if (config.readOnly) {
extensions.push(EditorState.readOnly.of(true));
}

// Add onChange listener if provided
if (config.onChange) {
extensions.push(
EditorView.updateListener.of((update: ViewUpdate) => {
if (update.docChanged) {
config.onChange!(update.state.doc.toString());
}
})
);
}

const view = new EditorView({
doc: initialDoc,
extensions,
parent: editorRef.current,
});

viewRef.current = view;

formatEditor(view);

// Initialize the cursor position after the default schema, if present in the document.
const doc = view.state.doc.toString();
const schemaUrl = JSON.stringify(SCHEMA_URL);
const schemaLineEnd = doc.indexOf(schemaUrl);
if (schemaLineEnd > 0) {
const cursorPos = schemaLineEnd + schemaUrl.length;
if (cursorPos <= view.state.doc.length) {
view.dispatch({
selection: { anchor: cursorPos },
});
}
}

if (autofocus) {
view.focus();
}

return () => {
view.destroy();
viewRef.current = null;
};
// Only create the editor once, don't recreate on prop changes
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

// Handle document updates from parent without recreating the editor
useEffect(() => {
const view = viewRef.current;
if (!view || !config.initialDoc) {
return;
}

const currentDoc = view.state.doc.toString();
if (config.initialDoc === currentDoc) {
return;
}

// Only update if the change came from outside (not from user typing)
view.dispatch({
changes: {
from: 0,
to: view.state.doc.length,
insert: config.initialDoc,
},
});
}, [config.initialDoc]);

return <div ref={editorRef} className={className} />;
}

export default JSONSchemaEditor;
Loading