How to create a "new mode" for vim (i.e., loop and get user input, then execute the associated actions)
I'm trying to create something of a "new mode" in vim. The details of the mode are unimportant, but there is one thing I need to be able to do.
I need to do something like the following pseudo-code:
get user input (movement keys like "j" or complex keys like "dd")
while user_input != <esc>
execute the user input
endwhile
In other words, I need a loop that will read what the user is doing, then perform the associated action.
I've already got the following code:
开发者_开发问答let char = nr2char(getchar())
while char =~ '^\w$'
execute "normal ". char
let char = nr2char(getchar())
endwhile
This works fine for user movements (j
, k
, etc.), but fails for more complex multi-character commands like dd
.
Also, this is a small annoyance, but the cursor disappears during getchar(), meaning you effectively can't see the cursor (this is of less importance because of what I'm trying to do, but hopefully has a solution as well).
Does anyone have any idea how I can get multi-character actions to work?
I think you might be interested in submode.vim, if not to use it, to at least see how they've implemented this feature.
I usually redefine locally (:h map-<buffer>
, for instance) the things this new mode is meant to change. And I also override <esc>
to unregister those things from the mode.
This is the easier approach IMO.
Just for the record, for creating a new mode that accepts commands of multiple length you usually use something like a parse tree with keys and commands. Here is a simple version in vim:
let g:EvalTree = { 'root': {} }
function! s:AddKeyMapRecursive(root, keys, command) abort
if !has_key(a:root, a:keys[0])
let a:root[a:keys[0]] = { 'command': '', 'children': {} }
endif
if len(a:keys) == 1
let a:root[a:keys[0]].command = a:command
else
call s:AddKeyMapRecursive(a:root[a:keys[0]].children, a:keys[1 : ], a:command)
endif
endfunction
function! g:EvalTree.AddMap(keys, command) abort
call s:AddKeyMapRecursive(l:self.root, a:keys, a:command)
endfunction
function! s:GetNodeRecursive(root, keys) abort
if !has_key(a:root, a:keys[0])
return 0
endif
if len(a:keys) == 1
return a:root[a:keys[0]]
else
return s:GetNodeRecursive(a:root[a:keys[0]].children, a:keys[1 : ])
endif
endfunction
function! g:EvalTree.GetNode(keys) abort
return s:GetNodeRecursive(l:self.root, a:keys)
endfunction
You can insert in this tree like:
call g:EvalTree.AddMap('hw', ":echo 'hello world'<CR>")
call g:EvalTree.AddMap('DA', 'ggdG')
Later you can use this tree to eval things in the loop you mention. Although you will probably need a also a queue to do this. You can define one as:
let g:TextQueue = { 'text': '', 'index': 0 }
function! g:TextQueue.Push(c) abort
let l:self.text .= a:c
endfunction
function! g:TextQueue.Pop(...) abort
let l:self.index += get(a:, 1, 1)
endfunction
function! g:TextQueue.CheckFirst() abort
return l:self.text[l:self.index]
endfunction
function! g:TextQueue.Text() abort
return l:self.text[l:self.index : ]
endfunction
function! g:TextQueue.Empty() abort
return l:self.index >= strlen(l:self.text)
endfunction
function! g:TextQueue.ReInitialize() abort
let [l:self.text, l:self.index] = ['', 0]
endfunction
And using that queue and the tree make the evaluation loop. This is the part were I don't really know how to do it properly myself, but after trying a while a came to a functional (although ugly and very inefficient since eval some things much more than necessary) code that is this:
function! s:Execute(map) abort
execute 'normal! ' . a:map.command
redraw
call g:TextQueue.Pop(strlen(a:map.keys))
let [a:map.keys, a:map.command] = ['', '']
endfunction
function! s:LimitTimeHasElapsed(time) abort
return (a:time + 1 < localtime())
endfunction
function! g:EvalTree.Start() abort
let l:time = localtime()
let l:stored = { 'keys': '', 'command': '' }
while g:TextQueue.CheckFirst() !=# "\<Esc>"
let l:char_code = getchar(0)
if l:char_code
call g:TextQueue.Push(nr2char(l:char_code))
endif
if !g:TextQueue.Empty()
let l:possible_maps = g:EvalTree.GetNode(g:TextQueue.Text())
if type(l:possible_maps) != type({})
if !empty(l:stored.command)
call s:Execute(l:stored)
elseif g:TextQueue.CheckFirst() !=# "\<Esc>"
call g:TextQueue.Pop()
endif
let l:time = localtime()
continue
endif
if l:possible_maps.command !=# ''
let l:stored.keys = g:TextQueue.Text()
let l:stored.command = l:possible_maps.command
if l:possible_maps.children == {} || s:LimitTimeHasElapsed(l:time)
call s:Execute(l:stored)
let l:time = localtime()
endif
elseif s:LimitTimeHasElapsed(l:time)
let [l:stored.keys, l:stored.command] = ['', '']
call g:TextQueue.pop()
let l:time = localtime()
endif
else
call g:TextQueue.ReInitialize()
let l:time = localtime()
endif
sleep 20m
endwhile
endfunction
Now you simply have to define a several mappings. Ie:
call g:EvalTree.AddMap('hw', ":echo 'hello workd'\<CR>")
call g:EvalTree.AddMap('h', 'h')
call g:EvalTree.AddMap('l', 'l')
call g:EvalTree.AddMap('j', 'j')
call g:EvalTree.AddMap('k', 'k')
call g:EvalTree.AddMap('H', '0')
call g:EvalTree.AddMap('L', '$')
call g:EvalTree.AddMap('K', 'H')
call g:EvalTree.AddMap('J', 'L')
call g:EvalTree.AddMap('dd', 'dd')
call g:EvalTree.AddMap('Z', 'yap')
call g:EvalTree.AddMap('D', 'dap')
call g:EvalTree.AddMap('ab', ":echo 'hello'\<CR>")
call g:EvalTree.AddMap('abc', ":echo 'world'\<CR>")
call g:EvalTree.AddMap('abcd', ":echo 'hello world'\<CR>")
And finally, start the evaluation:
call g:EvalTree.Start()
You all may also be interested in watching my plugin 'EXtend.vim' and its ReadLine() function. It doesn't eval multi character commands but does a couple of things that are indeed pretty interesting, like emulating cursors and selections inside its submode.
精彩评论