Skip to content

ADR 0101: J joins lines

Status

Accepted

Context

J was the other missing vim command named in ADR 0099, alongside the text objects that became ADR 0100. Pressing it did nothing.

Joining looks like the simplest command in vim and is not: the space it inserts has four exceptions, and which of them applies is not guessable from the outside.

Decision

The spacing rules, taken from vim

Derived by running the keys and diffing the written file, not from memory:

lines vim's result
aaa + bbb aaa bbb — one space
ee + fff ee fff — indent stripped, no second space
gg + )hh gg)hh — no space before )
kk + ll kk ll — existing blanks kept as they are
ii + `` (empty) ii — nothing appended, no trailing space
` (empty) +jj` jj

So a single space, unless the first line already ends in blank, either line is empty, or the next begins with ). gJ skips all of it and takes both lines verbatim, indent included.

A count joins that many lines, not that many times: 3J makes one line out of three, and J is 2J. A count that overruns the end stops there rather than failing.

The cursor lands where the lines met

Not on the joined-on text and not at the line start: at the offset the newline used to occupy — the inserted space when there is one, and the next line's first byte when there is not. Checked against vim for J, 3J, the trailing-blank case and the ) case; all four columns agree.

Visual J joins what the selection touches

Every line the selection spans, however it was made — VjJ, VjjJ and vjJ all behave as vim does, which is to say charwise or linewise makes no difference here.

One undo step for the whole join

3J is two joins internally and one u in vim. The loop runs inside an undo session so it undoes as a unit, and . repeats the whole thing.

Consequences

23 comparisons against vim match: the six spacing cases, counts of 1, 3, 4 and 10, gJ with and without indent, the last line, the line above it, all three Visual forms, undo of J and of 3J, J twice, and . repeat.

J on the last line ate the trailing newline in the first version, because m_lineStarts has an entry for the position past it and the loop treated that as a line to join with. vimLastLine() — added for G in ADR 0099 — is the guard. That phantom entry has now produced three separate bugs; anything asking "is there a line below this one" should be asking vimLastLine().

gJ is included because parameterising the space rule was smaller than leaving it out, and it is the pair J is normally learned with. Neither respects 'joinspaces' — vim's two-spaces-after-a-period option — which is off by default and has no setting here to read.