I've started to work on a structural editor for my...
# share-your-work
m
I've started to work on a structural editor for my language coil https://coil-editor.netlify.app/ It's still early & buggy, but the idea is to directly turn the AST into html with contenteditable for leaf attribute nodes. So an expression like:
document.body
which is the coil AST:
Copy code
{type: :property_lookup, lhs: {type: :id_lookup, name: "document"}, property: "body"}
Turns into:
Copy code
<div data-kind="property_lookup">
  <div data-attr="lhs">
    <div data-kind="id_lookup">
      <div data-attr="name" contenteditable>document</div>
    </div>
  </div>
  <div data-attr="property" contenteditable>body</div>
</div>
And now we can easily query this expression with css selectors. So all the syntax highlighting AND formatting is done in css. It also means that static analysis can be done using querySelector.
Where I see this going is establishing a highly extendable editor system. I want it to be attainable for an average library author to ship with a linter, and editor plugins. To start I need a dead simple editor system. So with embedding AST information in the html, it gives web developers a clear path how to inspect the AST & come up with custom rules, or swap out parts of the ast with something more visually interesting (imagine putting in a colour picker in usages of a color function). I haven't realized its full potential yet, there might need to be more metadata in the DOM, but I'm starting small & seeing how far I can get.
d
I'd wondered about such things myself. You are lot further along than I am, though! The "ground truth" representation is still the AST, right? Do you have to do a lot of work to keep the DOM and AST in sync with each other? Are you using something like ProseMirror or…hmm…looks like your are writing your own! Cool!
m
The code to do the mapping from html to ast, and vice versa is here https://github.com/coil-language/coil-lang/blob/main/editor/src/ast.coil The file is 67 lines of coil so its totally manageable, and I suspect it shouldn't have to grow much larger. In terms of keeping them in sync, the data is stored in the dom, so when you edit a node, it will parse it as coil code & turn it back into dom. So I guess ground truth is html. We only turn it back into coil AST when we evaluate it. Keeping the state in the dom lets us use query selectors for all sorts of things. Here's an example of checking for using undefined variables: And here's the linter in coil
Copy code
fn lint-unused-variables {
  let ids = document.querySelectorAll("
      [data-kind=let] >
      [data-attr=assign_expr]
      [data-text]")
    ::map(:textContent)
    ::into(Set[])

  document.querySelectorAll("[data-kind=id_lookup] [data-text]")
    ::reject(:textContent ids)
    ::each($.setAttribute["data-warn", "Use of undefined variable"])
}
p
I think very cool, but I must admit I can't work out how to try it out. Would you be able to do a video where you explain the keypresses you do to make edits
m
Admittedly it might be too early for #share-your-work but here's an attempt to show. One thing is, its rough on mobile, you can eval it but editing is hard. There's also a handful of crucial features missing notably • ability to tack on an operator after a node -> going from
a.b
to
a.b.c
• delete the upcoming node - going from
a[0]
to
a::first()
is hard/impossible right now Also there's some awkwardness of dealing with the editor as ast nodes, you can only edit within a leaf node, to move on to the next part you must tab into. Here's a video, hope it helps.
I believe those problems are solvable, but right now I'm just showcasing the simplicity of the model, and the benefits of storing AST as html.
When you insert a new line, its temporarily a plain text editor for that line until you focus away & it parses it