Author Archives: misohena

2021-08-08

Companyのお節介な補完を抑制する

(2022-08-15追記:CompanyからCorfuへ移行しました)

Emacsのバッファ内での入力補完を行うcompany-modeですが、使っていると困ることも色々ありました。それで少し設定を変えて使っていたのですが、まだ不満が残っていたのでここらでゆっくり検討してみました。

一番困るのが誤入力を助長してしまうという点です。テキスト入力中に勝手に候補を出すまでは良いのですが、RETを押すと勝手に出してきた候補を選択してしまい不要な文字列を付け足してしまいます。

例えば 100 と入力して RET を押したら 100 の後に改行が入ることを期待するわけですが、改行を打つ直前に勝手に 100 で始まる文字列を候補に出してきて、それが入力されてしまうわけです。例えばバッファ内に他に 10000 と書いてある部分があると、 100 ではなく 10000 が入力されてしまうわけです。入力時に気がつけば良いのですが、気がつかずに後から計算結果が合わなくて発覚したこともありました。

自動起動禁止!

慌てて行った設定が次。

;; case1
;; 自動起動を禁止する。(self-insert系を除外する)
(setq company-begin-commands
      '(c-scope-operator c-electric-colon c-electric-lt-gt c-electric-slash))

もう補完候補を自動で出すのは止めなさい、と。self-insert系コマンドを company-begin-commands から外せば少なくとも通常の文字を入力しているときに突然候補が出るのを抑制できます。最も安全です。

他の解決策としては、company-dabbrev-char-regexp から数字や日本語を除外して穏当なものだけ候補に出すという方法もよく見かけました1が、文字種によって判別するというのはなんだか違う気がしました。数字だからダメ、アルファベットだから良いという訳では無いのです。 abcdefghi とどこかに書いてあるバッファで abc RETと打っても同じ問題が起きます。

company-backends から company-dabbrev を外すという方法もよく見ましたが2、私は前からdabbrev自体は使っていましたから、companyを使う代わりにdabbrevが使えなくなるのは困ります。

手動と自動でバックエンドを切り替える(それとモードによって自動起動禁止)

そこで思いついたのが、手動で起動したときだけ company-dabbrev を使い、自動で起動したときは使わないという方法です。それなら M-/ でdabbrevを使い続けることが出来ます。数字であっても M-/ で候補を出してくれて構わないんです。

;; case2
;; dabbrevは手動で起動したときだけ有効にする。
;; アイドルタイムから始まった場合は、一時的に company-backends から
;; company-dabbrev を取り除く。
(defun my-company-idle-begin (oldfun &rest args)
  (let ((company-backends (remq 'company-dabbrev company-backends)))
    (apply oldfun args)))
(advice-add 'company-idle-begin :around 'my-company-idle-begin)

ついでにプログラミング系のモード以外では自動起動を禁止してしまいましょう。 org-modeやtext-modeで自動的に候補が出たからと言って何だというのでしょう!

;; いくつかのモードで自動的に候補を出すのを禁止する。
;; プログラミング系のモードでは比較的大丈夫な場合が多い。
;; 文法上入力できるものが限られており、補完が正しい可能性が高いから。
(defun my-company-inhibit-idle-begin ()
  (setq-local company-begin-commands nil))
(add-hook 'org-mode-hook #'my-company-inhibit-idle-begin)
(add-hook 'text-mode-hook #'my-company-inhibit-idle-begin)

思うにプログラミング言語系のモードは候補が自動的に出ても問題が少ないような気がします。プログラミング言語では場所ごとに書けるものが文法的に限定されています。例えば行末には大抵 ; を入れる言語では改行の前(RETを押したくなる直前)に候補が出ること自体がほとんどありません。出てくる候補の正答率も高いでしょう。

というわけでこのくらいでしばらく使っていたのですが、org-modeで自動的に候補が出てこないというのは少し寂しい気もします。 M-/ で候補を出せると言っても候補があること自体に気がつけませんからね。

自動起動や一部のモードは無選択状態で開始

別に候補を出してくれるのは構わないんです。問題なのは勝手にRETやTABを奪ってしまうことなのです。

候補を出しつつ、例えば選択は↓キー(C-n)を押してからでないと出来ないようにすれば良いのです。 何か良い方法は無いか……とcompany.elを眺めていたら次のような文字が目に飛び込んできました。

When `company-selection-default' is nil, add a special pseudo candidates
meant for no selection."

なんと company-selection-default を nil にするだけで無選択状態から開始できるのです。

;; case3
;; 基本的に候補は無選択状態から始める。
;; 誤って確定してしまうのを防ぐ。
(setq-default company-selection-default nil)
(setq-default company-selection nil)

この変数は defvar でなぜか defcustom ではありません。 しかし試してみたところきちんと動いているようです。ザッとコードを確認しても問題は無さそうに見えます。 (2021-08-09追記:細かい問題が見つかりました。修正方法は末尾に追記してあります)

これで常に無選択状態から始まりますが、手動で開始したときは選択状態から始まっていた方が良いでしょう。入力がまだ不完全で補って欲しくて手動で起動(M-/)しているわけですから、候補を選択した状態から始まっていても問題は無いでしょう。嫌ならC-gを押せば良いだけです。また、やはりプログラミング系のモードではこれまで選択状態から始まっていて違和感が少なかったのでとりあえず同じようにしましょう。

;; 手動起動したときには選択状態から始める。何か選びたいはずなので。
;; 自動起動したときでもモードによっては選択状態から始める。
;; 文法的に正しい候補が出せる可能性が高いとき。
(defun my-company-should-select-first-p ()
  (or
   company--manual-action ;;手動で起動したとき。
   (and (boundp 'lsp-mode) lsp-mode))) ;;LSPが使えるモードは補完の精度が高いはずなので。 (memq major-mode '(c-mode c++-mode))とかでも可。
(defun my-company-auto-begin (oldfun)
  (let ((company-selection-default
         (if (my-company-should-select-first-p) 0 nil)))
    (funcall oldfun)))
(advice-add 'company-auto-begin :around 'my-company-auto-begin)

無選択状態の時であればRETやTABがそのまま入力できるかと思いきやそうなっていません。無選択状態なので確定(誤入力)はしませんが、かといってそのままRETやTABがバッファに入ったりはしません。単純に無視されます。なぜならcompany-active-mapにRETやTABが登録されているので、候補が出ている間はそれが実行されてしまうからです。(ちなみに他の通常の文字(company-active-mapに登録されていない)は無選択状態の時(正確にはcompany-require-matchではないとき)にはそのままバッファに入力できます。手動で起動したり一度でも選択操作すると候補とマッチする文字しか入力できなくなります)

なので無選択状態の時はRETやTABをバッファ本来のキーバインドで実行するようにしました(補完を中断してキーを読み取り前に戻す)。

;; 無選択状態の時にTABやRETが入力されたら
;; そのバッファのモード本来のTABやRETを実行する。
(defun my-company-complete-respecting-user-input (&rest args)
  "ユーザー入力を尊重した補完を行う。"
  (interactive)
  (if (null company-selection)
      ;; モード本来の割り当てを実行する。
      (progn
        (company-abort)
        (company--unread-this-command-keys))
    ;; companyの(リマップ元の)コマンドを実行する。
    (apply this-original-command args)))
(define-key company-active-map [remap company-complete-selection]
  ;;RETに割り当てられているコマンドをリマップ
  'my-company-complete-respecting-user-input)
(define-key company-active-map [remap company-complete-common]
  ;;TABに割り当てられているコマンドをリマップ
  'my-company-complete-respecting-user-input)

というわけで、今はこのくらいで使っています。

100 RET と押しても10000が入ったりはしませんし、 org-modeのテーブルセル内で 100 TAB 等と押しても大丈夫です。100とRETの間で一瞬候補は出ますが無視してRETやTABがそのまま入ります。いつも「10000ポイント」とか「10000円」とか候補に出てきてちょっと気が散りますがw どこから候補を持ってきてるんだ。

dabbrevで日本語が入りすぎるのは気になりますが、そのあたりはきっと company-dabbrev-char-regexp を調整すれば良いのでしょう。

またしばらくこれで使ってみようと思います。

P.S. helmやivyでも感じたのですが、RETが候補選択に奪われがちなのは近年の補完インタフェースを見ていて気になるところです。

2021-08-09追記: 候補が一つになったときに候補が表示されなくなる問題の修正

company-selection-default を nil にした時の問題ですが、候補が一つに絞り込まれたときに候補が表示されなくなる現象を見つけました。表示されないだけで補完自体は続いているらしく C-n を押すと候補がポイントの位置に表示されます。

company-selection-default を nil にしたときには「無選択」という仮想的な候補が追加されるわけですが、それを考慮していない場所があるようです。

companyでは候補が一つだけの時のfrontendと候補が二つ以上の時のfrontendが分かれています。一つだけの時はポイントの位置に表示して、二つ以上の時はツールチップ的なオーバーレイで表示します。この「候補が一つだけ」の判定が「無選択」という候補を考慮していませんでした。

次のように修正すれば直ります。

  (defun company--show-inline-p ()
    (and (not (cdr company-candidates))
+        (or company-selection-default (null company-candidates)) ;;追加
         company-common
         (not (eq t (compare-strings company-prefix nil nil
                                     (car company-candidates) nil nil
                                     t)))
         (or (eq (company-call-backend 'ignore-case) 'keep-prefix)
             (string-prefix-p company-prefix company-common))))

adviceで書くなら次のようにします。

(defun my-company--show-inline-p (old-fun)
  (and
   ;; Include "no selection" as candidates
   (or company-selection-default (null company-candidates))
   (funcall old-fun)))
(advice-add 'company--show-inline-p :around 'my-company--show-inline-p)

2021-08-09追記: 選択操作をした後に文字で絞り込むと無選択に戻される問題を修正

同じく company-selection-default を nil にしたときの問題です。

候補の選択操作(C-n等)をした後に現在選択しているのとは違う後続の文字を入力して他の候補を選択しようとすると無選択状態に戻されてしまいます。

例えばemacs-lispにおいて、 def で default と defun が候補に出たとして、 C-n で default を選択してから次に u を押すと無選択状態になってしまいます。ここは defun が選択されていて欲しいところです。このままTABやRETを押すと無選択状態ですから当然defunは挿入されません。

現在選択している候補が消えたのだからデフォルトの選択である「無選択」に戻しただけのつもりなのかもしれません。

しかし選択操作をした段階でユーザーの意識はポイントのカーソルからツールチップの選択状態に移っていますから、そのタイミングで入力した文字は候補の選択を変える動作に使って欲しいのです。companyでもcompany-explicit-action-pという関数があって、ユーザーが明示的に行動を起こしたかによって挙動を変更する仕組みがあります。その思想と整合性がとれていないとも言えます。

調べてみたところ、候補の更新処理である company-update-candidates に問題を見つけました。

(defun company-update-candidates (candidates)
  (setq company-candidates-length (length candidates))
  (if company-selection-changed
      ;; Try to restore the selection
      (let ((selected (and company-selection
                           (nth company-selection company-candidates))))
        (setq company-candidates candidates)
        (when selected
          (setq company-selection 0)
          (catch 'found
            (while candidates
              (let ((candidate (pop candidates)))
                (when (and (string= candidate selected)
                           (equal (company-call-backend 'annotation candidate)
                                  (company-call-backend 'annotation selected)))
                  (throw 'found t)))
              (cl-incf company-selection))
            ;; ★★★ここを直したい!★★★
            (setq company-selection company-selection-default
                  company-selection-changed nil))))
    (setq company-selection company-selection-default
          company-candidates candidates))
  ;; Calculate common.
  (let ((completion-ignore-case (company-call-backend 'ignore-case)))
    ;; We want to support non-prefix completion, so filtering is the
    ;; responsibility of each respective backend, not ours.
    ;; On the other hand, we don't want to replace non-prefix input in
    ;; `company-complete-common', unless there's only one candidate.
    (setq company-common
          (if (cdr company-candidates)
              (let ((common (try-completion "" company-candidates)))
                (when (string-prefix-p company-prefix common
                                       completion-ignore-case)
                  common))
            (car company-candidates)))))

直す場所が深いのですが、adviceで次のようにすれば直ります。

(defun my-company-update-candidates (old-fun candidates)
  (let ((old-selection-changed company-selection-changed)
        (old-selection company-selection)
        ;; call the original function
        (result (funcall old-fun candidates)))
    ;; keep company-selection-changed
    (setq company-selection-changed old-selection-changed)
    ;; keep company-selection that is not nil
    (when (and old-selection (null company-selection) candidates)
      (setq company-selection 0))
    result))
(advice-add 'company-update-candidates :around 'my-company-update-candidates)

呼び出しの前後で「ユーザーが一度でも選択を変更したかフラグ(company-selection-changed)」と「現在の選択肢番号(company-selection)」を適切に維持します。company-selection-changedをnilにしてしまうとユーザーが明示的に選択を変更したという意思がなかったことになってしまうのが違和感の原因だと思います。

2021-08-09追記: 選択状態のfaceを目立つものにする

company-selection-default を nil にする場合は、現在選択している候補がはっきり分かるようにした方が良いと思います。特に上では現在選択しているかどうかでRETやTABの挙動を変えてしまっていますからね。

色のセンスはありませんが、例えば:

(set-face-background 'company-tooltip-selection "#a62")

(実際にはcustomize-faceで変更しています)

1~2日使ってみての感想ですが、自動で候補が出たときに未選択状態から始まるのは慣れるまで時間が必要ですね。下を押してRETを押す習慣を付けなければなりません。あとモードによって選択状態から始めているのは統一感が無くてあまり良くなかったかもしれませんね。

2021-08-05 ,

org-modeをData URI Schemeに対応させる

とりあえず対応させてみました。

↓はData URIなんですけど表示されてますか?

赤い丸(SVG)
図1: 赤い丸(SVG)
富士山!(JPEG)
図2: 富士山!(JPEG)

↑の書き方(org-modeのソース)は次の通りです。

#+CAPTION: 赤い丸(SVG)
[​[data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTAwIiB3aWR0aD0iMTAwIj4NCiAgPGNpcmNsZSBjeD0iNTAiIGN5PSI1MCIgcj0iNDAiIHN0cm9rZT0iYmxhY2siIHN0cm9rZS13aWR0aD0iMyIgZmlsbD0icmVkIiAvPg0KPC9zdmc+DQo=]]

#+CAPTION: 富士山!(JPEG)
[​[data:image/jpeg;base64,/9j/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/sABFEdWNreQABAAQAAAAeAAD/7gAOQWRvYmUAZMAAAAAB/9sAhAAQCwsLDAsQDAwQFw8NDxcbFBAQFBsfFxcXFxcfHhcaGhoaFx4eIyUnJSMeLy8zMy8vQEBAQEBAQEBAQEBAQEBAAREPDxETERUSEhUUERQRFBoUFhYUGiYaGhwaGiYwIx4eHh4jMCsuJycnLis1NTAwNTVAQD9AQEBAQEBAQEBAQED/wAARCAAyAGQDASIAAhEBAxEB/8QAgAAAAgMBAAAAAAAAAAAAAAAAAAMBAgQFAQADAQEBAAAAAAAAAAAAAAAAAQIDBAUQAAEDAgQDBQYFBQAAAAAAAAEAAgMRBCExQRJRYQVxgcEiE/CRoTJCI9FighQGsVJy0hURAAICAQUBAQAAAAAAAAAAAAABEQIhMUFxEgNhIv/aAAwDAQACEQMRAD8A3UUUTdqjavTk4RdEbUyiCESAvaooupadIkkHq3NYos9uTz/qtEsdrb27nmNvosxNBUuP0tbriVm/aqcL9cGi821LwcOmuiKLbYRsuZWzOFHuJLmfTUHIjgFlvz+1vZWBpNuTVnFoOnv0VL0Tt10ZLpCkXRFFdhZI3cw7hy8VO1XJIuiKJm1RtSkYuiEzahEgP2oppqcgujH0xxP3JABwYKn4pzH9Ptdwic3e2geR55MchgsLeyWn6LXm3rgxQdMuZaFwETDjV3zU/wAfxXStrG3tjuY3fIM5X+A0WC6/kNnECIj6zxlo2vMrmTfyK4kG07RGHCtBSrRiQeTslm36X+I0SrX6zvz3QeKM8zjXZ/aKYbnDXksPUqMsXNc5xeW9ri0GrqcOSLa7bJbC8lpFGRvcTjQDCpp8Fnml/emR0cgMM3kYW4kNaKf1xRWsPgbeOSttIYLtkjDSCaIFjnUFZCK4/mcnXVsJ9gFXCRo81RUO4HvzWQPt/wB62IncyGNkcUTfMXStzw4N1KnqN3Pbwhsbmm4jf6jY8Hbg75wOzNVuo1J2yZ32jo/uxbo5AS1zW4iozpXPsVhO5jWm4ZRrhhKzFv6hm1DOrCS29a5YXMeNskrRSrqV2kYeamoTY7m1lkMDHje5m9ocKB7TjjzotFd7ol1WxZu17dzCHNOoxCNqqLSMOJjJilpVpYaB1eNcCUmPqMBO18jX/mb5T3tPgq7Jk9WP2oR69tt3eo2iESEHMuuuTTuqS5wGFK+mwgalrfMa8ysU3ULqYU3emzSOIbG/DE96zjHPAozyxUqqWxTbJLzqUBzndgUGmqN3AYJiN111B/8Az4bGJ3kA3SvIoXGtdvYCskE8kIcYnFsjsNwz20xAPNUNCa1FSpa0AgjHgUsIeWaIb2W39F8IAfG1zXOI3Fwca0x0WaaaWaUzSuL5XUJec/grUaRiFDgKcxqhQGSwvJjEYH/ciOJa7Q8WnQpkVw5txFdOJl9LbRoIaQW5B3LsWanvUhrgQ4dyMCya7vqdzckZQxjFsTK0B1OKyGutVYgUrka5ckbTSoIPfihQMrsbtrTmhFDWuqEAW+p2eff3oOXt8EIQIjUeKDl3IQgZQ5J306+KEJMELf8AKc/1I4exQhADOHjn3KW/j2IQkUU+kZ69in2xQhMRX3IQhAj/2Q==]]
Emacs上での見た目
図3: Emacs上での見た目

Data URIを使うとorg-mode文書のサイズが肥大化するのと引き換えに外部ファイルを管理する手間が省けたりします(こうやってエクスポートしてWeb上に上げた場合はHTTPリクエストが減るとか、一方で画像キャッシュが効かないとか色々あると思います)。

Web上を検索したらorg-modeとData URIに関する話題はチラホラ見かけたのですがそのものズバリというものは見つからなかったんですよね。同じ事を考える人は沢山いそうなのですが……。

ソースは次の場所に置きました。

misohena/org-inline-image-fix: A collection of fixes related to the image display feature in org-mode

が、かなりヤバイクソコードなのでご了承ください。

インライン画像表示まわりは例によって org-display-inline-images 関数にadviceをかけて実現しているのですが、この関数は元々結構長くて改造が容易ではありません。以前は丸丸上書きするような修正の仕方もしたのですが、org-mode側に修正が入ると直さなければならなくなるので面倒です(ちょっと前にリモート画像対応が少し入りました)。なので今回は極力元の関数を活かす方向で修正したのですがそれが悪夢の始まりでした。

長ったらしい関数の中の挙動を修正するにはそこから呼び出す関数を書き替えるしか手が無く、 cl-letf で沢山の関数を上書きして無理矢理実現しています。まるで針の穴を通すようなプログラミングにゾクゾクしてしまいました。まるでパズルゲームです。何度もこれはもうダメか、書き直すしか無いかと諦めかけました。 org-element-property が実は defsubst でバイトコンパイルがかかると呼び出されないということに気がついたときにはどうしようかと思いました。追い詰められて plist-get とか format とか基本的な関数を書き替えているのでどこでおかしくなるか分かりません。 org-display-inline-images を書き替える他のプログラムを使ったときの動作は保証いたしかねます。正直全部上書きした方がまだマシだったかもしれませんね。どうせorg-mode側で修正がかかったら全部おじゃんでしょう。

:after adviceをかけて、本家の処理が終わった後にもう一度バッファを走査するのが一番安全かもしれません。大きな文書では効率が落ちるかもしれないので今回は回避しましたが(画像を扱っている時点で今更?)。

YouTube動画へのリンクを画像に置き換えるコード(TobiasZawada/org-yt: Youtube links in org-mode)を見つけたのですが、それは:afterアドバイスで再走査していました。皆さん苦労しているようです。

私も今 org-display-inline-images には三つもアドバイスがかかっているので苦労しています(上のリポジトリにはこれまでのものをまとめてあります: URLリンク対応, 画像リンク即時表示, サイズ制限)。

元々のコードがもう少し改造しやすくなっていると良いのですが……。

あ、あとエクスポートはHTMLだけ対応ですのであしからず。こちらも面倒でした。

2021-08-04

ivy-switch-bufferでブックマーク内のファイルをブックマーク名で検索できるようにする

Ivyでは ivy-use-virtual-bufferst にすると ivy-switch-buffer (C-x b) で(recentfや)ブックマーク内のファイルが選べるようになりますが、候補として登録されるのはあくまでファイル名(ivy-virtual-abbreviate を設定するとディレクトリパスも含めることは出来る)だけでブックマーク名は登録されません。なのでいくら分かりやすいブックマーク名を付けていても ivy-switch-buffer でそれを元にファイルを検索することは出来ません。なのでそれを出来るようにしてみました。

(require 'ivy)

(defun my-ivy-bookmark-name-filename-list ()
  "Return bookmark (name . filename) list as virtual buffer format."
  (delq nil
        (mapcar (lambda (record)
                  (when record
                    (let ((name (bookmark-name-from-full-record record))
                          (filename (bookmark-get-filename record)))
                      (when (and name filename)
                        (cons
                         (propertize
                          (format "*bookmark:%s" name)
                          'face 'ivy-virtual)
                         filename)))))
                bookmark-alist)))

(defun my-ivy-bookmark-append-advice (orig-fun)
  (let ((result-orig (funcall orig-fun))
        (my-virtual-list (my-ivy-bookmark-name-filename-list)))
    (setq ivy--virtual-buffers (append ivy--virtual-buffers my-virtual-list))
    (append result-orig (mapcar #'car my-virtual-list))))

(advice-add 'ivy--virtual-buffers :around 'my-ivy-bookmark-append-advice)
;;(advice-remove 'ivy--virtual-buffers 'my-ivy-bookmark-append-advice)

ivy-switch-buffer が扱うのはあくまでバッファ名です。現実にあるバッファの名前を選んでそこへ切り替えます。なのでrecentfやブックマークのファイル名はバッファ名ではないので「仮想バッファ」の名前として扱うことで無理矢理処理しています。仮想バッファ名リストを生成する関数が ivy--virtual-buffers です。その関数は、 ivy--virtual-buffers という変数(関数と同じ名前でややこしいですが)に仮想バッファ名とファイル名の対応表を構築した上で仮想バッファ名のリストを返します。

なので ivy--virtual-buffers 関数にadviceをかけて仮想バッファを付け足すのが上のコードです。

ivy-switch-bufferで選べる候補が気に入らないという方はこのようなやり方で候補を追加してみてはいかがでしょうか。

ちなみに、 *scratch* が無くても常に *scratch* を選べるようにするには my-ivy-bookmark-append-advice が返すリストに一つ付け加えるだけです。

(defun my-ivy-bookmark-append-advice (orig-fun)
  (let ((result-orig (funcall orig-fun))
        (my-virtual-list (my-ivy-bookmark-name-filename-list)))
    (setq ivy--virtual-buffers (append ivy--virtual-buffers my-virtual-list))
    (append result-orig (mapcar #'car my-virtual-list) '("*scratch*"))))

こうしておくと、もし知らないうちに *scratch* を閉じてしまっていても *scratch* というバッファ名を選べて新しくバッファを作成することが出来ます。

2021-08-04

WindowsでflycheckがNULというファイルを作ってしまう問題

以前flycheckを試したときにNULというファイルが出来て消すのが大変だったのでflymakeでいいやーと思ったことがあったのですが、最近また調べたところ対処法を書いているサイトを見つけました。

Emacs Flycheck

下記の記述だと、Windowsで、gcc によっては、同じディレクトリに "NUL" というファイルが生成されることがある。

:command ("gcc" "-c" "-I../../inc" "-I." "-O1" "-Wall" source "-o" null-device)

その場合、下記のように temporary-file-name を指定すると回避できる

:command ("gcc" "-c" "-I../../inc" "-I." "-O1" "-Wall" source "-o" temporary-file-name)

checker定義内の 'null-device は同名の変数null-deviceの値で置き換えられて、Windowsだと "NUL" になります。これをCygwinやMSYS2/MinGWのgccが受け取るとコンパイルが成功したときにそのままNULというファイルを出力してしまうのが原因のようです。

同じようにcheckerを再定義すれば良いのかなと思ったのですが、flycheck.elを見てみると沢山のcheckerが定義されていてその中でnull-deviceも沢山使われていました。

そもそもcheckerを再定義するのも面倒ですし、かといってnull-deviceの値を書き替えるのも(他にどこで使われているのか分からないので)怖いですし、手っ取り早くadviceでnull-deviceをtemporary-file-nameに置き換えてしまうことにしました。

;;; Windowsでnull-deviceを使わないようにする。
;; null-deviceはNULなので、CygwinやMinGWだとNULというファイルを作ってしまう。
;; flycheck-command-wrapper-function でNULを置き換えても良いのだが
;; flycheck-substitute-argument でやった方が確実だし簡単。
(when (locate-library "flycheck")
  (with-eval-after-load 'flycheck
    (defun my-flycheck-substitute-argument (old-func arg checker &optional rest)
      (when (eq arg 'null-device)
        (setq arg 'temporary-file-name))

      (apply old-func arg checker rest))
    (advice-add 'flycheck-substitute-argument :around 'my-flycheck-substitute-argument)))

最近はLSPの方でエラー箇所を検出してしまうので必要ないのかもしれませんけどまたNULファイルが出来ると面倒なので念のため。

2021-08-03

2021年夏の新番組

最近は1話が月末まで遅れることが多くなってきましたね。

初評 テレビ開始 テレビCh 作品名 ネット配信
06/29(火) 25:34 日テレ ワンダーエッグ・プライオリティ 特別編  
07/01(木) 22:00 MX ピーチボーイリバーサイド dアニメ
07/01(木) 22:30 MX SCARLET NEXUS dアニメ
07/01(木) 23:30 MX ひぐらしのなく頃に 卒  
07/02(金) 24:00 MX ヴァニタスの手記 第1クール Amazon
07/02(金) 24:30 MX 100万の命の上に俺は立っている 第2シーズン  
07/02(金) 25:05 MX ぶらどらぶ(VLADLOVE) ※ネットでは既に配信済 dアニメ
07/02(金) 25:25 TBS系 乙女ゲームの破滅フラグしかない悪役令嬢に転生してしまった…X (第2期) dアニメ
07/02(金) 25:50 TBS系 俺、つしま Youtube
× 07/02(金) 26:25 TBS カノジョも彼女 ABEMA
× 07/03(土) 22:00 MX ぼくたちのリメイク ABEMA
07/03(土) 23:30 MX 魔法科高校の優等生 dアニメ
× 07/03(土) 25:00 MX かげきしょうじょ!! dアニメ
07/03(土) 25:30 MX 現実主義勇者の王国再建記 FOD
07/03(土) 25:30 テレ朝 RE-MAIN dアニメ
07/04(日) 21:30 MX 天官賜福 -吹替版- dアニメ
07/04(日) 22:00 MX 死神坊ちゃんと黒メイド dアニメ
07/04(日) 22:30 MX アイドリッシュセブン Third BEAT! (第3期) 第1クール  
07/04(日) 23:00 MX ゲッターロボ アーク dアニメ
× 07/04(日) 24:00 MX 探偵はもう、死んでいる。 dアニメ
07/04(日) 25:00 MX 指先から本気の熱情2-恋人は消防士- dアニメ
× 07/05(月) 25:30 テレ東 うらみちお兄さん dアニメ
07/05(月) 26:00 テレ東 精霊幻想記 ABEMA
07/06(火) 23:00 MX 転生したらスライムだった件 第2期 第2部 dアニメ
07/07(水) 07:05 テレ東 KICK&SLIDE  
07/07(水) 22:00 MX チート薬師のスローライフ~異世界に作ろうドラッグストア~ dアニメ
07/07(水) 22:30 MX TSUKIPRO THE ANIMATION 2  
07/07(水) 23:00 MX 月が導く異世界道中 dアニメ
07/07(水) 24:00 MX 小林さんちのメイドラゴンS (第2期) dアニメ
07/08(木) 24:00 MX 白い砂のアクアトープ ABEMA
07/08(木) ~ Netflix BIOHAZARD(バイオハザード):Infinite Darkness Netflix
07/09(金) 22:30 MX 迷宮ブラックカンパニー dアニメ
× 07/10(土) 22:30 MX D_CIDE TRAUMEREI dアニメ
07/11(日) 19:00 Eテレ ラブライブ!スーパースター!!  
07/11(日) テレ東 闇芝居 第9期 Paravi
× 07/12(月) 24:00 MX 出会って5秒でバトル dアニメ
× 07/14(水) 25:05 MX 女神寮の寮母くん。 dアニメ
07/14(水) 24:55 フジテレ NIGHT HEAD 2041 FOD
× 07/15(木) 24:30 MX Sonny Boy -サニーボーイ- dアニメ
07/18(日) 24:30 BS日テレ 戦乙女の食卓Ⅱ YouTube
07/20(火) ~ WEB配信 アサルトリリィふるーつ  
07/22(木) 24:55 フジテレ 平穏世代の韋駄天達 Amazon
07/31(土) 24:00 MX マギアレコード 魔法少女まどか☆マギカ外伝 2nd Season  
07/31(土) 26:00 テレ朝日 ジャヒー様はくじけない! dアニメ

今期は不作……凶作ですかね?

2021-08-03 ,

org-modeのコードブロックでHTMLを「実行」する

org-modeのコードブロック(org-babel)でHTMLを書いているとフラストレーションが溜まります。なぜならば書いたHTMLの結果を確認しづらいからです。

org-modeにはob-html.elが定義されていません。なので org-babel-execute:html も定義されておらず通常のコードブロックを評価する方法(C-c C-c)でHTMLを実行出来ません。

HTMLの実行とは何か

ちょとまって。実行? HTMLの実行とは何でしょう。HTMLはマークアップ言語ですからプログラミング言語のように実行と言われても何をするのかよく分かりません。ob-html.elが無いのはおそらくそのためではないでしょうか。

私が思う実行とは何でしょうか。

ブラウザで開く
HTMLの実行と聞いて真っ先に思い浮かべるのはブラウザで開くことでしょう。プログラミング言語では無いと言いますが現実的にはもはやアプリケーションプラットフォームでもあります。
HTMLをそのまま文書に埋め込む
例えばコードブロックを使ってHTMLの書き方を説明したとしましょう。表の書き方、段落の書き方、強調の書き方等々。エクスポートしたときにHTMLの書き方(コード)と表示結果を並べて表示したいことがあります。そのとき、HTMLでエクスポートするならコードの後にそのHTMLそのものを埋め込めれば手っ取り早く結果をブラウザで表示できるはずです。
単にHTMLをそのままファイルとして書き出す
単にそのまま別ファイルに書き出してくれれば十分なこともあるでしょう。結果欄にはそのファイルへのリンクを表示すれば読んでいる人はそれをクリックして結果を確認することが出来ます。ちなみに単に書き出すだけなら :tangle というヘッダー引数があるのですが、これは文芸的プログラミングのためのものでバッファ内のものを一度に全部書き出すコマンドしか見当たらず何だか用途が違う気がします。通常の :results file :file filename で書き出したいところです。
HTMLの文字列そのものを結果とする
コードブロックに書いてあるテキストをそのまま結果とするという考え方も出来ます。実はob-org.elはそうなっています。別の所から参照して活用するのに使えそうです。ちなみに上の二つの「実行」はこれが実現出来るだけで自動的に実現出来ます。「そのまま」というのがミソですね。前者はそのままhtmlエクスポートブロックにするだけ。後者はそのままファイルに書き出すだけです。結果をそのまま返すだけでエクスポートブロックで囲んだりファイルに書き出す処理はOrg Babel側でやってくれます。
スクリーンショットを撮る
結果を画像で表示できれば便利です。これを実現する方法はこれまでにいくつか見たことがあります。ネットを探すとPhantomJSやヘッドレスChromeを使用してスクリーンショットを取れるようにした事例が見つかります(krisajenkins/ob-browser, ob-html-chrome/ob-html-chrome.el)。
ブラウザで開きセッションを維持する
単にブラウザで開くだけで無く、開いたらそのままセッションを維持して、続くJavaScriptのコードブロックをそのセッションで実行できたら面白いのではないでしょうか。JavaScriptからHTML(というかDOM)を操作する方法を解説したいときに便利かもしれません。

とりあえず私がパッと思いつくのはこのくらいでしょうか。HTMLは単なる文書ですから、他にもいくらでもやりたい動作はあることでしょう。

ob-html.el を作る

まず ob-html.el が無いことが問題です。せめて ob-org.el レベルの物は用意しておいて欲しい所ですが無いのだから仕方ありません。

というわけで作りました。

misohena/ob-html

これを読み込ませるとHTMLのコードブロックが評価できるようになり、その結果を #+RESULTS: の場所に埋め込むことが出来るようになります。具体的には次のことが出来るようになります。

HTMLとしてそのまま埋め込む

次のコードブロックは評価するとHTMLのエクスポートブロック(#+begin_export html#+end_export)になります。

#+name: ob-html-ex1-1
#+begin_src html :exports both :results html :cache yes
<p><strong>つよつよ</strong> <ins>追加</ins> <del>削除</del></p>
#+end_src

上のHTMLは次のように表示されます。

#+RESULTS[9621841e579ea4feee64be832813d72c5428d389]: ob-html-ex1-1
#+begin_export html
<p><strong>つよつよ</strong> <ins>追加</ins> <del>削除</del></p>
#+end_export

上のOrg文書はHTMLでエクスポートすると次のように表示されます。

<p><strong>つよつよ</strong> <ins>追加</ins> <del>削除</del></p>

上のHTMLは次のように表示されます。

つよつよ 追加 削除

外部にHTMLファイルを生成し、そのファイルへのリンクを埋め込む

次のコードブロックは評価すると外部にhtmlファイル(example.html)を出力しそのファイルへのリンクが結果になります。

#+name: ob-html-ex2
#+begin_src html :results replace file :file example.html
<!DOCTYPE html>
<html>
  <head>
    <title>Hello</title>
  </head>
  <body>Hello</body>
</html>
#+end_src

どう表示されるかは下をクリックしてね。

#+RESULTS: ob-html-ex2
[​[file:example.html]]

:results には file を指定します。

外部にスクリーンショット画像ファイルを生成し、そのファイルへのリンクを埋め込む

次のコードブロックは評価すると外部にスクリーンショットを含むpngファイル(example.png)が生成されそのファイルへのリンクが結果になります。

#+begin_src html :results replace file graphics :file example.png :width 320 :height 64
<!DOCTYPE html>
<html>
  <head>
    <title>Hello2</title>
  </head>
  <body>Hello2</body>
</html>
#+end_src

#+RESULTS:
[​[file:example.png]]

:results には graphics を指定します。

スクリーンショットはとりあえずChromeのコマンドラインオプション(いわゆるヘッドレスChrome)で取得するようにしてあります。事前にchromeへのパスを正しく設定する必要があります。

C-c C-oでブラウザで開く

以上は何らかの結果を生成して #+RESULTS: のところに埋め込む方法でした。

しかし肝心の「ブラウザで開く」がまだ実現出来ていません。

org-modeからブラウザで開くというのはorg-babelの「評価」とは何か根本的に違うような気がします。私は別にファイルを書き出したいわけでは無いんです。ファイルを書き出されると管理が面倒なので出来れば書き出して欲しくありません。評価して結果を得たいわけでも無いんです。単にブラウザで見たいだけなんです。

「評価」というよりは「開く」と言った方が良さそうです。

開くと言えば C-c C-o 。org-modeだと org-open-at-point に割り当てられていてポイントにある要素を良い感じに開いてくれます。HTMLコードブロックの上で C-c C-o したら即ブラウザで表示してくれたら便利ではないでしょうか。実は結果がファイルになる場合はすでにそのファイルを開いてくれるようになっています。上の例で言えば、二番目(example.html)と三番目(example.png)は C-c C-o でファイルが開きます。しかし次のように結果が出力されないコードブロックで C-c C-o しても何も起きません。

#+begin_src html
<!DOCTYPE html>
<html>
  <head>
    <title>Hello</title>
  </head>
  <body>Hello</body>
</html>
#+end_src

このようなコードブロックはファイルが関連付けられていないのでブラウザで表示するのは難しいのですが、一時的にテンポラリファイルに書き出してブラウザで開き、その後テンポラリファイルを削除するというのはどうでしょう。

ob-html.elにそのような機能を追加してみました。 (org-babel-html-enable-open-src-block-result-temporary) 関数を呼び出すと org-babel-open-src-block-result 関数にadviceをかけてテンポラリファイルで表示できるようにします。

(require 'ob-html)
(org-babel-html-enable-open-src-block-result-temporary)

:results silentの時はブラウザで開く

ob-html.elでは:resultsのデフォルト値はsilentにしました。これは ob-org.el がそうだったのでそれを踏襲しました。org-mode全体でのデフォルトはreplaceなのになぜsilentなのでしょうか。org言語は結果を求めるものでは無いという考えがあるのかもしれません。それならばhtmlも同じです。

デフォルトでsilentなので何もヘッダー引数を指定しなかった場合は評価しても何も起こりません。 #+RESULTS: も挿入されません。エクスポートブロックや外部ファイル、画像を生成したければreplaceを指定する必要があります。

silentのときはほとんど何もしないわけですから、このときブラウザで開いてみるのはどうでしょう。

困るときもありそうなので設定で開くかどうか決められるようにしておきました。

セッション対応

残るはセッション対応ですが、これは少し保留させてください。

いくつかやる方法は考えつくのですが、これはhtmlコードブロックだけの範囲を超えています。JavaScriptコードブロックの改善も一緒にやらなければ意味がありません。

2021-08-03-ob-html-screenshot.png
2021-08-01 ,

EmacsのSkewerでライブWeb開発を試す

先日IndiumというJavaScriptの開発環境を試してみましたが、今回はそれと似た用途に使えるSkewerというものを試してみました。

skeeto/skewer-mode: Live web development in Emacs

SkewerはEmacsでライブWeb開発をするためのツールだそうです。

Emacs側で入力したJavaScriptをブラウザ側で実行する仕組みがコアになっています。Indiumと同じようにREPLやソースファイル内のJavaScriptをブラウザ側で実行できます。

また、その仕組みを応用してHTMLやCSSの現在編集している部分をブラウザに反映させる機能もあります(skewer-html-mode, skewer-css-mode)。

変更とリロードを繰り返すのでは無く、編集した部分だけを逐次ブラウザに反映させていきながら開発するスタイルということでライブ開発と呼んでいるのでしょう。

インストール方法

M-x package-install skewer-mode

simple-httpd や js2-mode も一緒に入ります(入っていなければ)。

JavaScriptの実行(ソースコード上とREPLバッファ)を試す

skewerサーバの起動

M-x run-skewer

→ ブラウザで 127.0.0.1:8080/skewer/demo が開きます。

(動作の詳細: M-x run-skewer を実行するとEmacsで実装されたhttpサーバが起動しローカルホスト(127.0.0.1:8080)の /skewer というURLでEmacs側と通信するスクリプトが取得できるようになります。 /skewer/demo ページはそのスクリプトを読み込んでいます。読み込まれたスクリプトは /skewer/get/skewer/post 等のURLにアクセスしてEmacs側と通信し、Emacs側からの要請に応えてブラウザ側で動作します)

(M-x list-skewer-clients を実行するとクライアントが一つ接続されていることが確認できます)

REPLを試す

  1. M-x skewer-repl

    *skewer-repl* というバッファが開きます。

  2. REPLに alert("hello"); と入れる

    → ブラウザ側でアラートダイアログが出ます。

  3. REPLに document.documentElement.innerHTML と入れる

    → バッファに "<head>\n <title>Skewer</title>\n <script src=\"/skewer\"></script>\n </head>\n <body>\n \n\n</body>" と出ます。(使っているブラウザの拡張機能によってはもっと複雑なコードが出る場合もあります。Evernote Webクリッパー!)

ソースファイルの実行を試す(skewer-mode)

  1. 適当な場所にexample.jsというソースファイルを作る(私は普段からjs2-modeを使っています)

    var cv;
    
    if(!cv){
        cv = document.createElement("canvas");
        cv.width = 640;
        cv.height = 480;
        document.body.appendChild(cv);
    }
    
    var ctx = cv.getContext("2d");
    ctx.clearRect(0, 0, 640, 480);
    ctx.fillStyle = "#28f";
    ctx.fillRect(0, 0, 640, 480);
    
    ctx.fillStyle = "#4c2";
    ctx.fillRect(0, 360, 640, 120);
    
  2. Emacsでソースファイルを開いて M-x skewer-mode
  3. ソースファイル内で C-c C-k (skewer-load-buffer) を実行する

    → ブラウザに絵が表示されます。

  4. ソースコード内の色(#xxxの部分)や座標などをいじって何度も C-c C-k

    → 都度変更が反映されます。

  5. C-c C-z

    → REPLバッファが出ます。

  6. REPLに document.body.innerHTML と入力する

    → うわぁ、見るんじゃなかった。どうやってロードしているのかが分かります。

HTMLの更新を試す

  1. M-x httpd-serve-directory で好きなディレクトリを指定

    → 127.0.0.1:8080/ でアクセスできるディレクトリ(ルート)がそのディレクトリになります。

  2. そのディレクトリの下にindex.htmlを作る

    <!DOCTYPE html>
    <html>
      <head>
        <title>Skewer Example</title>
        <script src="/skewer"></script><!-- これが重要 -->
      </head>
      <body>
        <p id="hello">hello, world</p>
      </body>
    </html>
    
  3. ブラウザで 127.0.0.1:8080/index.html を開く
  4. Emacsでindex.htmlを開き M-x skewer-html-mode
  5. worldをworlddddに変更して その場所で C-M-x (skewer-html-eval-tag)

    → ブラウザに反映されます。

    C-M-xではカーソル(ポイント)を置いている要素のみ更新されます。ただしbodyは更新出来ません。

    色々試してみると分かると思いますが、要素の順番を入れ替えたりすると正しく更新されなかったりします。

他にもskewer-css-modeを使えばcssファイルの更新も行えるようです。

任意のページをskewerと接続する

上の例はhtmlにscriptタグを埋め込まなければならないのがちょっとイケてませんよね。

実は必ずしもscriptタグを埋め込まなければならないわけではありません。要は http://127.0.0.1:8080/skewer からJavaScriptを読みさえすれば良いのです。

例えばブラウザのアドレスバーから javascript: プロトコルでスクリプトを読み込むという方法があります。次の文字列をアドレスバーに打ち込むとその時開いているページにskewerのスクリプトが読み込まれEmacsと繋がります。

javascript:(function(){var d=document;var s=d.createElement('script');s.src='http://localhost:8080/skewer';d.body.appendChild(s);})()

このURLをいわゆるブックマークレットにすれば簡単に好きなページとEmacsを接続できます。

他にも開発者用のコンソールからこのコードを実行する方法もあるでしょう。

Emacsパッケージに入っているskewer.jsを別サーバへコピーしてそこから読み込むのはNGです。あくまで M-x run-skewer で起動したEmacs上のhttpdサーバから読み込む必要があります。

この方法で任意のページ(ローカルファイルを含む)でREPLする手順は次のようになります。

  1. M-x run-skewer
  2. 好きなページをブラウザで開く
  3. 次のコードをそのページで実行する(ブックマークレットにしておくとクリック一つで実行できます)

    (function(){var d=document;var s=d.createElement('script');s.src='http://localhost:8080/skewer';d.body.appendChild(s);})()
    
  4. M-x skewer-repl

    *skewer-repl* というバッファが開きます。

  5. REPLに document.body.insertAdjacentHTML("afterbegin", "<p>hello!</p>"); などと打ち込む

    → ページの先頭にhello!と表示されます。

手打ちでhtmlやcssを編集しているときに思いついたら接続して更新を確認しながら作業するといったことが出来そうですね。

終了方法

ブラウザでskewerと繋がっているページを全て閉じます。閉じると M-x list-skewer-clients (gで更新)に出てくるクライアントが消えます。

M-x httpd-stop でhttpサーバが止まります。

org-modeのコードブロック(babel)から使う?

Org-babel-jsによればorg-modeのJavaScriptコードブロック(ob-js.el)はSkewerに対応しているようなこと書かれています。ヘッダー引数に :session "*skewer-repl*" を指定するのだとか。しかし実際にやってみると executing Js code block... というメッセージが出たまま待てど暮らせど反応が返ってきません。ob-js.elを見てみると org-babel-js-initiate-session でreplバッファが無いか無効ならskewerを起動しているのですが、新しく作成したREPLバッファではなくその無い(nil)か無効かのバッファを返しています。そこを直してみても今度はコードをブラウザに送り出した後反応が返ってきません。

Issueに上がっていた方法を試してみましたがこちらもサーバ側でエラーが出て正しく動作しませんでした。

Modification: make org babel js blocks use skewer when it is connected · Issue #65 · skeeto/skewer-mode

解決するにはもう少し詳しく調査する必要がありそうです。

SkewerとIndiumの比較

SkewerもIndiumもEmacs側のJavaScriptをブラウザで実行できるという点は同じです。

単にREPLがしたいだけならSkewerの方が少しだけ簡単でしょうか。Indiumは必ず設定ファイルを作らなければなりませんしデバッグ機能を有効にしたChromeを起動しなければならないので。

依存する外部ツールが少ないのもSkewerの良いところです。Node.jsは不要ですしブラウザもChromeに限定されません。

幅広いブラウザで使用できるのもSkewerの良いところです。IndiumはChromeのリモートでバッグ機能が必要ですが、Skewerはページに専用のスクリプトを注入することで実現しています。

HTMLやCSSを視野に入れている点もSkewerの良いところです。IndiumはあくまでJavaScriptの開発環境です。ただ、SkewerのHTMLやCSSの部分更新は(原理上)正しく機能しない場合も多々あります。

Skewerには本格的なデバッグ機能はありません。ブレークポイントを置いたりステップ実行したいならIndiumを使う必要があります。

SkewerはNode.js用のJavaScriptには対応していません。JavaScript開発というよりはあくまでWeb開発のためのツールです。

Skewerは侵入的であるのに対してIndiumは非侵入的です。Skewerはページにスクリプトを注入しなければなりません。それがページの動作に干渉する可能性はゼロではないでしょう。また、スクリプトを注入する方法を色々考えなければなりません。EmacsからブラウザへJavaScriptを読み込むときもbodyにscript要素を追加することで実現しています。気がつくとbodyがscriptだらけになっていることがありました。

全体的な完成度はChromeのデバッグ機能を使うという筋の良さも手伝いIndiumの方が高い気がしますが、一方で致命的なバグやドキュメントの不親切さも目立ち今回試すにあたってIndiumはかなりハマりました。

今回SkewerやIndiumを試したのはorg-modeのコードブロックから使えるのではないかと期待したからなのですが、結局どちらも使えなかったのにはガッカリしました。

2021-07-30 ,

EmacsのIndiumでJavaScript開発を試す

Emacs用のJavaScript開発環境であるIndiumを試してみました。

NicolasPetton/Indium: A JavaScript development environment for Emacs

一口に開発環境と言っても何が出来るのかよく分からないと思いますが、

  • Emacs側で入力したJavaScriptを実際の実行環境上で実行(評価)して結果を出力する
    • 入力毎に逐次実行するいわゆるREPL
    • 自由に書いて自由に実行できるscratchバッファ
    • JavaScriptソースコード内のコードを実行
  • デバッガー(ステップ実行や値の調査など)

といったことが出来るようです。

実行環境はChromeとNode.jsに対応していて、どちら用のJavaScriptでも実行したりデバッグしたり出来ます。Chromeで言えばデベロッパーツールとしてコンソールやデバッガーがありますが、あれがEmacsから操作できるといえば分かりやすいでしょうか。

私が試してみたきっかけはorg-modeのJavaScriptコードブロック(ob-js.el)がこのIndiumと連携可能だと書いてあったからなのですが、それはまた後ほど。

indiumでデバッグしているところ
図1: indiumでデバッグしているところ

手元の環境

  • Windows 10
  • Emacs 27.2
  • Google Chrome 92
  • Node.js 12

あたりが既に入っています。

インストール

  1. npm install -g indium

    Node.jsでIndiumサーバをインストールします。

  2. M-x package-install indium

    Emacsにindiumパッケージ(Indiumクライアント)をインストールします。

  3. indium-chrome.el の indium-chrome-data-dir部分を変更します。(2021-07-29時点)

    (defvar indium-chrome-data-dir
      (make-directory indium-chrome--default-data-dir t)
      "Chrome profile directory used by Indium.")
    

    ↓へ変更。

    (defvar indium-chrome-data-dir
      (progn
        (make-directory indium-chrome--default-data-dir t)
        indium-chrome--default-data-dir)
      "Chrome profile directory used by Indium.")
    

    必要に応じて.elcを削除したりコンパイルし直したりしてください。既にロードしてしまっている場合は変数の値を再設定してください。

    これをやらないとChromeがエラーを出します。

    Chromeの起動時に出たエラー。nilディレクトリ
    図2: Chromeの起動時に出たエラー。nilディレクトリ

    IndiumはデフォルトだとChromeを独立したプロファイルで起動します。そのプロファイルを置くディレクトリをmake-directoryで作るのは良いのですがmake-directoryの戻り値であるnilを変数に入れてしまっています。

プロジェクト設定を行う

  1. プロジェクトディレクトリを作る

    まずは何をするにもプロジェクト設定ファイルが必要です。その設定ファイルが置いてある場所を基準に色々な動作が決められているので、まずはそれを置くプロジェクトディレクトリを作りましょう。

  2. プロジェクト設定ファイルを作る

    プロジェクトディレクトリの下に .indium.json というファイルを作ります。

    内容は次の通り。

    {
        "configurations": [
            {
                "name": "Local Host 8080 Page",
                "type": "chrome",
                "url": "http://localhost:8080/index.html"
            }
        ]
    }
    

    nameは適当な名前を付けてください。

    typeは実行環境の種類です。chromeかnodeが指定出来るみたいです。今回はchrome。

    urlはtypeにchromeを指定した場合のオプションで、Chromeからどのページを開くか(デバッグ対象にするURL)を指定します。Chromeから開けるページならどこでもOKです。普通は自分の開発中のページを指定することになりますが、他人様のページを指定することも出来ますし(変なデータを送りつけるのは止めましょう)、ローカルファイル(file:///)を指定することも出来ます。

    例えば次のようにするとGitHubのIndiumのページがデバッグ対象になります。

    {
        "configurations": [
            {
                "name": "Indium Page",
                "type": "chrome",
                "url": "https://github.com/NicolasPetton/Indium"
            }
        ]
    }
    

    また次のようにするとローカルにあるファイルがデバッグ対象になります。

    {
        "configurations": [
            {
                "name": "My Local Directory",
                "type": "chrome",
                "url": "file:///home/hoge/indium-tutorial/index.html"
            }
        ]
    }
    

Windowsでローカルファイルを指定するときの注意点

Windowsでurlにローカルファイル(file:/// プロトコル)を指定した場合、デバッグ(ブレークポイントの設定)が正しく行えない問題に遭遇しました。

調べてみたところ、例えば file:///C:/home/hoge のようなURLが /C:/home/hoge のように頭にスラッシュが残されたパスに変換されてしまう箇所があるようです。例えばindiumサーバのソースコード server/helpers/workspace.js の resolveUrl 関数に fileプロトコルの時の処理があるのですが、ここで /C:/C:/ に変換していないためにWindowsではおかしなパスになってしまいます。他にも問題があるようで、そこを直しただけでは解決しませんでした(逆変換とかがどこかにある?)。その上のconvertRemoteRootを見るとremoteRootオプションで指定した文字列を空文字列で置き換えています。この動作を利用すれば問題を回避することが可能です。

回避方法ですが、remoteRootオプションにプロジェクトディレクトリへのフルパスを頭にスラッシュを付けて指定します。

{
    "configurations": [
        {
            "name": "My Local Directory(Windows)",
            "type": "chrome",
            "remoteRoot": "/C:/home/hoge/indium-tutorial/",
            "url": "file:///C:/home/hoge/indium-tutorial/index.html"
        }
    ]
}

remoteRootオプションがあると、その文字列を手元のプロジェクトディレクトリに置き換えてくれます。これでスラッシュから始まる不正なパスを正しいパスへ置き換えることが出来ます。

httpサーバの起動

urlに localhost:8080 などと指定した場合は自分で別途Webサーバを起動する必要があります。お手軽に起動できるサーバが色々あるみたいなので好きな物を使ってください。

Emacsで完結するならsimple-httpdがお手軽です。 M-x httpd-serve-directory だけで完了です。

色々遊ぶ

  1. まずプロジェクトディレクトリ下にindex.htmlを作ります。

    <!DOCTYPE html>
    <html>
      <head>
        <title>Test</title>
      </head>
      <body>
        <p id="hello">hello, world</p>
      </body>
    </html>
    
  2. プロジェクトディレクトリ下で M-x indium-launch を実行します。

    するとデバッグ用オプションの付いたChromeが起動してurlオプションで指定したページが開きます。(indium-chrome-data-dir変数の場所にChromeプロファイルが出来てその上で実行されるはず)

    Emacs側では *JS REPL* という名前のバッファが開きます(以下REPL)。

  3. REPLに document.getElementById("hello").innerText; (単に hello.innerText でも良い)と入れると "hello, world" が返ってきます。
  4. REPLに document.body.insertAdjacentHTML("beforeend", "Konnichiwa!"); と入れるとブラウザに Konnichiwa! と表示されます。
  5. プロジェクトディレクトリの下にexample.jsを作ります。

    var cv = document.createElement("canvas");
    cv.width = 640;
    cv.height = 480;
    var ctx = cv.getContext("2d");
    function draw(color){
        ctx.fillStyle = color || "blue";
        ctx.fillRect(0, 0, 640, 480);
        document.body.appendChild(cv);
    }
    draw();
    
  6. REPLに次のように入れるとexample.jsがロードされて画面に青い矩形が出ます。

    var script = document.createElement("script");
    script.src = "example.js";
    document.body.appendChild(script);
    
  7. REPLに次のように入れると赤い線が出ます。

    ctx.lineWidth = 2;
    ctx.strokeStyle = "red";
    ctx.moveTo(0, 0);
    ctx.lineTo(640, 480);
    ctx.stroke();
    
  8. Emacsでexample.jsを開いてdraw()関数内の一行目にブレークポイントを置きます。 M-x indium-add-breakpoint か、または M-x indium-interaction-mode の後に C-c b b でブレークポイントを置けます。
  9. REPLに draw("red"); を入れるとブレークポイントの位置にカーソルが飛びます。
  10. l を押すとローカル変数一覧が出ます。colorが"red"になっていることが分かります。 SPC でステップ実行したり c で実行を再開したりできます。
  11. M-x indium-quit で接続を切ります。専用プロファイルで起動しているChromeも閉じましょう。

Node.jsでも遊ぶ

Node.js用のJavaScriptをデバッグすることも出来ます。

新しくプロジェクトディレクトリを作り、設定ファイルを作り、適当なJavaScriptファイルを作ります。

まずは設定ファイル(.indium.json)を作ります。

{
    "configurations": [
        {
            "name": "Example",
            "type": "node",
            "program": "node",
            "args": "./example.js",
            "inspect-brk": true,
            "remoteRoot": "/c:/home/hoge/indium-node-example/"
        }
    ]
}

typeはnodeとします。

argsに実行するJavaScriptファイルを指定します。

inspect-brkがtrueだと最初でブレークします。

Windowsでは残念ながらNode.jsでも /c:/ で始まるパスの問題でうまくデバッグが出来ませんでした(M-x indium-list-script-sources で確認できます)。それを回避するためにremoteRootを指定しています。

適当なJavaScriptファイル(example.js)を作ります。

function concat(a, b){
    return a + b;
}
const hw = concat("hello, ", "world");
console.log(hw);

あとはこのディレクトリで M-x indium-launch すれば最初の所(上の例では const hw のところ)に飛びます。ステップインしてconcat関数の中に入り、 *JS REPL* バッファで a とか b とか打ってみてください。変数の値が確認できます。

org-modeのコードブロック(babel)から使う?

Org-babel-jsによればorg-modeのJavaScriptコードブロック(ob-js.el)はIndiumに対応しているようなこと書かれています。ヘッダー引数に :session "*JS REPL*" を指定するのだとか。しかし(プロジェクトファイルを作った上で)実際にやってみると org-babel-execute:js: Symbol’s function definition is void: indium-run-node というエラーが出ます。Indiumには現在 indium-run-node という関数は無いのでどうやら現在は動かなくなってしまっているようです。

IndiumにもIssueが上がっています。

Question: org-mode ob-js.el sessions · Issue #234 · NicolasPetton/Indium

org-modeから使うなら自分で改造する必要があると思います。

2021-07-29

新しい寝タブ用タブレットスタンドを購入

以前寝タブ用にタブレットスタンドを購入したのですが最近はほとんど使っておらず布団の脇に放置されていました。というのも

  1. アームが短く頭のすぐ近くに置かなければならないこと
  2. 頭の近くにあると使わないときに邪魔なので遠ざけたいがアームが曲げづらく土台ごと遠ざけなければならないこと
  3. 土台ごと遠ざけると次に使うのが億劫になること(土台は布団の下に挟み込まれているので簡単には動かない)

あたりが原因だと思います。他にもアームが曲げづらいので使っている間の調整が難しいこともあると思います。

使わないアームが布団の脇にずっと放置されているのも何だか不愉快になってきたので、代わりのものを探してみたところ良さそうな商品が見つかりました。

タブレットスタンド スマホ ホルダー 360度回転可能 38cm-140cm高さ調節可能 主体調節でき 折り畳み式 フレキシブルアーム 寝ながら 根元強化 下垂防止 3.5~10.6インチ 便利スタンド

これこれ、こういう昔ながらの電器スタンドのアームみたいなタイプが欲しかったんです。以前探したときにはなかなか見つからなかったのですが最近はタブレットスタンドのバリエーションもかなり増えてきたのかもしれませんね。

届いたので早速組み立てて使ってみました。

一番下の直線部分は2本のパイプをパイプに切られたネジで連結する仕組みになっています。1本だけ使って低くすることも可能。それを土台のネジに繋げます。

直線パイプの上、一番最初の可動部分はアームパーツを下のパイプ上部の穴に差し込むだけになっています。なのでこの部分が垂直軸まわりに360度自由に動きます。邪魔なときにアームをよけておくのに使えます。

アームはかなり広範囲に動きます。パイプ2本だと高いかなと思ったのですが、アームによってかなり低い位置まで下ろせます。もちろん高い位置にもできるので目からの距離を使用中に柔軟に調整できます。

唯一の欠点はタブレットを挟む部分がタイトなことでしょうか。バネによってタブレットを挟んで固定するのですが、10インチタブレットだとかなりギリギリです。挟む力がかなり強いので挟みっぱなしだとそのうち本体が変形しないか心配になります。まぁ、多分大丈夫だとは思いますが……。

というわけでかなり満足度の高いタブレットスタンドでした。

2021-07-28

Emacsでモードライン内の上下に空白を増やす

この間MeiryoKe_ConsoleとInconsolataを合成したフォントを作成してEmacsではそれを使うようにしたのですが、合成したフォントはMeiryoKe_Consoleの寸法がベースになっているのでグリフの上下に余白が少なくモードラインの上下も何だか詰まった感じになってしまいました。

上下が詰まったモードライン
図1: 上下が詰まったモードライン

以前はInconsolataに上下の余裕がたっぷりあったので気にならなかったのですが。

というわけでちょっと余白を増やしてみました。モードラインの内容は mode-line-format 変数で指定できますが、その中ではテキストプロパティを自由に設定できるようになっています。テキストプロパティを設定できると言うことは、そう、おなじみのdisplayプロパティが使えるというわけですね。

今回はモードラインの先頭にdisplayプロパティによって空白を作り出し、その空白の幅、高さ、アセントを設定することで上下の余白をコントロールすることにしました。

;; モードラインの上下内側に空白を設ける。
;; 変数をリスキーだと指定しないとテキストプロパティが反映されないので注意。
(defvar my-mode-line-height
  '((:propertize " " display (space . (:width (1) :height (22) :ascent (16)))))) ;;w=1px, h=20px, ascent=16px
(put 'my-mode-line-height 'risky-local-variable t)

;; mode-line-formatの先頭に'my-mode-line-heightを挿入する。
;; 末尾に挿入すると長いファイル名で右端がフレーム外に出ると空白も消えてしまう。
;; バッファローカル変数なのでsetq-defaultを使う。
(setq-default mode-line-format
              (append (list "" 'my-mode-line-height) ;;先頭は文字列じゃないとダメ
                      mode-line-format))

:height でモードライン全体の高さを指定します。 :ascent でベースラインより上の高さを指定します。 :ascent を増やすと上の余白が増えて下の余白が減ります。

上下に余白を増やしたモードライン
図2: 上下に余白を増やしたモードライン

他にもface(mode-line や mode-line-inactive)の:box属性を使って境界線の幅を増やし色もバックグラウンドと同じにする方法もあるようです。今時のフラットな見た目ならこっちの方が手軽かもしれません。左右に少し空白が空くとは思いますが数ピクセルくらいなら気にならないでしょう。

参考: faces - Padding around modeline text? - Emacs Stack Exchange