HyperSnips:VSCode上的自動補全神器

讓幻想飛發表於2021-12-16

發現一個小眾但是巨好用的VSCode自動補全外掛:HyperSnips

image

作者顯然受到了 這位小哥 的啟發,將 Vim Ultisnips 的大部分功能搬到了VSCode上。並用 JavaScript 重寫了 Python 的部分程式。有了這個外掛,你不需要使用令人望而生畏的Vim,就能愉快地寫 LaTeX/markdown,甚至在數學課上用 markdown/LaTeX 做筆記。

更有趣的是,由於這個外掛具有可程式設計能力,你甚至可以拿到 VSCode 的 API 介面,用這個外掛寫“外掛”,或者幹一些有趣的事情。

你可以實現 這篇文章 中的幾乎所有功能,足以看出其強大:

  • 一個自動變大的盒子:
    image

  • 可以自動給\(\LaTeX\)的命令加斜槓\
    image

  • 可以自動展開分數(//\frac{}{}1/\frac{1}{})

  • 使用字尾片段修改字母樣式(Ecal\mathcal{E}q_idot\dot{q_i}等等);
    image

    image

  • 自動生成m*n矩陣:
    image

以上演示使用markdown完成(不是LaTeX,但是LaTeX同樣可以使用該外掛),預覽是Markdown Preview Enhanced外掛。這是非常好用的markdown同步預覽外掛,只是更新有些慢。

使用方法

安裝

  • 下載VSCode,在外掛市場安裝 HyperSnips
    注意安裝後右鍵外掛選擇“安裝另一個版本”,安裝0.2.3版本。最新的0.2.4有嚴重的bug,會導致無法使用,暫時不要更新。(2021/12/16)

image

  • 按下 Ctrl+Shift+P ,搜尋 Open Snippets Directory ,開啟存放程式碼片段的資料夾
    新建 *.hsnips 檔案,如希望在LaTeX補全就建立latex.hsnips,希望補全markdown就建立markdown.hsnips 。如果希望使用全域性片段,則建立 all.hsnips
    然後就可以輸入片段了。

基本用法

輸入片段的方法基本按照 Vim Ultisnips 的方法,參照幫助文件。這和VSCode自帶的片段是對應的。同樣也可以使用佔位符,可以使用Tab跳來跳去(如$1表示第一個Tab Stop,$2第二個,特別的,$0是最後一個,${1:text}表示第一個佔位符中有預設的文字)。

舉例:

snippet hello "greeting"
Welcome to ${1:HyperSnips}!
endsnippet

snippetendsnippet標誌了片段的開始和結束,hello是觸發字元。"greeting"是片段名稱(沒什麼用)。第二行就是要展開的片段。如果用 VSCode 內建片段的寫法(json),就是:

{
    "greeting":{
        "perfix":"hello",
        "body":"Welcome to ${1:HyperSnips}!"
    }
}

基本的功能都有的。輸入片段(或者其中的幾個字母),按下 Tab ,你可以得到:

image

自動展開

使用 A 標記,可以自動展開:

snippet hello "greeting" A
Welcome to HyperSnips!
endsnippet

這樣只要輸入觸發字元,整個片段就會立即展開。上面的快速輸入,幾乎都是使用了A標記。另外還有iwM等標記,詳見幫助文件( @infinity1900 翻譯,感謝!):

A:自動展開。預設是按下tab時觸發程式碼段,Flag 設定為 A,程式碼段將在 Trigger 匹配後立即啟用,這對於 regex 正規表示式程式碼段特別有用。

i: 詞內展開*。預設情況下,觸發器僅在前面帶有空格字元時才匹配觸發。如果設定 為 i,則不管前面的字元如何,都會觸發帶有此選項的程式碼段,例如,可以在單詞的中間觸發程式碼段。

w: 單詞邊界*。使用此選項時,觸發器將在單詞邊界處匹配。

M: Multi-line mode - By default, regex matches will only match content on the current line, when this option is enabled the last hsnips.multiLineContext lines will be available for matching.

  • 只對非正規表示式有效

內嵌程式碼、動態片段、正則識別

HyperSnips支援在片段中使用JavaScript程式碼。如下面可以返回當前日期:

snippet dategreeting "Gives you the current date!"
Hello from your hsnip at ``rv = new Date().toDateString()``!
endsnippet

image

其中,JavaScript片段使用四個 ` 括起來,其中rv表示返回的片段。

  • 一開始那個小盒子,是外掛中的示例程式碼:
snippet box "Box" A
``rv = '┌' + '─'.repeat(t[0].length + 2) + '┐'``
│ $1 │
``rv = '└' + '─'.repeat(t[0].length + 2) + '┘'``
endsnippet

同樣支援正規表示式。如“自動數字下標”的片段是這樣寫的:

snippet `(?<=[A-Za-z])(\d)` "auto subscript" iA
_``rv = m[1]``
endsnippet

效果就是:a1a_1m[1]表示正則識別處的第一個組合。除此之外還有t變數,用的不多。

  • 自動展開分數 會麻煩一些。

image

//\frac{}{} 最容易,直接寫就行;
如果要實現1/\frac{1}{}:

snippet `((\d+)|(\d*)(\\)?([A-Za-z]+)((\^|_)(\{\d+\}|\d))*)/` "Fraction no ()" A
\frac{``rv = m[1]``}{$1}$0
endsnippet

而如果要實現(1+2)/\frac{1+2}{} ,可能還需要藉助JavaScript:

snippet `^.*\)/` "Fraction with ()" A
``
    let str = m[0];
    str = str.slice(0, -1);
    let lastIndex = str.length - 1;

    let depth = 0;
    let i = str.length - 1;

    while (true) {
        if (str[i] == ')') depth += 1;
        if (str[i] == '(') depth -= 1;
        if (depth == 0) break;
        i -= 1;
    }

    let results = str.slice(0, i) + "\\frac{" + str.slice(i+1, -1) + "}";
    results += "{$1}$0";
    rv = results;
``
endsnippet

這和原作者的邏輯是一樣的,但是轉寫成了JavaScript。

全域性程式

.hsnips檔案開頭加入如下片段:

global

endglobal

即可在其中定義全域性函式。這裡定義的函式可以在當前檔案中的任何片段中使用。
比如生成矩陣的函式:

// generate matrix

function gen_matrix(nrow, ncol, index) {
    let results = "\n";
    let order = 1;
    for (var i=0; i<nrow; i++){
        results += '    ';
        for(var j = 0;j <ncol;j++){
            results += "$" +(order ).toString() + ((j == ncol -1) ?  " \\\\\\" : " & ");
            order ++;
        }
        results += index ? "\n" : "";
    }
    return results;
}

再使用以下片段:

snippet `(bm|pm|m|vm)at([1-9])([1-9])` "matrix" iA
\begin{``rv = m[1]``atrix}``
    rv = gen_matrix(m[2],m[3],1);
    ``\end{``rv = m[1]``atrix}$0
endsnippet

bmat33 就能給出一個3*3 的\bmatrix 矩陣:

image

環境/作用域

有時我們希望片段只有在數學環境中展開,或者只在文字環境中展開。比如,你不希望在正文中寫“green”的時候突然展開成“gr_en"吧(下標自動展開)。HyperSnips提供了這一方法:

在開頭的global 中,定義如下:

function math(context) {
    return context.scopes.some(s => s.includes("math"));
}

這個作用是,檢查當前文字的標記和作用域(token & scope),如果其中含有math,判斷為數學環境並返回true。然後再我們需要的片段前面加上:

context math(context)

就可以讓片段只在數學環境中展開。如前面片段如果寫成:

context math(context)
snippet // "frac" A
\frac{$1}{$2}
endsnippet

效果:

image

檢查哪一種環境,可以按下Ctrl+Shift+P ,使用開發人員: 檢查編輯器標記和作用域(Inspect Editor Tokens and Scopes) 命令,得到如下視窗,框中就是作用域(scope)。這個標誌了當前所處的環境。

image

更多玩法

上述我們提到,HyperSnips支援Javascript,且放在global中的函式可以全域性通用。考慮到VSCode外掛本質上也是JavaScript寫的,那麼一些小外掛能做的事情,HyperSnips為什麼不能做?

在global區域中放置如下程式碼:

const vscode = require("vscode");
var editor=vscode.window.activeTextEditor
var document=editor.document

然後發現,你可以使用 VSCode 的 API 介面了!

可能只需要幾行程式碼,你就能做出一個“外掛”!

好麼,既然能夠程式設計了,那就……沒有什麼好怕的了( ̄︶ ̄*))

  • 比如,四行程式碼加在global區域中:
setInterval(function(){myTimer()},1000);
function myTimer(){
    vscode.window.setStatusBarMessage(new Date().toLocaleTimeString());
}

你就得到了一個在狀態列顯示時間的小外掛!

image

  • 再比如,在global 中加入如下函式
function cycle(arr,str){
    let count = 0 ;
    while (str!=arr[++count]&&count<arr.length);
    return arr[(count+1)%arr.length]
}

然後考慮到分號;在LaTeX中幾乎用不到,而且Tab太容易和其他快捷鍵衝突,於是放一些類似於這樣的片段

snippet `(\\rightarrow|\\Rightarrow|\\longrightarrow|\\Longrightarrow|\\to|\\implies)(| );` "change ⇨" Ai
``
let r=["\\rightarrow","\\Rightarrow","\\longrightarrow","\\Longrightarrow","\\to","\\implies"];
rv=cycle(r,m[1])+m[2];
``
endsnippet

然後有趣的就來了:

image

按下分號,可以換一個樣式。雖然沒什麼用,但是很好玩啊哈哈哈哈 靈感來自 @張同學

甚至括號也不在話下(當然程式就更長一些):

image

  • 再比如,原生的hyperSnips外掛沒有提供{VISUAL} 變數(當前選中的文字),不妨利用API寫一個:在global里加上:
// get ActiveTextEditor

vscode.window.onDidChangeActiveTextEditor((e) => {
    editor=vscode.window.activeTextEditor;
    document=editor.document
});


let selectedText = "";

// get selected text

vscode.window.onDidChangeTextEditorSelection((e) => {
    const newSelectedText = e.textEditor.document.getText(e.selections[0]);
    if (newSelectedText) {
        selectedText = newSelectedText;
    }
});

// return text

function VISUAL() {
    let sText=selectedText.replace(/\\\\/g,"\\\\\\ ").replace(/\}/g,"\\}");
    selectedText="";
    return sText;
}

然後比如配置如下的片段:

context math(context)
snippet `Aln` "align" iA
\begin{aligned}
    ``rv = VISUAL();``$0
\end{aligned}
endsnippet

就可以使用了
image

儘管有時有bug,但是至少管用啊

所以現在,僅剩的限制就是想象力了吧

Bug

  • 當疊加了太多的佔位符時,可能出現多游標。這是VSCode自身的Bug
  • 偶爾的環境/作用域識別錯誤,需要重開一下檔案

參考:

歡迎關注、點贊、交流~~~

相關文章