Tuesday, March 27, 2018

Vim (for non-programmers) Part Three: Refactoring my .vimrc File, Chapter Five; Correct Easy Markup of Markdown Headlines (Building on Chris Toomey's "Your First Vim Plugin")

At some point in the quest to have The World's Greatest .vimrc File, a person will naturally begin to explore the wider surround, see what the community is up to on the weird-ass-solutions-to-incredibly-narrow-problems front. Since, sometimes, work, life, and other responsibilities exert their gravitationals* upon a person, one helpful way to perform this exploration is to throw on a YouTube video in the background while waiting for the requiring hordes to get bored waiting for me to do the needful and go bother somebody else. I.e., it can be kind of fun to half-listen to somebody, and sometimes you get interesting hints while you're ostensibly, or even actually, working on something else...

*Read: suck.

A great place to start is with Chris Toomey's "Your First Vim Plugin" talk.

Toomey comes across like a delightful fellow, smart and approachable, and his basic approach to creating a plugin seems exactly right to me:

  • Find a problem (something that's difficult, or time-consuming, or repetitive to do)
  • Figure out a working solution (that is easier, or faster, or that eliminates repetition)
  • Drop it in your .vimrc
  • Tinker with it as you find that it needs improvement
  • Eventually abstract it and pull it out of your .vimrc, if you feel like it or if you feel like sharing it

Since I tend to take a lot of my notes in Markdown**, his Markdown underlining approach really made sense to me: a quick and dirty mapping to make something an H1 headline or H2 headline fits my workflow nicely—particularly after abstracting it a tiny bit so that one mapping, <leader>h1, adds the appropriate headlining markup whether I'm writing HTML or Markdown (which Vim knows because it's smart about filetypes!).

**What's Markdown? For most people, it's a way to write very lightly formatted text that can be submitted to another program, and out is spit nicely formatted HTML or something that looks pretty pasted into a Word doc or something. For me, it's a way to tell Vim "Please treat a raw .txt file like it's something else, including pretty syntax highlighting that makes it easier to see what's what." It looks like so:

Here's Chris Toomey's approach:

:nnoremap <leader>h1 :normal! "yypVr=<esc>"
:nnoremap
– in normal mode, ignoring all other mappings
<leader>h1 – when I type the leader key, then h then 1, act like I typed the next line (without quotes)
:normal! "yypVr=<esc>"
yypVr=yy means "yank (copy) the entire line";
p means "put (paste), in this case, the entire line"
V means "visually select (highlight) the entire line"
r= uses the r command followed by another character to mean "replace the character under the cursor with the character typed after r"; in this case, since the entire line has been visually selected, in effect, every character in the line is under the cursor, so every character in the line gets replaced with an equals sign character, which is the Markdown character that makes a headline

It works, it's lightning-fast, and it's completely transparent! I dumped it into my .vimrc immediately, and changed two characters so it would also work for h2 tags (by adding a line of hyphens, not equals signs):

:nnoremap <leader>h2 :normal! "yypVr-<esc>"

This overall approach, in which you come up with a solution that can then be extended, is super congenial to me, and I'm beyond stoked that Toomey put it in front of me. After a while, though, the specific nature of the solution started to bug me a little bit.

The problem was that, at an abstract level, I didn't want to:

Copy a line
Paste a copy of the line beneath the first line
Replace all the characters in the second line with a Markdown markup character

What I wanted to do was:

Given a certain line
Create a line underneath it
With as many markdown markup characters as the given line had characters (including tabs or whatever) so it looks pretty

So I set about trying to make something that would do those things.

Oddly, I succeeded. Here's how it went down.

Vim's built-in programming language, VimScript, has a big library of built-in functions, some of which interact with text. You can check them out at :help functions and :help function-list. After poking around in them and fiddling around getting frustrated for a while, I was able to write the following one-liner, which did exactly what I wanted:

:call append(".", repeat("=", strdisplaywidth(getline("."))))

Let's read that from the inside out:

getline(".") is a function that fetches a line in the current buffer (read buffer for our purposes as "the file we're working on"); what goes in the parentheses is the line number, and "." is the wildcard that means "the line the cursor is on"
strdisplaywidth() is a function that looks at a string and tells you how wide it is, in characters displayed on the screen – including tabs and what have you. The trick with this function is that it can take as its argument another function; put the two functions together as we have, and it means "get the line under the cursor, then tell me how long it is"
repeat("=", 68) is trickier: the way the help describes it as a generic function is repeat({expression}, {count}), which means "do the {expression}, which is the thing inside quotes and before the comma, a number of times specified in {count}". Here I've told it that the expression is the equals sign, which is the character we use in Markdown to underline something that's a headline, and I've yet again fed it, instead of a number, the result of a function, our earlier "get the line under the cursor, then tell me how long it is"
append(".", {text}) is a bit of an easier one by now. append()'s arguments say "after a specified line, insert {text}". Here, we re-use the "." wildcard to specify that the line we want to start with is the line under the cursor, and the text we want to be inserted is the result of the repeat("=", strdisplaywidth(getline(".))) series of functions. Simple!

I can do this any time just by putting the cursor on the line I want and typing:
:call append(".", repeat("=", strdisplaywidth(getline("."))))

That's a lot of relatively persnickety typing to have to do, though. The approach I took was what I take to be a relatively common one, but one that I may very well be misunderstanding! What I did was:

  1. Wrapped the 'sucker in a (script-specific) function of my own
  2. Defined a special command to call the function
  3. Made a mapping that will call that command when I'm writing Markdown, but will call a different command when I'm writing something else

What I may be misunderstanding is why exactly I needed my own function. It seems to simplify using the same mapping to call a variety of different commands, but it may be an unneeded step. Anyway, as it lives in my .vimrc file, it looks like the following:

function! s:MarkdownHeadline1()
:call append(".", repeat("=", strdisplaywidth(getline("."))))
endfunction
command! MarkdownHeadline1 call s:MarkdownHeadline1()

function! s:MarkdownHeadline2()
:call append(".", repeat("-", strdisplaywidth(getline("."))))
endfunction
command! MarkdownHeadline2 call s:MarkdownHeadline2()


function! s:MakeHeadlines()
if &filetype == 'markdown'
echo "Markdown!"
" :nnoremap h1 yypVr=
:nnoremap h1 :MarkdownHeadline1
" :nnoremap h2 yypVr-
:nnoremap h2 :MarkdownHeadline2
:nnoremap li I-
" other stuff elided for clarity
endif
endfunction
command! MakeHeadlines call s:MakeHeadlines()

The MakeHeadlines() function is growing, slowly, and as I get it to do more stuff that I want it to do, it's slowly outgrowing its little name. By the time I'll abstract it into something more broadly useful and extract it into a real plugin, not just a chunk of code sitting in my .vimrc file, I will rename it "Corduroy."

Previous entries in Vim (for non-programmers):

Image yoinked from: http://zedisred.blogspot.com/2011/05/let-me-win-your-heart-and-mind-or-ill.html.

No comments: