Google Colab 上でのコード編集が捗る Chrome 拡張を開発した

## 結果
こうなった
![demo](https://user-images.githubusercontent.com/17039389/54288289-940c2b00-45ea-11e9-8bc0-ab730274352c.gif)

## 背景
最近、機械学習周りの勉強を Google Colab 上で行うようになった。ただ、コード編集機能に少々不満を感じていた。そこで、最近学習した Javascript と Chrome 拡張の知識を組み合わせて、コード編集が捗る Chrome 拡張を開発してみることにした。

## ソース

https://github.com/harupy/colavim

## 実装方法
Google Colab (Jupyter Notebook でも OK) を開いた状態で、 Chrome Dev Tool の Console に以下のコードを入力すると、各セルに **CodeMirror** という名前のオブジェクトが紐づいていることを確認することができる。

```javascript
const div = document.querySelector('div.CodeMirror')
div.CodeMirror
```

セル上で起こるイベントは、この CodeMirror オブジェクトによって管理されている。つまり、このオブジェクトのプロパティを書き換えることで、新たな機能を追加することができる（既存の機能を汚染しないように注意が必要）。開発時は、公式ドキュメントと GitHub 上のソースコードを参考にした。

- [CodeMirror: User Manual](https://codemirror.net/doc/manual.html)
- [CodeMirror: GitHub](https://github.com/codemirror/CodeMirror)

余談だが、Jupyter Notebook や Kaggle の Kernel でも CodeMirror が使われている。

## 実装した機能
- スニペット
- スニペットヒント
- Vim ("**実装**"ではなく、"**Enable**" したという表現の方が正しいかも)


## スニペット
CodeMirror オブジェクトがプリミティブな操作（カーソル移動、セル内文字列の取得・置換・削除など）を行う関数を持っているので、それらを組み合わせてスニペット機能を実現している。

```javascript
const snippets = {
  'inp'     : 'import numpy as np\n',
  'iplt'    : 'import matplotlib.pyplot as plt\n',
  'ipd'     : 'import pandas as pd\n',
  'isb'     : 'import seaborn as sns\n',
  'itf'     : 'import tensorflow as tf\n',
  'pdrc'    : 'pd.read_csv()'
};

const expandSnippetOrIndent = cm => {
  // cm: CodeMirror object
  const cursor = cm.getCursor();  // cursor position
  const cursorLeft = cm.getRange({line: cursor.line, ch: 0}, cursor); // カーソルより左側の文字列
  const match = cursorLeft.match(/[^a-zA-Z0-9_]?([a-zA-Z0-9_]+)$/);
  if (!match) {
    tabDefaultFunc(cm);
    return
  }

  const prefix = match[1];
  const head = {line: cursor.line, ch: cursor.ch - prefix.length};
  
  // マッチあり
  if (prefix in snippets) {
    const body = snippets[prefix];
    cm.replaceRange(body, head, cursor);
    const match = body.match(/\)+$/);
    if (match) cm.moveH(-match[0].length, 'char');
  } else {
    tabDefaultFunc(cm);
  }

  // TODO：展開候補が複数個があった場合には、ヒントを表示する機能を実装
}

// Tab にスニペット展開機能を割り当て
cell.CodeMirror.options.extraKeys['Tab'] = expandSnippetOrIndent;
```

## スニペットヒント
やっていることはスニペット機能とほぼ同じ。ヒント表示中に、ユーザーから入力があったら、ヒントが自動更新されるようにしてある。

```javascript
const showSnippetHint = cm => {
  // TODO: expandSnippetOrIndent と被っている処理は関数化
  const cursor = cm.getCursor();
  const cursorLeft = cm.getRange({line: cursor.line, ch: 0}, cursor);
  const match = cursorLeft.match(/[^a-zA-Z0-9_]?([a-zA-Z0-9_]+)$/);
  const prefix = match ? match[1] : '';
  const head = {line: cursor.line, ch: cursor.ch - prefix.length};
  const matchedPrefixes = Object.keys(snippets).filter(k => k.indexOf(prefix) > -1);
  matchedPrefixes.sort();

  const hintList = matchedPrefixes.map(key => {
    const displayText = snippets[key].replace('\n', '; ');
    const displayTextTrunc = displayText.length > 40 ? displayText.slice(0, 40) + '...' : displayText
    return {
      text: snippets[key], // 挿入される文字列
      displayText: `${key.padEnd(7, ' ')}: ${displayTextTrunc}`  // ヒントに表示される文字列
    }
  });
  
  const hintFunc = () => {
    return {
      list: hintList,
      from: head,
      to: cursor
    }
  }
  
  // ヒントを表示
  cm.showHint({
    hint: hintFunc,
  });
}

// ヒント表示中に、ユーザーから入力があったら、ヒントを更新する
const conCursorActivity = cm => {
  if (cm.state.completionActive) {
    showSnippetHint(cm);
  }
}

// Ctrl-h にヒント表示機能を割り当て
cell.CodeMirror.options.extraKeys['Ctrl-H'] = showSnippetHint;
cell.CodeMirror.on('cursorActivity', conCursorActivity);
```

# Vim
先駆者がいたので、それを参考にした。Chrome 拡張のファイル構成もこのレポジトリを真似ている。

- [Autovim - Chrome Web Store](https://chrome.google.com/webstore/detail/autovim/licohjbphilmljmjonhiifkldfahnmja)
- [thomcom/autovim](https://github.com/thomcom/autovim)


```javascript
const enableVim = cell => {
  cell.CodeMirror.setOption('vimMode', true);
  cell.CodeMirror.options.keyMap = 'vim';
  cell.CodeMirror.options.showCursorWhenSelecting = 'vim';
};
```

## まとめ
開発した Chrome 拡張のおかげで、 Google Colab 上での作業効率が大きく向上した。本記事で紹介している方法を利用して、Jupyter Notebook もハックしてみる予定。最後まで読んで頂き、ありがとうございました！