つーさにブログ

つうさにのメモ用ブログ

macOS上のEmacsで日本語入力時にカーソルがちらつく問題解決まで(Emacsに初めて貢献した話)

解決したので。

この記事は Emacs Advent Calendar 2019 の10日目の記事です。9日目は takaxp さんで、 11日目は Xi80 さんです。当初 Emacs Advent Calendar 2019 に投稿するつもりはありませんでしたが、空きがあったので参加させていただきました。

【追記】

続きを書きました。 tsuu32.hatenablog.com

加えて、本記事の構成を見直し、Emacsソースコードレベルの話は「技術的な話」として分離しました。

Emacs NS-port & macOS日本語入力 → カーソルちらつき問題

現在GNU Emacsは、Cocoaなネイティプアプリケーションとしても実装され、端末エミュレータ上だけでなくmacOSGUIアプリケーションとしても利用できます(Cocoa実装はNS-portと呼ばれる)。しかし、2019年12月現在リリースされているEmacs 26.3では、NS-portにおいて日本語を入力するときにカーソルがちらつくという問題があります。この記事は、それの原因究明の足跡についてと、私のEmacsへのパッチ取り込みについて書かれています。

カーソルちらつき問題の最初のバグ報告

debbugs.gnu.org

ちらつき問題を知らない人は、上のバグ報告 (Bug#23412) にYoutubeのリンクがあるので、それを見てみてください。macOSのインプットメソッドを利用して文字を入力する際、変換候補を選ぶときにカーソルがちらついてしまっています。

この問題のバグ報告がされたのはEmacs 25.1.50のときなので、長らく解決していない問題でした。

ちらつきの発生原因と暫定的な対策

ja.osdn.net

MacEmacs JPにて、hylomさんはこのコミット 9e77c1b のせいで問題が発生したということを見つけ出しています。その後、hylomさんはMacEmacs JPでちらつきを直すパッチを配布しています。

しかし、Emacs本家でのBug#23412スレッドは2年以上音沙汰がなく、Emacs本家ではちらつき問題は直らないままでした。

技術的な話

問題のコミットではinput_was_pendingという変数が導入されています。これは、Emacsの再描画について改善するためのもののようでした。

hylomさんのパッチは「input_was_pending導入をrevertすることで」ちらつきを直すパッチであり、これがそのままEmacs本家に取り込まれるのは難しいものでした(GNU EmacsmacOSでの問題のために機能をrevertするパッチを取り込む可能性はほとんどない)。

input_was_pending導入後でもmacOSのインプットメソッド利用時にちらつきが発生しないようにするパッチが作られる必要があったのです。

カーソルちらつき問題の解決(setMarkedText編)

lists.gnu.org

2019年10月ごろ、HaiJun Zhangさんはこの問題の解決方法を調査し、なぜmacOSのインプットメソッドが有効のとき入力を行うとちらつきが発生するのかを突き止め、emacs-develメーリングリストで流してくれました。そしてBug#23412スレッドで、EmacsのNS-portのメンテナであるAlan Thirdさんとのやり取りがされた後、この問題に対処するためのパッチがEmacs本家にpushされました。

これによって、ちらつき問題は完全解決...してませんでした。

Alan Thirdさんのpushしたパッチは「未確定文字列の変換候補を選択するときに発生するちらつき」を直すものでした。 「未確定文字列を確定するとき(例えば、変換候補を確定するためにRETを押したとき)に発生するちらつき」は残ったままでした。

といった状況のところで私はこの問題に興味を持ち、この問題を解決してみようと思い色々調べてみました。

技術的な話

HaiJun Zhangさんは、「インプットメソッドが有効のとき、キーボードの1文字入力で'(ns-unput-working-text)イベントと'(ns-put-working-text)イベントの2つのイベントが発生し、前者の'(ns-unput-working-text)イベントの後に再描画を行わないことがちらつき問題の解決策だ」ということを先ほどのメールで述べています。

debbugs.gnu.org

Bug#23412スレッドにてAlan Thirdさんは、setMarkedTextメソッド内で'(ns-unput-working-text)イベントを発生させる[self deleteWorkingText]を実行しないようにする1パッチを作成しました。

ただし上で述べたように、このパッチは「未確定文字列を確定するときに発生するちらつき」は解決しません。

私は最近Swiftを触っていてObjective-Cのコードもなんとなく読めるようになっていたので、EmacsCocoa実装部分を読んでみることにしました。

カーソルちらつき問題の解決(insertText編)

github.com

なんやかんや(Cocoaの勉強、パッチの作成、メールのやり取り)あって私のパッチがEmacs本家にマージされ、「未確定文字列を確定するときに発生するちらつき」問題も解決しました。

これでちらつき問題は完全解決しました!嬉しいですね。

技術的な詳細は以下の技術的な話を読んでください。メーリングリストでのやり取りの話は次の節をに書かれています。

技術的な話
NSTextInput プロトコル

NS-portのEmacsNSViewを継承した独自のEmacsViewを定義しています。EmacsViewNSTextInputプロトコルに準拠しています。このNSTextInputプロトコルmacOSのインプットメソッドと対話するためのメソッドが存在します2。これらはsrc/nsterm.mに書かれています。

yllan.org

新しいNSTextInputClientの記事は多くありましたが、古いNSTextInputについての記事はググっても上の記事くらいしかありませんでした。OSが適当なタイミングでinsertTextメソッドなどを呼ぶので、開発者はメソッドの中身を実装します3

キーボードから入力された文字が何かを知るには、insertTextメソッドsetMarkedTextメソッドを使います。

  • insertTextメソッド
    • 「インプットメソッドが無効の状態で入力されたとき」や、「インプットメソッド利用時に未確定文字列を確定するとき」などに呼ばれる。
  • setMarkedTextメソッド
    • 「インプットメソッド利用時に(確定以外が)入力されたときや、(スペースなどで)変換候補を選択するとき」などに呼ばれる。

EmacssetMarkedTextメソッドで受け取った未確定文字列をworkingText変数に保持します。EmacsworkingTextを初期化するメソッドとしてdeleteWorkingTextメソッドを定義していて、これはworkingTextの中身を消し、'(ns-unput-working-text)イベントを発生させます。'(ns-unput-working-text)イベントが発生すると結果的にEmacs lispns-unput-working-text関数が評価されます。

setMarkedTextメソッドは、実行されたときに'(ns-put-working-text)イベントを発生させ、結果的にEmacs lispns-put-working-text関数が評価されます。Emacs NS-portではelispns-put-working-text関数によって未確定文字列のインライン表示を実現しているようです。

(私の修正がマージされる前の)insertTextメソッドは始めに[self deleteWorkingText]を実行し、その後通常の入力イベントたちを発生させます(「a」や「あ」、「日」など)。よって、確定時にちらつきが発生してしまうのは、insertTextメソッドが[self deleteWorkingText]を実行することによって発生する'(ns-unput-working-text)イベントの後に再描画されてしまうせいでした。

'(ns-unput-working-text)イベントの後の再描画を抑制できるか

そもそもこれらの入力イベントは誰が受け取っているかというと、src/keyboard.cにあるread_char関数です。read_char関数の中でread_decoded_event_from_main_queue関数の返り値を受け取りますが、これに'(ns-unput-working-text)のようなイベントが入っています。

read_char関数の実装を注意深く見ればわかりますが、'(ns-unput-working-text)のようなspecial eventは紐づいた関数を実行した後、goto retryして再描画してしまいます。

debbugs.gnu.org

ということで、'(ns-unput-working-text)イベントだけ特別扱いしてgoto retryしても再描画しないパッチを作りました(上はそれを送ったメール)。

しかし、結果としてこのパッチは受け入れられませんでした。このパッチの(上に書いたような)意味をAlan Thirdさんに私が説明できないというのと、(HAVE_NSしてるものの)NS specificでないread_char関数に手を加えているというアレなパッチだからです。

この問題はnsterm.mやns-win.elの中だけで解決すべき問題でした。

NS specificな解決手法

debbugs.gnu.org

再描画が発生するのは'(ns-unput-working-text)イベントの後でした。ということで、'(ns-unput-working-text)イベントを発生させる[self deleteWorkingText]の実行をinsertTextメソッド内の先のほうではなく最後に持っていけばいいのでした(普通の入力イベントの後には再描画は発生しないため)。

ただ、これだけだとなぜか確定時に全て消えてしまいます。調べてみるとns-working-overlay作成時の引数が原因でした。

また、上の変更でundoの挙動がおかしくなってしまったので、「workingTextの表示はoverlayのafter-stringを使えばいいんじゃないか(意訳)」というHaiJun Zhangさんの提案を採用しました4

ここで非互換な変更があって、read-onlyバッファでインプットメソッド有効で入力すると、working textが表示されます。これは、今までのBuffer is read-only: #<buffer ~>とecho areaに表示されて、裏で未確定文字が残ってる状態よりは格段にいいと思います。(そしていつの間にかBug#1453も直ってることになってた)

Emacs本家への貢献のやりとりについて

www.shigemk2.com

Emacsへの貢献について書かれた日本語の記事がほとんどない中、上の記事がとても参考になりました。

GNU Bug Trackerへのメール

私の場合は、既にあったバグなので1453 <at> debbugs.gnu.orgにメールを出しました。

そのバグについて誰かと話すときは、1453 <at> debbugs.gnu.orgをCCに入れて、人のアドレスをToに入れるのが習わしっぽいです。

EmacsメンテナのEliさんからメールを初めて受け取ったときは何だかドキドキしました。

パッチの作成・送信

上の記事を参考にして、git format-patch HEAD^しまくりました。

メールは普通にMacのメール.appを使いました。

Copyright Assignment

最後にCopyright Assignment exemptとなりうるかについてのやり取りがありました。

We can accept small changes (roughly, fewer than 15 lines) without an assignment. This is a cumulative limit (e.g., three separate 5 line patches) over all your contributions.

Copyright Assignment - GNU Emacs Manual

上に書いたように、Emacsへの貢献はCopyright Assignmentを提出してなくても最大15行まで受け入れてくれます。

私の変更は15行より少し多かったっぽいですが、Eliさんが大丈夫と言ったので大丈夫でした。

終わり

Emacsパッケージの作者とのやりとりはだいたいGitHubだったので、本家Emacsの開発者とメールでやりとりするのは新鮮でした(ちょっと緊張した)。

該当の部分をgit blameで見てみてもNS portをmergeしたところ(2008年!)までしかわからず、それ以前は誰がどういう意図で書かれたのか知り用がないようなコードでしたが、そんなコードを編集できて楽しかったです。脈々と受け継がれて、今でも開発されているのがEmacsのすごいところです。

何はともあれ、マージされてよかった!おしまい。

参考


  1. 正確には「未確定の状態でESCが押されたとき以外は」[self deleteWorkingText]を実行しないようにしている。

  2. ちなみにNSTextInputはdeprecatedなプロトコルAppleは新しいNSTextInputClientの使用を推奨している。(Emacs Mac portではNSTextInputClientが使用されている)

  3. NSTextInputプロトコルのメソッドがいつ呼ばれて、実装するメソッドが何の値を返すべきかというは結構ブラックボックスです(調べた限り)。

  4. 今までworking textの表示にinsertを使っていたのがヤバイと思う。