Got all this rippling stuff working now. 
You can test the pathalogical case by entering a single "
on the first line and then CTRL+End to jump to the end of the file. This must ripple down the entire file, so you can see it takes a little while. Demo is currently set to 10K lines, on which it is not too bad.
You can also PageDown a few times, enter a "
, then jump back to the top and enter "
. Then jump to the End and you will see it is a lot quicker to get there, as the ripple from the start catches up with the second one, and then lines beyond that are in the correct state already since they were never processed. The rippling completes at that point, so only a small section of the file is processed.
Might still be a few bugs, but I am calling a success on this spike, since I completed everything I set out to:
- Flesh this idea out and implement a simple editor using it.
- Check it performs well on large files (say 1M lines) - Not good on the pathalogical case.
- Look at how the buffer can be more flexible than just holding lines of Strings. Need support for color syntax highlighting.
===
If I was doing this in a multi-threaded language, I would do the ripple processing on a background thread, so the pathalogical case would not make the editor unresponsive. This is not an option in Elm.
I could work around this by doing the ripple processing in chunks, and doing one chunk per message put through the update
function - cooperative multi-tasking in other words. I’ve done similar before and the results are never as good as you might like for keeping the UI responsive; but it is still an option.
Another workaround is to have the editor always close any "
by auto-inserting ""
, which should reduce the frequence with which this problem is hit.
===
As I worked on the implementation, things took their own direction and the code has come out a little differently to my initial sketch (doesn’t it always?).
I changed the toZip
and toArray
functions names to be toFocus
and fromFocus
, as this seemed better. The fromFocus
function now also get a first Maybe
parameter, which is the gap buffer element 1 item before the focus being tidied away. The reason for this, is so that any context from the previous line can be taken account of - if the previous line ended inside a quoted string, the next lines lexical context will also start out inside a quoted string:
type alias GapBuffer a b =
{ head : Array a
, zip :
Maybe
{ val : b
, at : Int
, tail : Array a
}
, length : Int
, toFocus : a -> b
, fromFocus : Maybe a -> b -> a
}
With that change the ripple function can just be:
type alias TextBuffer tag ctx =
{ lines : GapBuffer (Line tag ctx) (GapBuffer Char Char)
, ripples : Set Int
}
rippleTo : Int -> TextBuffer tag ctx -> TextBuffer tag ctx
rippleTo to buffer =
Every change to the TextBuffer
results in a ripple being created at the line on which the change was made. These ripples are held in a Set
so they are automatically de-duplicated, and also Set
is ordered, so they can be processed in order from earliest to latest.
Any ripple starting before the to
parameter will be processed. If the ripple stops uncompleted at the to
parameter, it is retained for futher processing from that point, when that is needed. Any ripple that gets overtaken by an earlier one is discarded, as there is now no need to process it.
This allows incremental processing of all the ripples, so is nice and efficient.