I am frequently editing markdown files, and when dealing with multi-line items in hierarchical lists, it would be great to have a rapid way to indent or un-indent a list item. For example, how can I most easily indent the longest multi-line list item in the following text?

- Some list item
- Another list item that is long and so
  is split onto multiple lines
  that are more
  than I want to have to count
  or select manually
  - A sub-list item that is also
    split onto multiple lines

If the list items were empty-line-separated then of course I could use >ap but as it is I am using 5>> which involves counting, and of course I have to be at the first line of the list item to use it.

Is there a better way?

(Note: I am mostly allergic to plugins.)


Maybe you could try defining a custom object li for list item. This way you could operate on it with any operator including >.

A way to do that is to define a visual mapping with :xnoremap which selects your object.
Then you would define your list item object with :onoremap li :normal vli<CR>.
The :onoremap mapping would tell Vim that whenever you hit li after an operator, it would be the same as if you were in visual mode and had selected your object with the motion li.

For example, let's say you want to create the object i_ for inside underscores to operate on bar in foo_bar_baz. You could include this in your vimrc:

xnoremap i_ T_ot_
onoremap i_ :normal vi_<CR>

Here the {rhs} of the :xnoremap mapping is T_ot_, which is a motion which should select the text between 2 consecutive underscores on the same line.

T_    move the left-hand side of the visual selection until the previous underscore
o     give the focus to the other side of the selection
t_    move the right-hand side of the selection until the next underscore

Now to delete, change, yank or select the text between the two surrounding underscores, you could type di_, ci_, yi_, vi_.

In your case, it's more complex because you don't want to operate on the text between 2 consecutive underscores but between 2 consecutive lines beginning with a dash.

So the tricky part would be the {rhs} of the visual mapping which selects your object.

Here's a possible implementation, which is not very robust but seems to work on the simple example you provided:

xnoremap <silent> li :<C-U>call <SID>ListItem()<CR>
onoremap <silent> li :normal vli<CR>

function! s:ListItem() abort
    if search('^\s*-', 'bcW') != 0
        call search('\v.\n\s*(-|$)', 'sW')
        normal! v``

It's the same thing as before, except this time the motion is managed by a function: ListItem().

The function looks backward for the pattern ^\s*-:

^     beginning of line
\s*   zero or more whitespace
-     a dash

If it finds something it then looks for the pattern \v^\s*(-|$):

\v       very magic mode
.        any character except a newline
\n       a newline
\s*      zero or more whitespace
(-|$)    a dash or an end of line

The search() function can be used with several flags, here there are:

b    look backwards instead of forward
c    accept a match at current cursor position
s    set the mark ' before jumping to the match
W    don't wrap around the end of the file

The last command in the function normal! v`` is responsible for selecting the object:

v      go into visual mode
``     move the left-hand side of the selection to the location before
       the last search (to the beginning of the item)

I don't think it's very robust, for example if your cursor is after the last item, hitting vli shouldn't select anything. But here, it will select the last item anyway. To solve this, the regexes passed to the search() function should be tweaked. For example, the first one should not match a ^\s*- if there's an empty line between it and the current cursor position.

Still, it seems to work on your example by hitting >li while the cursor is in any of the 3 items, without the need to count the number of lines or to be at the beginning of the item. So maybe it can be improved.

| improve this answer | |

How about

nnoremap <Leader>f ?^\s*\-\\|\%^<CR>V/\n^\s*\-\\|\%$<CR>

which would allow you to use <Leader>f to select the item your cursor is in in visual mode, so you can use > indent further? Of course you could easily integrate the indent into the keypress, but you'd need another remap to decrease the indentation level.

I assume you know the basics, here's what the regexps do:

  • ?^\s*\-\\|\%^ searches backwards for the next line that starts with whitespaces and a dash, or the start of the file (\\| for the logical OR, and \%^ for the beginning of the file)
  • /\n^\s*\-\\|\%$ searches for a newline (\n) followed the a line as above, and if that isn't found, go to the end of the file (\\| for the logical OR, and \%$ for the end of the file)

It has a rough edge, namely if you're on a dash itself, that follows only whitespaces, which works if the dash is indented itself, but if the dash is on column 1 it selects the previous entry.

Still though, imho it works good enough if you don't like a plugin, and since it only selects, you can easily spot if something's wrong, and you're just 2 keypresses away from the right cursor position to select the entry you wanted.

| improve this answer | |
  • Could you handle the rough edge by adding a + before the ? (Funny, I was thinking of ex mode, but I think this would work as well for normal mode. – Wildcard Mar 18 '16 at 1:43
  • What is <Leader> exactly? (I can post a new question if it's hard to explain.) – Wildcard Mar 18 '16 at 1:43
  • See :h Leader. It's a key (actually, a string, but everyone just uses one character) to be used for mappings just as above. If you did not set it yourself, it defaults to "\", so <Leader>f is the same as \f. The distinct advantage of Leader over explicitly using a key is that everyone can just set the leader to what they find comfortable. – PhilippFrank Mar 18 '16 at 8:17

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.