VimScriptでスネークケース、キャメルケースを切り替える拡張をつくってみる

この記事は [ユニークビジョン株式会社 Advent Calendar 2021](https://qiita.com/advent-calendar/2021/uniquevision) 8日目の記事です。

VimScriptへの理解を深めるため、VimScriptでスネークケース、キャメルケースを切り替える拡張をつくってみます。

# 作成中のスクリプトを読み込む
作成中のスクリプトは以下のコマンドで読み込むことができます。

```vimscript
:so %
```

`so`は`source`のエイリアスです。
開発中は何度もファイルを読み込みなおすので、上記コマンドは何度も実行することになります。

# 選択中の文字列を取得する

## 現在選択している行を出力する
`getline()`を使います。

```vimscript
echo getline("'<")
```

`getline()`は、渡した引数の行の文字列を取得することができます。
`'<`は選択の先頭を指すので、`getline("'<")`は選択の先頭の行の文字列を取得できます。

## 現在選択している行の列数を取得する
`col()`を使います。

```vimscript
echo col("'<")
```

`col()`は、渡した引数の列数を取得することができます。
先頭を指しているときは`1`が返ってくるため、配列の添え字に使うときは気を付ける必要があります。

## 選択している文字列を取得する
`getline()`と`col()`を組み合わせます。

```vimscript
let selected_line = getline("'<")
let first = col("'<") - 1
let last = col("'>") - 1
let result = selected_line[first : last]
echo result
```

`getline("'<")`で選択中の行の文字列を取得し、
`col("'<")`と`col("'>")`で選択範囲の列数を取得します。
(`'>`は選択の末尾を指します。)
行の文字列をスライスして選択部分を取り出します。
(スライスは末尾が含まれます。)
配列は0オリジンなため、`col()`で取得した値から -1 しています。

## 関数にまとめる
選択文字列を取得する処理を、1つの関数にまとめます。

```vimscript
function! s:get_visual_selection() abort
  " 複数行には未対応
  let l:selected_line = getline("'<")
  let l:first = col("'<") - 1
  let l:last = col("'>") - 1
  return l:selected_line[l:first : l:last]
endfunction
```

`function`を使うことで一連の処理を1つの関数にまとめることができます。
`!`は同名の関数があったときに上書きします。

`abort`はエラーが発生したときにその場で処理を終了させるためのキーワードです。
VimScriptではエラーが発生しても処理を継続するため必要になります。

`s:`や`l:`はスコープを指しています。
`s:`はそのファイル内でのみ有効です。
`l:`は関数内でのみ有効です。

# 別のケースに置換する
## 文字列を置換する
`substitute()`を使います。

```vimscript
echo substitute('hogehoge', 'hoge', 'fuga', 'g')
" fugafuga
```

`:help substitute()`で見ることができますが、
`substitute({expr}, {pat}, {sub}, {flags})`という定義になっています。

`{expr}`は、検索対象の文字列
`{pat}`は、検索文字列
`{sub}`は、置換文字列
`{flags}`は、置換のルールを指定します。

`{flags}`に`'g'`を指定すると、あてはまる全ての文字列が置換されます。
`{flags}`が空文字列のときは最初にあてはまる文字列だけが置換されるため、すべて変換したいときは`'g'`を指定する必要があります。

## 選択文字列をスネークケースにした文字列を取得する

```vimscript
function! ToSnake() abort
  let l:selection = s:get_visual_selection()
  let l:pat = '\u'
  let l:result = substitute(l:selection, l:pat, '_\l&', 'g')
  echo l:result
endfunction

function! s:get_visual_selection() abort
  " 複数行には未対応
  let l:selected_line = getline("'<")
  let l:first = col("'<") - 1
  let l:last = col("'>") - 1
  return l:selected_line[l:first : l:last]
endfunction
```

`ToSnake()`をたたくと選択文字列をスネークケースにしたものが出力されます。

`substitute`の`{pat}`には正規表現を使用することができ、
`\u`は大文字アルファベットが対象になります。
(helpには`[A-Z]`と同じとありますが、`[A-Z]`だと上手く動かなかったため、何か環境依存の問題があるかもしれません。)

`{sub}`の`\l`は、小文字にするという意味があり、`&`はマッチした文字列を指しています。
なので`\l&`はマッチした文字列を小文字にする、という意味があります。

## 選択文字列をキャメルケースにした文字列を取得する

```vimscript
function! ToCamel() abort
  let l:selection = s:get_visual_selection()
  let l:pat = '_\(\l\)'
  let l:result = substitute(l:selection, l:pat, '\u\1', 'g')
  echo l:result
endfunction

function! s:get_visual_selection() abort
  " 複数行には未対応
  let l:selected_line = getline("'<")
  let l:first = col("'<") - 1
  let l:last = col("'>") - 1
  return l:selected_line[l:first : l:last]
endfunction
```

スネークケースとの差異は`{pat}`と`{sub}`です。
`{pat}`の`\l`は小文字にマッチします。
`{sub}`の`\u`は大文字に変換するという意味で、`\1`はサブマッチの1つめを指しており、
`\u\1`で、`_\(\l\)`の`()`の中身だけを大文字にするという意味があります。

# 変換した文字列でバッファを置き換える

## 行番号を取得する
`line()`を使います。

```vimscript
echo line("'<")
```

`line()`は渡した引数の行番号を取得することができます。

## バッファを置き換える
`setline()`を使います。

```vimscript
setline(line("."), "hogehoge")
```

`setline({lnum}, {text})`という定義になっており、`{lnum}`の行を`{text}`に置換する、という意味があります。
なので、`setline(line("."), "hogehoge")`は現在行を"hogehoge"に置換します。
(`.`は現在位置を指します。)

## 変換した文字列でバッファを置き換える

```vimscript
function! ToCamel() abort
  let l:selection = s:get_visual_selection()
  let l:pat = '_\(\l\)'
  let l:result = substitute(l:selection, l:pat, '\u\1', 'g')
  call s:replace_line(l:result)
endfunction

function! ToSnake() abort
  let l:selection = s:get_visual_selection()
  let l:pat = '\u'
  let l:result = substitute(l:selection, l:pat, '_\l&', 'g')
  call s:replace_line(l:result)
endfunction

function! s:get_visual_selection() abort
  " 複数行には未対応
  let l:selected_line = getline("'<")
  let l:first = col("'<") - 1
  let l:last = col("'>") - 1
  return l:selected_line[l:first : l:last]
endfunction

function! s:replace_line(replaced) abort
  let l:selected_line = getline("'<")
  let l:left_last = col("'<") - 2
  let l:right_first = col("'>")
  let l:result = l:selected_line[0 : l:left_last] . a:replaced . l:selected_line[l:right_first : -1]
  call setline(line("'<"), l:result)
endfunction
```

`s:replace_line()`関数を新しく作りました。
元の行文字列と、変換後の文字列を組み合わせて新しい行文字列を作り、
`setline()`で置き換えるといった処理になっています。

これでスネークケース、キャメルケースへの変換ができるようになりました。
残りはキーマッピングするなどありますが、本記事では省略します。

# 最後に
実際に動かせるプラグインを書くことでVimScriptへの理解が深まりました。
いままでは公開されているスクリプトをただ使うだけでしたが、
これからは手直ししたり、新しい拡張を作れる足がかりになったのではないかと思います。

# 参考
[Vim scriptテクニックバイブル～Vim使いの魔法の杖](https://gihyo.jp/book/2014/978-4-7741-6634-6)