Prefix and suffix lines in Drafts

A week or two ago, this question from TDK_SA90 (my favorite cassette for taping albums) popped up on the Drafts forum. What TDK wanted was a Drafts action that would

append or prepend inserted text, and also, have it act on only selected lines in the draft - all lines if no selection made.

I already had an action that would add line numbers to selected text (or the entire draft if nothing was selected), and I had worked through the logic of the Drafts getSelectedLineRange function, so this seemed like both a simple extension of what I’d already done and an action I’d use myself.

I wanted the action to work like BBEdit’s Prefix/Suffix… command in the Text menu, which allows you to add or remove text from either the beginning or end of every selected line.

BBEdit Prefix-Suffix

The action’s user interface is a little different, but I think it’s easy to understand.

Drafts Prefix-Suffix

The action, which you can download, is just one JavaScript step:

javascript:
 1:  // Add prefix or suffix to the selected lines (or all lines).
 2:  
 3:  // Define the text that's going to be edited.
 4:  var [oldStart, oldLen] = editor.getSelectedRange();
 5:  if (oldLen == 0) { // use full draft
 6:    var [start, len] = [0, editor.getText().length];
 7:  }
 8:  else {
 9:    var [start, len] = editor.getSelectedLineRange();
10:  }
11:  var lineText = editor.getTextInRange(start, len);
12:  if (lineText.endsWith("\n")) { // trailing LF doesn't count
13:    lineText = lineText.slice(0, -1);
14:    len -= 1;
15:  }
16:  
17:  // Prepare the prompt and get the text from the user.
18:  var p = Prompt.create();
19:  p.title = "Prefix/Suffix Lines";
20:  p.addTextField("prefix", "Prefix:", "", {placeholder: "Prefix text", wantsFocus: true});
21:  p.addTextField("suffix", "Suffix:", "", {placeholder: "Suffix text", wantsFocus: true});
22:  p.addButton("Add");
23:  p.addButton("Remove");
24:  
25:  var didSelect = p.show();
26:  
27:  if (didSelect) { // make the changes
28:    // Get the affixes
29:    let prefix = p.fieldValues["prefix"];
30:    let suffix = p.fieldValues["suffix"];
31:    
32:    // Edit the selected lines.
33:    let lines = lineText.split('\n');
34:    
35:    if (p.buttonPressed == "Add") {
36:      lines.forEach((line, i) => lines[i] = prefix + line + suffix);
37:    }
38:    else { // remove
39:      if (prefix != "") {
40:        let preLen = prefix.length;
41:        lines.forEach(function(line, i) {
42:          if (line.slice(0, preLen) == prefix) {
43:            lines[i] = line.slice(preLen);
44:          }
45:        });
46:      }
47:      if (suffix != "") {
48:        let sufLen = suffix.length;
49:        lines.forEach(function(line, i) {
50:          if (line.slice(-sufLen) == suffix) {
51:            lines[i] = line.slice(0, -sufLen);
52:          }
53:        });
54:      }
55:    }
56:    
57:    // Replace the text in the editor and select it.
58:    let replacement = lines.join('\n')
59:    editor.setTextInRange(start, len, replacement);
60:    editor.setSelectedRange(start, replacement.length);
61:    editor.activate();
62:  }
63:  else { // put selection back
64:    editor.setSelectedRange(oldStart, oldLen);
65:    editor.activate();
66:  }

Lines 3–15 work out which lines are going to prefixed or suffixed. If any text is selected, those lines—extending out to the beginning of the first line and the end of the last line—will be edited. If no text is selected, then the entire draft will be edited. There’s a bit of trickiness when the selection ends with a linefeed character; that detail is covered in my post on lines and Drafts.

Lines 17–23 define the prompt you see above. The commands for the Prompt class are defined in the Drafts documentation.

Line 25 displays the prompt and, if the user didn’t cancel, sets didSelect to true and fills the various fields of the p variable that was defined in Line 18.

If the user canceled, Lines 64–65 are executed. These ensure that focus goes back to the draft (it was lost when the prompt appeared) and that whatever selection was active before the prompt is restored.

Lines 28—61 are the meat of the script. Line 33 splits the selected lines into an array of strings, one for each line. If the user tapped the Add button, Line 36 loops through the lines array and adds text to the beginning and end of each line. If the user tapped the Remove button, things are a more complicated.

If the user entered any prefix text, Lines 39–46 look for that text at the front of each selected line and delete it if it’s there. If the prefix text isn’t there, the line is left alone. If the user didn’t enter any prefix text, this entire set of lines is skipped.

Lines 47–54 do basically the same thing for suffix text.

Finally, Lines 58–61, replace the text in the draft with the lines that have just been editing, adjusts the selection, and restores focus to the draft.

If you’re wondering why I didn’t just use JavaScript’s replace command for some of this, it’s because prefixes and suffixes are anchored to the beginnings and ends of strings, and the only way I know to do that with replace is to use regular expressions with ^ and $. That would mean the text the user enters at the prompt would be interpreted as a regular expression, which can be annoying if the prefix or suffix text contains characters that have special meaning in a regular expression.

As you can see in the forum thread, Greg Pierce helped me clean up some of my JavaScript. That I have redirtied it since his help is no fault of his.