Now that we know how section movement works, let's remap the commands to work in a way that makes sense for Potion files.
First we need to decide what "section" should mean for a Potion file. There are two pairs of section movement commands, so we can come up with two "schemes" and our users can use the one they prefer.
Let's use the following two schemes to define where Potion sections start:
Using a slightly-expanded version of our sample factorial.pn
file, here's
what these rules will consider to be section headers:
# factorial.pn 1
# Print some factorials, just for fun.
factorial = (n): 1 2
total = 1
n to 1 (i):
total *= i.
total.
print_line = (): 1 2
"-=-=-=-=-=-=-=-\n" print.
print_factorial = (i): 1 2
i string print
'! is: ' print
factorial (i) string print
"\n" print.
"Here are some factorials:\n\n" print 1
print_line () 1
10 times (i):
print_factorial (i).
print_line ()
Our first definition tends to be more liberal. It defines a section to be roughly a "top-level chunk of text".
The second definition is more restrictive. It defines a section to be (effectively) a function definition.
Create a ftplugin/potion/sections.vim
file in your plugin's repo. This
is where we'll put the code for section movement. Remember that this code will
be run whenever a buffer's filetype
is set to potion
.
We're going to remap all four section movement commands, so go ahead and create a "skeleton" file:
noremap <script> <buffer> <silent> [[ <nop>
noremap <script> <buffer> <silent> ]] <nop>
noremap <script> <buffer> <silent> [] <nop>
noremap <script> <buffer> <silent> ][ <nop>
Notice that we use noremap
commands instead of nnoremap
, because we want
these to work in operator-pending mode too. That way you'll be able to do
things like d]]
to "delete from here to the next section".
We make the mappings buffer-local so they'll only apply to Potion files and won't take over globally.
We also make them silent, because the user won't care about the details of how we move between sections.
The code for performing the section movements is going to be very similar for all of the various commands, so let's abstract it into a function that our mappings will call.
You'll see this strategy in a lot of Vim plugins that create a number of similar mappings. It's easier to read and maintain than stuffing all the functionality in to a bunch of mapping lines.
Change the sections.vim
file to contain this:
function! s:NextSection(type, backwards)
endfunction
noremap <script> <buffer> <silent> ]]
\ :call <SID>NextSection(1, 0)<cr>
noremap <script> <buffer> <silent> [[
\ :call <SID>NextSection(1, 1)<cr>
noremap <script> <buffer> <silent> ][
\ :call <SID>NextSection(2, 0)<cr>
noremap <script> <buffer> <silent> []
\ :call <SID>NextSection(2, 1)<cr>
I used Vimscript's long line continuation feature here because the lines were
getting a bit long for my taste. Notice how the backslash to escape long lines
comes at the beginning of the second line. Read :help line-continuation
for
more information.
Notice that we're using <SID>
and a script-local function to avoid polluting
the global namespace with our helper function.
Each mapping simply calls NextSection
with the appropriate arguments to
perform the movement. Now we can start implementing NextSection
.
Let's think about what our function needs to do. We want to move the cursor to
the next "section", and an easy way to move the cursor somewhere is with the /
and ?
commands.
Edit NextSection
to look like this:
function! s:NextSection(type, backwards)
if a:backwards
let dir = '?'
else
let dir = '/'
endif
execute 'silent normal! ' . dir . 'foo' . "\r"
endfunction
Now the function uses the execute normal!
pattern we've seen before to perform
either /foo
or ?foo
, depending on the value given for backwards
. This is
a good start.
Moving on, we're obviously going to need to search for something other than
foo
, and that pattern is going to depend on whether we want to use the first
or second definition of section headings.
Change NextSection
to look like this:
function! s:NextSection(type, backwards)
if a:type == 1
let pattern = 'one'
elseif a:type == 2
let pattern = 'two'
endif
if a:backwards
let dir = '?'
else
let dir = '/'
endif
execute 'silent normal! ' . dir . pattern . "\r"
endfunction
Now we just need to fill in the patterns, so let's go ahead and do that.
Replace the first let pattern = '...'
line with the following:
let pattern = '\v(\n\n^\S|%^)'
To understand how the regular expression works, remember the definition of "section" that we're implementing:
Any line following a blank line that contains a non-whitespace as the first character, or the first line in the file.
The \v
at the beginning simply forces "very magic" mode like we've seen
several times before.
The remainder of the regex is a group with two options. The first, \n\n^\S
,
searches for "a newline, followed by a newline, followed by a non-whitespace
character". This finds the first set of lines in our definition.
The other option is %^
, which is a special Vim regex atom that means
"beginning of file".
Now we're at a point where we can try out the first two mappings. Save
ftplugin/potion/sections.vim
and run :set filetype=potion
in your sample
Potion buffer. The [[
and ]]
commands should work, but somewhat oddly.
You'll notice that when you move between sections your cursor gets placed on the blank line above the one we actually want to move to. Think about why this happens before reading on.
The answer is that we searched using /
(or ?
) and by default Vim places your
cursor at the beginning of matches. For example, when you run /foo
your
cursor will be placed on the f
in foo
.
To tell Vim to put the cursor at the end of the match instead of the beginning, we can use a search flag. Try searching in your Potion file like so:
/factorial/e
Vim will find the word factorial
and move you to it. Press n
a few times to
move through the matches. The e
flag tells Vim to put the cursor at the end
of matches instead of the beginning. Try it in the other direction too:
?factorial?e
Let's modify our function to use a search flag to put our cursor on the other end of the matches for this section:
function! s:NextSection(type, backwards)
if a:type == 1
let pattern = '\v(\n\n^\S|%^)'
let flags = 'e'
elseif a:type == 2
let pattern = 'two'
let flags = ''
endif
if a:backwards
let dir = '?'
else
let dir = '/'
endif
execute 'silent normal! ' . dir . pattern . dir . flags . "\r"
endfunction
We've changed two things here. First, we set a flags
variable depending on
the type of section movement. For now we only worry about the first type, which
is going to need a flag of e
.
Second, we've concatenated dir
and flags
to the search string. This will
add ?e
or /e
depending on which direction we're searching.
Save the file, switch back to your sample Potion file and run :set ft=potion
to make the changes take effect. Now try [[
and ]]
to see them working
properly!
It's time to tackle our second definition of "section", and luckily this one is much more straightforward than the first. Recall the definition we need to implement:
Any line that contains a non-whitespace as the first character, an equal sign somewhere inside the line, and ends with a colon.
We can use a fairly simple regex to find these lines. Change the second let
pattern = '...'
line in the function to this:
let pattern = '\v^\S.*\=.*:$'
This regex should look much less frightening than the last one. I'll leave it as an exercise for you to figure out how it works -- it's a pretty straightforward translation of our definition.
Save the file, run :set filetype=potion
in factorial.pn
, and try out the new
][
and []
mappings. They should work as expected.
We don't need a search flag here because putting the cursor at the beginning of the match (the default) works just fine.
Our section movement commands work great in normal mode, but we need to add a bit more to make them work in visual mode as well. First, change the function to look like this:
function! s:NextSection(type, backwards, visual)
if a:visual
normal! gv
endif
if a:type == 1
let pattern = '\v(\n\n^\S|%^)'
let flags = 'e'
elseif a:type == 2
let pattern = '\v^\S.*\=.*:$'
let flags = ''
endif
if a:backwards
let dir = '?'
else
let dir = '/'
endif
execute 'silent normal! ' . dir . pattern . dir . flags . "\r"
endfunction
Two things have changed. First, the function takes an extra argument so it knows
whether it's being called from visual mode or not. Second, if it's called from
visual mode we run gv
to restore the visual selection.
Why do we need to do this? Let's try something that will make it clear. Visually select some text in any buffer and then run the following command:
:echom "hello"
Vim will display hello
but the visual selection will also be cleared!
When running an ex mode command with :
the visual selection is always cleared.
The gv
command reselects the previous visual selection, so this will "undo"
the clearing. It's a useful command, and can be handy in your day-to-day work
too.
Now we need to update the existing mappings to pass 0
in for the new visual
argument:
noremap <script> <buffer> <silent> ]]
\ :call <SID>NextSection(1, 0, 0)<cr>
noremap <script> <buffer> <silent> [[
\ :call <SID>NextSection(1, 1, 0)<cr>
noremap <script> <buffer> <silent> ][
\ :call <SID>NextSection(2, 0, 0)<cr>
noremap <script> <buffer> <silent> []
\ :call <SID>NextSection(2, 1, 0)<cr>
Nothing too complex there. Now let's add the visual mode mappings as the final piece of the puzzle:
vnoremap <script> <buffer> <silent> ]]
\ :<c-u>call <SID>NextSection(1, 0, 1)<cr>
vnoremap <script> <buffer> <silent> [[
\ :<c-u>call <SID>NextSection(1, 1, 1)<cr>
vnoremap <script> <buffer> <silent> ][
\ :<c-u>call <SID>NextSection(2, 0, 1)<cr>
vnoremap <script> <buffer> <silent> []
\ :<c-u>call <SID>NextSection(2, 1, 1)<cr>
These mappings all pass 1
for the visual
argument to tell Vim to reselect
the last selection before performing the movement. They also use the <c-u>
trick we learned about in the Grep Operator chapters.
Save the file, :set ft=potion
in the Potion file and you're done! Give your
new mappings a try. Things like v]]
and d[]
should all work properly now.
This has been a long chapter for some seemingly small functionality, but you've learned and practiced a lot of useful things along the way:
noremap
instead of nnoremap
to create mappings that work as
movements and motions.execute 'normal! ...'
string programmatically.%^
(beginning of file).Go ahead and do the exercises (it's just a bit of :help
reading) and then grab
some ice cream. You've earned it after this chapter!
Read :help search()
. This is a useful function to know, but you can also use
the flags listed with the /
and ?
commands.
Read :help ordinary-atom
to learn about more interesting things you can use in
search patterns.