Adding code hover functionality to script-8
2019-10-29
The past two days I have been working on a feature for Script-8 that I've been super excited about for a couple of months now. I got really close yesterday, and decided to push the code further rather than right a blog post for the day, so today's post counts for two. Some background first, Script-8 is a fantasy console in the same vein as Pico-8 but uses Javascipt as the programming language and with a strong focus on dynamic and interactive programming. All changes are live as you write them, and there are existing features in the app for easily interacting with your source code and visualizing your program easily through sliders and time travel. I highly recommend checking it out, and I have witten about it before.
The particular feature I have been working on allows for even more powerful visualization of what a given peice of code actually does. I added code injection which hooks into any function under the mouse and highlights any graphics that function is responsible for.
As you can see, the graphics brightens visibly for each draw call under the mouse. This makes reading through someone's code much easier, and allows for simpler debugging of your own code. The above gif highlights this well. The awesome graphics in Thykka's Supplies game look as though they are just rendered as a single map call, but hovering over the draw calls individually shows that the sky coloring is actually a mixture of transparent pixels in the map and the clear color. This wouldn't be obvious without puzzling through the code.
The idea for the feature (as well as much of the coolest features in SCRIPT-8) comes from the brilliant Bret Victor and his presentations on Inventing on Principle and Learnable Programming. Both are awesome, and I plan on dipping back into their ideas for future Script-8 features.
How
I've created a PR with the change which is open and waiting on review and I have tried my best to comment the code as well as possible. So I will attempt to summarize the comments into a linear story here.
The high level system parses the code in the current tab, looks for CallExpressions which overlap the mouse position, picks the smallest one under the mouse, and rewrites the expression to set a highlight flag to true before the call, and reset the highlight flag back to false after the call is finished. This highlight flag is then inspected during the draw code and the pixel colors are modified accordingly.
Parse
To do the parsing I lean heavily on the fantastic Acorn.js which is a Javascript parser written in Javascript. It supports modern JS flawlessly and provides a concise and convenient API for selecting a given type of abstract syntax tree node.
let = null
let = Infinity
I pass the current code tab text into the parse command to get back a root node, and then I use Acorn's walk functionality to loop over all of the CallExpressions. I then check the current mouse text position to see if it is inside the bounds of the given node, and keep the smallest one that I find. This node is then stored in redux for use elsewhere.
Rewrite
Then before sending the code to the game engine to run, I rewrite the CallExpression under the mouse by wrapping it in a closure, and passing the closure to a helper function which manages the highlight flag. The code here is pretty well commented, so I will let it do the talking.
// This takes a tab and returns the code from that tab. It also looks at the call expression under the mouse (if one
// exists) and wraps it in the `_script8.injectHighlight` function to setup higlighting. Decorate is taken as an
// argument so that places which do not need decorated code can preserve the code as written by the user.
;
The injectHighlight function exists in a special global function which is available to the engine, but banned for user consumption. This makes it the perfect place to place the internal engine specifics. The function itself, sets the global highlight flag, runs the passed function storing the result in a variable, unsets the global highlight flag, and returns the stored result. This means that the wrapped expression can be used in the same places as the original expression as it returns the same values and exists on the same line (which is important to preseve error line numbers).
// This function is used in the hover highlight functionality. It is stored on the user inaccessible _script8 global
// variable and takes as argument a lambda wrapping some user expression. The flag `shouldHighlight` is then set to true
// while the passed in code is running and returned to false when it finishes. `shouldHighlight` is used to modify
// colors while true to indicate to the user how a given function call effects the drawing on the screen
Highlighting
From here I just modified the color lookup code to take the shouldHighlight
flag into account and brighten/darken
colors accordingly. Since every draw call depends on the color lookup functions to draw the correct color, I only needed
to hook in in this one place, and everything worked properly.
i
Thats it! For a relatively complicated feature, I think the overall solution is pretty elegant! Acorn does all the hard work, and hints at some ways I could add more interesting features in the future. Some I am concidering are:
- Storing the draw function text coordinates with the draw call itself so that the user could click on the game screen and jump to the location of the draw call that last drew in that pixel.
- Recording argument index under the mouse and exposing it as an API to allow users to build their own specialized debug code and to allow the draw calls to render what each parameter does.
- Sample variable state over the course of runtime to inform the slider feature what bounds to set the slider at. Currently it just uses integer values, but it would be cool to enable the feature in more circumstances for more precise values
I've enjoyed working on the Script-8 projects, and am excited about how useable and unique the editor experience has become. Hopefully the trend continues.
Till tomorrow,
Kaylee