行頭に文字を挿入するキーボードショートカットを作成する

# 行頭に文字を挿入するキーボードショートカットを作成する

カーソルのある任意の行の行頭に文字を挿入したいことがあるだろう．しかしながら，これを実現するには少し面倒くさい．
例えば，キーボードショートカットで `snippet` から `TM_CURRENT_LINE` を利用する方法が第一に思いつくことがあるが，これでは上手く動作しない．

本記事では，Markdown において `>` を行頭に挿入することを例に，行頭に文字を挿入するキーボードショートカットの作成方法を考えたい．

## 今回作成したいもの

Markdown のブロッククォートを簡単に打つものを実現させたい．

次のような文に対して，各行を選択している状態であるキーボードショートカット（今回は `Alt+Q` とする）を押下すると，行頭に `>` が挿入される．

```markdown
引用 1
引用 2

    引用 3-1
    引用 3-2
```

　 ↓ 複数行を選択して `Alt+Q` を押下

```markdown
> 引用 1
> 引用 2

    > 引用 3-1
    > 引用 3-2
```

[insertBlockQuote]: https://i.gyazo.com/dfabe85b0dda0196ea20d0edff0edaea.gif "insert block quote"


## それまで試してみたこと

> [\# Snippet syntax \#\# Variables \- Snippets in Visual Studio Code](https://code.visualstudio.com/docs/editor/userdefinedsnippets#_variables)

VSCode ではデフォルトでスニペットを作成することが出来る．スニペットでは `TM_CURRENT_LINE` を利用することで，カーソルのある行を選択することが出来る．
したがって，これを利用して次のようなスニペットを作成すると実現できそうである．

```jsonc
// keybindings.json
  {
    "key": "alt+q",
    "command": "editor.action.insertSnippet",
    "args": {"snippet": "${TM_CURRENT_LINE/^(.*)$/> $1/}"},
    "when": "editorTextFocus && editorLangId == markdown",
  },
```

しかし，次のような結果を得る．これは期待した動作ではない．

![insert block quote: snippets](https://i.gyazo.com/5db44051363e42ed47ca3b2d446e7b4e.gif)

もちろん各 1 行をすべて選択すれば，このスニペットであっても上手く動作するが，このような選択は実際に操作するのは非常に面倒くさい．

したがって，このスニペットによる操作は棄却するべきだろう．

## multi-command を利用する

> [ryuta46.multi-command \- VSCode Marketplace](https://marketplace.visualstudio.com/items?itemName=ryuta46.multi-command)

これは，一連のコマンド操作を 1 つのコマンドとしてショートカットキーから呼び出すことが出来るようになる拡張機能である．具体的な設定方法は他の記事に譲ることとしたい．

まずは，実現したい操作を具体的に書き出してみる．次のようになるだろう．

1. カーソルを置く or 複数行を選択する
2. 各行にカーソルを置く（マルチカーソル化）：`editor.action.insertCursorAtEndOfEachLineSelected`
3. カーソルを行の先頭に移動する：`cursorLineStart` and `cursorHome`
4. カーソルのある位置で `> ` を入力する：`"command": "editor.action.insertSnippet","args": {"snippet": "> "}`

これを実際に設定すると，次のようになる．settings\.json で作成した一連のコマンドシーケンスを keybindings\.json で呼び出すことが出来る．

```jsonc
// settings.json
  "multiCommand.commands": [
    {
      "command": "multiCommand.markdown.insertBlockQuote",
      "sequence": [
        "editor.action.insertCursorAtEndOfEachLineSelected",
        "cursorLineStart",
        "cursorHome",
        {"command": "editor.action.insertSnippet","args": {"snippet": "> "}},
        // "removeSecondaryCursors",
      ],
      "description": "Insert blockQuote symbol"
    },
  ],
```

```jsonc
// keybindings.json
  {
    "key": "alt+q",
    "command": "multiCommand.markdown.insertBlockQuote",
    "when": "editorTextFocus && editorLangId == markdown"
  },
```

もしも，押下後のマルチカーソルを解除しておきたい場合には，`removeSecondaryCursors` を有効にしておくと良いだろう．

ちなみに，`> ` を挿入する際に `"command": "type","args": {"text": "> "}` としてしまうと，`Ctrl+Z` で操作を戻す際に 2 回の操作を必要となる．（半角スペースのタイプと `>` のタイプの 2 回があるため）

もちろん，`"snippet": "> "` をさまざま変更すれば，好きな文字を行頭に挿入することが出来るようになるだろう．

![insert block quote][insertBlockQuote]

このように作成したコマンドは，複数回押下するとネストされる．トグル操作ではないため，取り消したい場合には普通に `Backspace` や `Delete` するしかないことに注意してほしい．

もしも，`Shift+Alt+Q` などとして取り消したい場合には，次のようにしておけば良いだろう．ただし，`deleteRight` の数は適宜調整する必要がある．

```jsonc
// settings.json
  "multiCommand.commands": [
    {
      "command": "multiCommand.markdown.deleteBlockQuote",
      "sequence": [
        "editor.action.insertCursorAtEndOfEachLineSelected",
        "cursorLineStart",
        "cursorHome",
        "deleteRight",
        "deleteRight",
        // "removeSecondaryCursors",
      ],
      "description": "Delete blockQuote symbol"
    },
  ],
```

最後に，一部コマンドの説明を付しておきたい．

|                      コマンド                       | キーボードショートカット | 説明                             |
| :-------------------------------------------------: | :----------------------: | :------------------------------- |
| `editor.action.insertCursorAtEndOfEachLineSelected` |      `Shift+Alt+I`       | 選択した各行の端にカーソルを挿入 |
|                  `cursorLineStart`                  |           none           | 行の先頭にカーソルを移動する     |
|                    `cursorHome`                     |          `Home`          | 行の先頭にカーソルを移動する     |

`cursorLineStart` と `cursorHome` は同じようでありながら，以下の条件ではかなり異なる挙動を示す．

- インデント
- 折り返し
- 複数回の押下

> https://github.com/Microsoft/vscode/issues/28440#issuecomment-308022892

今回，インデントされている場合はインデントの右側に `>` を挿入したかったため，`cursorLineStart` のあとで `cursorHome` することにしている．

# 余談

今回は Markdown での例を示したが，これ以外であってもかなり応用が利くのではないだろうか．

もちろん，行末への挿入も可能である．
行末への挿入の実例として，`\\` を行末に挿入するコマンドを定義してみたい．これは LaTeX での使用を想定している．

```jsonc
// settings.json
  {
    "command": "multiCommand.latex.insertDoubleBackslash",
    "sequence": [
      "editor.action.insertCursorAtEndOfEachLineSelected",
      "cursorEnd",
      {"command": "editor.action.insertSnippet","args": {"snippet": " \\\\\\"}},
      "removeSecondaryCursors",
    ],
    "description": "Insert double backslash"
  }
```

ちなみに，HyperSnips というかなり高機能なスニペットを提供する拡張機能も存在する．キーボードショートカットではなくスニペットで解決させたい場合には，これを利用しても良いだろう．

> [draivin.hsnips \- VSCode Marketplace](https://marketplace.visualstudio.com/items?itemName=draivin.hsnips)

Vim の [UltiSnips](https://github.com/SirVer/ultisnips) に影響を受けているらしい．