パッケージのイベントハンドラをユーザーに提供する手法メモ


## はじめに

Atomは[Emitter](https://atom.io/docs/api/latest/Emitter)を利用して気軽にカスタムイベントを利用することが出来る。
各パッケージでもこれを利用しているのをよく見かけるが、これを利用者側や他パッケージにも提供して各イベントにフックさせる方法がないかを考えてみた。

## 1. メインモジュールにハンドラを定義する

明示的に公開していないパッケージも含め、メインモジュールに定義されているものであれば利用できる。
（これはイベントハンドラがなくてもEmitterのオブジェクトにアクセスできるなら利用できる）

```coffeescript
atom.packages.onDidActivatePackage (pkg) ->
  if pkg.name == 'some-package'
    # ハンドラが定義されている場合
    pkg.mainModule.onDidSomeEventHandler ->
      # 具体的な処理
    # Emitterオブジェクトにアクセスできる場合
    pkg.mainModule.emitter.on "on-did-some-event", ->
      # 具体的な処理
```

欠点としてはメインモジュールを経由してアクセスできるものに限るため、更に別モジュールで定義されているイベントにはアクセスできない。

## 2. メインモジュールにイベントハンドラ提供用メソッド、またはオブジェクトを追加する

後述する方法を採用するならばこちらを使う必要は感じられないが、パッケージのメインモジュールにアクセスできる仕様を利用してメインモジュールに各ハンドラをまとめたオブジェクトを定義、またはオブジェクトを返すメソッドを定義する方法が考えられる。

### 提供側

```coffeescript:main.coffee
  emitter: null
  eventHandler: null

  activate: (state) ->
    @emitter = new Emitter
    @eventHandler =
      onDidSomeEventHandler: ->
        @onDidSomeEventHandler.bind @

  onDidSomeEventHandler: (callback) ->
    @emitter.on "on-did-some-event", callback
  
  provideEventHandler: ->
    @eventHandler
```

### 利用側

```coffeescript
atom.packages.onDidActivatePackage (pkg) ->
  if pkg.name === 'some-package'
    pkg.mainModule.provideEventHandler.onDidSomeEventHandler ->
      # 具体的な処理
```

パッケージで利用する場合、自身のパッケージが後でアクティブになる可能性があるため`activate`メソッド内で確認する必要がある。

```coffeescript:main.coffee
activate: (state) ->
  # すでに有効化されているかの確認
  if atom.packages.isPackageActive 'some-package'
    pkg = atom.package.getActivePackage 'some-package'
    @registerSomePackageEventHandler pkg
    
  # 対象パッケージを一度無効化して再度有効化する可能性があるため、上記結果がどうであれonDidActivatePackageをフックしておく
  atom.packages.onDidActivatePackage pkg ->
    if pkg.name === 'some-package'
      @registerSomePackageEventHandler pkg

registerSomePackageEventHandler: (pkg) ->
  pkg.mainModule.provideEventHandler.onDidSomeEventHandler ->
    # 具体的な処理
```

このような方法を取る場合はパッケージ作者が意図的にイベントハンドラを公開したい場合であるはずなので、別モジュールで定義されているイベントも拾えるように作られていることを期待できる。

## 3. サービスAPIを利用する

拙作[vim-modeline](https://atom.io/packages/vim-modeline)のv0.2.0で実装した方法で、サービスAPIを利用してイベントハンドラを公開する方法。
提供側の手間がさらに増えるが、利用側のコードがif文でごちゃごちゃせず、パッケージから利用する場合も有効になる順序を考えないでいい分すっきりと実装できるように感じる。

### 提供側

上記「2」のコードに加えて`package.json`にサービスAPIに関する記述を追記する。

```json:package.json
"providedServices": {
  "some-package-event-handler": {
    "description": "some-package event handler",
    "versions": {
      "1.0.0": "provideEventHandler"
    }
  }
},
```

### 利用者側

パッケージで利用するならばサービスAPIを利用するための記述を`package.json`に追記する。

```json:package.json
"consumedServices": {
  "some-package-event-handler": {
    "versions": {
      "^1.0.0": "consumedSomePackageEventHandler"
    }
  }
},
```

```coffeescript:main.coffee
consumedSomePackageEventHandler: (handler) ->
  handler.onDidSomeEventHandler ->
    # 具体的な処理
```

`init.coffee`で利用する場合は[起動スクリプトからService APIを利用する](http://qiita.com/from_kyushu/items/7131ca82bf4aab7e3b8b)に書いた方法でサービスAPIを利用できる。
（パッケージでもこの手法は取れるが、正規の手法である上記`package.json`に記載する方法を利用すべきだろう）

```coffeescript:init.coffee
atom.packages.serviceHub.consume "some-package-event-handler", "^1.0.0", (handler) ->
  handler.onDidSomeEventHandler ->
    # 具体的な処理
```

このような方法を取っているパッケージは他に見たことがないため今のところ拙作パッケージでしか利用できないが、他パッケージの動作に応じて処理を走らせたいことはよくあるのでぜひ他パッケージでもこのような方法を取ってイベントを公開してほしい、と常々思っている。

## おまけ: vim-modelineのイベントハンドラを利用するケース例

私の`init.coffee`からの引用だが、文字コードを自動設定するために下記のようにしている。

1. vim-modelineのパース処理を待つ
2. vim-modelineのパース処理終了イベント`onDidParse`をフックし、結果の文字コード(fileencodingプロパティ)を確認
3. 文字コードが設定されていない場合、文字コードを自動判別してセット

文字コードの自動セットは例によって[encoding-selector](https://atom.io/packages/encoding-selector)の処理を参考にしている。
[auto-encoding](https://github.com/t-ishii/auto-encoding)というパッケージもあるが、vim-modelineと併用する場合最終的にどちらの文字コードがセットされるかはコントロールできない[^1]ため私は利用していない。

```coffeescript
atom.packages.serviceHub.consume "vim-modeline-event-handler", "^1.0.0", (handler) ->
  handler.onDidParse ({editor, options}) ->
    detectEncoding(editor) unless options?.fileencoding
    
detectEncoding = (editor = null)->
  editor = atom.workspace.getActiveTextEditor() unless editor
  try
    filePath = editor.getPath()
  catch error
    return
  return unless fs.existsSync(filePath)

  jschardet = require 'jschardet'
  fs.readFile filePath, (error, buffer) =>
    return if error?
    {encoding} = jschardet.detect(buffer) ? {}
    encoding = 'utf8' if encoding is 'ascii'
    setEncoding(encoding, editor)
    
setEncoding = (encoding, editor = null) ->
  iconv = require 'iconv-lite'
  editor = atom.workspace.getActiveTextEditor() unless editor
  return false unless iconv.encodingExists(encoding)
  encoding = encoding.toLowerCase().replace(/[^0-9a-z]|:\d{4}$/g, '')
  editor.setEncoding(encoding)
  return true
```

[^1]: 基本イベント登録順に実行されるが、auto-encodingは`onDidOpen`を、vim-modelineは`observeTextEditors`を利用しているためイベント発火タイミングが予想できない。auto-encodingはEmitter自体利用しておらず処理状況に応じて何か処理を行うことが出来ないため、vim-modeline側としてはauto-encodingがインストールされていた場合には「auto-encodingの処理結果が優先されてモードラインの記述と異なる文字コードがセットされる可能性がある」と言った警告を表示して各々対応してもらうしか方法がない。