CodeMirrorでEmoji AutoCompleteする

[CodeMirrorでEmoji AutoCompleteをする](https://muunyblue.github.io/502317c9b4695a9476eb981d5fdf43a8.html "CodeMirrorでEmoji AutoCompleteをする")に書いたけどQiitaにも転載。

---

":"に続けて打ち込むと自動で補完してくれるようになります。

![demo](https://gyazo.com/68de503622f29ed4213c201a75b870c1.gif)

ちゃんとブラウザ上で動いている様子です。

EmojiリストはGitHub APIから取得しています。

[Locks](https://github.com/yymm/locks "yymm/locks: Localstoraged Rich Slide Editor, Anyone available!")というLocalStorageを使ったスライドエディタを作っているときにできた副産物です。

# 必要なパッケージ

- CodeMirror
  - show-hint addonのコードを編集するのでソースコードが必要
- Axios

パッケージの読み込み方法は各自におまかせします(scriptタグでもビルドツールでも構いません)。

# やり方

```javascript
// sample data
let emojiList = ['apple:', 'abc:', 'axz:', 'bee:', 'beam:', 'bleach:']

let emojiComplete = function(cm) {
  CodeMirror.showHint(cm, function() {
    let cur = cm.getCursor(), token = cm.getTokenAt(cur)
    let start = token.start, end = cur.ch, word = token.string.slice(0, end - start)
    let ch = cur.ch, line = cur.line
    let currentWord = token.string
    while (ch-- > -1) {
      let t = cm.getTokenAt({ch, line}).string
      if (t === ':') {
        let filteredList = emojiList.filter((item) => {
          return item.indexOf(currentWord) == 0 ? true : false
        })
        if (filteredList.length >= 1) {
          return {
            list: filteredList,
            from: CodeMirror.Pos(line, ch),
            to: CodeMirror.Pos(line, end)
          }
        }
      }
      currentWord = t + currentWord
    }
  }, { completeSingle: false })
}

cm.on('change', emojiComplete)
```

ひとまずemoji画像のないシンプルな例です。cmはCodeMirrorインスタンスです。

CodeMirrorのchangeイベントにshowHintを仕込んで実現しています。

showHintの中身が肝です。

最初の4行はshowHintを書く際の決まり文句のようなものでカーソル位置や対象の単語情報を変数に記録しています。

while文内では、単語を一文字ずつ遡っていって":"が見つかったら補完候補を絞り込んで、補完候補が存在する場合は表示するようにしています。

showHintの戻り値は、補完のリストと補完する位置を指定します。位置は今の位置から単語終了位置までになります。

これで自動的に":"のあとに文字を入れると補完候補が表示されます。

showHint関数に指定している、completeSingle: falseは必須です。細かい理由はわかりませんがこれがないと無限ループするようです。

emojiListが末尾に":"が付いて不格好なのは、補完完了時に末尾の":"まで補完されるようにしているからですがこれは回避できます、これは以下の内容で説明します。

## Emojiのリスト取得

GitHub APIにEmoji一覧を取得するAPIがあるので利用します。

[Emojis | GitHub Developer Guide](https://developer.github.com/v3/emojis/ "Emojis | GitHub Developer Guide")

先程の例のemojiListをapiで取得したものに置き換えれば完全なリストになります。

```javascript
let res = await axios.get('https://api.github.com/emojis')
if (res.status !== 200) {
  console.log('Error...')
  return
}

let emojiList = []
for (let key in res.data) {
  emojiList.push(`${key}:`)
}
```

## Emoji画像の表示

show-hint addonの補完候補には文字列しか表示できません。

実際にCodeMirrorの[addon/hint/show-hint.js](https://github.com/codemirror/CodeMirror/blob/master/addon/hint/show-hint.js)を見てみると、文字列リスト or {text: str, displayText: str}のリストを指定する仕様になっています。

画像を表示させるようにshow-hint.jsを書き換えます。

```diff
      if (cur.render) cur.render(elt, data, cur);
-     else elt.appendChild(document.createTextNode(cur.displayText || getText(cur)));
+     else elt.innerHTML = cur.displayText
      elt.hintId = i;
```

[219行目](https://github.com/codemirror/CodeMirror/blob/master/addon/hint/show-hint.js#L219)あたりです。

displayTextで指定する補完候補表示をinnerHTMLで表示できるようにしました。

{text: str, displayText: str}のリストでの補完候補の指定は、textは実際に補完する際に使われる文字列でdisplayTextが補完候補を表示するときに使われる文字列になるので、emojiListを以下のようにします。

```javascript
let res = await axios.get('https://api.github.com/emojis')
if (res.status !== 200) {
  console.log('Error...')
  return
}

let emojiList = []
for (let key in res.data) {
  emojiList.push({text: `${key}:`, displayText: `<img width="15" height="15" src="${res.data[key]}" alt="icon" async></img> ${key}`})
}
```

displayTextをGitHub APIで取得した画像を小さくしたもの + Emoji名の形にします。

これで冒頭のGifキャプチャーのように表示されるようになります。

かなりの量の画像を取得するのでimgタグにはasyncオプションをつけています。

# 完全なコード

~~書き換えたshow-hint.jsを使用する必要があるので注意。~~
`render` function を利用すれば show-hint.js の書き換えは不要。
参考: https://github.com/codemirror/CodeMirror/blob/5.33.0/addon/hint/show-hint.js#L212


```javascript
let cm = CodeMirror(/* Dom Element */, {
  // Options
})

//
// Emoji Complete
//
let res = await axios.get('https://api.github.com/emojis')
if (res.status !== 200) {
  console.log('Error...')
  return
}

let emojiList = []
for (let key in res.data) {
  emojiList.push({
    text: `${key}:`,
    render: (element) => {
      element.innerHTML = `<img width="15" height="15" src="${res.data[key]}" alt="icon" async></img> ${key}`})
    }
  })
}

let emojiComplete = function(cm) {
  CodeMirror.showHint(cm, function() {
    let cur = cm.getCursor(), token = cm.getTokenAt(cur)
    let start = token.start, end = cur.ch, word = token.string.slice(0, end - start)
    let ch = cur.ch, line = cur.line
    let currentWord = token.string
    while (ch-- > -1) {
      let t = cm.getTokenAt({ch, line}).string
      if (t === ':') {
        let filteredList = emojiList.filter((item) => {
          return item.text.indexOf(currentWord) == 0 ? true : false
        })
        if (filteredList.length >= 1) {
          return {
            list: filteredList,
            from: CodeMirror.Pos(line, ch),
            to: CodeMirror.Pos(line, end)
          }
        }
      }
      currentWord = t + currentWord
    }
  }, { completeSingle: false })
}

cm.on('change', emojiComplete)
```

# 参考記事

- [CodeMirrorでAutoCompleteする - Qiita](http://qiita.com/mizchi/items/15441d0e285f86125b0a "CodeMirrorでAutoCompleteする - Qiita")
- [CodeMirror: User Manual](https://codemirror.net/doc/manual.html#addons "CodeMirror: User Manual")
- [javascript - How to customize autocomplete function of the CodeMirror - Stack Overflow](https://stackoverflow.com/questions/40015901/how-to-customize-autocomplete-function-of-the-codemirror "javascript - How to customize autocomplete function of the CodeMirror - Stack Overflow")
- [css - CodeMirror - AutoComplete "options" not setting right - Stack Overflow](https://stackoverflow.com/questions/22699024/codemirror-autocomplete-options-not-setting-right)