Dethroning the King of Text Objects: Visual Line Mode

:: text-editing vim emacs composiphrase

As mentioned last time, I’ve been reconsidering visual line mode. Don’t get me wrong, I love visual line mode. But vi and its descendants are very line-centric. Lines are first-class, while every other way of looking at or splitting up the text, or “text object”, is sort of second class. There are so many features of vi and vim that operate on lines specifically, that just don’t work with other ways of looking at text. Eg. ex mode operates specifically on lines, or ranges of lines, and specifically not on character regions with partial lines. Visual line mode and paste work together to operate cleanly on lines. The default keymap has keys that work extra conveniently on lines. There is an extra goto-line command, and marks have extra line mode instead of just going to the mark position. Etc.

(Note, this is part 3 in a series about my “new composable text editor”, Composiphrase. See part 2 here. See part 4 here, which is the main part that you should read if you just want to read one of them.)

And that’s all with good reason. Most programming language formatting arranges things, to at least some degree, in a line-centric manner. Some programming languages use significant line indentation to make lines a core part of the syntax. Some data formats are line-centric, especially older table formats like CSV, and the many CSV-like formats. Unix tools typically operate on lines of text. So operating on lines is a very useful approximation for working with pieces of program syntax, and useful for many data formats, and useful for many kinds of ad-hoc text work.

But working with lines has limits, and there are other useful ways of operating on text that I want to be as convenient as working on lines. Another useful approximation is indent trees. In other words, you can view a text file as a tree based on how much indentation there is for each line🌴. For example:

Parent line
  Child line
    Grand child line
    Grand child 2
  Child line 2
  Child line 3
    Another grand child
Another root-level parent line.
  This line is not a sibling of any of the above lines.

🌴: I wrote some initial elisp code for navigation and selection by indentation tree a couple of years ago in my dotfiles repo. I’ve used it a fair bit, mostly for things like moving between top-level definitions or walking over big nested sections of code. “Proper” tree walking, like with Treesitter, is obviously better in many ways. But approximate generic operations are really valuable, too! Anyway, in my composable editing project I’ve expanded my indent-tree code a bit and put it with my other new and adjusted movement and text object handling code in Composiphrase Objects repo.

Lines with the same level of indentation, that aren’t separated by a line with less indentation, are siblings. Not only are programs typically formatted in a line-centric manner, they are typically formatted as indentation trees, where the indentation is a rough approximation of the indentation of the concrete syntax tree. Now, like with lines, how useful this is varies by programming language. Languages like C and Javascript are typically formatted with closing braces that match the indentation of the opening brace (or its line), like this:

function foo() {
  if (bar()) {
    while (baz()) {
      printf("quux");
    }
  }
}

In this case, the closing brace for the while statement is a sibling of the indent tree with the while identifier. So in these situations, indent trees are useful for things like navigating to a sibling function definition, or going the end of a big indented section to find sibling statements. But less useful for, say, selection.

Lisps fare better with indentation trees, because closing parentheses are typically placed on the same line:

(define (foo)
  (when (bar)
    (while (baz)
      (printf "quux"))))

Closing parentheses are part of the same indentation tree that the open parentheses are in. But still, if you use indentation trees for selection, you will select trailing parentheses that belong to outer levels of the s-expression tree.

But some languages follow an indentation tree quite strictly. Indentation trees are a very good approximation for use on Python syntax trees, for example. Selecting an indentation tree in Python is great for using copy/paste().

(): Though, you likely have to manually adjust indentation depth after moving code. Those delimiters are useful, after all, since they allow the computer to infer proper nesting from the syntax, and automatically fix formatting, rather than inferring syntax from formatting. Among other things! Go all the way to Lisp, and the paretheses provide tons of benefits, and, no, Treesitter and LSPs don’t obviate even the ones related to text editing. Stop throwing those tomatoes, Lisp haters. This is my blog. I can talk up Lisp all I want here. You just need to learn the traditional Lisp formatting algorithm, and then it’s easy to read. Hey, stop. No more throwing vegetables. I’m serious. You’ve been reading the algebra notation used by most languages since middle school, and infix math notation since early childhood, and you spent all of that time memorizing the order of operations, and then even more after you started programming memorizing a subset of the order of operations for C. But if you lay aside your prejudice benefitting the familiar, spend the tiniest fraction of that time getting used to this new notation in Lisp, you will find that it is not only not hard, but in fact easy and full of beauty.

Ok, this has waxed on about indention trees long enough. But my point is that indentation trees are often a better and more useful approximation for working with code than lines by themselves are, while still being completely language agnostic. Yet vi and friends provide no functionality for working with them. You could add a plugin to an extensible vi, like Vim (with pain of Vimscript) or Neovim, but it would never feel like a first class citizen like lines. And you would have to find room in the overcrowded key map for any operations.

Of course there are other useful text objects, including other trees, and in particular the actual concrete syntax tree that you can get from a proper language parser. These are all second-class at best in Vim. Some newer vi-like editors are changing this to at least some degree: the Helix editor uses Treesitter to provide text objects based on the actual syntax tree. And I’m sure there are NeoVim plugins, and probably even some Vim plugins. But, of course, it is still not quite as first class as lines.

Ok, let’s be real, lines are super convenient and ingrained into how we work, and it is hard to imagine that other views will ever be quite as central to our workflow. You could imagine, for example, toggling from line numbering to numbering top-level indentation trees, or top-level s-expressions, or top-level syntax tree nodes, or numbering sentences or words. But line numbers match the display, and are a very simple, general abstraction that works well between tools and with the way we think about programs. I don’t think I would realistically want to display or operate in terms of tree numbering. And, in my personal key configuration, and probably in the Composiphrase demo configuration that I’m going to try to finish up this week, movement by line is the only movement that I’ve left as a single key (for non-repeats, at least). Maybe the line is still the king of text objects.

But, I clearly do want to use syntax trees, indentation trees, and other text objects for navigation, selection, and other operations. Where is the dividing, ahem, line?

Consider visual line mode. What if you had visual Treesitter mode? Instead of ensuring that the selection always covers whole lines, and that paste inserts copied lines properly between other lines, you could imagine that in visual Treesitter mode that it always selects whole trees, and pastes trees properly at tree boundaries. Or visual indentation tree mode, where the selection properly covers indentation trees, so that copying will not leave out children lines, and perhaps automatically adjusts indentation to fit. Or visual s-expression mode that works with paredit or smartparens. Or visual sentence mode, or visual paragraph mode, or maybe somewhat underwhelmingly, visual word mode.

If I find visual line mode (and its corresponding paste special handling) useful and convenient (which I do), then likely I’ll feel the same about visual text object modes for my most-used text objects. It is less clear than with other features, but I feel pretty confident that it is a good idea. I’ve been using and loving Smartparens-based structural editing for years, and love it. Does it matter much to have a specialized visual selection mode? It would cut down on a few operations, requiring less movement to align both ends of a region just right to capture things correctly. And again less effort to get to just the right position for pasting. So... the same kind of convenience it brings to line mode.

Is it useful enough that I’ll actually build it? That I’ll build it in the near term? Unclear, given other priorities. But the extra handling required to implement visual line mode probably wouldn’t take much work to generalize to arbitrary text objects, I think. And some of the logic overlaps other functionality that I want, such as group text object transposition (IE transposing multiple of a text object at once based on region, rather than transposing one at a time). So maybe I will implement it... It’s certainly a cool idea that fits nicely with Composiphrase, which is already focused on text objects as a central piece of the composable text editing language. Maybe it is coming to a Composiphrase composable editor (inside Emacs) near you, the summer (or even spring, or late winter!) blockbuster of 2025. Meh, maybe. No promises.

Except that I promise that I will eventually get this series to a point where I’m talking about what Composiphrase is, aside from that it’s a modal editor focused on text objects, with the vague promise that it takes composability and the idea of a text editing “language” more seriously than other modal editors.