2

I tried creating the following macro:

  1. search an empty line above my location
  2. go down one line
  3. shift-v
  4. find an empty line down
  5. go up one line
  6. :sort

This should sort the entire section I'm pointing at...

Here is my attempt:

let @q = '\/^$ N j <S-V> \/^$ n k :sort'

However, when trying to run the macro (using @q) I get one line down, and the following error: E35: No previous regular expression

Removing the '\' from the macro gave me this error:

E486: Pattern not found: ^$ N j <S-V>

And now I'm stuck...

p.s. If you have an idea how to make this macro robust so that if I don't have a line above or down (because I'm sorting till end / start of doc) I'll still be able to sort, that'd be cool, thx!

  • 1
    Dang I was going to self-answer a Q about creating a sort operator for vim :) good Q. Expect my answer soon. – D. Ben Knoble Oct 7 at 12:57
8

As simple as that:

nnoremap <Leader>s Vip:sort<CR>

ip is an "inner paragraph" object, i.e. a piece of text between two empty lines.

  • And how do I call this? – CIsForCookies Oct 7 at 12:05
  • 1
    @CIsForCookies By default <Leader> is equal to "backslash", so press "backslash" and then "s" in normal mode. Also, you can define it the way you like. – Matt Oct 7 at 12:08
9

You've already (rightly) accepted an answer that provides a much simpler method of achieving your goal, but I thought I'd address a few of the other issues raised by your question.

The problems with your :let command

To debug your macro, a first step might be to try recording a working macro, and then comparing this with the macro created by your let command. I've run your command, and recorded a series of steps into the "r register, so lets take a look at the differences by running the :registers command:

"q   \/^$ N j <S-V> \/^$ n k :sort
"r   /^$^MNjVnk:sort^M
  1. Okay, so first off, the "q register begins with a backslash, and has another one midway though. You were correct in your guess that these are not required, but as you discovered, there are further problems with your code.

  2. The "q register contains several spaces. Some of these should in fact be <CR> characters (displayed in the output with the notation ^M meaning <Ctrl-M>), and others are simply not required at all.

  3. The "q register contains the string <S-V>, whereas it should simply contain an upper-case V.

  4. The "q register contains both a second instance of the search command and an instance of the jump to next match motion, n. Only one of these is required.

What went wrong

I think these all stem from a few misunderstandings about a). how macros are stored and/or b). how control characters are included in strings.

A macro contains a series of keystrokes, and nothing else. You don't separate operations with space characters: if a space character is included that equates to a press of the Space on your keyboard.

Similarly, if you want to run a command-line command, you need to include a <CR> character to emulate pressing Return.

The <S-V> notation is not required to insert upper-case characters into a string: V is already an upper-case character. Furthermore, in Vimscript, single quotes ' denote a "literal" string, which contains exactly the characters included (with the sole exception that two quotes stand for one quote). Double-quotes " on the other hand, allow you to include control characters by using the <angle-bracket> notation and prepending the opening bracket with a backslash.

Thus, this sets the variable v to contain the the string "<S-V>":

:let v = '<S-V>'

Whereas this is a long-winded way of setting the variable v to contain the string "V":

:let v = "\<S-V>"

How to fix it

So to create the correct :let command you have two options:

  1. Enter the following into your vimrc: let @q = '' and then, having recorded the working macro into a register "q, paste the (current) contents of that register in between the single quotes by pressing e.g. "qP. The downside of this is that, while it works, it's a bit icky to include literal control characters in your vimrc, so perhaps a better solution is to:

  2. Rewrite the :let command to use a double quoted string:

    let @q = "/^$\<CR>NjVnk:sort\<CR>"
    

How to improve it

Firstly, there is a better way of searching backwards than first searching forwards and then going back with N, which is to use ? instead of /.

Also, there is a better way of moving to the line after or before a blank line than running the command j or k, which is to use a "search offset". At the end of your search command, add +1 to place the cursor on the line after the match, or -1 to place the cursor on the line before the match. Because we are searching backwards, we need to first end the regular expression with ?. (When searching forwards, you'd use /.)

You can repeat a search by omitting the regular expression entirely, but you can still change the search offsets or other flags.

Thus:

let @q = "?^$?+1\<CR>V//-1\<CR>:sort\<CR>"

Finally, you specifically asked about how to handle the start and end of file. You can do so by updating your regular expression.

When searching upwards, instead of searching for a blank lines we'll search for either:

  • The start of the file, or,
  • The first character on the line following a blank line.

We can no longer use an line-offset, but instead we need to leave the cursor on the end of the match by ending our search with the e offset to leave the cursor on the final character of the match.

And when searching downwards, we'll search for either:

  • The end of the file, or,
  • The last character on the line preceding a blank line.

To search for the start of the file you can use \%^, and to search for the end of the file you can use \%$.

To search for A or B you can use the notation A\|B in your regular expression.

So:

let @q = "?\\%^\\|^$\\n.?e\<CR>V/\\%$\\|.\\n^$/\<CR>:sort\<CR>"
  • This is an amazing answer, but the quick-fix by @Matt is what I actually would have asked for, had I known... Anyway, thx for the explanation, it's very helpful! – CIsForCookies Oct 10 at 6:33
  • @CIsForCookies Yep, as far as I'm concerned, Matt's answer is the correct one for your issue, but I thought explaining some of the surrounding context of the problems you were having would be beneficial for future readers and just for general informational purposes. – Rich Oct 10 at 8:20
3

Just to show another way to do it, I've been using this sort operator for about 2.5 weeks and loving it.

Once you set up the code, you can use gs (or another mapping of your choice) as follows:

  • in normal mode as an operator like d, y, gu, or others to sort a range of lines
  • in visual mode, to sort the highlighted lines

Note that, because we are sorting, the operator always forces the motions to be linewise.

So you could then do gsip, similarly to the accepted answer.

You'll need the following code:

" ~/.vim/autoload/sort.vim
function! sort#operator(type, ...) abort

  let l:visual = a:0

  if l:visual
    '<,'>sort
  else
    '[,']sort
  endif

endfunction

and

" ~/.vim/plugin/sort_op.vim

if exists('g:loaded_sort_op')
  finish
endif
let g:loaded_sort_op = 1

nnoremap <silent> gs :set operatorfunc=sort#operator<CR>g@
vnoremap <silent> gs :<C-u>call sort#operator(visualmode(), 1)<CR>

(It doesn't make sense to add the single line variant gss since we are sorting.)

  • Good stuff, although simple vnoremap <silent>gs :sort<CR> should be sufficient for the visual mode in this case. – Matt Oct 7 at 15:08
  • @Matt lol you're probably right—I had been using this "pattern" for another operator I built that same day, and didn't consider it. The advantage of my way is that the implementation can change without changing the interface. (For example, if I needed to prompt for :sort options or some such) – D. Ben Knoble Oct 7 at 15:10

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.