May 24, 2021 Vim
Now that we know how segment movement works, let's remap these commands to make them work for Potion files.
First of all, we have to decide what the meaning of the "segment" in the Potion file is. There are two pairs of segment move commands, so we can summarize two sets of combinations, and our users can choose the one they like.
Let's use two combinations to decide where the segments in Potion are:
Slightly expand our
factorial.pn
which is where those rules are used as segments:
# 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 is more liberal. It defines a segment as a "top-level block of text."
The second definition is a little more strict. It defines a segment as a function definition.
Create
ftplugin/potion/sections.vim
T
his will be where we place the segment move code.
Remember that once the filetype of
filetype
to
potion
the code here executes.
We'll remap all four segments of the move command, so continue and create a skeleton:
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. T
hat way you'll be able to do things like
d]]
to "delete from here to the next section". N
ote that we
noremap
nnoremap
because we want these commands to work in operator-pending mode as well.
This way you can use
d]]
to remove content from this to the next paragraph.
We set the mappings to work on buffer-local, so they only work on Potion files and do not replace global options.
We've also set up the option because users shouldn't care about the details of how we implement segment movement.
The code that implements segment movement in each command will be very similar, so let's abstract it out of a function for mapping calls.
You'll see this strategy frequently in Vim plug-ins that create some similar mappings. This is not only easier to read, but also easier to maintain than stacking all the features in individual maps.
Add
sections.vim
file:
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>
Here I use Vimscript's line-breaking feature because I don't want to see long, smelly code. N
ote that the backslash is escaped in front of the second row.
Read
:help line-continuation
to learn more.
Note that we
<SID>
the local namespace of the script to avoid polluting the global space.
Each map simply calls
NextSection
to implement the corresponding movement.
Now we can start
NextSection
Let's consider what our function needs to do. W
e want to move the cursor to the next segment, and one simple way to move the cursor is to
/
?
?
Command.
Edit
NextSection
this:
function! s:NextSection(type, backwards)
if a:backwards
let dir = '?'
else
let dir = '/'
endif
execute 'silent normal! ' . dir . 'foo' . "\r"
endfunction
Now this function uses execute normal! that
execute normal!
t
o execute
/foo
?foo
on the value of the
backwards
It's going to be a good start.
Moving on, we obviously need to search for something other than
foo
depending on whether we use the first or second definition of the segment header.
Change
NextSection
to 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 matching pattern, let's move on to it.
Replace the first let pattern with the following line:
let pattern = '...'
let pattern = '\v(\n\n^\S|%^)'
If you don't understand what this regular expression does, recall the definition of "segment" that we're implementing.
Any line after an empty line where the first character is a non-empty character, as well as the first line of the file.
At the
\v
forces a switch to "very magic" mode, as several times before.
The remaining regular expressions consist of two options. T
he
\n\n^\S
This is exactly the first case in our definition.
The other is
%^
in Vim is a special positive symbol that represents the beginning of the file.
Now is the time to try the first two mappings. S
ave
ftplugin/potion/sections.vim
and execute in your Potion example
:set filetype=potion
[[
]]
work, but they will look odd.
You've probably noticed that moving the time mark between segments will be an empty line above where you really want to move. Before you read on, think about why.
The problem is that
/
( or
?
s
earches, and by default Vim moves the cursor to the beginning of the match.
For example, when you execute
/foo
cursor will be located in f in
foo
f
In order for Vim to move the cursor to the end of the match instead of the beginning, we can use the search flag. Try searching in the Potion file:
/factorial/e
Vim will find
factorial
and take you there. P
ress n
n
to move between matches.
e
causes Vim to move the cursor to the end of the match instead of the beginning.
Try it in the other direction:
?factorial?e
Let's modify our function to place the cursor at the other end of the matching segment header with a search marker.
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 places here. F
irst, we set the value of the
flags
variable according to the type of segment movement.
Now we only need to deal with the first case, so the tag e is
e
Second, we connect
dir
and flags in the
flags
This will add
?e
or /e in the direction we
/e
Save the file, switch back to the Potion sample file, and
:set ft=potion
the changes take effect.
Now try to
]]
[[
our results in the . . . and . .
It's time to deal with our second definition of "segment", which is luckily much simpler than the first. Revisit the definition we need to implement:
Any first character is a non-empty character, including a line equal to the sign and ending with a colon.
We can use a simple regular expression to find such a line.
The second let
let pattern = '...'
in the modification function is like this:
let pattern = '\v^\S.*\=.*:$'
This regular expression is much less scary than the last one. I use the task of pointing out how it works as your exercise -- it's just a straight translation of our definition.
Save the file,
factorial.pn
at the
:set filetype=potion
and then try
][
the new . . . and
[]
They should be able to work on schedule.
We don't need to search for tags here, because the default move to the beginning of the match is exactly what we want.
Our segment movement commands work fine in nomal mode, but for them to work in visual mode, we need to add something. First, change the function to 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. F
irst, the function takes an extra argument so it knows whether it's being called from visual mode or not. S
econd, if it's called from visual mode we run
gv
to restore the visual selection. T
wo places have changed. F
irst, the function accepts one more argument so that it knows if it is in visual mode.
Second, if it is used in visual mode down, we perform
gv
restore the visual selection area.
Why are we doing this? C ome on, let me show you. Feel free to select some text in visual mode and execute the following command:
:echom "hello"
Vim will
hello
but the range selected in visual mode will also be emptied!
When
:
executes a command in ex mode, the range of visual selections is always emptied.
gv
command re-selects the previous visual selection range, which is equivalent to undoing the emptying.
This is a useful command, and you will benefit from it in your daily work.
Now we need to update the previous map to pass
0
to the new
visual
parameter:
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 here is too complicated. Now let's add the visual pattern map as the last 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 maps set the value of the
visual
parameter
1
to tell Vim to re-select the last selection range before moving.
Here's what we learned in the Grep Operator
<c-u>
too.
Save the file, set
set ft=potion
and you're done! T
ry your new map.
Commands such as
d[]
now work properly.
v]]
This is a lengthy chapter, and although we've implemented only a few seemingly simple features, you've learned (and fully practiced) the following useful lessons:
noremap
nnoremap
create commands that can be used as moves and actions.
%^
the beginning of the file).
Stick to it and finish the exercise (just read some documents) and enjoy some ice cream. You deserve it!
Read
:help search()
T
his is a function worth knowing, but you can also use
/
?
The tags that are listed together.
Read:
:help ordinary-atom
to learn more interesting things you can use in search mode.