Skip to content

Conversation

@adamziel
Copy link
Collaborator

@adamziel adamziel commented Oct 23, 2025

Motivation for the change, related issues

Adds a "File browser" tab to Playground details view with a file explorer tree and a code editor:

CleanShot.2025-10-23.at.23.31.22.mp4

When working on Beta PHP Playground I realized it's just a different UI for WordPress Playground. A UI that would be highly useful in Playground core. Therefore, instead of expanding on the PHP Playground idea, let's make the website just as useful for developers.

Notable features:

  • Deferred loading – code editor and file explorer assets are only loaded after opening the tab
  • File explorer
    • Context menu with rename and delete options
    • Drag&drop between folders
    • Drag&drop files from desktop
    • Type to find file
    • Arrow-based directory tree navigation
    • Single click to open a file, double click to open a focus
  • Code editor
    • Auto-save
    • Reopens the last opened file after navigating to another tab and returning to the code editor

Remaining work

  • Analyze the impact on the manifest.json and offline bundle. How can we make these downloads non mandatory?

Testing Instructions (or ideally a Blueprint)

Play with the file explorer and the code editor, confirm it works as expected without too rough edges.

* just as the web request layer would.
*/
HTTPS: (await playground.absoluteUrl).startsWith('https://') ? 'on' : '',
HTTPS: (await playground.absoluteUrl).startsWith('https://')
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is just reformatting

@adamziel adamziel marked this pull request as ready for review October 23, 2025 22:48
@adamziel adamziel requested a review from a team as a code owner October 23, 2025 22:48
@bgrgicak
Copy link
Collaborator

This will be such a great addition to the website, great work @adamziel!

I found a few quirks, but otherwise it works really well.

Type to find file interferes with renaming

Try creating a folder 123 and after that rename a file to include 1 in the name. As soon as you press 1, the rename modal closes and the folder with the name 123 is highlighted. Type to find should be disabled during renaming to avoid issues like this.

Screen.Recording.2025-10-24.at.06.57.28.mov

Deleted files keep coming back after recreating the same directory

I had a directory 123 with a file test.231.php and deleted it (this part isn't in the video).
After that, whenever I create a directory with the name 123 and reload the page.
The directory is repopulated with the previously deleted data, and it removes my data.

In this video, I created a directory 123 with a test.php file in it, after reloading the 123 directory contained only the test231.php file which I deleted in the past.

Screen.Recording.2025-10-24.at.07.01.18.mov
@adamziel
Copy link
Collaborator Author

Thanks @bgrgicak! I've just addressed these two issues.

Copy link
Member

@brandonpayton brandonpayton left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I read over the changes, and everything seems nicely, simply put together. Nice work!

Will test this next.

mkdir(path: string) {
return FSHelpers.mkdir(this[__private__dont__use].FS, path);
const result = FSHelpers.mkdir(this[__private__dont__use].FS, path);
this.dispatchEvent({ type: 'filesystem.write' });
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would there be any value in including the path with the event? Same question for the other FS event dispatches.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potentially, once something needs that path. Today we only need a way of telling the journal "hey, something changed in the filesystem, please synchronize that with OPFS".

'.git': {
type: 'boolean',
description:
'When true, include a `.git` directory with Git metadata (experimental).',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's weird this wasn't committed before! See #2787

focusTarget.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice.

renameHandledRef.current = false;
if (typeof window !== 'undefined' && window.requestAnimationFrame) {
window.requestAnimationFrame(() => {
if (typeof window !== 'undefined' && requestAnimationFrame) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The previous version of this code which explicitly referenced the window property made more sense to me. But mostly, this change made me pause and wonder:
Is there a reason for this change that I'm missing?

🤔 If we're trying to reduce references to window for some reason, the following would go further:

Suggested change
if (typeof window !== 'undefined' && requestAnimationFrame) {
if (typeof requestAnimationFrame === 'function') {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah good note, let's explore restoring that

return;
}

const wasWaitingForDoubleClick = clickTimeoutRef.current !== null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This surprised me, though I bet there is a good reason for it. Doesn't the underlying browser platform decide whether a click is a single or double click? Is there a case where you get individual click events that make up a single double-click intent?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is an onDoubleClick event, but I couldn't get consistent results with it. Also, we want to open the file right away without any visible delays – and we'd get a delay in a data model where we handle either a "singular click" event or a "double click" event. In here, we're getting each specific click event and open the file right away. Then, we still need to decide if it's a double click. If yes, we also move the focus into the code editor – once that second click arrives.

*
* Optional chunks are placed in assets/optional/ via vite.config.ts manualChunks configuration.
*/
/^\/assets\/optional\/.*/, // All optional assets (CodeMirror, language extensions, etc.)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch.


const seemsLikeBinary = (buffer: Uint8Array) => {
// Assume that anything with a null byte in the first 4096 bytes is binary.
// This isn't a perfect test, but it catches a lot of binary files.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This made me happy. Nice heuristic.

setTimeout(() => {
const savedPos = cursorPositionsRef.current.get(path);
if (savedPos !== undefined) {
editorRef.current?.setCursorPosition(savedPos);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if this is out of bounds due to a shortened file?

EDIT: It turns out that setCursorPosition() clamps the position to the length of the content.

const clampedPos = Math.min(
pos,
viewRef.current.state.doc.length
);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be too paranoid, but perhaps we should clamp this on the lower end as well.

The position shouldn't normally become negative, but faulty math involving substring length and a text index could do it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point! Let's do it in a follow-up

if (!view) {
return;
}
// Save focus state before reconfiguring editable
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why?

Copy link
Collaborator Author

@adamziel adamziel Oct 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure, perhaps we don't need that. I did use LLMs with this one and was a bit lenient on these new components, I'm not sure what to think about that kind of workflow yet. It seems to be prone to introducing those kind of strange code segments. I'll explore removing it in a follow-up.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure, perhaps we don't need that. I did use LLMs with this one and was a bit lenient on these new components, I'm not sure what to think about that kind of workflow yet. It seems to be prone to introducing those kind of strange code segments. I'll explore removing it in a follow-up.

@adamziel Thanks for explaining. Honestly, it may be a reasonable tradeoff if we can mostly identify most of these weird bits. It would also be really cool if git blame could somehow tell us particular lines were generated by AI. Then we might feel more confident about questioning and changing an unexplained detail in the future.

Copy link
Member

@brandonpayton brandonpayton left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This tests really well for me. Excellent work! This makes Playground even more accessible and useful.

One idea for an enhancement:
For binary files like images and videos that can be read by the browser, maybe we could consider making those viewable, in place of the editor, when they are selected.

Thank you so much for this, @adamziel!

@adamziel adamziel merged commit 495a9cd into trunk Oct 25, 2025
29 checks passed
@adamziel adamziel deleted the file-browser branch October 25, 2025 14:49
@adamziel
Copy link
Collaborator Author

For binary files like images and videos that can be read by the browser, maybe we could consider making those viewable, in place of the editor, when they are selected.

Great idea! Let's explore it in a follow up

adamziel added a commit that referenced this pull request Oct 26, 2025
Equips the Playground file browser with a preview feature for images, videos, and audio. Follows up on #2813. <img width="700" alt="CleanShot 2025-10-26 at 23 47 51@2x" src="https://github.com/user-attachments/assets/f15ff15d-db39-4370-8924-3fa98dfe824c" /> ## Testing instructions Open the file explorer and open any image, video or audio file. Confirm you can see the preview. <!-- START COPILOT CODING AGENT SUFFIX --> <details> <summary>Original prompt</summary> > > ---- > > *This section details on the original issue you should resolve* > > <issue_title>Preview images and binary files in the file browser</issue_title> > <issue_description>For binary files like images and videos that can be read by the browser, maybe we could consider making those viewable, in place of the editor, when they are selected. > > Follows up on #2813</issue_description> > > ## Comments on the Issue (you are @copilot in this section) > > <comments> > </comments> > </details> - Fixes #2818 <!-- START COPILOT CODING AGENT TIPS --> --- ✨ Let Copilot coding agent [set things up for you](https://github.com/WordPress/wordpress-playground/issues/new?title=✨+Set+up+Copilot+instructions&body=Configure%20instructions%20for%20this%20repository%20as%20documented%20in%20%5BBest%20practices%20for%20Copilot%20coding%20agent%20in%20your%20repository%5D%28https://gh.io/copilot-coding-agent-tips%29%2E%0A%0A%3COnboard%20this%20repo%3E&assignees=copilot) — coding agent works faster and does higher quality work when set up for your repo. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: adamziel <205419+adamziel@users.noreply.github.com>
adamziel added a commit that referenced this pull request Oct 27, 2025
Changed --site-manager-site-list-width from 320px to 300px to make the sidebar with saved sites more compact. This improves the #2813 experience. ## Testing Instructions (or ideally a Blueprint) Go to local Playground confirm the left sidebar is more narrow and yet still useful --------- Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment