开发者

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.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜