January 27, 2020 at 11:48 PM by Dr. Drang
If you’re a Drafts user, you’ve probably noticed this oddity in how it displays—or, more accurately, doesn’t display—the trailing final line number for drafts with a certain type of content. Here, for example, are two ever-so-slightly different versions of a draft:
Where is the extra character in the version on the right? It’s a trailing linefeed character after the fifth line. If I had timed the screenshots to show the blinking cursor at the end of the file, you’d see that the end of the file on the left is just after the final e, while the end of the file on the right is at the start of the sixth line. But Drafts doesn’t consider that sixth proto-line worthy of numbering.1
Generally speaking, I agree with Greg Pierce’s decision on this. A linefeed character at the end of a file doesn’t really mean there’s another line there. But there are times when I wish the line number was there. For example, when I’m writing a blog post and there are Markdown reference links at the bottom of the draft, I sometimes look at the last number and wonder if there’s a linefeed at the end of it. If there isn’t, that means I’ve screwed something up and the next reference link I add (which is done through this Drafts action) will be tacked onto the end of the last line instead of being on a line of its own.
The script I ended up with was an improvement on the one you’ll see in my forum answer, but it does basically the same thing: increment the number of hashes in the selected lines until it gets to three hashes, at which point it resets to zero. Here’s the script:
In a nutshell, the first stanza extends the selection to encompass all the lines that are at least partially selected and then splits that into an array, with each element consisting of one line of text (with no trailing linefeed). The second stanza operates on each of those lines in turn. The third stanza then glues the lines back together with linefeed characters and replaces the selected lines in the editor with the glued-together string.
The part I really care about—the part I think is reusable—is the first stanza, Lines 1–7. There are many situations in which appending, prepending, or otherwise transforming a series of lines is useful, and this section of code is a nice setup for that kind of operation.
It starts by calling the
getSelectedLineRange function of the global
editor object on Line 2. According to the documentation, this
Get[s] the current selected text range extended to the beginning and end of the lines it encompasses.
The value returned is a two-element array consisting of the starting character position and the length of the text range. The key to
getSelectedLineRange, and what makes it different from
getSelectedRange, is that the starting character position is the beginning of the line containing the start of the selection and the range extends to the end of the line containing the end of the selection.
If the action is called with this selection
start will be 11 (there are 11 characters [remember the linefeed] before the S at the start of the second line) and
len will be 35 (12 from the second line, 11 from the third, and 12 from the fourth). Note that the end of the fourth line is just after the linefeed that separates it from the fifth.
Given that all the other linefeeds in this range are included, it’s consistent to include the one at the end of the last line in the range. As we’ll see, though, there’s a situation for which the last character in the range won’t be a linefeed, and we’ll have to code defensively to cover that situation.
Line 3 uses
getTextInRange to grab the range we just described and put it in the variable
Looking ahead to Line 7, we see that we’re going to use
split("\n") to convert
full into the array
lines. Because we don’t want
lines to include an empty element at the end, we should strip off any final linefeed from
full before running
split. That’s what the
replace in Line 4 deals with. It insures that the last character of
full is removed if it’s a linefeed.
Why can’t we just remove the last character of
full without checking for what it is? When won’t
full have a linefeed at the end? That’s where the case we looked at back at the beginning of this post comes in. If the last line of the draft doesn’t end with a linefeed, and that line is part of the selection, then
full won’t end with a linefeed.
Line 5 adjusts the value of
len to account for the (usually) new length of
full, and Line 6 selects all the text we’re going to be transforming.2 Finally, Line 7 splits
After the operations on
lines is done, we use
join("\n") to put
lines back together into a single string (Line 24) and replace the selection in the editor with that string (Line 25).
If you’re thinking this is one of those posts I write mostly for myself, you’re right. With luck, writing this will help me remember to use this split-operate-rejoin process then next time I need to process a series of lines in Drafts. If it helps you too, that’s a bonus.
In the Editor settings, Drafts calls the little numbers in the left margin paragraph numbers instead of line numbers, which makes sense because of the way word wrapping works. But if you’re writing code or other structured text, it’s better to think of them as line numbers. ↩
This selection isn’t strictly necessary, but it makes the later substitution of the transformed text into the editor easy, and I like to highlight the text being worked on. ↩