vue-codemirror を試す

CodeMirror っていう JavaScript 製のエディタコンポーネントの Vue.js 用コンポーネント vue-codemirror を触ってみたときの備忘録。

最終的なコードはここにある。

github.com

vue-codemirror をプロジェクトに読み込む

import Vue from 'vue'
import VueCodemirror from 'vue-codemirror'

import 'codemirror/lib/codemirror.css'

Vue.use(VueCodemirror)

こんな感じ。

これを plugins/vue-codemirror.js に切り出して、 main.js

import '@/plugins/vue-codemirror'

コンポーネントの利用

ドキュメント通りでできるけど、このあとのベースラインとして。

<template lang="pug">
  #app
    codemirror(v-model="code" :options="cmOptions")
</template>

<script>
export default {
  name: 'App',

  data() {
    return {
      code: '',

      cmOptions: {
      }
    }
  }
}
</script>

インデントまわりのカーソル移動をいい感じにする

これでだいたいエディタっぽいアプリをつくることはできるんだけど、インデントまわりに不満がある。個人的には以下のようにしたい。

  • インデントはタブではなく、ホワイトスペースを利用してほしい
  • インデントの先頭でバックスペースを押したとき、文字単位ではなくインデント単位で直前の空白を削除してほしい

CodeMirror のオプションを指定したらいい感じになるかなと思ったけど、うまくいかなかった。調べたら以下のような先達の tips があった。

Setting for using spaces instead of tabs · Issue #988 · codemirror/CodeMirror · GitHub

しかしこの通りにやってもうまくいかなかったので、ベースを利用しつつ、自分の好みになるように書いてみた。

Vue.use(VueCodemirror, {
  options: {
    // based on https://github.com/codemirror/CodeMirror/issues/988#issuecomment-549644684
    extraKeys: {
      Tab: (cm) => {
        if (cm.getMode().name === 'null') {
          cm.execCommand('insertTab');
        } else {
          if (cm.somethingSelected()) {
            cm.execCommand('indentMore');
          } else {
            cm.execCommand('insertSoftTab');
          }
        }
      },
      Backspace: (cm) => {
        if (!cm.somethingSelected()) {
          let cursorsPos = cm.listSelections().map((selection) => selection.anchor);
          let indentUnit = cm.options.indentUnit;
          let shouldDelChar = false;

          for (let cursorPos of cursorsPos) {
            const { start, end, string, type } = cm.getTokenAt(cursorPos)
            if (type !== 'indent' && type !== null
              || start !== 0 || end !== cursorPos.ch || end === 0
              || string.trimStart() !== ''
              || end % indentUnit > 0) {
              shouldDelChar = true
              break
            }
          }

          if (!shouldDelChar) {
            cm.execCommand('indentLess');
          } else {
            cm.execCommand('delCharBefore');
          }
        } else {
          cm.execCommand('delCharBefore');
        }
      },
      'Shift-Tab': (cm) => cm.execCommand('indentLess')
    }
  }
})

保存アクションで dirty flag を消す

CodeMirror には、 CMD-S (CTRL-S) で保存するキーバインドがデフォルトで定義されており、その際呼び出されるアクションとして save という (カラの) アクションが定義されている。

また、 CodeMirror コンポーネント自体にも save という (カラの) メソッドが定義されている (上記のアクションとは別物)。

これらをくみあわせて、保存アクションをしたときに dirty flag をリセットするようにしたい。

ちなみに CodeMirror には開発者が活用できる以下のような内部状況フラグがある。

まずは save action の上書きから。

// default save action の上書き
VueCodemirror.CodeMirror.commands.save = (cm) => {
  // 保存されるときに "clean" 扱いにする
  cm.markClean()

  // 現状ではなにも内容の定義されていない、コンポーネントの save メソッドを呼び出しておく
  cm.save(cm)
}

つぎはコンポーネント側。

    codemirror#editor(ref="cmComponent" v-model="code" :options="cmOptions" @input="cmChanged")
export default {
  name: 'App',

  data() {
    return {
      code: '',
      isClean: true,

      cmOptions: {
        // ...
      }
    }
  },

  computed: {
    cmComponent() {
      return this.$refs.cmComponent
    }
  },

  mounted() {
    this.cmComponent.codemirror.save = (cm) => {
      this.isClean = cm.isClean()
    }
  },

  methods: {
    cmChanged() {
      this.isClean = this.cmComponent.codemirror.isClean()
    }
  }
}

コンポーネントの save メソッド側で、 手元のコンポーネントの clean フラグに CodeMirror の isClean フラグの状況を反映させる。 また、 CodeMirror コンポーネントの change 時にもそのフラグの状況を反映させる。

そもそも CodeMirror の clean フラグを使わなくっても、 CMD-S を自前でハンドリングしたり、 change のときだけ not clean にしたりすればいいんじゃ?って思うかもだけど、実は CodeMirror 側では、 Undo 等して、編集差分が発生しなくなったときに clean になったりする。なので、 clean フラグ自体は CodeMirror 側に負わせるのがよい。

(とはいえ、 save action とコンポーネントの save method をどちらもオーバライドしているこのやりかたはすこしくどすぎるかも。 save action の上書きだけでよさそう。あと、 change イベントと save action をハンドリングして内部のフラグをもってきているから、いまいちリアクティブみがない。もっとよい方法ないのかな。)

集大成として Markdown editor もどき

以上のサンプルコードとして、 Markdown editor もどきをつくった。

再掲になるけど、

github.com

https://user-images.githubusercontent.com/42583/77849162-8715a600-7204-11ea-8501-943fa39cdea4.png

べつだんリアルタイムプレビューを出すのはそこまで難しくないんだけど、 clean フラグのとりまわしをやりたかったので、 あえて CMD-S を押さないとプレビューに反映されないようになっている。

Spring Boot で ApplicationContext を初期化せずに application.properties の内容を取得する

このへんのソースを読むと、一応以下のように書けるっぽい?

なんか目的外使用っぽくてよくないですけど。

StandardEnvironment env = new StandardEnvironment();
new ConfigFileApplicationListener().postProcessEnvironment(env, new SpringApplication());

log.info("{}", env.getProperty("spring.datasource.url"));

ちなみにやりたかったことは、テストコードで ApplicationContext を利用せずに MyBatis の mapper を利用したかった。

StandardEnvironment env = new StandardEnvironment();
new ConfigFileApplicationListener().postProcessEnvironment(env, new SpringApplication());

DataSource dataSource = DataSourceBuilder.create()
        .type(Class.forName(env.getProperty("spring.datasource.type")).asSubclass(DataSource.class))
        .url(env.getProperty("spring.datasource.url"))
        .username(env.getProperty("spring.datasource.username"))
        .password(env.getProperty("spring.datasource.password"))
        .driverClassName(env.getProperty("spring.datasource.driver-class-name"))
        .build();

Configuration mybatisConf = new Configuration(new Environment("test", new JdbcTransactionFactory(), dataSource));
mybatisConf.setMapUnderscoreToCamelCase(true);
mybatisConf.addMapper(HogeHogeMapper.class);

SqlSession sqlSession = new SqlSessionFactoryBuilder()
        .build(mybatisConf)
        .openSession();
try {
    HogeHogeMapper mapper = sqlSession.getMapper(HogeHogeMapper.class);

    // ...
} finally {
    sqlSession.close();
}

でもここまで書いて結果的には利用しなかった。

Spring Initializr の desktop app を Electron で書いた

IntelliJ IDEA の Ultimate 版だと Project 生成のときに Spring Initializr を利用して作成することができるんだけど、 Community Edition にはその機能がない。単に Web の Spring Initializr を実行して zip アーカイブをダウンロードして展開すればいいだけなんだけど、その一手間がダルいので Electron でアプリケーションにした。

Spring Initializr には API (というか metadata 仕様?)があるんだけど、それを利用してるんではなくて、単純に内蔵ブラウザで Initializr のサイト表示して、 Generate Project ボタン押されたときに*1それをフックして、 node.js でダウンロードして指定位置に展開しているだけです。

カスタマイズした Initializr で使いたいときは、コマンドラインからその URL を指定することもできます。

*1:というかフォームが submit されたときに

Synaptics Clickpad で右クリックを有効にする on Ubuntu Precise (12.04)

2本指タップで右クリックになるし、それに慣れちゃったけど、一応

オンラインで変更するには

$ xinput set-prop "SynPS/2 Synaptics TouchPad" "Synaptics Soft Button Areas" 3872 0 3984 0 0 0 0 0

数字はよそから拾ってきた適当なものだけど。
これで一応右下をクリックすると右クリック扱いとなる。

Skype4Py で bot を作る

コンタクトリストのユーザーがオンラインになったら「おかえり」というストーキング bot

ちなみに Ubuntu 12.04 (Precise) で Skype4Py (パッケージ名 python-skype) をインストールするにはレポジトリに ppa:skype-wrapper/ppa を追加する必要がある (パッケージ skype-wrapper.deb 自体はインストールする必要はない)。

# -*- coding: utf-8 -*-

import Skype4Py
import Queue
import logging

# Skype4Py のログを出力する (DEBUG だと大量なので注意; 便利だけど)
logging.basicConfig(level=logging.DEBUG)

# ALTER CHAT の expected response が変わったので修正
def my_Chat__Alter(self, AlterName, Args=None):
    return self._Owner._Alter('CHAT', self.Name, AlterName, Args,
                              'ALTER CHAT %s' % AlterName)
Skype4Py.chat.Chat._Alter = my_Chat__Alter


q = Queue.Queue()

def on_online_status(user, status):
    print '%s (%s): %s' % (user.Handle, user.FullName, status)
    if status == 'ONLINE':
        q.put(user.Handle)

def main():
    skype = Skype4Py.Skype()
    skype.OnOnlineStatus = on_online_status
    skype.Attach()

    while True:
        try:
            item = q.get(True, 86400)   # 0 にすると KeyboardInterrupt が効かない
            chat = skype.CreateChatWith(item)
            chat.SendMessage('おかえり')
            chat.Leave()
        except Queue.Empty:
            pass

main()

skypeグローバル変数にぶっこんで,on_online_status() から CreateChatWith() してもいいんだけど,やり取り等は別スレッドで動いているらしく,ちょいと気持ち悪かったので Queue でスレッド間通信することにした。おかげで while True: time.sleep(1) みたいなことをしなくてもよくなった (実質 Queue.get() してるので同じだけど)。
上のサンプルでは Queue にユーザーハンドルだけつっこんでるけど,将来的にはそれなりのコマンドオブジェクト等をつっこむようにしたほうがいいですね。

uWSGI でファイルが更新された時にリロードする

最初は inotifyx 使って自力で書こうとしてたんだけど,ブロックしてうまくいかなかったりして,thread でも立てなきゃいけないのかなと思って uwsgidecorators を読んでたら,そもそも uwsgidecorators (というかそもそも uwsgi-core) にファイル更新検知機能がついてた。

メインとなる wsgi ファイルが main.wsgi というファイル名だったとして,監視するモジュール((別モジュールだてにしなくてもいい (main.wsgi に入れ込む) のかもしれないけど追ってない追記: やってみたら別モジュールに独立させなくてもうまくいった。小さい捨て WSGI スクリプトであればリロードのロジックを入れ込むのが楽。(もちろん,WSGI アプリケーションとして独立させるなら入れ込まないほうがいいけど) )) watcher.py は:

# watcher.py
import uwsgi
from uwsgidecorators import filemon

@filemon('main.wsgi')
def reloaded(num):
    uwsgi.reload()

対応する uWSGI のパラメータは,たとえば:

; uwsgi.ini
[uwsgi]
master = true
plugins = python,http
http = :8000
wsgi-file = main.wsgi
import = watcher

ファイルがいっぱいあったらどうすんねーんと思うかもしれないけど,@filemon デコレータは,ディレクトリの監視もできます。

ちなみに内的には inotify 機構を使っているので無駄はないはず。IN_ALL_EVENTS レベルの監視をしてるんで,touch とかしてもちゃんとリロードされます。



あと今回の話と関係ないけど,たとえば 1 分間リクエストがなかった場合にワーカーを自動的に kill するには

idle = 60

のようにしておけばよい。uWSGI 使うようなシチュエーションでそんなにメモリをケチるシーンはないかもしんないけど。

追記: idle (や lazy) と @filemon の相性はよくないかもしんない。ワーカーがいない状態で編集してリクエスト投げたら編集前の内容だったことがあった。気のせいかもしれないけど。


もういっこわりと便利なオプションは harakiri

harakiri = 60

のようにしておくと,処理に 60 秒以上かかるワーカーは強制的に落とされます。

NLog で動的にログの出力状況を変更する

設定のオンオフで,ログを出力するかどうかを変えたり,出力先 (ファイルや TextBox など) を変更したい場合。

たとえば NLog.config が以下のようなときに

<?xml version="1.0" encoding="utf-8"?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <targets>
    <target name="file" xsi:type="File" fileName="${basedir}/app.log"/>
  </targets>
</nlog>

target は定義されているけど rule は定義されていないので,このままではログはどこにも出力されていない。

この状況で上記の file target にログを出力するように変更するには,

using NLog;
using NLog.Config;

NLog.Targets.Target target = LogManager.Configuration.FindTargetByName("file");
LoggingRule rule = new LoggingRule("*", LogLevel.Debug, target);

LogManager.Configuration.LoggingRules.Clear();
LogManager.Configuration.LoggingRules.Add(rule);
LogManager.ReconfigExistingLoggers();

のようにする。

LogManager.Configuration.LoggingRulesList<LoggingRule> なので,List ジェネリックスのメソッドを使って操作すればよい。ただし操作しただけだとすでに生成されてしまっているロガーには (その設定変更が) 伝わらないので LogManager.ReconfigExistingLoggers() する必要がある。