Friday, June 04, 2021

Poor Man's Projectional Editing

Projectional Editing is programming that goes beyond simple text entry. Notable projects are Lamdu and Jetbrain's MPS and to some extent Intellij itself.

Historically, programmers have been reluctant to stray too far from textual programming. A good compromise approach is to represent language features both textually and graphically. For example, the exponents in LegibleMathematics are both superscripted and ^ prefixed.

We could support the property of always keeping the program valid, and still give the feel of textual editing:

  1. as the user types, insert code to make the program valid
    e.g. if the user types if, then we'll insert (true) {}

  2. in the inserted code, automatically select the next bit that isn't forced, to clarify that it will be replaced as the user types
    e.g. in the case of inserted (true) {}, we'll select the (true)

  3. in general, whenever the user types the same thing as the next required bit, just advance the insertion point over it
    e.g. in some existing code if (x) { y(); } the user clicks before the semicolon and types ; }, we won't change anything at all, and just advance the insertion point to after the }

    If we want to support whitespace, then we could change the traversed text to use the new whitespace that the user types.
The above behavior enables the user to just obliviously type brand new code and have it work like in a traditional editor.

The third point is inspired by Parinfer, and Parinfer also has an idea for editing existing code. Inserting a brace somewhere automatically removes the following brace, and removing a brace will insert one as late as possible. e.g.
void f() {
    if (foo) {
        bar();
        baz();
    }
    qux();
    quux();
}

Removing the } after baz() will automatically insert one after quux().

Inserting } after bar() will automatically remove the other } in the function.

Of course we want tab completion, and once we have that and the above, we don't have to worry about intermediate invalid edits. In languages with stronger types than java, it could be that there isn't necessarily any automatically choosable value, in which case we'd have to use typed holes. In mainstream languages, we could use null at the worst.

It's nice to default to a value that causes the new code to essentially be an identity function, e.g. if the user types + then we'll append 0. If the user types * then we'll append 1. But of course this isn't always possible.

Changing a symbol usage changes just that one usage. Changing a symbol at its definition (or initialization in python for example), instead refactors the symbol and renames all of its usages. Changing a signature to remove parameters just updates all usages to not supply those parameters. Changing a signature to add parameters updates all usages to supply a default value as above.

Lastly, with this approach we can be very clever about cut & paste. A "cut" which would involve an invalid program is no trouble; when the "paste" comes, we can do the appropriate refactoring. In the example function above, if the user cuts the baz() call, and pastes it outside the function, we can automatically change the code to:
void f() {
    if (foo) {
        bar();
        g();
    }
    qux();
    quux();
}

void g() {
    baz();
}

And of course select the new function name g so that it can be easily changed. And some pastes would be illegal, e.g. pasting qux(); quux() in place of foo.

So, this "always valid" approach is very nice for refactoring. Its main benefit however is to enable powerful live coding, e.g. devcards.

Git gotcha

Since the rise of github, most teams build software using a pull request workflow. Therefore the local "master" branch is a bit of a gotcha. You can commit to it, but that will only ever cause you grief. That is, you'll forget about those commits, and they'll eventually cause a conflict, and you'll get something mysteriously broken when you just want "master". Fix it with this:

       
            git branch -D master
            git branch master origin/master
       
 
And actually consider not having a local master at all, and always using origin/master instead.