RAKUS Developers Blog | ラクス エンジニアブログ

株式会社ラクスのITエンジニアによる技術ブログです。

Atomのプラグインを作成してみた【入門】

f:id:matsutaira:20210423105625p:plain

はじめに

こんにちは、matsutairaです。僕は普段Atomというテキストエディタを使用しており、実用的なプラグインから面白いプラグインなど必要なプラグインを入れて作業しています。個人的には、実用的なプラグインより面白いプラグインを使うのが好きですが、見つけるのも大変なので自分でプラグインの作成ができないかと思い作成方法を調べてみました。今回は調べた内容を元にプラグインの作成を行いたいと思います。

目次

Atomとは

そもそもAtomとは、GitHubが開発したオープンソーステキストエディタです。

  • メリット

    • UIが直観的で使いやすい
    • 無料公開のプラグインが多数存在し、作業効率アップや息抜きになる
    • プラグイン同様多数のテーマがあり、カスタマイズ性が高い
    • ショートカットが使いやすい
  • デメリット

    • 起動に多少時間がかかる
    • 動作が重い場合がある

とはいえオープンソースであること、かつデメリットを補えるだけのメリットがあるため、使う価値のあるテキストエディタだと思います。使ったことが無い方はこの機会に是非使ってみてください。
atom.io

プラグイン作成

本題のプラグインの作成に入ります。今回作成するプラグインは「入力を検知し何かをする」をコンセプトに作成したいと思います。

パッケージ生成

Atomにはデフォルトでパッケージジェネレータというものが存在しており、プラグインの作成に必要なパッケージの雛形を自動で生成してくれます。
メニューバーの[パッケージ]から[パッケージ ジェネレータ]を選択します。まずは雛形を生成してみましょう。
※コマンドパレット(Ctrl+Shift+P)から「Package Generator: Generate Package」でも生成できます。 f:id:matsutaira:20210420155546p:plain

保存する場所と名前を入力し[Enter]を押します。
今回は「erai-bot」という名前で作成します。
f:id:matsutaira:20210421182507p:plain

生成が完了すると新規でウィンドウが立ち上がります。
f:id:matsutaira:20210421183636p:plain

たったこれだけでプラグインの作成に必要な雛形を簡単に作ることができ、デフォルトでこの機能が入っているのはかなりありがたいです。

パッケージ構造

次はパッケージの構造を見てみましょう。

erai-bot
├ keymaps
│ └ erai-bot.json
├ lib
│ ├ erai-bot-view.js
│ └ erai-bot.js
├ menus
│ └ erai-bot.json
├ spec
│ ├ erai-bot-spec.js
│ └ erai-bot-view-spec.js
├ styles
│ └ erai-bot.less
├ .gitignore
├ CHANGELOG.md
├ LICENSE.md
├ package.json
└ README.md
  • keymaps:キーバインドの定義
  • lib:機能の定義
  • menus:メニュー表示の定義
  • spec:テスト用フォルダ
  • styles:cssやlessによるスタイルの定義
  • package.json:packageの定義を記述

というような構造になっています。今回はkeymaps、libの2つのみを編集していきます。
※自分で作成するプラグインは、C:/Users/~/github/erai-bot配下に置かれ、同時にC:/Users/~/.atom/packages配下(インストールしたプラグインが格納される場所)にショートカットが作成されます。

機能作成

それでは実際に機能を作成していきます。
まずは使えるAtomAPI を確認しましょう。
数あるAPIの中から今回はTextEditor というAPINotificationManagerを利用します。

  • NotificationManager:画面右上にメッセージを表示
  • TextEditor:文字入力検知や文字の自動入力、削除、セーブの検知が可能

この2つのAPIを基本に作成します。
まずは、生成したままのプラグインを起動してみます。デフォルトではCtrl+Alt+Oがショートカットになります。
f:id:matsutaira:20210422165742p:plain
このままでは、プラグインを起動/停止したときに毎回表示されてしまいますし、ON/OFF状態がわかりずらいので右上にメッセージを表示してみます。
以下のようにerai-bot.jsを編集します。

  toggle() {
    atom.notifications.addInfo('ON/OFF');
    // ↓使用しないのでコメントアウトor削除
    // console.log('EraiBot was toggled!');
    // return (
    //   this.modalPanel.isVisible() ?
    //   this.modalPanel.hide() :
    //   this.modalPanel.show()
    // );
  }

atom.notifications.addInfoを使うことで画面右上にメッセージを表示できます。
nofiticationsの種類は、 * Success * Info * Worning * Error * FatalError の5種類あり、それぞれ種類によって色やアイコンが変わります。optionでアイコンを設定することも可能です。
f:id:matsutaira:20210422184314p:plain
編集した箇所の反映はCtrl+Sだけではできないので、保存とウィンドウをリロードCtrl+Shift+F5をします。
もう一度プラグインを起動させてみます。
f:id:matsutaira:20210422170207p:plain
画面右上にメッセージが表示されるようになりました。しかしこのままだとプラグインの起動/停止をするごとにメッセージが表示されるので、まだ起動状態かがわかりません。さらに記述していきます。

  toggle() {
    this.active = !this.active;
    if (this.active) {
      return this.disable();
    } else {
      return this.enable();
    }
    // atom.notifications.addInfo('ON/OFF');
  },

  enable() {
    atom.notifications.addInfo('えらい!bot起動');
  },

  disable() {
    atom.notifications.addInfo('えらい!bot停止');
  }

新たにenable関数とdisable関数を追加しました。これによりプラグインが起動している状態からは停止→disable関数呼び出し、プラグインが停止している状態からは起動→enable関数呼び出しに遷移することが可能となりました。 f:id:matsutaira:20210422172050p:plain

次は文字を入力するだけで褒められるようにします。 まず、activate関数部分を編集します。ここにはプラグインの読み込み時にロードされる内容を記述します。

  activate(state) {
    this.eraiBotView = new EraiBotView(state.eraiBotViewState);
    this.modalPanel = atom.workspace.addModalPanel({
      item: this.eraiBotView.getElement(),
      visible: false
    });

    // 入力検知用
    this.editor = atom.workspace.getActiveTextEditor();
    this.editorChangeSubscription = this.editor.getBuffer().onDidChange(this.onChange.bind(this));

    // Events subscribed to in atom's system can be easily cleaned up with a CompositeDisposable
    this.subscriptions = new CompositeDisposable();

    // Register command that toggles this view
    this.subscriptions.add(atom.commands.add('atom-workspace', {
      'erai-bot:toggle': () => this.toggle()
    }));
  },

次は、enable関数とdisable関数を編集します。

  enable() {
    // 起動時に有効化
    this.editorChangeSubscription = this.editor.getBuffer().onDidChange(this.onChange.bind(this));
    atom.notifications.addInfo('えらい!bot起動');
  },

  disable() {
    // disposeで入力検知を停止
    this.editorChangeSubscription.dispose();
    atom.notifications.addInfo('えらい!bot停止');
  },

次は、onChange関数を作成します。

  onChange(input) {
    atom.notifications.addInfo('入力できてえらい!');
  }

入力するたびに「入力できてえらい!」と褒められるようになりました。 プラグインの起動中はエディタ上に変化がある度にメッセージが表示されるので、EnterやSpaceの入力でも褒められます。 f:id:matsutaira:20210422174600p:plain
また、

  onChange(input) {
    if (input.newText === "\n") {
      atom.notifications.addInfo('Enter押せてえらい!');
    }
  }

のようにすることで、Enterの入力のみを検知することも可能です。
ただし、エディタ上へ入力したもののみ検知できるので、CtrlやShitfは検知できないので注意が必要です。

最後に保存するたびに褒められるようにします。
まず、activate関数部分を編集します。

  activate(state) {
    this.eraiBotView = new EraiBotView(state.eraiBotViewState);
    this.modalPanel = atom.workspace.addModalPanel({
      item: this.eraiBotView.getElement(),
      visible: false
    });

    // 入力検知用
    this.editor = atom.workspace.getActiveTextEditor();
    this.editorChangeSubscription = this.editor.getBuffer().onDidChange(this.onChange.bind(this));

    // 保存検知用
    this.editorSaveSubscription = this.editor.getBuffer().onDidSave(this.onSave.bind(this));

    // Events subscribed to in atom's system can be easily cleaned up with a CompositeDisposable
    this.subscriptions = new CompositeDisposable();

    // Register command that toggles this view
    this.subscriptions.add(atom.commands.add('atom-workspace', {
      'erai-bot:toggle': () => this.toggle()
    }));
  },

次は、enable関数とdisable関数を編集します。

  enable() {
    // 起動時に有効化
    this.editorChangeSubscription = this.editor.getBuffer().onDidChange(this.onChange.bind(this));
    this.editorSaveSubscription = this.editor.getBuffer().onDidSave(this.onSave.bind(this));
    atom.notifications.addInfo('えらい!bot起動');
  },

  disable() {
    // disposeで入力検知を停止
    this.editorChangeSubscription.dispose();
    this.editorSaveSubscription.dispose();
    atom.notifications.addInfo('えらい!bot停止');
  },

最後にonSave関数を作成します。

  onSave() {
    atom.notifications.addSuccess('保存できてえらい!');
  }

プラグインの起動中は保存をするたびに「保存できてえらい!」と褒められるようになりました。
f:id:matsutaira:20210422183320p:plain

最後にショートカットキーを変えます。変更はkeymapsにあるerai-bot.jsonを編集します。

{
  "atom-workspace": {
    "ctrl-alt-q": "erai-bot:toggle"
  }
}

これでCtrl+Alt+Qプラグインの起動/停止ができるようになりました。
一通りの動作はこれで完了ですが、若干物足りないのでさらに機能を追加しましょう。

一定時間入力がない場合にメッセージを表示する設定を入れます。

var timeoutId;

// 中略

  onChange(input) {
    // タイマー停止
    clearTimeout(timeoutId);
    atom.notifications.addInfo('入力できてえらい!');
    this.startTimer();
  },

// 中略

  disable() {
    // タイマー停止
    clearTimeout(timeoutId);
    // disposeで入力検知を停止
    this.editorChangeSubscription.dispose();
    this.editorSaveSubscription.dispose();
    atom.notifications.addInfo('えらい!bot停止');
  },

// 中略

startTimer() {
    // 60秒後にメッセージを表示
    timeoutId = setTimeout(() => {
      atom.notifications.addError('休憩できてえらい!');
    }, 60000);
  }  

f:id:matsutaira:20210422185237p:plain

某太陽神の名言を表示しつつ、プラグインの起動状態を強制的に維持する設定を入れます。

var count =  0;

// 中略

  disable() {
    if (count < 1) {
      // アクティブ状態に強制移行
      this.active = !this.active;
      count = count + 1;
      atom.notifications.addError('諦めんなよ!');
      atom.notifications.addError('諦めんなよ、お前!!');
      atom.notifications.addError('どうしてそこでやめるんだ、そこで!!');
      atom.notifications.addError('もう少し頑張ってみろよ!');
      atom.notifications.addError('ダメダメダメ!諦めたら!');
      atom.notifications.addError('周りのこと思えよ、応援してる人たちのこと思ってみろって!');
      atom.notifications.addError('あともうちょっとのところなんだから!');
      atom.notifications.addError('俺だってこのマイナス10度のところ、しじみがトゥルルって頑張ってんだよ!');
      atom.notifications.addError('ずっとやってみろ!必ず目標を達成できる!');
      atom.notifications.addError('だからこそNever Give Up!!');
    } else {
      // タイマー停止
      clearTimeout(timeoutId);
      count = 0;
      // disposeで入力検知を停止
      this.editorChangeSubscription.dispose();
      this.editorSaveSubscription.dispose();
      atom.notifications.addInfo('えらい!bot停止');
    }
  },

f:id:matsutaira:20210422190140p:plain

何秒後にメッセージを表示するかとオン状態の維持回数をユーザが設定できるようにします。

  config: {
    "TimeSetting": {
      "type": "integer",
      "default": 60,
      "description": "最終入力時間から何秒経過後にコメントを表示するか"
    },
    "CountSetting": {
      "type": "integer",
      "default": 1,
      "description": "強制的に機能ON状態を継続する回数"
    }
  },

// 中略

  disable() {
    if (count < (atom.config.get(`erai-bot.CountSetting`))) {
      // アクティブ状態に強制移行
      this.active = !this.active;
      count = count + 1;
      atom.notifications.addError('諦めんなよ!');
      atom.notifications.addError('諦めんなよ、お前!!');
      atom.notifications.addError('どうしてそこでやめるんだ、そこで!!');
      atom.notifications.addError('もう少し頑張ってみろよ!');
      atom.notifications.addError('ダメダメダメ!諦めたら!');
      atom.notifications.addError('周りのこと思えよ、応援してる人たちのこと思ってみろって!');
      atom.notifications.addError('あともうちょっとのところなんだから!');
      atom.notifications.addError('俺だってこのマイナス10度のところ、しじみがトゥルルって頑張ってんだよ!');
      atom.notifications.addError('ずっとやってみろ!必ず目標を達成できる!');
      atom.notifications.addError('だからこそNever Give Up!!');
    } else {
      // タイマー停止
      clearTimeout(timeoutId);
      count = 0;
      // disposeで入力検知を停止
      this.editorChangeSubscription.dispose();
      this.editorSaveSubscription.dispose();
      atom.notifications.addInfo('えらい!bot停止');
    }
  },

// 中略

  startTimer() {
    // ユーザ設定から秒数を取得
    var time = atom.config.get(`erai-bot.TimeSetting`);
    timeoutId = setTimeout(() => {
      atom.notifications.addWarning('休憩できてえらい!');
    }, time * 1000);
  }

f:id:matsutaira:20210422190713p:plain

以上でプラグインは完成です。プラグインのコードは以下に記載しますので、気になった方はこちらを参考にプラグインの作成に役立ててみてください。

'use babel';

import EraiBotView from './erai-bot-view';
import { CompositeDisposable } from 'atom';

// グローバル変数
var timeoutId;
var count = 0;

export default {

  // ユーザ設定用
  config: {
    "TimeSetting": {
      "type": "integer",
      "default": 60,
      "description": "最終入力時間から何秒経過後にコメントを表示するか"
    },
    "CountSetting": {
      "type": "integer",
      "default": 1,
      "description": "強制的に機能ON状態を継続する回数"
    }
  },

  eraiBotView: null,
  modalPanel: null,
  subscriptions: null,

  activate(state) {
    this.eraiBotView = new EraiBotView(state.eraiBotViewState);
    this.modalPanel = atom.workspace.addModalPanel({
      item: this.eraiBotView.getElement(),
      visible: false
    });

    // 入力検知用
    this.editor = atom.workspace.getActiveTextEditor();
    this.editorChangeSubscription = this.editor.getBuffer().onDidChange(this.onChange.bind(this));

    // 保存検知用
    this.editorSaveSubscription = this.editor.getBuffer().onDidSave(this.onSave.bind(this));

    // Events subscribed to in atom's system can be easily cleaned up with a CompositeDisposable
    this.subscriptions = new CompositeDisposable();

    // Register command that toggles this view
    this.subscriptions.add(atom.commands.add('atom-workspace', {
      'erai-bot:toggle': () => this.toggle()
    }));
  },

  deactivate() {
    this.modalPanel.destroy();
    this.subscriptions.dispose();
    this.eraiBotView.destroy();
  },

  serialize() {
    return {
      eraiBotViewState: this.eraiBotView.serialize()
    };
  },

  toggle() {
    this.active = !this.active;
    if (this.active) {
      return this.disable();
    } else {
      return this.enable();
    }
  },

  enable() {
    // 起動時に有効化
    this.editorChangeSubscription = this.editor.getBuffer().onDidChange(this.onChange.bind(this));
    this.editorSaveSubscription = this.editor.getBuffer().onDidSave(this.onSave.bind(this));
    atom.notifications.addInfo('えらい!bot起動');
  },

  disable() {
    if (count < (atom.config.get(`erai-bot.CountSetting`))) {
      // アクティブ状態に強制移行
      this.active = !this.active;
      count = count + 1;
      atom.notifications.addError('諦めんなよ!');
      atom.notifications.addError('諦めんなよ、お前!!');
      atom.notifications.addError('どうしてそこでやめるんだ、そこで!!');
      atom.notifications.addError('もう少し頑張ってみろよ!');
      atom.notifications.addError('ダメダメダメ!諦めたら!');
      atom.notifications.addError('周りのこと思えよ、応援してる人たちのこと思ってみろって!');
      atom.notifications.addError('あともうちょっとのところなんだから!');
      atom.notifications.addError('俺だってこのマイナス10度のところ、しじみがトゥルルって頑張ってんだよ!');
      atom.notifications.addError('ずっとやってみろ!必ず目標を達成できる!');
      atom.notifications.addError('だからこそNever Give Up!!');
    } else {
      // タイマー停止
      clearTimeout(timeoutId);
      count = 0;
      // disposeで入力検知を停止
      this.editorChangeSubscription.dispose();
      this.editorSaveSubscription.dispose();
      atom.notifications.addInfo('えらい!bot停止');
    }
  },

  onChange(input) {
    // タイマー停止
    clearTimeout(timeoutId);
    atom.notifications.addInfo('入力できてえらい!');
    // タイマー開始
    this.startTimer();
  },

  onSave() {
    atom.notifications.addSuccess('保存できてえらい!');
  },

  startTimer() {
    // ユーザ設定から秒数を取得
    var time = atom.config.get(`erai-bot.TimeSetting`);
    // タイマーのIDを取得しつつタイマー開始
    timeoutId = setTimeout(() => {
      atom.notifications.addWarning('休憩できてえらい!');
    }, time * 1000);
  }

};


おわりに

プラグインの作成はそこまで難しいことはなく、JavaScriptの知識さえあれば簡単に作成できました。僕自身JavaScriptの知識はほぼ皆無なので、この程度のプラグインしか作成できませんでしたが、少しの知識でもプラグインを作成することができました。個人的には、アニメーションや音等が出るような面白いプラグインを作成したかったですが、できることはできたかなと思います。プラグインの作成自体、試行錯誤を重ねながらだったため、新鮮で勉強にもなりました。より勉強を重ねて自分の納得するプラグインを作成できればと思います。
皆さんもAtomで自分だけのオリジナルプラグインを作成してみてはいかがでしょうか。


  • エンジニア中途採用サイト
    ラクスでは、エンジニア中途採用を積極的に行っております!
    ご興味ありましたら是非ご確認をお願いします。 career-recruit.rakus.co.jp

  • カジュアル面談お申込みフォーム
    どの職種に応募すれば良いかわからないという方は、カジュアル面談も随時行っております。
    以下フォームよりお申込みください。
    forms.gle

  • イベント情報
    会社の雰囲気を知りたい方は、毎週開催しているイベントにご参加ください! rakus.connpass.com

Copyright © RAKUS Co., Ltd. All rights reserved.