Tools shape one’s perceptions and thinking. Programming languages constitute a common example: consider for instance how regular iteration encourages a certain mindset, while recursion coupled with purity encourages different trains of thoughts.
What is true with languages remains true, for instance with text editors. And is maintained outside of the programming realm.
A side-effect is that, once we’re comfortable with a given way to achieve a certain result, it can be psychologically demanding to switch to another approach.
As an example, however great debuggers are, not having a debugger forces you to read code more carefully, and to think more. Perhaps then, it’s not much of a loss not to have a debugger in the long run (over decades). But that doesn’t mean debuggers should be absolutely avoided either.
In particular, acme(1) encourages a peculiar mindset, by comparison with mainstream editors. I’ll try in this article to explore this difference, while highlighting a similar dichotomy in woodworking.
Why?
Being more mindful of such a situation can help to overcome difficulties arising when learning a new arbitrary tools.
In the wood
As already stated, what holds true in programming also holds true for other disciplines. Woodworking is interesting because it’s a much older discipline than programming, and thus, it has had the time to be refined: at mankind’s scale, software programming is but in its infancy.
Consider woodworking planes (again): here are a bunch of Japanese planes: (their designed didn’t really evolved with the industrial revolution; Western’s did):
So, what’s a Japanese plane I ask you? You may answer, well, a piece of sharpened metal stuck in a squareish piece of wood. But you have good eyes, so you’ll notice that there there are two flat pieces of metal, held together by a little metallic cylinder.
It’s likely that even if you were given the opportunity to hold one for some time, you wouldn’t be able to find much more to say about them. If you a bit hackerish, you may observe that they would make a decent ad-hoc square.
A Japanese plane is a really a fine tool: some of its key characteristics aren’t loudly advertised, nor easily perceptible to the uninitiated. There’s not much to look at, but everything has been carefully designed, likely refined over centuries by master crafstmen:
- the weight of the blade, which increases the plane’s stability, and whose dimensions contribute to a slow wear;
- as the planes deteriorate, both sole and blade can be replaced; leftovers can be used to make smaller planes, which have practical advantages over bigger ones;
- the advantages of a laminated, bi-metal construction: blades are made by laminating two different kinds of metal, not only reducing costs, but also easing sharpening, reducing shattering, etc.
- the sole’s setup, aiming at reducing friction: only a very small portion of the wood is actually in contact with the surface to be planed;
- how the planes are used by pulling instead of pushing, thus increasing control/stability, and reducing (in most cases) the amount of physical effort needed (Chinese woodworkers are known to use both motions: it seems in some cases, pushing can still be a reasonable choice);
- how to use friction/vibrations to set/remove the blade out of its wedge, instead of a more sophisticated design;
- etc.
If acme(1) is comparable to a Japanese plane, then mainstream editors are closer to industrial thickness planers. This should motivate enough the fact that learning to use an editor as dry as acme(1) (or sam(1)}) can be demanding, from the point of view of someone used to mainstream editors.
Good enough; reactive, stable
Acme’s not perfect. But, as Unix once was, it is, for better or for worse, good enough. Some elements could be refined, or at least experimented with.
Acme’s fast: no syntax highlighting, a small C codebase, a text-only interface, fixed features set (⇒ almost no updates), low resources usage.
Note: I’ve had a few crashes with Edit X//
(a way to
iterate on multiple windows), but I rarely need to use it anyway, and when
I do, I’ve got a few tools
for it.
Discoverability: meh
The editor was built in an isolated, research setting, a few decades ago (before Internet as we know was a even thinkable, as is evident from the design of 9P, Plan9’s backbone). You can try to toy with the editor on your own, but it hasn’t been built with a regular, modern user in mind, and so likely, you won’t get far.
Instead, it comes with a few well-written manual pages, and some research papers:
- acme(1) (main page);
- acme(3) (C interface);
- acme(4) (9P/filesystem interface, see intro(5), allowing to programmatically interact with the editor to some degree);
- keyboard(6) / keyboard(7) (p9p) (special characters input, e.g. ☺)
- Acme: A User Interface for Programmers (.pdf).
As acme(1) borrows a few things from sam(1), the two following papers are worth studying as well.
You have to read those. Because acme(1) is now more broadly use, you can find additional content online, but however useful, they will never be able to compensate for the man pages/papers. This is how the system was originally documented and used.
Mouse
I’ve never used acme(1) with a 3 buttons mouse (perhaps once or twice, at most). I used to have a clickable wheel a few years ago, and this worked just fine. I’ve been on a Thinkpad for around 8 years, so there are three clickable “mouse buttons” right above the touchpad, under my thumbs. This is just as great, perhaps even better.
That is to say, a fancy mouse isn’t mandatory.
Edition speed
Learning to use a new text-editor requires to adjust multiple habits at once. Furthermore, acme(1) often demands a considerable mindset shift.
So it’s likely that one will be slow to operate acme(1) at first. In the long run, I’m not so sure: as far as I’m concerned, what’s most time-consuming is thinking, debugging, testing. Not editing/typing code. To say it otherwise, the time gained by optimizing edition is negligible in front of the time spent thinking, designing and testing code (premature optimization then).
A text editor shift will almost always result in an alteration of the workflow. While I barely have that feeling anymore (I only do when I’m rushing), I still don’t think it’s a bad thing that your editor forces you to slow down.
It’s often a good idea actually.
Syntax highlighting
“Just say no” / “Plan nein”.
The choice is pragmatic: the tangible benefits of syntax highlighting are meager, compared to its cost:
- slower (to run, to develop);
- (much) more complex:
- requires the ability to (partially) parse a bunch of languages (this can be subtle & bug-prone);
- without, the GUI code only has to display monochromatic text, in a single font per buffer.
This is furthermore true if we highlight the context: a text-editor, developed in a research setting, a few decades ago, in a team involving what should come close to the most advanced Unix users (its literal creators).
I don’t mean to say that syntax highlighting is useless: besides providing a more colorful experience, it can be semantically useful, for instance to automatically detect visually missing opening/closing parenthesis/brace/brackets. But again, as far as I’m concerned, I don’t mind being a bit more cautious. That last use-case could also be resolved with a small little parser.
win(1)
win(1)
(documented in acme(1)) is an example of
a sophisticated C program interacting with the editor via
the 9P API (documented in acme(4)).
It allows to open shells
(thus REPL, ssh(1) sessions, etc.) within an acme(1)
buffer. Nothing outstanding, but definitely useful.
Moving around
The standard UNIX shortcuts (^A, ^E, ^U, ^W ^H; I almost ever use ^H), plus arrows, all allow for decently smooth “local movements”. For global movements, or increased precision, the mouse/touchpad (and the following tips!) do just fine. The scrollbar has some refined features in this regard too, but I barely ever use it.
Tip: One thing I learnt when trying to use ed(1) for a while (out of curiosity, but it’s proven to be a valuable experience in many ways), is that to move around in a big document, you have “natural anchors” formed by essentially unique text patterns (rare words/sequences of characters/regexp). They constitute a very convenient, cheap way to move around quickly, editor-agnostic.
Tip: I don’t think it’s clearly documented, but the home/end keys will by default send you to the previous edition location. Meaning, I can edit text at some spot, scroll around to look at something, click here or there, and then use home/end to get back at the previous editing location.
Or, you can avoid clicking around, and simply use the arrows to
refocus the windows to the cursor’s position.
Or, write/comment/observe unique pieces of text to anchor yourself.
(You may want to try those)
Here’s the two videos highlighting some differences in blade adjustment, between a modern Western plane and Japanese. Essentially, especially on high-end planes, there are knobs to adjust every setting of a Western plane; mind the simplicity of the traditional Japanese approach.
Sam’s language
sam(1) is an earlier Plan9 editor, also written by Pike, best described as an “ed(1) on steroid”: it
- features an edition interface similar to ed(1)’s, only with more expressive commands (“sam(1)’s language”);
- has an optional GUI;
- etc.
Most of the time, you can get away without using sam(1)’s language. I’d say, 95% of automated edition tasks I have to do can be performed via sed(1), awk(1), or sh(1), which are convenient to call from acme.
There are two main exceptions. One of them is the variable renaming idiom (that I’ve actually wrapped in a shell script, as I’m too lazy to count the dots). The other is when I need to edit arrays of (dozens/hundreds of) hashes of indented JSON-like formats (e.g. Data::Dumper).
The former idiom is covered in those papers:
Here’s an example of the latter use-case:
Example: The goal of the following command is to columnize (via qcol, which understands quoted strings and is able to preserve indentation) a series of lines located between two specific pattern in a file. A key concept in sam(1)’s language is the notion of dot, which can be understood as “the currently selected text”. The novelty in comparison to other Unix tools is that this dot can spawn multiple lines.
Edit ,x/\[\]token{\n/+,/^[ ]+}, nil},/- | qcol -k -n
...
{
"single byte tokens",
scanAll,
[]interface{}{strings.NewReader(" \t\t\r\n(). :<× >"), ""},
[]interface{}{[]token{
token{tokenLParen, 2, 1, "(", 0, 0., nil},
token{tokenRParen, 2, 2, ")", 0, 0., nil},
token{tokenDot, 2, 3, ".", 0, 0., nil},
token{tokenColon, 2, 6, ":", 0, 0., nil},
token{tokenLChevron, 2, 7, "<", 0, 0., nil},
token{tokenProduct, 2, 8, "×", 0, 0., nil},
token{tokenRChevron, 2, 10, ">", 0, 0., nil},
token{tokenEOF, 2, 11, "", 0, 0., nil},
}, nil},
},
{
"multi-bytes words",
scanAll,
[]interface{}{strings.NewReader("hello world"), ""},
[]interface{}{[]token{
token{tokenName, 1, 1, "hello", 0, 0., nil},
token{tokenName, 1, 7, "world", 0, 0., nil},
token{tokenEOF, 1, 12, "", 0, 0., nil},
}, nil},
},
...
...
{
"single byte tokens",
scanAll,
[]interface{}{strings.NewReader(" \t\t\r\n(). :<× >"), ""},
[]interface{}{[]token{
token{tokenLParen, 2, 1, "(", 0, 0., nil},
token{tokenRParen, 2, 2, ")", 0, 0., nil},
token{tokenDot, 2, 3, ".", 0, 0., nil},
token{tokenColon, 2, 6, ":", 0, 0., nil},
token{tokenLChevron, 2, 7, "<", 0, 0., nil},
token{tokenProduct, 2, 8, "×", 0, 0., nil},
token{tokenRChevron, 2, 10, ">", 0, 0., nil},
token{tokenEOF, 2, 11, "", 0, 0., nil},
}, nil},
},
{
"multi-bytes words",
scanAll,
[]interface{}{strings.NewReader("hello world"), ""},
[]interface{}{[]token{
token{tokenName, 1, 1, "hello", 0, 0., nil},
token{tokenName, 1, 7, "world", 0, 0., nil},
token{tokenEOF, 1, 12, "", 0, 0., nil},
}, nil},
},
...
Here’s how it works:
,
is an alias for0,$
, which is a compound address of the forma1,a2
, matching the text locating betweena1
anda2
. In our case,0
indicates the (very) beginning of the file, and$
the (very) end of the file. What this does is set the dot to the whole file;x/pattern/ command
is a looping construct: within the previously selected dot, it will look for the givenpattern
(regexp), set the dot to this pattern, and executecommand
on this selected dot. This will be performed for every match ofpattern
withinx//
’s dot. Then:pattern
:/\[\]token{\n/
is an ad-hoc regular expression (not line restricted either as you can see), marking the beginning of a section we want to indent;command
:- the command we’re executing starts with a compound address of
the form
a1,a2
, which again, sets the dot to the text located betweena1
anda2
:a1
: is a+
in our case, which is a shortcut for.+1
, meaning, we want to select the line right after the (current) dot;a2
: is again a compound address, of the forma3-a4
, wherea3
:/^[ ]+}, nil},/
, is another ad-hoc regular expression marking the end of the section we target;a4
: is-
, a shortcut for-1
, meaning we want to select the line before a3;
- this compound address is followed by a
| qcol -k -n
, which means that we want to send the content of the dot to the given (shell) command, and replace the dot by the output of this shell command.
- the command we’re executing starts with a compound address of
the form
Indentation is a rather basic task; note that we could also have used awk(1), but this would have been more verbose to write. We could have performed more sophisticated edition tasks, such as conditionally adding/removing lines/fields, either via sam(1)’s language or with any random shell command.
Workbenches are a key tool in woodworking. Here’s two videos highlighting some differences between a Japanese floor workbench and a regular Western bench (note the absence of a vise in the Japanese case, and ponder on the inherent difficulty of building a vise from scratch).
Typing unicode characters
A somewhat hidden feature: keyboard(7) describes a way to input “arbitrary” unicode characters (emphasis is mine)
Characters in Plan 9 are runes (see utf(7))). Any rune can be typed using a compose key followed by several other keys. The compose key is also generally near the lower right of the main key area: the Option key on the Mac and the Alt key on Unix systems. To type a single rune with the value specified by a given four-digit hexadecimal number, type the compose key, then a capital X, and then the four hexadecimal digits (decimal digits and a to f). For a longer rune, type X twice followed by five digits, or type X three times followed by six digits. There are shorthands for many characters, comprising the compose key followed by a two- or three-character sequence. The full list is too long to repeat here, but is contained in the file /usr/local/plan9/lib/keyboard […]
Here’s an excerpt from that last file:
08D b( ₍ subscript opening parenthesis
208E b) ₎ subscript closing parenthesis
20AC e$ € euro symbol
2102 CC ℂ double-struck capital c
210A $g ℊ script small g
210B $H ℋ script capital h
210D HH ℍ double-struck capital h
210F -h h- ℏ planck constant over 2 pi
For example, if you want to type ℏ, you have, in that order to:
- press/release ALT (the compose key)
- press/release “-”
- press/release “h”
Or:
- press/release ALT (the compose key)
- press/release “h”
- press/release “-”
Improvements ideas
Here’s a rough set of ideas to “improve” the editor. Bear in mind that this is from the point of a view of someone using acme(1) on Unix; this wouldn’t make as much sense on Plan9.
- 9P is close to useless on an Unix system (plumber(1) aside,
maybe; sometimes, I wonder is 80% of what I’m using/could use the
plumber(1)
for, again on Unix, not on Plan9, isn’t better performed by a boring
sh(1) script with a series of
if/else
): some RPCs over a named pipe/socket would be easier to work with; - The editor could then be fully programmatically controllable;
- The tagline could be fully editable (I’ve found the restriction almost always useless);
- Building on the previous points, we could then have all
commands (
Del
,Snarf
, etc.) be external executables; - Perhaps delegating the window management to, well, a window manager. This would require some more thinking, especially given the tagline/buffer interaction;
In many ways, the result would start to look like a sam(1) with chording patches & other goodies. Which means, another approach could perhaps be to start from sam(1)’s codebase instead of from acme(1)’s.
Comments
By email, at mathieu.bivert chez: