I had a brain-wave last week. I thought it would take me about a day to implement but it took 3 to get it working almost reasonably and the rest of the week to polish it and create some proper use-cases to exercise it properly. But that is done now and I am quite happy with the result. It related to the rendering side of edlib.
Commonality among renderers
My original idea for the rendering side of an editor was they there would be a number of quite different renderers, each took a document and a pane and drew the contents of the one onto the other. I even implemented several of there: render-text to handle a standard text document with line-wrap, render-dir to render either a directory or a document-list in an “ls -l” format – with the format being configurable based on attributes. And render-hex which takes the same text document, but displays the contents as hex, 16 bytes per line.
Having done this I noticed that was quite a lot of commonality. Detail were different but each render need to keep track of which part of the document was currently displayed and to be able to move that part around whenever the ‘point’ moved outside of the document. It also needed to handle movement commands which were display-based, like ‘end of line’ or ‘down one page’. All this duplication was rather boring and I didn’t relish writing it for the next renderer I needed which would help with file-name completion.
Rendering Components: line at a time.
My brain wave was that I could avoid this duplication by abstracting out the rendering of “lines”. Every display I was working with was essentially line oriented. If I could get the underlying document to provide lines: lines of text, lines of hex characters, lines of directory contents, then a single renderer could draw them all, wrapping or truncating long lines, scrolling up or down to display the point, counting lines that fit so that ‘page down’ could mean something.
One stumbling block that probably consumed half the time is that lines don’t always exist in a “text” file. For a “hex” file, a line is exactly 16 bytes, for a directory listing it is a single entry, but for a text file it is from one newline character to the next. If a file has a million characters and no newlines, then the it would appear to be just one line and handling that line entirely in the renderer would be clumsy. I first explored having the renderer tell the document “it is OK to give me just some of the line if it is long” but the indeterminism there caused lots of problems.
So my current solution is to leave it to the document to manage. When asked for a line it must always provide a line, but the definition of a “line” is somewhat flexible as long as it is well defined and stable. For example the document should decide that a line cannot have 2 characters that are on a 1KB boundary. If that threatens, then the second becomes the start of a new line. This might lead to some strange rendering, particularly if line-wrapping is disabled. But it should at least be repeatable.
Rendering Components: stacking
The way I implemented the “give me a line” functionality was to send a command up the pane stack from the display pane until it reached a pane with a document. The document would return a line. It quickly became clear that I could insert a different pane in the stack which did something different.
So if a text document is asked for a line, it returns something between newline characters. However if a “hex” pane is asked for a line, it asks the document for characters and returns 16 of them formatted as hex. More complex pipelines should be possible.
My plan for filename completion is to have a directory document with an “format attributes” pane which just presents the “name” attribute in each line. Then on top of that I stack a “completion” pane which knows that the current prefix is and hides any line that don’t start with it. It also allows easy movement and selection of an entry. On top of this pane is the render-lines pane which actually draws those lines (the one which match the prefix) onto the pane (probably in a drop-down menu).
Having this sort of flexibility makes the task of writing a new renderer much less daunting so I’ll probably do more of it. I suspect I’ll eventually need something other than “lines”. I have in mind a “Column” renderer which expects short lines and stacks them into columns of a fixed height, and then scrolls left or right if there are too many lines for all the columns. This is how some UIs allow selection from a large number of files. And of course some displays might not be line-based at all – maybe a renderer that interprets SVG and draws pictures?
Details of render-lines
There is a little more sophistication needed in the document or underlying pane than just providing lines of text. It also needs to be able to find the lines of text and map cursor position to position in that text. It would also be nice to support attributes: bold, underline, text color etc.
So there are actually two commands that can be sent. “render-line-prev” is given a mark and must move it to the beginning of the line, or the beginning of the previous line, depending on the repeat-count that is part of the command. “render-line” has three modes. In the standard mode (no repeat count given) it provides a text version of the line starting at the given mark, and moves the mark to the start of the next line. In the ‘find-point’ mode (negative repeat count) it only renders until the mark reaches point. Then it returns a partial line. That allows render-lines to work out where in the pane the cursor should be. Finally a non-negative repeat count means to render until that many characters have been produced in the line, and then stop returning with the mark only moved far enough to produce those characters. That allows render-lines to convert a pane-position into a document position so that when you click somewhere the point moves accordingly.
To support attributes, render-lines allows the reported line to contain attributes enclosed in angle brackets: <bold,fg:red>sometext</> will display sometext in bold red characters. This is probably a preliminary design and could well change. To include a “<” in the displayed text, use two “<<“.
There is room for more information flow from the text generator to the line renderer. In hex mode it is nice to have the address at the start of the line and it would be nice if that didn’t shift to the left when the content moves because of a narrow pane. It would also be nice to allow a line to be rendered differently when ‘point’ is on it: currently lines are cached and not re-rendered until the document changes. Treating the point-line specially would allow extra highlighting of the cursor and a “reveal-codes” like approach to WYSIWYG editing where you only see the markup codes when the cursor is on them.
So there is a little way to go still before this brain-wave if fully realized, but I’m really quite happy with how it has progressed so far – if only it hadn’t taken so long.