Repository navigation
Let simx extensions contribute an editor tab, write project files, and read project assets and the compiled program - #11705
Open
Eric Anderson (humanapp) wants to merge 58 commits into
Conversation
Simx extensions can read the open project's listed files and write files under a folder named by their registration key. Identity is frame-attested (the driver's messagechannel tag); reads range over pxt.json's files list; mutations take scope-relative names with the host prepending the folder, so out-of-scope writes are impossible.
A write or delete may carry `dirty: true`, routing the change through pxt's own user-edit pipeline (open editor model updated, simulator marked dirty, typecheck fed into the existing debouncers, so a batch of flagged writes yields one auto-run exactly as merged keystrokes do); unflagged requests remain byte-identical artifact writes that never disturb a running simulator. The simx's file access is now scoped to its own folder.
…tify on its changes
…ect before it has loaded
…river A simulator extension registration may now carry an editor descriptor (index, label, optional icon and wordmark) enabled by a separate allowEditorTab grant; the driver resolves the tab's page and wordmark the way it resolves the extension's index, refuses and logs a descriptor on a registration that is not permanent, and adds editor=1 to the simulator frame URL only when the registration declares and enables a tab. The simulator-extension spawn path moves out of the message handler into a public startSimulatorExtension(key), used by the first channel packet exactly as before and by the editor when it opens a tab, finding an existing frame by its channel and never duplicating it; extension frames report each page load so the editor can broker a port. The projfs and projassets gates also serve an extension's editor tab frame, found by its data-simxkey under the registration's grants, while every other message from a tab frame is dropped so it never reaches the simulators or receives their broadcasts; the column frame's open request passes the same gate under allowEditorTab, and each frame's replies go through one function per frame. pxtservices/simxeditor.ts defines the tab protocol, and pxtsim tests cover the registration, the spawn invariant, the gates, the tab frame's isolation and the open request.
A new SimxEditor hosts each registration's editor tab in the editor area, selected like the Assets editor by a virtual marker file (owner~repo.simxeditor) created the first time the tab is opened, so switching to it never stops, reruns or restarts the simulator; its unloadFileAsync is a no-op and it shows no editor toolbar. SimxEditorHost keeps one tab frame per registration for the life of the project, loaded at the resolved page with the simulator frame's URL parameters plus role=editor, the same sandbox and permissions under the same grants, and data-simxkey and data-simxrole tags; it hides frames rather than unloading them, shows the loading indicator until the tab posts ready (accepted only from the frame's window and origin), posts shown, hidden, closing, simulator command and simulator state events, starts the extension's simulator frame when it creates a tab frame, and brokers a MessageChannel between a ready tab and the loaded simulator frame of the same key in either creation order and again on every simulator frame load. The editor toggle lists the tabs of the extensions the open project includes in the existing language dropdown after JavaScript and Python under a separator, shows the dropdown while any is offered, shows the active tab as the dropdown's trigger, renders a wordmark image in place of the label with the label as accessible name, and hides tabs where Assets hides; the column frame's open request selects the tab and is answered. Project-asset subscriptions are kept per frame, so both frames of an extension are notified. Editor tests cover registrations, marker files, the open request, the toggle layout, the menu item content and the host's frames, events and brokering.
The simulator extensions page gains the editor tab: its registration (descriptor and grant, permanent requirement, wordmark rendering), the marker file it adds to a project, its two frames and their roles, the ready event and the brokered port, the lifecycle and simulator events, the rule that a hidden tab stays loaded but unrendered so computation belongs in the simulator frame, the grants and shared request queue of both frames, and the open request; it also documents the existing aspectratio runtime message.
The marker file that selects a simulator extension's editor tab now follows the serial editor's file instead of assets.json: it is still created virtual with a throwaway buffer, so switching to it leaves the simulator alone, but it is never saved, shown in the file explorer, synced to the cloud, committed to or compared with GitHub, shared or exported. A new pxtlib predicate, pxt.isSessionOnlyFile, covers the serial file and any name ending in the new pxt.SIMX_EDITOR_MARKER_SUFFIX, and replaces the by-name serial checks in EditorPackage.getAllFiles and sortedFiles and in the four workspace ignored-file lists, so both kinds of file share one rule; the docs now describe the marker as session-only and the suffix as reserved, and the editor tests cover the predicate and the marker names it matches.
Choosing an extension editor tab from the language dropdown left the menu open: the dropdown closes when focus leaves it, which happens after JavaScript or Python because the opened text editor takes focus, but opening a tab focused nothing. The tab host gains focus(), the tab editor focuses the shown tab once the editor is displayed after the switch, and choosing the tab already shown focuses it, as choosing the active text language does.
… its left A simulator extension editor tab now shows its label as text, with its icon on the text's left, the way MakeCode lists Python and JavaScript, instead of a wordmark image that could not take the menu's text color and disappeared in high contrast. The editor.wordmark field and its wordmarkUrl resolution are removed. editor.icon is either a font icon's class names, as MakeCode's own entries use, or the path to an SVG in the extension's artifact (any value that contains a "/" or ends in ".svg"); the simulator driver resolves a path as it resolves index, including against devUrl under simxdev, into an iconUrl on the tab descriptor. The toggle draws an SVG icon as a currentColor CSS mask on a span with the box Semantic UI gives a font icon in the same slots, so it follows the item's text color in every theme, while class names keep the font-icon path. The docs and the editor and driver tests cover the class-or-path rule, the descriptor's iconUrl and the mask style.
…gle while Blocks is active The editor toggle's slot after Blocks kept showing Python after a switch back to Blocks, because the last chosen text language is a saved preference, but reverted to JavaScript after a switch back from a simulator extension editor tab, because nothing remembered the tab. The tab host now remembers the tab shown last, from when it is shown until the editor shows JavaScript or Python or the tab's frame is removed with its project, and the toggle layout shows that tab in the slot while no text language or tab is active, falling back to the language preference otherwise; the user-level language preference is left unchanged, since a tab belongs to its project's session, and choosing Blocks changes neither.
…undo, redo and zoom A simulator extension editor tab hid the editor toolbar, as the serial editor does, which also hid the simulator collapse button that the editor shows only with the toolbar, so a simulator collapsed from Blocks could not be expanded from the tab, and Download was gone there too. The tab now shows the toolbar like Blocks, the text editors and Assets, and a new editor capability, hasEditorToolbarEditingControls (true by default, declared on IEditor), lets an editor with no undo, redo or zoom of its own leave those controls out of the toolbar rather than show them disabled; the tab returns false. The toolbar's undo, redo and zoom decision moves unchanged into editorToolbarControls.ts with the new input, so it is tested without a DOM, the tab's editor area now ends above the toolbar in the same layout rules as its peers, and the simulator extension docs say what the toolbar offers on the tab.
In fullscreen-simulator mode every simulator frame takes an equal share of the screen, so an extension whose frame is a thin status or branding strip takes half the screen beside the game. A new SimulatorExtensionConfig field, hideInFullscreen (default false), lets a registration opt out: when the driver spawns such an extension's frame it adds the simx-hide-fullscreen class to the wrapper and has-simx-hide-fullscreen to the simulator container, and unload clears the container class. Under .fullscreensim the marked frame is display: none, marked important so that the reveal animation's visible transition class cannot show it for a moment, and the first frame takes the full width as an only child does; the rules follow the existing :not(:first-child) rule, which has the same specificity. The frame stays mounted, so its messaging continues through fullscreen, and an editor without the feature ignores the field. Adds the simdriver mocha harness (testsimdriver gulp task) with tests for the classes set on spawn and cleared on unload.
Simulator extensions registered with allowAssetRead can now list the open project's songs: a list request for the song kind answers each song in the project's own assets with its kind, its id (the key of its entry in the project's asset file, stable across renames) and its display name when it has one, read from the live TilemapProject the way images are; songs supplied by extensions are not listed, and change notifications already cover songs because they are project-wide. The client's list is now generic over the kind it lists, so its listing types the assets as entries of that kind (list("image") resolves with images) while the defaulted listing and reply types still accept every kind and the messages on the wire are unchanged.
A simulator extension registration may now set allowDownloads, which adds allow-downloads to the sandbox of the extension's frames, its simulator frame and its editor tab frame alike, so a download the frame starts, such as saving a file the extension produced through an anchor with a download attribute, reaches the browser instead of being dropped; the grant is carried through the driver's registration type, SimulatorFrameGrants and simulatorFramePermissions, and through the editor tab registration to the tab frame's permissions, with driver and editor tests covering the grant alone and with the other grants.
Removes the review-scaffolding comments that marked each change for its pull request text, together with the rationale written beside them, keeping only the comments that state an invariant, a precondition or an intent the code does not make plain, folded into the JSDoc of the symbol they describe where one exists; no code line changes.
The simulator driver now records fullscreen-simulator mode and posts a fullscreen message with enabled to every simulator frame when the mode changes, and to a simulator extension frame each time its page finishes loading while the mode is on, so a frame spawned during fullscreen knows it. The editor tab host posts a matching simxeditor fullscreen event to every ready tab on each change and once after a tab is ready, after simulatorstate. The editor calls both from setSimulatorFullScreen and resets them to off when a project unloads, since that path clears the fullscreen state directly. The simulator extensions doc describes the message and the new lifecycle event, and the simdriver and editor test suites cover both.
Session-only marker matching is limited to top-level names and projfs refuses the reserved .simxeditor suffix at any depth; the undeliverable closing event is dropped in favour of documenting pagehide, and tabs close before the simulator unloads; a tab's simulator frame is started on every show and re-created after the driver unloads; every project load closes every tab and marks the project closed, so deferred projfs and projassets requests answer PROJECT_CLOSED, and the driver unloads frames created while it was unloaded; open requests are refused wherever the editor toggle lists no tabs; start is posted when a run reaches the simulator and restart only on a fast restart; the driver gate checks that the open project includes the extension and that a request's origin matches its frame, finds tab frames through a registration instead of a DOM scan, answers tab open requests and handler throws, names the cause of PERMISSION_DENIED, logs an enabled tab without an editor descriptor, and resets computed registration URLs on every pass.
Projfs serves each registration from its repository's folder (owner/repo/), so every key of one repository and both frames share one quota, rate window, pending cap and queue, and a registration is in the project whenever the project depends on any extension of its repository (isSimxInProject compares repository slugs, so a key naming a subfolder passes the gate and its tab is offered, and a key naming no repository never does); written names follow the file explorer's rule (now pxt.isValidProjectFileName) with length, segment and per-folder file caps, and names count toward the quota; over-quota content and requests past the pending cap (reads included) are refused on arrival with replies kept in order, a write that shrinks an over-quota folder is accepted, the rate window uses a monotonic clock, and deletes clear testFiles entries; the projfs and projassets clients fail unpostable requests with FAILED and post-dispose requests with DISPOSED, honour slowReplyMs 0 and default to the parentOrigin URL parameter; projassets holds one subscription per frame (the client sends one request and registers each callback once), notifies on simulator theme preference changes, prunes removed frames, releases the project on close and checks UNSUPPORTED_KIND before waiting; pxtlib fires the project change listener for asset loads and cleanups and reports throwing listeners instead of aborting; the driver exports FRAME_DATA_ORIGIN for the editor tab host.
Restructure the simulator extensions page so the editor tab is one capability among several: a registration table of every simx field with its default and the frames it applies to, the trust model of the grants, project files (requests, folder, name rules, limits, ordering, error codes) and project assets (kinds, subscriptions, rate limit, error codes). Refuse pxt.json as the last segment of a written projfs name, and rate-limit project-asset list requests per repository with the sliding-window admission projfs now shares. Run the simulator driver tests on distinct editor and simulator origins through the window listener, record target origins, drop contentWindow from removed fake frames, remove the startFrame stubs, add tests for a frame spawned during a run and a reply to a removed frame, fix the flagged editor tests, use the real frame permissions in the tab host test, and rename the remaining domain-specific fixtures.
The editor tab's loading state is now MakeCode's text loader, as Blocks shows while it loads, reading "Loading <label>..." under the spinner: SimxEditorHostOptions.createLoader takes the tab's registration label, each tab records a loader created beside its frame, updateLoader shows the shown tab's loader while that tab is not ready and hides every other, and close removes the loaders with the frames; the SimxEditorHost suite now covers per-tab loaders across two tabs and close, and the editor tab docs say the indicator carries the tab's label.
The project-file and project-asset clients read the message of an exception thrown by postMessage through a cast to Error, so extensions that compile the clients under strict TypeScript, where a catch variable is unknown, type-check them.
Extend the project-asset provider from image and song to all six pxt asset types: tile (qualified ids as tilesets reference them, the generated blank tile included), tilemap (pxt's own tileset-file encoding and its ordered tileset, each member joined through resolveTile and carrying pixels only when the project or an extension holds the tile), animation (interval and frames in the image encoding), json (value and file name), and song data in pxt's song encoding; type the client's listing per kind, document every kind's fields on the Project assets page, and cover each kind and the tileset member shapes in the host and client suites.
Replace the same-tick setTimeout(0) coalescing of project-asset change notifications with a trailing window bounded by a maximum wait: a changed is sent PROJASSETS_NOTIFY_DEBOUNCE_MS (250) after the last change of a burst and no later than PROJASSETS_NOTIFY_MAX_DELAY_MS (2000) after its first, timed through the host's injected clock, with the asset-change flag carried on the pending notification so closing or replacing the project drops both; run the projassets host suite on the fake clock, cover a burst within the window, a stream longer than the maximum delay, and a change followed by project close, and state the timing on the Project assets docs page.
A write whose name ends in .jres, in any letter case and at any depth, is refused with INVALID_NAME, as pxt.json already is: a jres written into the extension's folder was read into the project's assets only at the next project load and then re-saved by the editor into its own asset files, leaving two copies of each asset with the extension's going stale; the docs say assets are not created through project files, and the Names rule lists .jres beside pxt.json.
…d simx tab
A simulator extension's editor config gains creates, the fully qualified fixed-instance types whose instances its tab creates; the driver carries it beside editorUrl, and the editor honours a claim only for a registration the project includes whose repository declares the type (first in registration order wins, other claims logged once per project load). For such a type the loader uses the new simxinstancedropdown field, a FieldDropdown that lists the instances and, while the tab is offered, ends in a localized "New {0}..." entry with the reserved value @simxcreate@; picking it keeps the field's value and calls pxt.react.createSimxInstance, which opens the tab and has the host post { type: "simxeditor", event: "action", action: "create", qName } once the tab is ready (at once, or right after the events following its ready event; one pending action per tab, replaced by a later request, dropped when the frames are removed), with SimxEditorActionEvent added to pxtservices/simxeditor.ts, docs updated, and editor and simdriver tests covering registration parsing, creator lookup, option building and action delivery.
Adds pxsim.SIMX_API_LEVEL = 1, the level of the simulator-extension surface the editor serves (the projfs, projassets and simxeditor message families with their requests, replies, events and asset kinds, and the registration fields), documented with the rule that it goes up by one whenever a change adds to or alters what a frame can observe of that surface; the simulator driver sets it as the apiLevel parameter on every simulator extension frame's URL and the editor-tab host sets it on every tab frame's URL, and projfs.ts, projassets.ts and simxeditor.ts each gain a standalone getSimxApiLevel reader returning the level, or 0 when the parameter is absent or malformed, so a frame can learn at startup which requests the editor answers instead of waiting on a slow-reply hook; the driver and host suites assert the parameter against the constant, each reader is tested for a present, absent and malformed level, and the simulator extensions docs name apiLevel and add an API level section with a table of levels.
A simulator extension frame or editor tab frame registered with the allowProgramRead grant can send a projprogram compile request and receive the program the simulator runs: the compiled JavaScript and the run options the editor passes the simulator driver for a plain run of the open project, or COMPILE_FAILED with the compile’s diagnostics. The simulator driver gates the family beside projfs and projassets, for simulator and tab frames alike. ProjprogramHost compiles the project as the editor holds it, without setting diagnostics or touching the editor or simulator. It answers each repository’s requests in order, runs one compile at a time, rate-limits each repository’s compiles, and refuses with PROJECT_CLOSED a request whose project closes before its program is ready. compileProjectAsync is the effect-free core of compileAsync. buildSimulatorProgram assembles the JavaScript and run options for both simulator.run and the host. pxtservices/projprogram.ts defines the messages and the dependency-free ProjprogramClient. The simulator extensions page documents the grant, its trust note and the compiled program API at API level 1. Driver, host and client tests cover the gate, ordering, rate limit, project switches and the client contract.
…e empty When a fixed-instance type has no instances and the dropdown offers neither the edit nor the create entry, getInstanceDropdownOptions now returns one entry reading "No <type> yet" with the reserved value @noinstance@ instead of an empty list, which Blockly rejects by throwing when the field is built or its dropdown opens; picking the entry changes nothing and calls neither editor hook, and the entry may be the field's value while the type has no instances, with tests for the option shapes, the field's value and the inert selection.
The simulator-extensions page under common-docs, which the editor serves to end users and uploads for translation, returns to a short introduction for extension authors with one section on what a simulator extension can do and a link to the reference; the reference itself, the registration fields and grants, the frames, each message family with its limits, ordering and error codes, the editor tab and the asset encodings, moves verbatim to pxtservices/simulator-extensions.md beside the clients an extension copies, and pxtservices gains a README describing the folder's editor-driving modules and the simulator extension clients.
simulator.setFullscreen now sets the simulator driver's fullscreen flag and tells the simulator extension tab host in one place, replacing the pair of calls the editor wrote by hand on entering or leaving fullscreen-simulator mode and on unloading a project, and postSimxEditorFullscreen goes away; the project-tools state test, which evaluates unloadProjectAsync in a sandbox with hand-listed globals, gains the simulator stub that method needs so its four cases run again.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this adds
A simx can now do four things, each behind an opt-in flag in its registration:
allowEditorTab,editor): a page of the extension shown in the editor area, listed in the language dropdown below JavaScript and Python.allowFileWrite): list, read, write and delete files in the extension's own folder of the project, so that what it authors travels with the project. it can also listen for modifications to its files.allowAssetRead): list the project's assets (images, tiles, tilemaps, animations, songs and JSON files), as well as read the project name, color palette and simulator theme, and listen for changes.allowProgramRead): compile the open project in the background and receive the JavaScript, to run it outside the simulator frame (prep for headless test runs in a simx).Also a new blocks dropdown field: a simx can declare the fixed-instance types its is capable of minting, and every block dropdown of such a type then ends in "New ..." and "Edit ..." entries that hand off to the simx.
Existing simx registrations do not gain any new functionality after this merges. All new capabilities default to disabled and must be explicitly opted into in their targetconfig.json registration.
Check it out (a Wendoo simx!):

Tests and docs
tests/simdriver-testis new on this branch: registration, frame spawning and grants, the request gate, tab frames, fullscreen.tests/pxt-editor-testcovers every host and client, request queueing, rate limits, notification timing, the label and creator lookups, and the dropdown entries.common-docs/extensions/simulator-extensions.mdupdated to mention editor tab feature.pxtservices/simulator-extensions.mddescribes the feature in detail.TODO
pxt-simx-sample