安裝
Ultisnips 外掛安裝分兩部分,一個是 ultisnips 外掛本身,另外一個是程式碼片段倉庫。一般來說把預設的程式碼片段倉庫下載下來按需修改後上傳到自己的 github 即可。如果你和我一樣也使用 vim-plug 來管理外掛的話,新增下面的程式碼到你的 vimrc 中儲存重新整理即可
1 2 3 |
Plug 'SirVer/ultisnips' # 你自己的程式碼倉庫 git 地址 Plug 'keelii/vim-snippets' |
上面的示例中所有的程式碼片段都存放在外掛安裝目錄下面的 vim-snippets/UltiSnips
中,檔案命名格式為 ft
.snippets, ft
就是 vim 中的 filetype
,其中有個 all.snippets
是唯一一個所有檔案都適用的程式碼片段
配置
快捷鍵設定,我一般使用 tab 來觸發程式碼片段補全,且不使用 YCM (官方文件表示使用YCM的話就不能使用tab補全)
1 2 3 4 5 6 |
let g:UltiSnipsExpandTrigger="<tab>" " 使用 tab 切換下一個觸發點,shit+tab 上一個觸發點 let g:UltiSnipsJumpForwardTrigger="<tab>" let g:UltiSnipsJumpBackwardTrigger="<S-tab>" " 使用 UltiSnipsEdit 命令時垂直分割螢幕 let g:UltiSnipsEditSplit="vertical" |
依賴
ultisnips 外掛需要你的 vim 支援 python,可以在 vim 命令模式下使用下面的檢測你的 vim 版本是否支援 python
1 2 3 |
# 1 表示支援 :echo has("python") :echo has("python3") |
定義一個程式碼片段
定義格式
1 2 3 |
snippet 觸發字元 ["程式碼片段說明" [引數]] 程式碼片段內容 endsnippet |
最小化的一個程式碼片段
1 2 3 4 5 |
snippet if "if (condition) { ... }" if (${1:true}) { $0 } endsnippet |
這時當你在 vim 中輸入 if 敲 tab 就會展開一條 if 語句,第一個觸發點是 if 條件表示式,最後一個是 if 語句體
${1:true}
表示這是第一個觸發點,佔位符為 true
,如果佔位符沒有預設值可直接使用 $1
, $2
, $3
…
可視選擇區的內容為佔位符
1 2 3 4 5 |
snippet if "if (...)" if (${1:true}) { ${VISUAL} } endsnippet |
${VISUAL}
表示在 vim 中使用可視模式下選擇的文字,這個在重構程式碼的時候非常有用(後面會有高階用法),上個圖感受一下:
程式碼片段的引數
b
表示觸發字元應該在一行的開始i
表示觸發字元可以在單詞內(連續展示會使用這個選項)w
表示觸發字元的前後必須是一個字母分界點r
表示觸發字元可以是一個正規表示式t
表示展開的程式碼片段中如果有製表符,原樣輸出,即使你的 vimrc 裡面設定了 expandtabm
表示刪除程式碼片段右邊的所有空白字元e
表示自定義上下文A
表示自動觸發,不需要按 tab,類似於 VIM 中的 abbr
內容直譯器
Ultisnips 定義的程式碼片段中支援三種不同的語言注入:shell, vimscript, python,在程式碼片段中用反引號表示
shell 程式碼
就是在你的命令列 shell 能執行的程式碼片段,比如輸出當前時間
1 2 |
➜ date 2018年 8月27日 星期一 18時19分38秒 CST |
在程式碼片段中用反引號「`」引用即可
1 2 3 |
snippet today Today is the `date`. endsnippet |
輸入 today 按 tab 展開後(格式和上面shell中的不一樣,估計是因為 vim 語言設定的問題):
1 |
Today is the Mon Aug 27 18:24:51 CST 2018. |
vimscript 程式碼
使用 indent
來輸出當前縮排值,使用字首 !v
表示是 vimscript
1 2 3 |
snippet indent Indent is: `!v indent(".")`. endsnippet |
python 程式碼
在程式碼片段中解釋執行 python 程式碼是 ultisnips 最強大的功能,以字首 !p
開始。系統會向 python 中注入一些變數,可以使用 python 程式碼直接對其進行操作
fn
– 表示當前檔名path
– 當前檔名的路徑t
– 佔位符的字典,可以使用t[1], t[2], t.v
來取佔位符內容snip
– UltiSnips.TextObjects.SnippetUtil 物件的一個例項match
– 正則程式碼片段時返回的匹配元素(非常強大)
其中最常用的 snip
物件提供了下面一些變數:
snip.rv
表示 return value,python 程式碼執行後處理過的字串賦給 rv 即可snip.fn
表示當前檔名snip.ft
表示當前檔案型別snip.v
表示 VISUAL 模式變數,其中snip.v.mode
表示模式型別,snip.v.text
表示 VISUAL 模式中選擇的字元
佔位符選擇
UltiSnips 支援使用快捷鍵切換佔位符,我使用 <tab>
和 <shift-tab>
來切換 下一個
和 上一個
佔位符,佔位符切換的作用域為當前程式碼片段內部(即使佔位符已被修改過),當游標移動出去以後就不起作用了
自定義上下文
自定義上下文可以通過正則匹配來決定程式碼片斷是否可用,比如判斷在指定的 if 語句裡面才起作用的程式碼片斷,定義格式如下:
snippet 觸發字元 “描述” “表示式” 引數
比如我們定義一個 只有 在上一行以 if (DEVELOPMENT) {
開頭才可以展開的程式碼片段
1 2 3 |
snippet dbg "if (DEVELOPMENT) dbg" "re.match('^if \(DEVELOPMENT\) \{', snip.buffer[snip.line-1])" be debugger; endsnippet |
常見用法
行內連續展開
這個常見於需要連續展開程式碼片段的情況,比如,有兩個片段,一個列印變數,一個處理 JSON 序列化。這時需要使用引數選項 i
n-word
使用正則程式碼片段
通常寫程式碼的時候需要使用 log, print 等來列印上下文中的變數。使用普通片段按 cl 展示 console.log() 然後把變數字元複製進括號,這樣操作會比較複雜。使用正則來動態匹配前面的字元可以很好的解決這個問題
1 2 3 4 5 6 7 8 9 10 11 12 |
# 展開 console.log snippet "([^\s]\w+)\.log" "console.log(postfix)" r console.log(`!p snip.rv = match.group(1)`)$0 endsnippet # 當前行轉換成大寫 snippet "([^\s].*)\.upper" "Uppercase(postfix)" r `!p snip.rv = match.group(1).upper()`$0 endsnippet # 上一個單詞轉換成小寫 snippet "([^\s]\w+)\.lower" "Lowercase(postfix)" r `!p snip.rv = match.group(1).lower()`$0 endsnippet |
動圖演示
注意:正則程式碼片段只適用於單行文字處理,如果是多行轉換還是得用到下面的 python + VISUAL 程式碼片段來處理
使用 python 直譯器 + VISUAL 模式實現程式碼註釋功能
通常我們需要使用一大堆外掛來實現各種程式碼的註釋功能。不過 Ultisnips 提供了 VISUAL 模式可以提取 vim 可視模式中選擇的內容到程式碼片段裡面,於是我們就可以結合起來製作一個具有註釋功能的程式碼片段
流程大概是這樣的:
- 進入 vim 可視模式,選擇要註釋的內容
- 按 tab,清除選擇內容
- 輸入程式碼片段觸發字元,按 tab 完成
由於實現的 python 程式碼相對複雜一些,主要分成兩個方法。單行註釋和多行註釋,注意 Ultisnips 中可以直接寫 python 但是大段的方法建議放在外掛目錄下面的 pythonx 目錄下面,使用的時候在對應的程式碼片段中的全域性 python 程式碼 global !p
引入即可
單行註釋(pythonx/javascript_snippets.py):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
def comment(snip, START="", END=""): lines = snip.v.text.split('\n')[:-1] first_line = lines[0] spaces = '' initial_indent = snip._initial_indent # Get the first non-empty line for idx, l in enumerate(lines): if l.strip() != '': first_line = lines[idx] sp = re.findall(r'^\s+', first_line) if len(sp): spaces = sp[0] break # Uncomment if first_line.strip().startswith(START): result = [line.replace(START, "", 1).replace(END, "", 1) if line.strip() else line for line in lines] else: result = [f'{spaces}{START}{line[len(spaces):]}{END}' if line.strip() else line for line in lines ] # Remove initial indent if result[0] and initial_indent: result[0] = result[0].replace(initial_indent, '', 1) if result: return '\n'.join(result) else: return '' |
多行註釋:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
def comment_inline(snip, START="/* ", END=" */"): text = snip.v.text lines = text.split('\n')[:-1] first_line = lines[0] initial_indent = snip._initial_indent spaces = '' # Get the first non-empty line for idx, l in enumerate(lines): if l.strip() != '': first_line = lines[idx] sp = re.findall(r'^\s+', first_line) if len(sp): spaces = sp[0] break if text.strip().startswith(START): result = text.replace(START, '', 1).replace(END, '', 1) else: result = text.replace(spaces, spaces + START, 1).rstrip('\n') + END + '\n' if initial_indent: result = result.replace(initial_indent, '', 1) return result |
程式碼片段定義:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
global !p from javascript_snippets import ( comment, comment_inline ) endglobal # ... snippet c "Toggle comment every single line" `!p snip.rv = comment(snip, START='// ', END='') `$0 endsnippet snippet ci "Toggle comment inline." `!p snip.rv = comment_inline(snip, START="/* ", END=" */") `$0 endsnippet |
動圖演示
不同的語言可以在對應的片段檔案中定義並傳入註釋符號引數即可,有了這個功能就可以愉快的刪除其它的 vim 註釋外掛了 ?