The Rake and Its Prongs

When you type in a text editor, you always type out of some range.

When your insertion point or cursor is blinking at the end of a word you just typed, you expect to still be “in touch” with the word, and that the next key you press will for example add a character to that word. This is a useful deception for us human users. It’s not actually part of the technological underpinnings.

The blinking hairline cursor invites us to continue typing. But where, actually?

Similarly to appending characters to words, a user expectation is that shortcuts that operate on the word-level should operate on the word boundary right before the blinking cursor. That’s the expectation in a language like English at least, where you separate words with whitespace or punctuation, and these clear word boundaries are everywhere. As readers and writers of that language, we’re trained to work with word boundaries.

Now a String has no notion of such things. It just contains characters, and you need to find a word on your own.

This came up again when I worked on DeclarativeTextKit to implement WordRange(...) as a selection. The interesting edge cases are literally that: thinking about the edges, the word boundaries: How do you find them?

How do you know whether the blinking cursor is “inside” a word’s range?

In the land of NSString and NSRange, you can ask a range whether it contains a character location:

extension NSRange {
    /// - Returns: Whether `location` is in the receiver.
    func contains(_ location: Int) -> Bool {
        return NSLocationInRange(location, self)
    }
}

NSLocationInRange(_:_:) describes the test it performs:

true if loc lies within range—that is, if it’s greater than or equal to range.location and less than range.location plus range.length.

In code:

func NSLocationInRange(_ loc: Int, _ range: NSRange) -> Bool {
    return loc >= range.location 
        && loc < range.location + range.length
}

A test of NSRange.contains(_:) like this is not sufficient to cater to the user expectations I just mentioned: it will return false for the position of the blinking cursor right after a word. The blinking hairline’s location does not satisfy loc < range.location + range.length, aka its upperBound, because it’s actually == to the latter.

Prongs and Gaps

Illustration of a wooden rake

I learned this metaphor of a rake, its prongs, and the gaps between the prongs from Reto Byell when we talked about array indices and coordinate offsets in 2D space. I imagine a Japanese dry garden (“Zen Garden”) when visualizing this, where a rake is used on gravel to form small canyons and hills that represent ripples in water.

Here’s a crude ASCII representation of a rake:

 |   |   |   |   |   |        the prongs of the rake
  \__|___|___|___|__/

You need 2 prongs to create 1 gap. To get a rake that produces 10 gaps at once, you need 11 prongs. Let N be the number of gaps, then you need N+1 prongs on your rake.

Transfering to text editing, the string “Hello” has 5 characters.

In a string with 5 characters, the locations (as character offsets in the string) are {0, 1, 2, 3, 4}. These are values inside of the range.

NSRange behaves like this; the word range of “Hello” is (0...4) (or (0..<5), excluding 5), and NSRange.contains(_:) via NSLocationInRange will return true for any of these values.

   H   e   l   l   o  
 |   |   |   |   |   |    the prongs of the rake
  \__|___|___|___|__/     
   0   1   2   3   4      gaps = character offset

Once we add the blinking cursor, denoted by ˇ in the following illustration, and add its location in terms of character offsets, you’ll see how “typing at the end of a word” is a human construct that neither the string nor the NSRange understand:

   H   e   l   l   o   ˇ     
 |   |   |   |   |   |      the prongs of the rake
  \__|___|___|___|__/       
   0   1   2   3   4   5    gaps = character offset

Character offset 5 is not contained in the range 0...4 (aka 0..<5). But you can type there. Let’s say you add an exclamation mark, then the string becomes:

   H   e   l   l   o   !
 |   |   |   |   |   |   |
  \__|___|___|___|___|__/
   0   1   2   3   4   5 

And with that, the character offset 5 now is inside the range of the string’s contents. There’s one more prong, and one more gap to the rake. You typed into the location that didn’t exist to make it so that it does now.

The effect becomes more pronounced when you think of an empty string.

There, the valid range is 0..<0. That is an empty range, which means nothing is contained with the NSLocationInRange check. No possible character offset is considered to be ‘inside’:

   ˇ     
 |      a sole prong of a 'rake'
 |      actually no gap will make
   0   

Yet the cursor blinks.

When you type, you always type out of range.

I never found a good word or concept for this.

I do have an NSRange extension, though, that takes care of this:

extension NSRange {
    /// Unlike `contains(_:)`, also returns `true` *at* the end position, which is not a valid position to read from, but a valid location to append content to.
    ///
    /// - Return: `true` iff `location` is between or equal `lowerBound` or `upperBound`.
    func isValidInsertionPointLocation(at location: Int) -> Bool {
        // Insertion into an empty range at the 0 location, or in a non-empty range at the after-end position are appending operations.
        return location >= 0
            && lowerBound <= location
            && location <= upperBound
    }
}

Note that the main trick here is to test location <= upperBound instead of <. To make sure that you don’t accidentally type into the -1 or NSNotFound location, check for >= 0 as well.

The name of this check, isValidInsertionPointLocation(at:), is a bit long, but it’s not leaving anything to guesswork.

Again, I have not found a succinct way to express this idea.

With an NSRange (or any range of character offsets),

  • you count the gaps between the prongs of a rake when you read,
  • yet you count the prongs of the rake when you write.

Hairlines and Boxes

With blinking block cursors of the terminal days, you didn’t have the notion of typing between characters that hairline cursors suggest.

This clearly shows you are on the position of the letter “s”.

You rather select the destination character offset with the box. From there, you could replace/overwrite (which wasn’t that uncommon, actually), or insert a character, shoving the selected one and all that came after it one location to the right.

This also introduced the ‘lie’ of typing at the end-of-line or end-of-file position to append.

There's nothing beneath the blinking box this time, yet you can type there to append.

If we hadn’t settled on this dual nature of editing text on digital devices with blinking cursors, including the position right after the last location that actually exists, aka typing ahead into the nether and letting the machine do the string expansion, we could not append at the at-the-end position like we do. To type at all, every text would have to have at least 1 character inside upon which to place the cursor. Instead of treating the end-of-file (EOF) as a marker, we’d display it as a character.

Demo of transmitting text with a SIEMENS T1000 teletyper that prints transmitted characters and punches the data output

In a tele-typing environment, transmitting character by character until the EOF (or end-of-transmission (EOT)) sequence appears makes sense, because you operate on a stream of data with latency. Like morse code, you press a key, the system sends the key code. Eventually, you denote that you’re finished. (In actual teletyping hardware evolution, the text would be buffered so you can compose first, send later, and even delete characters in case of typos.)

Editing text digitally and interactively on a monitor still works with a stream-based system. A piece of text can be represented as a string, (a literal sequence of tied-together) characters. Where once you aimed at a piece of paper to show where the next character would go, now there’s a blinking box. The blinking cursor was new there.

The blinking hairline from graphical user interfaces is even better suited for the task. It emphasizes the space between characters where the next character will go.

A space that doesn’t actually exist.

It’s all a mental model, a fantasy representation.

While the characters and text under the hood is still the same, the blinking hairline suggests that we can insert something in the in-betweens, and also at a position after the end of the text. When we write at the end of a file, we continuously write into an empty space, and the text buffer catches up with us to capture the characters we’ve just typed.

It works beautifully.

But as software developers, we need to think a bit harder about what “inside” means, and that can become a bit weird.