如何使用Python編寫vim外掛

Yggdroot發表於2017-12-04

前言

vim是個偉大的編輯器,不僅在於她特立獨行的編輯方式,還在於她強大的擴充套件能力。然而,vim自身用於寫外掛的語言vimL功能有很大的侷限性,實現功能複雜的外掛往往力不從心,而且執行效率也不高。幸好,vim早就想到了這一點,她提供了很多外部語言介面,比如Python,ruby,lua,Perl等,可以很方便的編寫vim外掛。本文主要介紹如何使用Python編寫vim外掛。

準備工作

1. 編譯vim,使vim支援Python

在編譯之前,configure的時候加上--enable-pythoninterp--enable-python3interp選項,使之分別支援Python2和Python3
編譯好之後,可以通過vim --version | grep +python來檢視是否已經支援Python,結果中應該包含+python+python3,當然也可以編譯成只支援Python2或Python3。

現在好多平臺都有直接編譯好的版本,已經包含Python支援,直接下載就可以了:

  • Windows:可以在這裡下載。
  • Mac OS:可以直接brew install vim來安裝。
  • Linux:也有快捷的安裝方式,就不贅言了。

2. 如何讓Python能正常工作

雖然vim已經支援Python,但是可能:echo has("python"):echo has("python3")的結果仍是0,說明Python還不能正常工作。
此時需要檢查:

  1. 系統上是否裝了Python?
  2. Python是32位還是64位跟vim是否匹配?
  3. Python的版本跟編譯時的版本是否一致(編譯時的版本可以使用:version檢視)
  4. 通過pythondllpythonthreedll來分別指定Python2和Python3所使用的動態庫。
    例如,可以在vimrc裡新增
    set pythondll=/Users/yggdroot/.python2.7.6/lib/libpython2.7.so

經此4步,99%能讓Python工作起來,剩下的1%就看人品了。

補充一點
對於neovim,執行

pip2 install --user --upgrade neovim
pip3 install --user --upgrade neovim複製程式碼

就可以新增Python2和Python3的支援,具體參見:h provider-python

從hello world開始

在命令列視窗執行:pyx print("hello world!"),輸出“hello world!”,說明Python工作正常,此時我們已經可以使用Python來作為vim的EX命令了。

操作vim像vimL一樣容易

怎麼用Python來訪問vim的資訊以及操作vim呢?很簡單,vim的Python介面提供了一個叫vim的模組(module)。vim模組是Python和vim溝通的橋樑,通過它,Python可以訪問vim的一切資訊以及操作vim,就像使用vimL一樣。所以寫指令碼,首先要import vim

vim模組

vim模組提供了兩個非常有用的函式介面:

  • vim.command(str)
    執行vim中的命令str(ex-mode),返回值為None,例如:

    :py vim.command("%s/\s\+$//g")
    :py vim.command("set shiftwidth=4")
    :py vim.command("normal! dd")複製程式碼
  • vim.eval(str)
    求vim表示式str的值,(什麼是vim表示式,參見:h expr),返回結果型別為:

    • string: 如果vim表示式的值的型別是stringnumber
    • list:如果vim表示式的值的型別是一個vim list(:h list
    • dictionary:如果vim表示式的值的型別是一個vim dictionary(:h dict

    例如:

    :py sw = vim.eval("&shiftwidth")
    :py print vim.eval("expand('%:p')")
    :py print vim.eval("@a")複製程式碼

vim模組還提供了一些有用的物件:

  • Tabpage物件(:h python-tabpage
    一個Tabpage物件對應vim的一個Tabpage。
  • Window物件(:h python-window
    一個Window物件對應vim的一個Window。
  • Buffer物件(:h python-buffer
    一個Buffer物件對應vim的一個buffer,Buffer物件提供了一些屬性和方法,可以很方便操作buffer。
    例如 (假定b是當前的buffer) :

    :py print b.name            # write the buffer file name
    :py b[0] = "hello!!!"       # replace the top line
    :py b[:] = None             # delete the whole buffer
    :py del b[:]                # delete the whole buffer
    :py b[0:0] = [ "a line" ]   # add a line at the top
    :py del b[2]                # delete a line (the third)
    :py b.append("bottom")      # add a line at the bottom
    :py n = len(b)              # number of lines
    :py (row,col) = b.mark('a') # named mark
    :py r = b.range(1,5)        # a sub-range of the buffer
    :py b.vars["foo"] = "bar"   # assign b:foo variable
    :py b.options["ff"] = "dos" # set fileformat
    :py del b.options["ar"]     # same as :set autoread<複製程式碼
  • vim.current物件(:h python-current
    vim.current物件提供了一些屬性,可以方便的訪問“當前”的vim物件

屬性 含義 型別
vim.current.line The current line (RW) String
vim.current.buffer The current buffer (RW) Buffer
vim.current.window The current window (RW) Window
vim.current.tabpage The current tab page (RW) TabPage
vim.current.range The current line range (RO) Range

python訪問vim中的變數

訪問vim中的變數,可以通過前面介紹的vim.eval(str)來訪問,例如:

:py print vim.eval("v:version")複製程式碼

但是, 還有更pythonic的方法:

  • 預定義vim變數(v:var
    可以通過vim.vvars來訪問預定義vim變數,vim.vvars是個類似Dictionary的物件。例如,訪問v:version

    :py print vim.vvars["version"]複製程式碼
  • 全域性變數(g:var
    可以通過vim.vars來訪問全域性變數,vim.vars也是個類似Dictionary的物件。例如,改變全域性變數g:global_var的值:

    :py vim.vars["global_var"] = 123複製程式碼
  • tabpage變數(t:var
    例如:

    :py vim.current.tabpage.vars["var"] = "Tabpage"複製程式碼
  • window變數(w:var
    例如:

    :py vim.current.window.vars["var"] = "Window"複製程式碼
  • buffer變數(b:var
    例如:

    :py vim.current.buffer.vars["var"] = "Buffer"複製程式碼

python訪問vim中的選項(options

訪問vim中的選項,可以通過前面介紹的vim.command(str)vim.eval(str)來訪問,例如:

:py vim.command("set shiftwidth=4")
:py print vim.eval("&shiftwidth")複製程式碼

當然, 還有更pythonic的方法:

  • 全域性選項設定(:h python-options
    例如:

    :py vim.options["autochdir"] = True複製程式碼

    注意:如果是window-local或者buffer-local選項,此種方法會報KeyError異常。對於window-localbuffer-local選項,請往下看。

  • window-local選項設定
    例如:

    :py vim.current.window.options["number"] = True複製程式碼
  • buffer-local選項設定
    例如:

    :py vim.current.buffer.options["shiftwidth"] = 4複製程式碼

兩種方式寫vim外掛

  • 內嵌式

    py[thon] << {endmarker}
    {script}
    {endmarker}複製程式碼

    {script}中的內容為Python程式碼,{endmarker}是一個標記符號,可以是任何字串,不過{endmarker}前面不能有任何的空白字元,也就是要頂格寫。
    例如,寫一個函式,列印出當前buffer所有的行(Demo.vim):

    function! Demo()
    py << EOF
    import vim
    for line in vim.current.buffer:
     print line
    EOF
    endfunction
    call Demo()複製程式碼

    執行:source %檢視結果。

  • 獨立式
    把Python程式碼寫到*.py中,vimL只用來定義全域性變數、map、command等,LeaderF就是採用這種方式。個人更喜歡這種方式,可以把全部精力集中在寫Python程式碼上。

非同步

  • 多執行緒
    可以通過Python的threading模組來實現多執行緒。但是,執行緒裡面只能實現與vim無關的邏輯,任何試圖線上程裡面操作vim的行為都可能(也許用“肯定會”更合適)導致vim崩潰,甚至包括只一個vim選項。雖然如此,也比vimL好多了,畢竟聊勝於無。

  • subprocess
    可以通過Python的subprocess模組來呼叫外部命令。
    例如:

    :py import subprocess
    :py print subprocess.Popen("ls -l", shell=True, stdout=subprocess.PIPE).stdout.read()複製程式碼

    也就是說,從支援Python起,vim就已經支援非同步了(雖然直到vim7.4才基本沒有bug),Neovim所增加的非同步功能,對用Python寫外掛的小夥伴來說,沒有任何吸引力。好多Neovim粉竟以引入非同步(job)而引以為傲,它什麼時候能引入真正的多執行緒支援我才會服它。

案例

著名的補全外掛YCM和模糊查詢神器LeaderF都是使用Python編寫的。

缺陷

由於GIL的原因,Python執行緒無法並行處理;而vim又不支援Python的程式(github.com/vim/vim/iss… ),計算密集型任務想利用多核來提高效能已不可能。

奇技淫巧

  • 把buffer中所有單詞首字母變為大寫字母

    :%pydo return line.title()複製程式碼
  • 把buffer中所有的行映象顯示

    例如,把

    vim is very useful
    123 456 789
    abc def ghi
    who am I複製程式碼

    變為

    lufesu yrev si miv
    987 654 321
    ihg fed cba
    I ma ohw複製程式碼

    可以執行此命令::%pydo return line[::-1]

總結

以上只是簡單的介紹,更詳細的資料可以參考:h python

相關文章