動的なスニペットを実現する HyperSnips を使いこなす

## Abstract

VSCode使いのみなさん、スニペット機能使ってますか？

![](https://code.visualstudio.com/assets/docs/editor/userdefinedsnippets/ajax-snippet.gif)
(公式サイトより)

自作スニペットを作成して、コーディングを快適に行っている人もたくさんいることでしょう。ですが、通常の自作スニペットには**致命的な弱点があります**。それは**静的なスニペットしか定義できない**ということです。
つまり、「正規表現などを利用して、動的にスニペットを定義することができない」ということです。

本記事では、正規表現も利用可能な、動的なスニペットを定義することができる HyperSnips を紹介します。

https://marketplace.visualstudio.com/items?itemName=draivin.hsnips&ssr=false#overview

## 本記事の環境

- vscode 1.85.1
- hypersnips v0.2.8
- windows 10

ショートカットキーなどはwindowsで記載します。
Mac等のユーザーは適宜読み替えてください。

## HyperSnips とは

HyperSnipsとは、**動的スニペット**を実現するVSCodeの拡張機能のひとつです。
次の公式のデモが使用例になっています。

![](https://github.com/draivin/hsnips/raw/HEAD/images/welcome.gif)

ユーザーが入力した内容に応じて、**スニペットが変化している**ことがわかります。
本記事では、動的(静的)スニペット: ユーザーの入力に応じて変化することができる(できない)スニペット として定義して用います。

動的スニペットは何の役に立つのかと言えば、例えばこのような遊び方ができます。

https://x.com/Kyo_s_s/status/1718837105353515015?s=20

~~私はこのツイートからこの拡張機能を知りました。~~

## usage

スニペットを定義するためには、スニペットファイルをもちろん記述する必要があります。
HyperSnips用のスニペットファイルは`.hsnips`ファイルです。

コマンドパレットから`HyperSnips: Open snippets directory`を実行して、設定ファイルを置いておくディレクトリを開けます...(と公式には書いてましたが自分の環境ではダメでした...?)
で、手動で探すなら下記になります。

- Windows: %APPDATA%\Code\User\hsnips\(language).hsnips
- Mac: $HOME/Library/Application Support/Code/User/hsnips/(language).hsnips
- Linux: $HOME/.config/Code/User/hsnips/(language).hsnips

(language) は、言語が入ります。(どの言語で使うかに対応します。)

:::note info
設定 `hsnips.hsnipsPath` でこのディレクトリは別の場所に設定できます。
:::

全ての言語モードで有効にするためには、`all.hsnips`を作成しておきます。

### スニペットファイルの文法

スニペットファイルは次のような形になっています。

```
# global ブロック
global
// javascript code
endglobal

# スニペット定義ブロック
context expression
snippet trigger "description" flags
body
endsnippet
```

global ブロックはcontextや、スニペットを動的に変化させるときに利用する関数を定義できます。

#### スニペット定義ブロック

何書いてるか意味わからないと思うので、まずはHello worldから。

##### Basic

```all.hsnips
snippet hello "hello"    # トリガー 説明
hello, world    # スニペット本体
endsnippet
```

保存してから適当なファイルでhelloと打つと、スニペットが登録されているはずです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/483769/306114ce-b8e8-babd-4f23-8e856c3db383.png)

ここで、Aフラグを追加してみます。
A : **A**utomatic snippet expansion; トリガーワードに一致したら即座にスニペットを展開するフラグです

```all.hsnips
snippet hello "hello" A    # トリガー 説明 flag
hello, world    # スニペット本体
endsnippet
```

適当なファイルで hello と打つと、変換候補を見るまでもなく一瞬でスニペットが呼ばれるようになります。
なんとなく基礎が見えてきましたか？

次に、スニペット本体にJavaScriptの結果を挿入する方法を紹介します。

```all.hsnips
snippet hello "hello" A
`` rv = 'hello, world.'.toUpperCase() ``
endsnippet
```

スニペット本体は、通常通りの文字列だけでなく、**JavaScriptの実行結果**を挿入することができます。
JavaScriptを挿入するには、下記の形で利用します。
\`\` (JavsScript コード) \`\`
ただし、表示されるのは自動変数 `rv` (return value?) の値のみです。
なので基本的には \`\` rv = (hogefuga) \`\` の形が基本になります。

##### tab変数

それでは最初のデモを再現するスニペットを見てみます。

```all.hsnips
snippet box "Box" A
``rv = '┌' + '─'.repeat(t[0].length + 2) + '┐'``
│ $1 │
``rv = '└' + '─'.repeat(t[0].length + 2) + '┘'``
endsnippet
```

まず最初のスニペットの宣言部分。
`box "Box" A`: `trigger description flag`に対応。
- box : スニペットのトリガーワード
- "Box" : スニペットの説明
- A : **A**utomatic snippet expansion; トリガーワードに一致したら即座にスニペットを展開する

もうここは簡単ですね。問題はスニペット本体でしょう。

\`\`rv = '┌' + '─'.repeat(t[0].length + 2) + '┐'\`\`
↑ `t` って誰やねん？！？？！

まず前提知識として、\\$1, \\$2, ... を知る必要があります。
これはタブステップのための文字列です。普通のスニペットを触ったことがある人は知っていると思います。

わからない人は、簡単なスニペットで試してみると良いかと思います。

```all.hsnips
snippet test "test" A
Hello, $1.
endsnippet
```

\\$1として部分に、カーソルが飛んで入力を待つような形になったでしょう。

自動変数 `t`: **t**abs はユーザーが入力中の内容 \\$1, \\$2 を格納してる変数です。
`t[0]`は\\$1に対応します。 (0-indexなのでややこしいですが...)
下記のスニペットを定義して試してみるとよくわかります。

```all.hsnips
# `t`: tabs. $1, $2, ...の値を配列として持っている
snippet ttest "ttest"
``rv = t[0]`` $1 $2 ``rv = t[1]``
endsnippet
```

この知識を持ったうえで、下記のスニペット定義とデモを見比べてみると、確かにユーザーが入力している内容に応じて上下のバーが増えていることがわかりますね。

```all.hsnips
snippet box "Box" A
``rv = '┌' + '─'.repeat(t[0].length + 2) + '┐'``
│ $1 │
``rv = '└' + '─'.repeat(t[0].length + 2) + '┘'``
endsnippet
```

![](https://github.com/draivin/hsnips/raw/HEAD/images/welcome.gif)

##### 正規表現によるトリガーの設定

メールアドレスの正規表現にマッチしたらトリガーするスニペット、なんてものも定義できます。
マッチした結果は自動変数 `m`: **m**atch で受け取れます。
`m[0]`はマッチした文字列本体がはいっており、`m[1]`以降に()で囲った部分を取ってこれます。

```all.hsnips
# `m`: match. 正規表現のマッチ結果が入っているオブジェクト。
snippet `([\w\-._]+)@([\w\-._]+)\.[A-Za-z]+` "mtest"
m[0] = `` rv = m[0] ``
m[1] = `` rv = m[1] ``
m[2] = `` rv = m[2] ``
endsnippet
```

##### flag 一覧

A以外理解がまだできていません...のでひとまず公式からコピペ。

- A: **A**utomatic snippet expansion - トリガーにマッチしたら即座に展開する

- i: **I**n-word expansion* - By default, a snippet trigger will only match when the trigger is preceded by whitespace characters. A snippet with this option is triggered regardless of the preceding character, for example, a snippet can be triggered in the middle of a word.

- w: **W**ord boundary* - With this option the snippet trigger will match when the trigger is a word boundary character. Use this option, for example, to permit expansion where the trigger follows punctuation without expanding suffixes of larger words.

- b: **B**eginning of line expansion* - A snippet with this option is expanded only if the tab trigger is the first word on the line. In other words, if only whitespace precedes the tab trigger, expand.

- M: **M**ulti-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.

##### 自動変数一覧

`t`, `m` 以外にも、自動変数は存在します。

```all.hsnips
# `t`: tabs. $1, $2, ...の値を配列として持っている
snippet ttest "ttest"
``rv = t[0]`` $1 $2 ``rv = t[1]``
endsnippet

# `m`: match. 正規表現のマッチ結果が入っているオブジェクト。
snippet `([\w\-._]+)@([\w\-._]+)\.[A-Za-z]+` "mtest"
m[0] = `` rv = m[0] ``
m[1] = `` rv = m[1] ``
m[2] = `` rv = m[2] ``
endsnippet

# `w`: workspace. ワークスペースのURI文字列。
snippet wtest "wtest"
value: ``rv = w``
type : ``rv = (typeof w)``
endsnippet

# `path`: path of document. 今開いているファイルのURI文字列。
snippet pathtest "pathtest"
``rv = path``
endsnippet
```

#### global ブロック

スニペット本体のJSコードで呼び出せる関数や変数を定義できます。

後は、疑似的にVSCodeデフォルトのスニペットで使える組み込み変数を取得・利用するハックに利用したりします。

https://github.com/draivin/hsnips/issues/81

## 遊び方ギャラリー

### ファイル上でサクッと計算をする

https://x.com/Kyo_s_s/status/1718837105353515015?s=20

```all.hsnips
snippet `\beval ?(.*)=` "eval"
`` rv = eval(m[1]) ``
endsnippet
```

正規表現で = 手前の入力を受け取り、それをJSの`eval`で評価しています。

## まとめ

本記事では、動的スニペットを定義することができる拡張機能 HyperSnips を紹介しました。
ユーザーの入力に応じて、スニペットを変化させることで、色々と面白いことができることは間違いありません。
面白い拡張機能なのに、日本語はおろか英語の解説記事も特になさそうだったので、記事にしてみました。
もし面白いネタを思いついたら、ご自身で記事を書いてこの拡張機能を広めてください。

## 参考文献

https://zenn.dev/shena46/articles/latex-snippets-vscode

多分唯一の日本語の紹介記事。だけど文法の解説とかはない。