Fishのスニペットプラグインが物凄く便利

私の愛用しているFishのプラグインのひとつに[expand](https://github.com/oh-my-fish/plugin-expand)が存在します。
このプラグイン、まあすごいものでして個人ランキングでトップ3に入るほどです。

## このプラグインは何？

至って簡単、プレースホルダーのないスニペットプラグインです。



たとえば config.fish に以下の記述をしたとしましょう。

```sh:config.fish
expand-word -p '^nil$'    -e "echo '>/dev/null ^&1'"
expand-word -p '^github$' -e "echo https://github.com/"
```

ここでターミナルを開き、pオプションで入力した正規表現にマッチする文字列を入力し、Tabを入力すると

```sh:ターミナル
# Before (カーソル位置は各コマンドラインの末尾にあるとする)
$ echo hoge fuga nil
$ git clone github

# After
$ echo hoge fuga >/dev/null ^&1
$ git clone https://github.com/
```

このようにeオプションで指定したコマンドの出力で置換してくれるのです。



加えて、このプラグインは `fzf` や `peco` と連携して置換内容を決定できる機能もあります。

```sh:config.fish
expand-word -p '^git$' -e 'echo git@github.com:'
expand-word -p '^git$' -e 'echo https://github.com'
expand-word -p '^git$' -e 'echo https://gits.github.com'
expand-word -p '^git$' -e 'echo https://rawgithubusercontent.com'
```

```sh:ターミナル
# Tabを入力するとfzfやpecoが起動、上記4つから選択できる
$ git clone git
```



もちろん単語の指定や出力コマンドなどは自由に変えることができるので、一般的なスニペットプラグイン以上に発展的な使い方ができる代物です。

特に入力文字数が多くなりがちな対話シェルにおいては非常に有用なプラグインだと思っています。他にきちんと説明している記事がないのが驚きというくらいオススメです。

---

……と、結構推してはいるのですが。

私としては「まだまだ可能性があるのでは？」と思うところがあるのです。
ということで以下のような活用案を考えてみました。



## もっと便利に：置換対象を広げる

### 問題

まず私としては気に入らない部分がひとつ。
**空白を含む置換キー**に対応していないのです。

```sh:ターミナル
$ expand-word -p '^t1 t2 t3$' -e "echo OK!"

# Before
$ echo t1 t2 t3

# After
$ echo t1 t2 t3
```

何に困るかはいつか述べるとして、とりあえず対応させましょう。



### 調査

調べてみると、以下の2つは等価になります。

```sh
expand-word -p 'regex' -e 'cmd'
expand-word -c "expand:match 'regex'" -e 'cmd'
```

`expand:match` とは何者でしょうか。

```sh
function expand:match -d 'Matches a pattern against the current word' -a pattern
  commandline -t | grep -E -q "$pattern"
end
```

`commandline -t`、つまり空白区切りで得られる現在位置のトークンを利用していることが分かりますね。これが上記問題の原因と思われます。



加えて、実際に置換する処理にあたる `expand:choose-next` にもこんな記述が。

```sh
if test -n "$replacement"
    commandline -t -r "$replacement"
end
```

やはりこちらも置換対象は現在位置のトークンになっています。こりゃだめなわけだ。



### 実装

定義部分の代替として `commandline -bc` を使ってみます。行頭から現在のカーソル位置までを取得するコマンドですね。

```sh
function expand:morematch -a pattern
	# expand:replaceに利用するため、一致部分を出力するようにする
	commandline -bc | grep -E -o "$pattern"
end
```



そして `commandline -t -r` に代わる部分も新たに実装。

```sh
# commandline -bc -r はエラーになる
# commandline -b -r した後に commandline -C NUM することで再現する
function expand:replace -a before after
	test -n "$before"; or return
	test -n "$after";  or return
	
	set -l bc (commandline -bc)
	set -l pattern "$before\$"
	if string match -qr "$pattern" "$bc"
		set -l new_bc (string replace -r "$pattern" "$after" "$bc")
		commandline -b -r (string replace "$bc" "$new_bc" (commandline -b))
		commandline -C (string length $new_bc)
	end
end
```



これらを用いて各種ソースを修正。

```diff
  # plugin-expand/functions/expand:execute.fish

  set -g __expand_replacements
+ set -g __expand_matched
  
  for expansion in $__expand_expanders
  set -l expansion (echo $expansion)
  
  if eval "$expansion[1]" > /dev/null
    if set -l replacements (eval "$expansion[2]" | sed '/^\s*$/d')
      set __expand_replacements $__expand_replacements $replacements
+     set __expand_matched $__expand_matched (eval "$expansion[1]")
    end
  end
```

```diff
  # plugin-expand/functions/expand:choose-next.fish

  if test -n "$replacement"
-   commandline -t -r "$replacement"
+   expand:replace "$__expand_matched[1]" "$replacement"
  end
```



そして結果はこちら。

```sh
# Before
$ expanded-word -c "expand:morematch 't1 t2 t3\$'" -e "echo OK!"
$ echo t1 t2 t3

# After
$ echo OK!
```

やったぜ。



## さらに便利に：プレースホルダーに対応させる

上述した通り、このプラグインはスニペットプラグインのようでありながらもプレースホルダーに対応していません。あくまで「カーソル位置の単語を指定のコマンドで置換する」ぐらいの役割しかないためです。

しかしこういう場面を考えてみましょう。

```sh:ターミナル
# After
$ git clone htpps://github.com/
```

これ、以下のようなプレースホルダーが用意できればよりよくなるのではないでしょうか？

```sh:ターミナル
# なんらかのキーを押すと{n: ...}に順次進んでいく
$ git clone https://github.com/{1: username}/{2: reponame}.git
```



### 実装

まずはプレースホルダーの書式を考えてみましょう。
一般的なのは上記のように括弧で囲むか、`$1` のように変数っぽくする方法があります。

ただし後者については `awk` のように他コマンドで使用することもありますし、隣接する文字列との区別ができなくなる可能性もあります。ここは[Fishの文化](http://fish.rubikitch.com/test/#75e352304489e25c3e0500db1af330d3)を利用して滅多に使われないであろう `[[]]` で囲った部分をプレースホルダーにしましょう。

```sh
$ git clone https://github.com/[[]]/[[]].git
```



次にプレースホルダーへのジャンプについて考えましょう。
どのキーを押すことでジャンプするのかはユーザーの設定に任せるとして、ここではただジャンプする機能だけを考えてみます。

前述の通り、カーソル位置までのコマンドライン文字列は `commandline -bc` で取得でき、その後のカーソルジャンプは `commandline -C` で実現できます。

```sh
function jump_to_first_placeholder
	# 最初の[[]]の中ににジャンプするだけ
	set -l bc (commandline -bc)
	
	if string match -q "*[[]]*" "$bc"
		# プレースホルダーがあるならジャンプ
	    commandline -C (string replace -r ']].*$' '' "$bc" | string length)
	end
end

function jump_to_next_placeholder
	# 次の[[]]の中にジャンプするだけ
	set -l b  (commandline -b)
	
	set -l bc (commandline -bc)
	# ブレースホルダー全体を含んだ状態にしておく
	# TODO: [[plugi|n]] のようにプレースホルダー内の末尾にカーソルがない場合に対応させる
	string match -qr '.*]]$' "$bc"
		or set -l bc "$bc]]"
		
	set -l after_c (string replace "$bc" '' "$b")
	
	if string match -q "*[[]]*" "$after_c"
		set -l offset   (string length "$bc")
		set -l distance (string replace -r ']].*' '' "$after_c" | string length)
		commandline -C (math "$offset + $distance")
	else
		# 次にジャンプする場所がなければ最初のプレースホルダーへ
		jump_to_first_placeholder
	end
end
```

ついでにプレースホルダーを外す作業も必要ですね。

```sh
function exist_placeholder
	string match -qr '(\[{2)|(\]{2})' (commandline -b)
end

function remove_placeholders_force
	# コマンド実行直前に実行することを想定
	commandline -b -r (string replace -ar '(\[{2)|(\]{2})' '' (commandline -b))
end
```



とりあえず物は揃ったのでキーバインドを設定してみましょう。

```sh:config.fish
# Ctrl-j (\n) でスニペット展開とプレースホルダーの移動
bind \n  expand:execute jump_to_placeholder
function jump_to_placeholder
	if set -qg __placeholder_jumped
		jump_to_next_placeholder
	else
		set -g __placeholder_jumped
		jump_to_first_placeholder
	end
end

# Enter や Ctrl-m (\r) で全プレースホルダー除去してからコマンド実行
# プレースホルダーがない場合はそのままコマンドを実行
bind \r  remove_placeholders execute
function remove_placeholders
	if exist_placeholder
		remove_placeholders_force
		set -e -g __placeholder_jumped
	end
end
```



初期値とかホルダーの説明とかほしいところですが、とりあえずはこんな感じでいいでしょう。

うまくいけたらプラグイン化するかもしれません。


## 参考リンク

- [公式](https://fishshell.com)
  - [ドキュメント](https://fishshell.com/docs/current/index.html)
- [oh-my-fish/plugin-expand](https://github.com/oh-my-fish/plugin-expand)
- [全訳！fishシェル普及計画](http://fish.rubikitch.com/)