Atomパッケージを作ってみよう

この記事はDeNA Advent Calendar 2016 6日目の記事です。

こんにちは。エンジニアの加瀬です。 普段はモバイルゲームの開発をしております。

皆さんは開発をするときにどのエディタをメインに使っていますか? Vim、Emacs、それともIDEでしょうか。
昔は自分もVimを使っていたのですが、ちょっとしたプラグインを自分で作ってみようと思ったときにハードルの高さにぶつかってしまい、去年あたりからAtomをメインエディタとして使うようになりました。
AtomはCoffeeScript(後述しますが今はJSでもOKです)でパッケージ(プラグイン)を作ることができます。プラグイン作成のためにニッチな言語を習得する必要がないことからパッケージを作るハードルは比較的低いと言えます。

ですが、自分が実際に作り始めてみるとまだまだドキュメントやサンプルが少なかったことから少し苦労しました。ちょっとでもパッケージ作りに興味を持った人が最初の段階で躓いてしまわないように、基礎のところを解説したいと思います。

パッケージジェネレータ

それでは早速パッケージを作ってみましょう。 Atomが標準でパッケージのテンプレートを作るコマンドを用意してくれていますので、まずはそれを実行します。
コマンドパレット(もしくはメニューのPackagesから)からPackage Generator: Generate Packageを実行します。 保存場所とパッケージ名を尋ねられるので、ここではmy-packageとしておきます。
するといくつかのファイルとディレクトリが作られ、~/.atom/packagesと~/.atom/dev/packagesの下に今作られたmy-packageのディレクトリへのシンボリックリンクが貼られます。

packagesはAtom標準以外のサードパーティのパッケージが保存されているディレクトリで、Atom起動時にここのパッケージが読み込まれます。dev/packagesは開発モード時に読み込まれるディレクトリです。

自分が作ったパッケージを公開したり、あるいは誰かが公開しているパッケージをforkして開発する場合にはdev/packagesにシンボリックリンクを貼ってatom --devから開発モードで起動するといいでしょう。

CoffeeScriptかJSか

そもそもAtomが登場した当初はまだCoffeeScriptが流行っていた頃でしたのでCoffeeScriptで書かれたエディタとして話題を集めていたように記憶しています。なのでそのパッケージを作るにも当然CoffeeScriptと思われますが、2016年現在では素のJSを使うこともできます。それどころかES2015で書くことすらできます。

先ほどのGenerate Packageで作られたテンプレートはCoffeeScriptで書かれていました。もしCoffeeScriptではなくてJSで書きたい場合は、package-generatorというコアパッケージのSettingsでPackage Syntaxをjavascriptに切り替えてから再度Generate Packageを実行してください。

本記事ではJSでテンプレートを生成した前提で解説をしていきます。ですがCoffeeScriptの場合でも.jsを.coffee、.jsonを.csonに置き換えて見てもらえば後は文法が少し異なるだけで問題ありません。

package.jsonの解説

まずはpackage.jsonから見ていきましょう。 package.jsonにはこのパッケージのメタ情報や依存ライブラリの情報を書いていくことになります。


{
  "name": "my-package",
  "main": "./lib/my-package",
  "version": "0.0.0",
  "description": "A short description of your package",
  "keywords": [
  ],
  "activationCommands": {
    "atom-workspace": "my-package:toggle"
  },
  "repository": "https://github.com/atom/my-package",
  "license": "MIT",
  "engines": {
    "atom": ">=1.0.0 <2.0.0"
  },
  "dependencies": {
  }
}

大体のメタ情報は最初から書かれている内容を見れば分かると思うので、見慣れないactivationCommandsだけ解説します。

Atomは最初から全てのパッケージを読み込むわけではなく、遅延読み込みが可能になっています。
実は最初から書かれているこのactivationCommandsだけでそれが実現されており、atom-workspaceという場所でmy-packageのtoggleコマンドを実行しようとしたときに初めてmy-packageが読み込まれる、という意味になっています。

atom-workspaceはAtomのどこでもという意味で、toggleはGenerate Packageでテンプレートを生成したときに初めから用意されているコマンドです。 後ほど解説しますが、toggle以外のコマンドを自分で定義すればもちろんそれをactivationCommandsに割り当てることも可能です。

npmも使えるのか?

packages.jsonはパッケージのメタ情報を書くだけではなく、nodejsでの開発と同様にnpmに必要な情報を書くことも可能です。
依存ライブラリはnpm installdependenciesdevDependenciesに自動登録できますし、必要であればscriptsを作って自分でlintやwatchといったコマンドを登録しても問題ありません。

lib/my-package.js

次にパッケージのコマンドがどこで登録されているか見てみましょう。
lib/my-package.jsはパッケージの起動や終了時の処理、コマンドの登録を行う場所になっています。


// ES2015で書くために必要な記述
'use babel';

import MyPackageView from './my-package-view';
import { CompositeDisposable } from 'atom';

export default {

  myPackageView: null,
  modalPanel: null,
  subscriptions: null,

  // パッケージ起動時に実行される処理
  activate(state) {
    // toggleコマンドで表示されるモーダルのViewオブジェクト
    this.myPackageView = new MyPackageView(state.myPackageViewState);
    this.modalPanel = atom.workspace.addModalPanel({
      item: this.myPackageView.getElement(),
      visible: false
    });

    // イベントを管理するために便利なCompositeDisposable
    // Atomの各所で見かけるが今回は省略
    this.subscriptions = new CompositeDisposable();

    // コマンド登録
    this.subscriptions.add(atom.commands.add('atom-workspace', {
      'my-package:toggle': () => this.toggle()
    }));
  },

  // パッケージ終了時に実行される処理
  deactivate() {
    this.modalPanel.destroy();
    this.subscriptions.dispose();
    this.myPackageView.destroy();
  },

  // パッケージの次回起動時に状態を保存しておく処理
  // 今回は省略
  serialize() {
    return {
      myPackageViewState: this.myPackageView.serialize()
    };
  },

  // toggleコマンドの実際の処理
  toggle() {
    console.log('MyPackage was toggled!');
    // モーダルViewの表示、非表示
    return (
      this.modalPanel.isVisible() ?
      this.modalPanel.hide() :
      this.modalPanel.show()
    );
  }

};

自動的に生成されたMyPackageViewにはtoggleコマンドで表示されるモーダルのHTMLが記述されています。今回は解説しませんが、パッケージで使われるView要素はロジックとは別のモジュールに分けられていることが多いです。

my-packageに新しい機能を追加する

ここまででパッケージのコマンドの登録方法とその実装を見てきました。次はいよいよこのmy-packageに新しい機能を追加してみましょう。

今回はAtomが提供している通知機能を使ってこんな通知を出してみることにします。

atom-my-package-notify.png

Atom APIドキュメント

まずはAtomのAPIドキュメントから通知機能を使うためのAPIを調べましょう https://atom.io/docs/api/

通知機能はNotificationManagerというクラスです。ざっと見てみるとaddSuccessやaddInfo, addErrorというメソッドが並んでおり、いかにもそれっぽいですね。
このNotificationManagerはatom.notificationsからいつでも使えると書いてありますので早速試してみましょう。

コマンドの追加

今回はHello World!と表示させるだけのシンプルな通知を出すコマンドを追加します。
addInfo()を呼び出すnotify()というメソッドを作り、既にコマンド登録されているtoggleの下にnotifyを新しく追加します。


// 変更を加えたメソッドだけ抜粋

  activate(state) {
    this.myPackageView = new MyPackageView(state.myPackageViewState);
    this.modalPanel = atom.workspace.addModalPanel({
      item: this.myPackageView.getElement(),
      visible: false
    });

    this.subscriptions = new CompositeDisposable();

    this.subscriptions.add(atom.commands.add('atom-workspace', {
      'my-package:toggle': () => this.toggle(),
      // notifyを追加
      'my-package:notify': () => this.notify()
    }));
  },

  toggle() {
    console.log('MyPackage was toggled!');
    return (
      this.modalPanel.isVisible() ?
      this.modalPanel.hide() :
      this.modalPanel.show()
    );
  },

  notify() {
    atom.notifications.addInfo('Hello World!');
  }

コードを書き換えて動作確認をする前にAtomをリロードしてパッケージを再読込する必要があります。メニューのView→Developer→Reload Windowでリロードできるのですが、これから何度も使用することになるのでショートカットを覚えてしまいましょう。
Macであればctrl + option + command + Lです。

リロードできたらコマンドパレットを呼び出し、MyPackageを検索します。 toggleしか実行できるコマンドがありませんので、とりあえずtoggleを実行します。package.jsonで解説したようにmy-packageは起動コマンドとしてtoggleが設定されていますので、逆にtoggleを実行するまでは別のコマンドを実行できません。
toggle実行後に再度コマンドパレットで検索をすると今度はnotifyも選べるようになっており、実行するとHello World!という通知が出たはずです。とても簡単な機能ですが、my-packageに新しいコマンドを追加することができました!

activationCommands

新しいコマンドを追加することはできましたが、my-packageの機能を使う前に毎回toggleを実行するのはとても不便ですね。この問題はpackage.jsonのactivationCommandsを削除することで解決できます。
activationCommandsを削除してリロードしてから再度コマンドパレットでMyPackageと検索してみてください。今度は最初からnotifyを実行できるはずです。

activationCommandsが存在しない場合、Atom起動時にパッケージが自動的に読み込まれます。 便利になる反面、CPUやメモリをガンガン使う場合は何もしなくても'重い'パッケージになってしまうので、そのような場合はactivationCommandsを設定して必要になるときまでパッケージの起動を遅らせることができないか検討しましょう。

デバッグ

AtomではChromeと同じ開発者ツールを使ってデバッグができます。
メニューのView→Developer→Toggle Developer Toolsか、Chromeと同じでcommand + options + iのショートカットで起動できます(Macの場合)

JSのデバッグには欠かせないconsole.log()や、debuggerによるブレークポイントの設定ももちろん可能です。

開発者ツールのコンソールではAtomで使われているパッケージが読み込まれている状態なので、ここに直接 atom.notifications.addInfo('Hello World!') と入力しても先ほど実装したnotify()と同じように通知を出すことができます。 APIドキュメントを読みながら色々と実験をするときに重宝するでしょう。

テスト

テストはspecディレクトリに存在します。今回は割愛しますが、もしもパッケージを公開するのであればテストも追加した方がいいでしょう。 ターミナル上でapm testを実行するとspecに存在するテストが全て実行されます。

パッケージ公開

一通りの機能ができたらぜひパッケージを公開してみましょう。 今回は解説しませんが、Atomのドキュメントにパッケージを公開する方法が書いてありますのでぜひ挑戦してみてください。
http://flight-manual.atom.io/hacking-atom/sections/publishing/

終わりに

駆け足になってしまいましたが、パッケージ作成の基本を紹介しました。 Atom独自のお作法を覚えてしまえば、後はJS(CoffeeScript)とCSSで書くことができるので学習コストはそれほど高くないと思います。

ですが、パッケージについてのドキュメントは残念ながらそれほど充実している環境ではないというのが自分の感想です。
Atomのパッケージは公式、サードパーティのどちらもGitHub上で公開されていますので、他の人が作成したパッケージの構成やソースコードを読むことが理解への近道になるはずです。 普段自分が使用しているパッケージや、これから自分が作ろうとしているものに近い機能を提供しているパッケージを参考にするといいでしょう。

プライベートや業務でAtomにこんな機能あったらいいな、と思ったらぜひパッケージ作成に挑戦してみてください。

最後に、拙作ではありますがAtom上でVimのタブ機能を再現するパッケージを公開していますので興味を持った方は使ってみて頂けると嬉しいです。
ソースコードもGitHubに公開してありますので、こちらもぜひ見てみて下さい。
https://atom.io/packages/atom-vim-like-tab
https://github.com/Kesin11/atom-vim-like-tab

ツイート
シェア
あとで読む
ブックマーク
送る
メールで送る