Sublime TextとRailsを連携させて開発を楽にする

Happy Elements株式会社 カカリアスタジオ Advent Calendar 2016の9日目の記事です。
担当は [@ryooo](https://github.com/ryooo) です。よろしくお願いします。

# 要旨
Sublime Textのプラグインからrailsの開発用APIを叩くことで快適な開発環境ができる。

# どういうことができるか
## ○○に応じた××を調べられる
### ID1〜3のアイテム名を調べる
![g1.gif](https://qiita-image-store.s3.amazonaws.com/0/7847/0ddacc79-07e9-9492-38d4-309014dbfa93.gif)

### スキルID38, 39それぞれのモンスターIDを調べ、その後スキルID38のモンスター名を調べる
![g2.gif](https://qiita-image-store.s3.amazonaws.com/0/7847/2fccbbe2-4853-2045-6768-da97fa136451.gif)


## 開発中にRubyのコードを動かす
![g3_2.gif](https://qiita-image-store.s3.amazonaws.com/0/7847/b684e343-db98-2174-c561-784ff679022c.gif)


# 説明
## Sublime側のコード(Python)
### 概要
#### GetDictionaryCommand
マルチカーソルで選択されている行の末尾に、サーバーから受け取った辞書をもとに値を追記する実装です。

#### ExecRailsCommand
マルチカーソルで選択されているrubyコードの末尾に、サーバーから受け取った実行結果を追記する実装です。
### 実装（紹介用の例）
```py
import sublime
import sublime_plugin
import json
import urllib.request
import urllib.parse

# 設定例(Setting User)
# "App.host" : "localhost",

# binding例
# { "keys": ["ctrl+m"], "command": "get_dictionary" },
# { "keys": ["ctrl+e"], "command": "exec_rails" },

class GetDictionaryCommand(sublime_plugin.TextCommand):
    def run(self, edit):
        self.view.window().show_input_panel('Dictionary Type', '', lambda s: self.view.window().run_command("get_dictionary_and_replace",{"arg":s}), None, None)

class GetDictionaryAndReplace(sublime_plugin.TextCommand):
    def run(self, edit, arg):
        # 接続先サーバーをステータスバーに出力
        settings = sublime.load_settings('Preferences.sublime-settings')
        host = settings.get('App.host')
        if not host:
            host = "localhost"
        sublime.status_message("host is " + host + ".")
        
        with urllib.request.urlopen("https://" + host + "/辞書データをreturnするAPI/?type=" + urllib.parse.quote(arg)) as res:
            body = res.read().decode("utf-8")
            jsonData = json.loads(body)
            for selection in self.view.sel():
                if self.view.substr(selection) in jsonData:
                    row = self.view.line(selection)
                    self.view.replace(edit, row, self.view.substr(row) + " # " + jsonData[self.view.substr(selection)])
            self.view.sel().clear()

class ExecRailsCommand(sublime_plugin.TextCommand):
    def run(self, edit):
        host = "localhost"
        sublime.status_message("host is " + host + ".")
        for selection in self.view.sel():
            code = self.view.substr(selection)
            try:
                with urllib.request.urlopen("https://" + host + "/コードを実行して結果を返すAPI/?q=" + urllib.parse.quote(code)) as res:
                    body = res.read().decode("utf-8")
                    row = self.view.line(selection)
                    self.view.replace(edit, row, self.view.substr(row) + " # " + body)
            except urllib.error.HTTPError as e:
                row = self.view.line(selection)
                self.view.replace(edit, row, self.view.substr(row) + " # err " + e.reason)
        self.view.sel().clear()
```


## Rails側のコード
### 概要
typeに応じた辞書jsonを返すAPIです。
リターンした辞書を元に、Sublime側でコメントを追記するので、このAPIの実装次第でやりたいようにカスタムできます。

### 実装（紹介用の例）
```rb
class XXXController < ActionController::Base
  def dictionary
    ret = case params[:type]
    when 'item'
      # アイテムID => アイテム名
      Hash[*::Item.all_with_cache.map{|r|[
        r.id,
        r.name,
      ]}.flatten]
    when 'monster'
      # モンスターID => モンスター名
      Hash[*::Monster.all_with_cache.map{|r|[
        r.id,
        r.name,
      ]}.flatten]
    when 'monster skill_id'
      # モンスタースキルID => モンスターID(カンマ区切り)
      val = Hash.new {|h, k| h[k] = []}
      ::Monster.all_with_cache.each do |m|
        val[m.skill_id] << m.id
      end
      Hash[*val.map{|k, v|[k, "#{v.join(',')}"]}.flatten]
    else
      raise 'unknown type'
    end
    
    return render :json => ret
  end
end
```

### コードを実行して結果を返すAPI
実装は割愛しますが、受け取ったコードを実行して結果をreturnするようなコードなので、**認証された環境や外部からアクセスできない環境でしか絶対に受け付けないこと、また必ず検証を行うことが前提**となります。

# おまけ
## gifの中で使っている連番入力を簡単にするプラグインの紹介
※ 昔[ここ](http://qiita.com/ryooo321/items/734a81a411d2f2eea6b3)で投稿したものの簡易版です。
### 概要
マルチカーソルの先頭の数字を初期値として、連番に変換してくれるプラグインです。
![ni.gif](https://qiita-image-store.s3.amazonaws.com/0/7847/789e1f59-4244-d7d1-7f6e-8d4e873ca1df.gif)
### 実装（紹介用の例）
```py
# binding例
# { "keys": ["ctrl+n"], "command": "increment_numbers" },

class IncrementNumbersCommand(sublime_plugin.TextCommand):
    def run(self, edit):
        start_value = int(self.view.substr(self.view.sel()[0]))
        counter = 0
        for selection in self.view.sel():
            self.view.insert(edit, selection.begin(), str(start_value + counter))
            counter = counter + 1
        for selection in self.view.sel():
            self.view.erase(edit, selection)
```

# 最後に
**Happy Elements株式会社 カカリアスタジオでは、一緒にものづくりをする仲間を募集しています！**

ものづくりが大好きな方は是非お気軽にご応募下さい。

[カカリアスタジオ 採用情報](http://cacalia.jp/recruit/)
> ひとりひとりのチカラを最大限に引き出せるこのうえない環境と体制、文化があればこそ、最高のものづくりをつながると私たちは信じています。カカリアスタジオのメンバーとして、私たちと一緒に働きたいと思う仲間を募集しています。