I’m deep in the middle of implementing the design discussed in the earlier post. The ‘navigation’ mode stuff we’ve seen before, it’ mostly done and shown here. The other mode is the ‘explorer’ mode. You navigate around, select a subexpression for editing, and up pops a crazy advanced autocomplete box with a ton of functionality, which is what I’ve been building (have just finished v1, actually), learning a lot about Reflex in the process. It’s a complex component, with a lot of requirements:
A tall order!
Since this explorer widget is going to be used for editing terms, types, type declarations, and other stuff like symbols (for instance when renaming a function parameter), I wanted something somewhat generic. Building components like this is easy with Reflex, you have total control over the results, and the full power of Haskell to abstract over whatever you wish. After some futzing around, I’ve landed on the following signature, which I’ll explain:
explorer :: forall t m k s a . (Reflex t, MonadWidget t m, Eq k, Semigroup s) => Event t Int -> (s -> String -> Action m s k a) -> Event t (m ()) -> Dynamic t s -> m (Dynamic t s, Event t (Maybe a)) explorer keydown parse topContent s = ... data Action m s k a = Request (m s) [(k, Bool, m a)] -- `Bool` indicates whether the choice is selectable | Results [(k, Bool, m a)] | Cancel | Accept a
Let’s walk through this:
Dynamic t s(for any state accumulated by the various async request made), and an
Event t (Maybe a), which will be
Nothingif the widget closes with a cancellation action, and
Just aif it closes due to an accept action. It’s then up to the parent node to do something with this accepted value. Note that the
atype can be anything, so it might have some instruction to the caller like “replace the selection with this term, and reopen the explorer for the next expression to my right in the layout”.
keydown, receives the keyboard event to use for up/down keyboard navigation within the explorer. You don’t want to just bind to the global keyboard event for the entire window. With the DOM, you can attach keyboard listeners on a per-element basis, but they are only consulted when the element has “focus”. The DOM’s concept of focus is pretty ugly, rather fragile, and it’s requested using a side-effecting function, gak! In a nice FRP system like what Reflex provides, it’s easy to focumulate your own concept of focus and manage it explicitly using regular code, not DOM magic.
parsehandles parsing the searchbox. It receives the current state, of type
s. In the simplest case,
smight be a list of results, keyed by string, and
parsedoes some string matching. Note that
parsecan trigger acceptance or cancellation of the explorer, or another asynchronous request for more results. The
Resultsconstructor indicates that the results can be fully satisfied without a trip to the node. The
Requestconstructor triggers an asynchronous fetch of additional state. The results are tagged with a key and a boolean. The key is used to provide stickiness of selection as new results arrive, and the boolean indicates whether the result is selectable or not.
topContentis static content filled in asynchronously. It’s shown right below the explorer.
The implementation of this function is 60 lines of code. And it actually works… the results are reallly ugly at the moment, but the implementation supplies some CSS classes that can be used to make it look nice. If you are for some reason interested in seeing the ugly test page I put together, you can build the code and then launch this file.
Now that the tough part seems worked out, I’m going to use this to implement the term explorer. Then we can hook everything together and try writing some actual Unison expressions for real! Since the language has
let rec bindings, we can actually write some nontrivial programs. I’m excited for this moment, and I plan on posting the editor right here online for people to try out. I look forward to getting feedback from people on what they think about the editing experience!