Takuji->find;

Sansan株式会社でAndroidのテックリードをやってます、技術的な記事を書いているつもり

Atomの自動補完プラグイン「autocomplete-plus」のPerl用Providerを書いている話

これは はてなエンジニアアドベントカレンダー2016 23日目の記事です。

qiita.com

developer.hatenastaff.com

昨日は id:takuya-a さんの文字列アルゴリズムの学びかたでした。

こんにちは、はてなでアプリケーションエンジニアとしてWebサービスAndroidアプリ(たまにiOSアプリ)を開発しているid:takuji31です。

私は普段VimPerlを書いているのですが、最近AtomPerlを書きたくなってAtomの自動補完プラグインである「autocomplete-plus」のPerl用Providerを書いているので、今日はそのことについて話します。

先に謝罪しておきますと、本来はこのエントリーで公開しましたと告知する予定でしたが、まだ実用的なクオリティーには達していないので、開発中とお知らせするだけになります 🙇

github.com

Atomとは

Atomとは(恐らくこの記事を読むような方はご存知だとは思いますが)、Githubが公開しているオープンソーステキストエディターです。

atom.io

A hackable text editorとある通り、プラグインを書くことでかなり自由にHackすることができます。

autocomplete-plus

autocomplete-plusはAtomの自動補完プラグインです。

github.com

最近のバージョンのAtomに標準でバンドルされていて、インストールするだけで様々な言語のコード補完を行うことができます。

また、標準で用意されている言語以外にも、Providerを用意することで自分でコード補完の候補を作ることができます。

既に存在するProviderの一覧はGitHubのautocomplete-plusのWikiに一覧があります

github.com

見てみるとPerl用がないですね、ググってみた感じもないようでしたので、これは作るしかないと思いました。

Providerを作る

雛形を自動生成する

ProviderはAtomのPackageとして作る必要があります。

Atomでは開発用にPackageを生成してくれるメニューがあります、メニューの Packages -> PackageGenerator -> Generate Atom Package を実行しましょう。

コマンドパレットを開いて Package Generator: Generate Package を探して実行してもよいです。

f:id:takuji31:20161222183702p:plain

実行するとPackageの作成先を指定するダイアログが出ますので、適当な名前を決めて入力します。

決定すると指定したパスにサンプルコードと共にPackageが生成されます。

hatena-advent-calendar-2016/
├── CHANGELOG.md
├── LICENSE.md
├── README.md
├── keymaps
│   └── hatena-advent-calendar-2016.json
├── lib
│   ├── hatena-advent-calendar-2016-view.js
│   └── hatena-advent-calendar-2016.js
├── menus
│   └── hatena-advent-calendar-2016.json
├── package.json
├── spec
│   ├── hatena-advent-calendar-2016-spec.js
│   └── hatena-advent-calendar-2016-view-spec.js
└── styles
    └── hatena-advent-calendar-2016.less

以下のファイルとディレクトリはProviderには不要なので消します。

  • keymaps
    • このPackageのキーマップを定義するファイル
  • menus
    • メニューのエントリーを定義するファイル
  • styles
    • Packageのスタイルを定義するファイル
  • lib/*.js
    • サンプルコードですが(Providerでは)一切使わないので消します。

Packageに必要な設定

不要なファイルを整理したら、 package.json の一番上の階層に以下の設定を追加します。

{
  "providedServices": {
    "autocomplete.provider": {
      "versions": {
        "2.0.0": "provide"
      }
    }
  }
}

provide の部分はJS側で定義するメソッド名なので、自由に決めて構いません。

Packageのメインオブジェクトを作る

lib 以下のPackage名と同じjsファイルからexportしたオブジェクトがプラグイン機構の本体になります。

JavaScript以外にもCoffeeScriptも直接使うことができますが、私は業務でTypeScriptを使っていることもあり、TypeScriptで書いています。

/// <reference path="../typings/bundle.d.ts" />

import {Config} from "./config";
import {Provider} from "./provider";
import {UseAndRequireCompletionProvider} from "./use-and-require-completion-provider";

class AutocompletPerlProvider {
  providers: Provider[] = null
  config =  Config.config
  activate() {

  }
  deactivate() {
    this.providers = null
  }
  provide() {
    if (this.providers === null) {
      this.providers = [new UseAndRequireCompletionProvider()];
    }
    return this.providers
  }

}
export = new AutocompletPerlProvider()

このように provide メソッドの中で実際の補完候補を提供するProviderのインスタンスを作って返してやります。Providerは複数指定可能です。

設定項目を追加

メインのオブジェクトに config フィールドを追加することで、そこに定義されている設定項目がAtomの設定画面に自動的に追加されます。

先ほどのコードでは config フィールドの中身は Config.config を参照しているので、その中身を見てみましょう。

class Config {
  static config = {
    perlPath: {
      type: 'string',
      default: 'perl'
    },
    cartonPath: {
      type: 'string',
      default: 'carton'
    },
    useCarton: {
      type: 'boolean',
      description: 'Use carton when building completion suggestions. only effects when cpanfile and carton executable exists.',
      default: false,
    }
  }
  static get perlPath():String {
    return atom.config.get('autocomplete-perl.perlPath');
  }
  static get cartonPath():String {
    return atom.config.get('autocomplete-perl.cartonPath');
  }
  static get useCarton():boolean {
    return atom.config.get('autocomplete-perl.useCarton');
  }
}
export {
  Config
}

上記のコードでは3つの設定項目が指定されています。

このような定義を作成することで、Atomの設定画面にあるPackageの設定項目が自動的に生成されて以下のようになります。

f:id:takuji31:20161223090629p:plain

このコードでは設定の定義以外にも、設定値を取り出すのに便利なプロパティーを定義しています。このようにしておくと、実際のコード側で簡単に取り出せて便利でしょう。

Providerを作る

Providerは決まったメソッドとフィールドを持ったクラスである必要があります。

その仕様については、autocomplete-plusのWikiにまとめられています。

Provider API · atom/autocomplete-plus Wiki · GitHub

最低限 selector フィールドと getSuggestions メソッドがあればProviderとしては機能します。

/// <reference path="../typings/bundle.d.ts" />

import {Provider, SuggestionInfo} from "./provider";
import {ISuggestion} from "./suggestion";
import {Range, Point} from "atom";

class UseAndRequireCompletionProvider extends Provider {
  selector : string = ".source.perl"
  getSuggestions(info : SuggestionInfo) : Promise<ISuggestion> {
    return new Promise((resolve) => {
        var suggestions =  // ここで補完候補を生成する
        resolve(suggestions)
    });
  }
}
export {
  UseAndRequireCompletionProvider
}

atom-autocomplete-perl では型でこの辺りの制約をはっきりさせておきたかったので、ベースのクラスやインターフェイスを別ファイルで定義しました。

provider.ts

/// <reference path="../typings/bundle.d.ts" />
import {ISuggestion} from './suggestion';

interface SuggestionInfo {
  editor: AtomCore.IEditor;
  bufferPosition: TextBuffer.IPoint;
  scopeDescriptor: AtomCore.ScopeDescriptor;
  prefix: string;
  activatedManually: boolean;
}

abstract class Provider {
  selector : string = ".source.perl"
  abstract getSuggestions(info : SuggestionInfo) : Promise<ISuggestion[]>
}
export {
  Provider,
  SuggestionInfo
 }

suggestion.ts

/// <reference path="../typings/bundle.d.ts" />

type SuggestionType = 'variable'|'constant'|'property'|'value'|'method'|'function'|'class'|'type'|'keyword'|'tag'|'snippet'|'import'|'require';

interface ISuggestion {
  displayText?: string
  replacementPrefix?: string
  type?: SuggestionType;
  leftLabel?: string
  leftLabelHTML?: string
  rightLabel?: string
  rightLabelHTML?: string
  className?: string
  iconHTML?: string
  description?: string
  descriptionMoreURL?: string
}

class TextSuggestion implements ISuggestion {
  constructor(public snippet: string, public type: SuggestionType) {
  }
}
class SnippetSuggestion implements ISuggestion {
  constructor(public text: string) {

  }
}

export {
  ISuggestion,
  TextSuggestion,
  SnippetSuggestion,
  SuggestionType
}

この定義のおかげで、簡単にProviderを増やすことができますね。

あとはひたすら getSuggestions に渡ってくる入力の情報から非同期に補完候補を生成してresolveするだけです。

とは言うものの、今の時点でまだこれは使っていません、なぜなら簡単なキーワードだけなら自動的に補完候補を生成してくれる便利APIがあるのです。

SymbolProvider Config API

簡単なキーワードなら、SymbolProvider Config API を使うことで補完候補を生成できます。

SymbolProvider Config API · atom/autocomplete-plus Wiki · GitHub

たとえばPerlのビルトイン関数は settings/language-perl.cson に以下のように書くだけです。

'.source.perl':
  autocomplete:
    symbols:
      builtin:
        suggestions: [
          'abs'
          'accept'
          'alarm'
          'atan2'
          'bind'
          'binmode'
          'bless'
          // ...
          'waitpid'
          'wantarray'
          'warn'
          'write'
          'y'
        ]

これだけであとは勝手に補完できるようになります。

f:id:takuji31:20161223093650p:plain

ビルトイン関数は最初Providerを作って補完できるようにしていましたが、こちらの設定に変えました。

atom-autocomplete-perl の今後について

このProviderは、今のところビルトイン関数の補完だけ動きます。

今後以下のような機能の提供を予定しています。

  • package名補完
  • クラスメソッド補完
  • (構想段階ですが) Smart::ArgsData::Validator でバリデーションした変数の型を雑に推測してそれっぽい補完候補を返す

ひとまずpackage名補完だけ追加したら、全国のPerl Mongerの皆様が利用できる状態にしたいなぁと思っています。

まとめ

AtomのPackageは簡単に作ることができました。node.jsを使うことができますので、npmにある大量の資産を活かしたり、他の言語のコードをシステム経由で実行することもできます。

あなたもこの機会にAtomをHackしてみませんか?

はてなではJavaScriptやAltJSが好きなエンジニアを募集しています!

hatenacorp.jp

明日の担当は id:tarao です、お楽しみに!