Vim - CPAN 模組搜尋之整合 可程式化編輯器所帶來的好處 (一)

※ 前言

接觸 Unix-like 系統的朋友們應該都知道 Vi / Vim 以及 Emacs 這兩套赫赫有名的編輯器。這兩套編輯器之為何有名,是因為這兩者都為使用者帶來可程式化 (Programmable)的便利性。使用者可為自己撰寫常用功能的指令稿,可以將一連串複雜的編輯動作或甚至系統呼叫簡化成一組簡單的組合鍵。

但在概念上來說,Vim 的概念較為不同,Vim 將撰寫文字這個動作切割出來成文字的插入模式 (Insert Mode) 而另外獨立出一個一般模式 (Normal Mode)。在一般模式裡頭, Vim 接收使用者輸入的一序列的鍵碼,並在決策樹 (Decision Tree)中,建立路徑來找出對應命令並且執行。

另一個特點則是,Vim 將所有文字都視為文字物件 (Text Object),每一個段落、字串、一句話、一個章節、一個字、一個程式碼區塊,任何可辨別出範圍的文字都是文字物件。使用者可以將在一般模式 (Normal Mode) 所定義的命令來選取、操作該物件,除了剪下、複製、貼下等基本命令外,使用者還可自訂其他命令,譬如說,對該文字物件來做程式碼的重新排列,做字元轉換,或做縮排調整,或將文字物件傳送給瀏覽器做搜尋、張貼等等。

此外,使用者可以輕易的將編輯器與外部程式做整合,任何外部程式皆可由 Vim 內呼叫,並且再對這些指令產生的結果做特定的處理。使用者可以撰寫自己的指令稿(script) ,再由 Vim 裡頭呼叫該指令稿來做事情。 除此,使用者還可直接在 Vim Script 內直接嵌入 Perl , Ruby , Python 或是 MzScheme 等程式語言,並且透過 Vim 本身提供的 API 來對 Vim 編輯器本身做操作。


※ 與 CPAN 整合之應用

什麼是 CPAN?CPAN (Comprehensive Perl Archive Network) 為 Perl 綜合典藏網,集世界 Perl 模組作者所撰寫的大量的模組,包含影像處理、生物資訊、字串處理、數學運算等等用途的模組,包羅萬象。而透過 Perl 提供的 CPAN Shell 可以用來搜尋、安裝或查詢 CPAN 上的模組。

不過每次在搜尋 CPAN 時,我們都必須要從編輯器上切換到終端機,接著輸入 cpan 命令來做模組搜尋,cpan 則要在另外花上 3-4 秒載入模組索引來做搜尋,所以這一直是很耗費時間的一件事情。

事情若是能夠做的更有效率,那麼我們就更該用有效率的方式來做。

所以筆者試著利用 Vim 這樣的特性來實現這樣一個用於操作 CPAN 模組的 Vim Plugin。讓在 Vim 裡可以啟用一個搜尋視窗,載入索引快取索引,然後利用第一列來輸入搜尋樣式 (Pattern)。每當樣式更新,便隨即更新搜尋結果。使用者可選擇模組,並針對該模組查詢文件、作者、版本,或直接編輯原始碼,讓 CPAN 模組相關操作可以更便利易用。(相關的展示參見註 1)

※ 模組索引檔

要能夠搜尋模組,那麼得先有模組索引檔才行。 CPAN Shell 或是 CPANPLUS Shell 各有存放索引的位置。CPAN Shell 通常是放於家目錄底下的 .cpan 資料夾內,實際路徑為 $HOME/.cpan/sources/modules/02packages.details.txt.gz ,CPANPLUS 則是放置於  $HOME/.cpanplus/02packages.details.txt.gz 。使用 zcat 命令可將該索引顯示出來,如下:

 

    $ zcat ~/.cpan/sources/modules/02packages.details.txt.gz 

 

    File:         02packages.details.txt
    URL:          https://www.perl.com/CPAN/modules/02packages.details.txt
    Description:  Package names found in directory $CPAN/authors/id/
    Columns:      package name, version, path
    Intended-For: Automated fetch routines, namespace documentation.
    Written-By:   Id
    Line-Count:   71007
    Last-Updated: Wed, 04 Nov 2009 03:27:58 GMT
    A                                  0.01  A/AD/ADAMK/Module-Install-0.80.tar.gz
    AAA::Demo                         undef  J/JW/JWACH/Apache-FastForward-1.1.tar.gz
    AAA::eBay                         undef  J/JW/JWACH/Apache-FastForward-1.1.tar.gz
    AAC::Pvoice                        0.91  J/JO/JOUKE/AAC-Pvoice-0.91.tar.gz
    AAC::Pvoice::Bitmap                1.12  J/JO/JOUKE/AAC-Pvoice-0.91.tar.gz
    AAC::Pvoice::Dialog                1.01  J/JO/JOUKE/AAC-Pvoice-0.91.tar.gz
    ... 略

開頭幾行所列舉的為該索引檔的相關資訊,包含最後更新時間、來源網址、敘述等等。接下來其他行則是所有 CPAN 模組的索引資料,基本上我們只需要第一欄即可,可利用 將此檔案導給 (pipe) cut 命令來做切割,並利用 grep -v 命令將開頭那幾行額外的資訊濾除:

    $ zcat ~/.cpan/sources/modules/02packages.details.txt.gz \
        | grep -v '^[0-9a-zA-Z-]*: '  \
        | cut -d' ' -f1

有了將模組名稱過濾出來的指令,便能夠開始將結果導入至 Vim 來做整合了。


※ Vim Script

要撰寫 Vim Script ,首先就得了解 Vim 在執行期間載入 Vim Script 的步驟。在啟動 Vim 時,Vim 會先載入家目錄底下的 .vimrc 檔案,接著去載入 $VIMRUNTIME(註 2)路徑底下的所有資料夾裡頭副檔名為 .vim 的檔案。

基本上,我們所要實做的 Vim Script 必須放在 $VIMRUNTIME/plugin/ 底下,另外我們也需在 $VIMRUNTIME/autoload/ 底下放置共用的 (common) 函式。

為了要讓索引檔讀取的函式可以共用,所以可以放置在 $VIMRUNTIME 路徑底下的 autoload 資料夾內,取名為 perl.vim 用以存放 Perl 相關的 Vim 函式。

autoload 基本上是為了提供像函式庫之類的功能,讓常用、通用的函式可以放在一起,重複使用。要撰寫函式用於 autoload ,則我們需要在每個函式名稱前加上檔名。舉例來說,我們撰寫 $VIMRUNTIME/autoload/libperl.vim 時,則函式的定義必須為:

    fun libperl#function_name()
    endf

那麼在呼叫定義在 autoload 內的函式時,則需要寫:

    cal libperl#function_name()

Vim 便會自動去呼叫 libperl.vim 內所定義的函式。(事實上放在 autoload/ 裡頭的腳本 (Script) 都會先被預先載入,而非在叫用時才去載入。)

接下來可以設計一管理視窗的物件,來管理顯示搜尋結果所使用的 Buffer 以及視窗分割。

由於 Vim 沒有提供原生的物件導向機制,所以是用 Dictionary 的變數類型來做物件的模擬,並將函式的參照儲存於 Dictionary 內。Dictionary 基本上就是我們常用的雜湊  (Hash)。

而 Vim 裡頭物件的定義是像這樣寫的:

    let obj = { 'property': 123 , 'foo':'bar' }

在 obj 變數定義函式的方式如下:

    function obj.func1(arg1,arg2)
    endf 
    function obj.func2()
    endf

所以我們先定義好搜尋視窗物件,並且針對用於搜尋的 Buffer 做一些特定的初始化。

    let swindow#class = {
        \'buf_nr' : -1 ,
        \'mode' : 0 , 
        \'predefined_index': [] ,
        \'max_result': 100
        \}
    let swindow#class.version = 0.3
    fun! swindow#class.split( ... )
    .... 略

接著在 swindow#class.split 函式內,使用簡單的檢查來決定是否建立新的 Buffer 或是將已有的 Buffer 載回,或是若已有視窗開啟那麼直接切換到該視窗:

    if ! bufexists( self.buf_nr )  
        " 該 Buffer 不存在時
        let self.buf_nr = bufnr('%')
        " 接著做 Buffer 的初始化動作
        " ... 略
    elseif bufwinnr(self.buf_nr) == -1
        " 該 Buffer 已經存在
        " 載回該 Buffer
        execute self.buf_nr . 'buffer'
    elseif bufwinnr(self.buf_nr) != bufwinnr('%')
        " 該 Buffer 已經在某視窗內,但目前視窗非此視窗
        " 切換到該視窗
        execute bufwinnr(self.buf_nr) . 'wincmd w'
    endif

而初始化 Buffer 的一些設定如下:

    setlocal noswapfile  buftype=nofile bufhidden=hide
    setlocal nobuflisted nowrap cursorline nonumber fdc=0

需要注意的是使用 setlocal 命令來指定該設定只對 Buffer 生效。

其他設定之說明如下:

    * noswapfile       指不將 Buffer 放置到 swap 檔案內
    * buftype=nofile   指 Buffer 類型非檔案
    * bufhidden=hide   隱藏 Buffer
    * nobuflisted      在使用者使用 :buffers 指令時,不列出該 Buffer
    * nowrap           文字超過欄寬時不換行
    * nonumber         不使用行號
    * fdc=0            折疊(Fold) 欄寬為 0

接著對 swindow#class 物件定義其他函式如:

    swindow#class.init_basic_mapping()   " 初始化快速鍵
    swindow#class.init_syntax()          " 初始化語法標記 (Syntax Highlighting)
    swindow#class.render(lines)          " 產生結果至畫面
    swindow#class.get_pattern()          " 取得搜尋樣式
    swindow#class.close()                " 關閉視窗
    " ... 略
在 init_basic_mapping() 裡頭,我們必須對每一種快捷鍵 (mapping) 
皆使用 "<buffer>" 的選項來限制該快捷鍵只於此 Buffer:
    fun! swindow#class.init_basic_mapping()
        imap <buffer>     <Enter> <ESC>j<Enter>
        imap <buffer>     <C-a>   <Esc>0i
        imap <buffer>     <C-e>   <Esc>A
        imap <buffer>     <C-b>   <Esc>i
        imap <buffer>     <C-f>   <Esc>a
        inoremap <buffer> <C-n> <ESC>j
        nnoremap <buffer> <C-n> j
        nnoremap <buffer> <C-p> k
        " ... 略
    endf

以及用來更新畫面的 render() 函式:

    fun! swindow#class.render(lines)
        " 儲存原有游標位置
        let old = getpos('.')

 

        " 取得最尾行行號,如果超過兩行便將第二行開始到尾端 ($) 都刪除
        if line('$') > 2
            silent 2,$delete _
        endif

        " 最後將要顯示的串列 (List) 繪製到 Buffer 上
        let r=join( a:lines , "\n" )
        silent put=r

 

        " 重設游標
        cal setpos('.',old)
    endf

定義 update() 函式,這個函式是更新搜尋樣式所對應的事件處理函式:

    fun! swindow#class.update()
        cal self.update_search()
        cal self.update_highlight()
        startinsert
    endfunc

基本上 update() 函式是呼叫 update_search() 來更新搜尋結果,並且呼叫
update_highlight() 來更新色彩標示。

接著定義搜尋函式 update_search(),取得搜尋樣式,並且從索引中過濾符合項目,
最後更新到 Buffer 上。

    fun! swindow#class.update_search()
        let pattern = self.get_pattern()
        let lines = self.filter_result( pattern , self.predefined_index )
        if len(lines) > self.max_result
            let lines = remove( lines , 0 , self.max_result )
        endif
        cal self.render( lines )
    endf

其他部份在這裡就不在闡述。
詳細程式碼在 Github 上可以取得。 ( https://github.com/c9s/search-window.vim )


※ 定義 CPAN Search Window

有了基本的搜尋視窗物件,那麼就可以繼承它來實做延伸功能。由於 Vim 不支援原生的物件導向,所以對於繼承一機制採用的作法是將父類別的 Dictionary 變數複製至子類別。

    let s:CPANWindow = copy( swindow#class )

接著定義索引函式:

    fun! s:CPANWindow.index()
        if self.search_mode == g:CPAN.Mode.Installed
            cal PrepareInstalledCPANModuleCache()
            return g:cpan_installed_pkgs
        elseif self.search_mode == g:CPAN.Mode.All
            cal PrepareCPANModuleCache()
            return g:cpan_pkgs
        elseif self.search_mode == g:CPAN.Mode.CurrentLib
            cal PrepareCurrentLibCPANModuleCache()
            return g:cpan_curlib_pkgs
        else
            return [ ]
        endif
    endf

在上面這個部份,針對了不同的模式,做不同索引的搜尋。其中 PrepareCPANModuleCache 便是將 CPAN Package Index 取回的函式。而 PrepareCurrentLibCPANModuleCache 函式用來搜尋當前目錄下的 lib/ 底下所有的模組名稱。PrepareInstalledCPANModuleCache 所作的則是搜尋在系統的 Perl 模組安裝路徑下所有的模組名稱。

而像 PrepareCPANModuleCache 便是用於取得我們前面所提到的指令的結果,而為了避免重複執行 grep 以及 gunzip ,我將結果存至一個快取中,裡頭只存放模組名稱。

    let cmd = 'zcat '.path."| grep -v '^[0-9a-zA-Z-]*: ' | cut -d' ' -f1 > " . g:cpan_source_cache
    call system( cmd )

所以之後只需要利用內建函數 readfile() 來載入模組清單就可以了。

    let pkgs = readfile( g:cpan_source_cache )
 

接著,為了要讓每輸入一個字,便更新搜尋結果,需要利用 autocmd 來設定事件處理函式 (Event Handler)。而 autocmd 提供了 "CursorMovedI" 這個事件,該事件是指,在插入模式 (Insert Mode) 時,游標移動所觸發的事件。因此我們定義:

    autocmd CursorMovedI <buffer>       call s:CPANWindow.update()

指定呼叫 update() 函式,以更新搜尋結果以及色彩標示。 ( update() 函式是從
 swindow#class 繼承來的。 )

我們還需要另外定義一些快捷鍵,讓搜尋的項目可以有其他功能,所以我們可在
init_buffer() 函式中定義以下對應:

    " $ 鍵用以呼叫 perldoc 視窗,將該模組文件載入。
    nnoremap <silent> <buffer> $  :call  perldoc#window.open(expand('<cWORD>'),'')<CR>
    " f 鍵使用 cpanf 來安裝該 Perl 模組的最新版本
    nnoremap <silent> <buffer> f   :exec '!sudo cpanf ' . expand('<cWORD>')<CR>
    " Enter 鍵開啟該模組原始檔
    nnoremap <silent> <buffer> <Enter> :call libperl#open_module()<CR>
    " 安裝該 Perl 模組
    nnoremap <silent> <buffer> I  :exec '!'.g:cpan_install_command.' '.getline('.')<CR>
    " 將搜尋樣式傳給瀏覽器,使用 CPAN Website 搜尋
    inoremap <silent> <buffer> @   <ESC>:exec '!' .g:cpan_browser_command .
            ' https://search.cpan.org/search?query=' . getline(1) . '&mode=all'<CR>

該寫的都差不多了,最後一步只需要將快速鍵對應到 CPAN 搜尋視窗的 open() 函式即可,於是:

    com! OpenCPANWindowS   :call s:CPANWindow.open('topleft', 'split',g:cpan_win_height)
    nnoremap <silent> <C-c><C-m>        :OpenCPANWindowS<CR>

便可以利用 Ctrl-C Ctrl-m 來呼叫 CPAN 模組搜尋視窗。

Enjoy!

 

註 1.  完整程式碼可於 Github 取得以及 Screencast

註 2. $VIMRUNTIME , 在 Unix 底下通常為家目錄底下的 .vim 資料夾。

    對於不同平台,Vim 說明文件的 "initialization" 項目,對各種平台讀取
    Vim 設定檔的格式如下:

        "$HOME/.vimrc" (for Unix and OS/2) (*)
        "s:.vimrc"  (for Amiga) (*)
        "home:.vimrc" (for Amiga) (*)
        "$VIM/.vimrc" (for OS/2 and Amiga) (*)
        "$HOME/_vimrc" (for MS-DOS and Win32) (*)
        "$VIM/_vimrc" (for MS-DOS and Win32) (*)

    相關其他的啟動步驟也可在 starting.txt 章節找到。可使用 :help starting.txt
    或者 :tab help starting.txt 查看。


作者簡介

Cornelius,目前在 AIINK(愛印網),以 Perl 語言開發的 Jifty web framework 從事網站開發相關工作。於 CPAN - Perl 模組典藏網維護多個 Perl 模組,參與 Jifty, SD 等 Perl 相關開放原始碼專案 。主要以 Vim 做為開發工具,著有 cpan.vim , perl-completion.vim , perldoc.vim 等多個 vim 相關 Plugin。
 
 Github: https://www.github.com/c9s
 Twitter: https://twitter.com/c9s
 Plurk: https://www.plurk.com/c9s
 Blog:  https://c9s.blogspot.com/
 Google group: https://groups.google.com/group/vim-taiwan




自由軟體鑄造場電子報 : 第 139 期 Vim - CPAN 模組搜尋之整合

分類: 技術專欄