I recently started to write a lot of coffeescript at work, so I bumped into an issue that I’ve been avoiding for some time: manipulating indent-based languages. Theoretically, it should be easier (no “ends” or closing braces, right?), but I have a lot of tools to move code around, wrap it in blocks, or view it nicely, that just don’t work with semantic indentation. So, I had to experiment to come up with some vimscript to make it more comfortable. It’s all a work in progress, but it’s been rather useful for me so far.

Originally, I intended to write a single post, but I started off rather verbosely, so I decided to make it a series instead and explain the code in more detail along the way. To begin with, I’ll describe the process of writing two simple text objects that might be useful in day-to-day coffeescript development.

If you’re unfamiliar with text objects in Vim, you could get a good explanation from the help files with :help text-objects. Another nice introduction is Chapter 15 from “Learn Vimscript the Hard Way” by Steve Losh. It’s rather short, so I recommend you skip through it in any case.

Basic Tools

First, we’ll define two functions that don’t make much sense on their own, but will be useful a bit later. The first one does the following:

  • Takes the indent of the current line as a “base”.
  • Iterates through the buffer, line by line.
  • Returns the last line with a level of indent the same or larger than the base.
function! s:LowerIndentLimit(lineno)
  let base_indent  = indent(a:lineno)
  let current_line = a:lineno
  let next_line    = nextnonblank(current_line + 1)

  while current_line < line('$') && indent(next_line) >= base_indent
    let current_line = next_line
    let next_line    = nextnonblank(current_line + 1)
  endwhile

  return current_line
endfunction

The second one does the same thing, except upwards in the buffer:

function! s:UpperIndentLimit(lineno)
  let base_indent  = indent(a:lineno)
  let current_line = a:lineno
  let prev_line    = prevnonblank(current_line - 1)

  while current_line > 0 && indent(prev_line) >= base_indent
    let current_line = prev_line
    let prev_line    = prevnonblank(current_line - 1)
  endwhile

  return current_line
endfunction

We could have combined these in a single one, but it wouldn’t really make much of a difference code-wise. The repetition between them is, regrettably, difficult to avoid, but given their small size, I wouldn’t worry too much about it.

Indent Text Object

A popular method of working with indent-based languages is an “indent text object”, which basically selects any same-level indented code. You can find one implementation here. It’s originally meant for python and some considerations have been made to make it more convenient there, but it might work nicely for coffee as well. A different one by Kana Natsuno can be found here. That one’s interesting as being built on a generic text object framework, textobj-user

That said, making our own simple indent text object is rather trivial given the above helpers:

onoremap ii :<c-u>call <SID>IndentTextObject()<cr>
onoremap аi :<c-u>call <SID>IndentTextObject()<cr>
xnoremap ii :<c-u>call <SID>IndentTextObject()<cr>
xnoremap ai :<c-u>call <SID>IndentTextObject()<cr>

function! s:IndentTextObject()
  let upper = s:UpperIndentLimit(line('.'))
  let lower = s:LowerIndentLimit(line('.'))

  if lower > upper
    exe upper
    exe 'normal! V'.(lower - upper).'j'
  else
    normal! V
  endif
endfunction

Put together, the IndentLimit helpers fetch the area around the current line with the same level of indentation. Getting Vim to recognize it in the relevant mappings is a matter of marking the area in visual mode. To do that correctly, the first thing we need to check is that the lower limit is below the upper one:

if lower > upper
  exe upper
  exe 'normal! V'.(lower - upper).'j'
  " ...

That exe upper may look a bit weird. Remember that upper is a line number, so let’s say that number is, for example, 42. exe 42 will translate to executing the command 42, which is equivalent to typing :42 in the command-line, which in turn makes Vim jump to line 42 in the current buffer. So, fun fact – any number is a completely valid Vim command.

The second case means that the lower limit is not below the upper one, so we either have a mistake in the code, or lower == upper – the text object is a single line. This could easily happen in a situation like this with the cursor on bar:

if foo?
  bar
else
  baz

In that case, we simply mark the line with normal! V.

So now, we can select a given block of sequential code by hitting Vii, delete it with dii, and so on. You might notice that ii and ai are the same in this implementation. It might make sense to have ai mark one line above as well, or maybe change ii to look for the next level of indentation or something. I never really got used to using an “indent” text object, though, so I can’t say which would be useful. Consider the implementation an exercise for the reader :).

Function Text Object

A very similar (coffee-specific) text object is the “function” one. “Change inner function” is one action I tend to do rather often, for example. The initial implementation is pretty simple as well:

  • Find something that looks like the start of a function upwards in the buffer.
  • Find the body of the function by grabbing the code that’s indented one level deeper than the function start.
  • Mark the area, including or excluding the function start depending on the type of text object (“a” or “i”)

Or, translated into code:

onoremap if :<c-u>call <SID>FunctionTextObject('i')<cr>
onoremap аf :<c-u>call <SID>FunctionTextObject('a')<cr>
xnoremap if :<c-u>call <SID>FunctionTextObject('i')<cr>
xnoremap af :<c-u>call <SID>FunctionTextObject('a')<cr>

function! s:FunctionTextObject(type)
  let function_start = search('\((.\{-})\)\=\s*[-=]>$', 'Wbnc')
  if function_start <= 0
    return
  endif

  let body_start   = function_start + 1
  let indent_limit = s:LowerIndentLimit(body_start)

  if a:type == 'i'
    let start = body_start
  else
    let start = function_start
  endif

  if indent_limit > start
    exe start
    exe 'normal! V'.(indent_limit - start).'j'
  else
    exe 'normal! V'
  endif
endfunction

Notice that, this time, the s:FunctionTextObject function is invoked with a parameter that depends on the specific mapping. An “inner” text object (if) would operate on the body of the function, while an “around” one (af) would manipulate the entire function definition. So, we could change the body of a function with cif, and we could move a function around with daf and pasting.

There’s a fair amount of Vim magic here that may not be immediately apparent, so let’s break it down a little.

let function_start = search('\((.\{-})\)\=\s*[-=]>$', 'Wbnc')
if function_start < 0
  return
endif

The first thing we need to do is find the start of the function. The search call will do that and return the relevant line, or 0 if nothing is found. In the latter case, we just return early, since there’s no function to find.

The invocation of search is a pretty terse bundle of logic. The first argument is the pattern we’re looking for, and the second consists of control flags for the behaviour of search. All available flags are listed here, but if you don’t feel like reading through that:

  • b stands for “backwards”, which makes sense for a text object that marks the function we’re currently in.
  • W indicates we don’t want to wrap around the ends of the buffer. No point in finding the function at the other end of the file, after all.
  • n tells the call not to move the cursor. While it may be useful to put the cursor at the spot we’re working on, it won’t be necessary in this case.
  • c accepts a match for the regex at the cursor position.

As for the regex:

  • \(...\) is the grouping operator, and a \= at the end makes the group optional.
  • (.\{-}) would be the parameter list. The \{-} modifier is equivalent to Perl’s .*? – a nongreedy match for 0 or many of anything.
  • \s* is something you should already be familiar with, matches any whitespace.
  • [-=]>$ is either a -> or => at the end of the line.

Putting it all together, it should start with an optional () group with anything in it, then maybe some whitespace, and an -> or => arrow at the end of the line, which mostly describes a coffeescript function. If you’d like to learn a bit more about Vim regexes, you could jump to one of my older blog posts.

Moving on along:

let body_start = function_start + 1
if body_start > line('$') || indent(body_start) <= indent(function_start)
  if a:type == 'a'
    normal! V
  endif

  return
endif

let indent_limit = s:LowerIndentLimit(body_start)

The start of the actual function body would be the line after the function definition. Of course, we need to check if one is present at all. If body_start is greater than the last line number, then there’s definitely no body. If its indentation isn’t larger than the one of the function definition, it’s the same case. In this situation, we need to bail early, so we return, but if the type of the text object is an “around” one, we should mark the function definition at least.

If there is a body, its last line will be found by the s:LowerIndentLimit helper we defined earlier.

if a:type == 'i'
  let start = body_start
else
  let start = function_start
endif

Nothing complicated here: If we type the object with if (“inner function”), we start from the contents, otherwise, with an af, we start from the function definition.

And as for the last part, it’s exactly the same as in the indent text object:

if indent_limit > start
  exe start
  exe 'normal! V'.(indent_limit - start).'j'
else
  exe 'normal! V'
endif

This simply marks the content in visual mode, careful to do the right thing if there’s only one line selected.

Some minor tweaks

An obvious helper function to extract would be the one piece of code that’s exactly the same in both text objects:

function s:MarkVisual(start_line, end_line)
  if a:end_line > a:start_line
    exe a:start_line
    exe 'normal! V'.(a:end_line - a:start_line).'j'
  else
    normal! V
  endif
endfunction

Not only does it help in this case, it’s also a fairly often-used helper function, so you might want to make it global (or autoloaded) and use it in other situations as well. For our purposes here, though, let’s rewrite it a tiny bit into this:

function! s:MarkVisual(command, start_line, end_line)
  if a:start_line != line('.')
    exe a:start_line
  endif

  if a:end_line > a:start_line
    exe 'normal! '.a:command.(a:end_line - a:start_line).'jg_'
  else
    exe 'normal! '.a:command.'g_'
  endif
endfunction

We’ve added one more argument, command, that could be either ‘V’ or ‘v’ and determines the type of visual mode to enter. To have the function work well with characterwise visual mode, there’s two more changes:

  • We don’t jump to the given start line if it’s the same as the current one.
  • We execute an additional g_ after each command in order to mark the complete line.

These tweaks don’t change the previous behaviour (in a way we care for), so we can safely replace it in the indent text object, leaving it a lean three lines:

function! s:IndentTextObject()
  let upper = s:UpperIndentLimit(line('.'))
  let lower = s:LowerIndentLimit(line('.'))
  call s:MarkVisual('V', upper, lower)
endfunction

As for the function text object, we can improve it a bit by using characterwise visual mode:

function! s:FunctionTextObject(type)
  let function_start = search('\((.\{-})\)\=\s*[-=]>$', 'Wbc')
  if function_start < 0
    return
  endif

  let body_start = function_start + 1
  if body_start > line('$') || indent(nextnonblank(body_start)) <= indent(function_start)
    if a:type == 'a'
      normal! vg_
    endif

    return
  endif

  let indent_limit = s:LowerIndentLimit(body_start)

  if a:type == 'i'
    let start = body_start
  else
    let start = function_start
  endif

  call s:MarkVisual('v', start, indent_limit)
endfunction

We’ve replaced a normal! V with normal! vg_ and we’re using s:MarkVisual with a v argument. Also note that the n in the search function’s modifier list is missing. This means that the search will move the cursor to the beginning of the found text. So now, we’d get much better results on code like this:

db.query "show tables", (err, result) ->
  console.log err, result

Instead of caf removing the entire first line, it’ll stop just after the first comma, allowing you to work only on the function even in this case.

Summary

  • Fetching “blocks of code” is fairly simple with indent-based languages. The IndentLimit helpers will be useful in future blog posts as well.
  • Writing text objects is tricky and usually requires a certain amount of experimenting, but it could be worth it. It definitely makes sense to use a ready-made plugin for text objects, but it could be a good idea to work on creating your own to match your preferences and specific use cases.
  • The search function is a very often-used one, along with its cousin, searchpairpos. Learning its control flags (and Vim regexes) can help a lot for building various tools.

The entire code is available as a gist. You can also use the function text object from my coffee_tools almost-plugin.