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なネイティプアプリケーションとしても実装され、端末エミュレータ上だけでなくmacOSでGUIアプリケーションとしても利用できます(Cocoa実装はNS-portと呼ばれる)。しかし、2019年12月現在リリースされているEmacs 26.3では、NS-portにおいて日本語を入力するときにカーソルがちらつくという問題があります。この記事は、それの原因究明の足跡についてと、私のEmacsへのパッチ取り込みについて書かれています。
カーソルちらつき問題の最初のバグ報告
ちらつき問題を知らない人は、上のバグ報告 (Bug#23412) にYoutubeのリンクがあるので、それを見てみてください。macOSのインプットメソッドを利用して文字を入力する際、変換候補を選ぶときにカーソルがちらついてしまっています。
この問題のバグ報告がされたのはEmacs 25.1.50のときなので、長らく解決していない問題でした。
ちらつきの発生原因と暫定的な対策
MacEmacs JPにて、hylomさんはこのコミット 9e77c1b のせいで問題が発生したということを見つけ出しています。その後、hylomさんはMacEmacs JPでちらつきを直すパッチを配布しています。
しかし、Emacs本家でのBug#23412スレッドは2年以上音沙汰がなく、Emacs本家ではちらつき問題は直らないままでした。
技術的な話
問題のコミットではinput_was_pending
という変数が導入されています。これは、Emacsの再描画について改善するためのもののようでした。
hylomさんのパッチは「input_was_pending
導入をrevertすることで」ちらつきを直すパッチであり、これがそのままEmacs本家に取り込まれるのは難しいものでした(GNU EmacsがmacOSでの問題のために機能をrevertするパッチを取り込む可能性はほとんどない)。
input_was_pending
導入後でもmacOSのインプットメソッド利用時にちらつきが発生しないようにするパッチが作られる必要があったのです。
カーソルちらつき問題の解決(setMarkedText編)
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)
イベントの後に再描画を行わないことがちらつき問題の解決策だ」ということを先ほどのメールで述べています。
Bug#23412スレッドにてAlan Thirdさんは、setMarkedText
メソッド内で'(ns-unput-working-text)
イベントを発生させる[self deleteWorkingText]
を実行しないようにする1パッチを作成しました。
ただし上で述べたように、このパッチは「未確定文字列を確定するときに発生するちらつき」は解決しません。
私は最近Swiftを触っていてObjective-Cのコードもなんとなく読めるようになっていたので、EmacsのCocoa実装部分を読んでみることにしました。
カーソルちらつき問題の解決(insertText編)
なんやかんや(Cocoaの勉強、パッチの作成、メールのやり取り)あって私のパッチがEmacs本家にマージされ、「未確定文字列を確定するときに発生するちらつき」問題も解決しました。
これでちらつき問題は完全解決しました!嬉しいですね。
技術的な詳細は以下の技術的な話を読んでください。メーリングリストでのやり取りの話は次の節をに書かれています。
技術的な話
NSTextInput プロトコル
NS-portのEmacsはNSView
を継承した独自のEmacsView
を定義しています。EmacsView
はNSTextInput
プロトコルに準拠しています。このNSTextInputプロトコルにmacOSのインプットメソッドと対話するためのメソッドが存在します2。これらはsrc/nsterm.mに書かれています。
新しいNSTextInputClientの記事は多くありましたが、古いNSTextInputについての記事はググっても上の記事くらいしかありませんでした。OSが適当なタイミングでinsertText
メソッドなどを呼ぶので、開発者はメソッドの中身を実装します3。
キーボードから入力された文字が何かを知るには、insertText
メソッドsetMarkedText
メソッドを使います。
insertText
メソッド- 「インプットメソッドが無効の状態で入力されたとき」や、「インプットメソッド利用時に未確定文字列を確定するとき」などに呼ばれる。
setMarkedText
メソッド- 「インプットメソッド利用時に(確定以外が)入力されたときや、(スペースなどで)変換候補を選択するとき」などに呼ばれる。
EmacsはsetMarkedText
メソッドで受け取った未確定文字列をworkingText
変数に保持します。EmacsはworkingText
を初期化するメソッドとしてdeleteWorkingText
メソッドを定義していて、これはworkingText
の中身を消し、'(ns-unput-working-text)
イベントを発生させます。'(ns-unput-working-text)
イベントが発生すると結果的にEmacs lispのns-unput-working-text
関数が評価されます。
setMarkedText
メソッドは、実行されたときに'(ns-put-working-text)
イベントを発生させ、結果的にEmacs lispのns-put-working-text
関数が評価されます。Emacs NS-portではelispのns-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
して再描画してしまいます。
ということで、'(ns-unput-working-text)
イベントだけ特別扱いしてgoto retry
しても再描画しないパッチを作りました(上はそれを送ったメール)。
しかし、結果としてこのパッチは受け入れられませんでした。このパッチの(上に書いたような)意味をAlan Thirdさんに私が説明できないというのと、(HAVE_NSしてるものの)NS specificでないread_char
関数に手を加えているというアレなパッチだからです。
この問題はnsterm.mやns-win.elの中だけで解決すべき問題でした。
NS specificな解決手法
再描画が発生するのは'(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本家への貢献のやりとりについて
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のすごいところです。
何はともあれ、マージされてよかった!おしまい。
参考
- https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/TextEditing/TextEditing.html#//apple_ref/doc/uid/10000157-SW1
- CocoaのText Systemについて
- IMEを使う(macOS編) - Qiita
- NSTextInputClientについて日本語の記事
- Mac/API/NSTextInputClientメモ - wxStyledTextCtrlでの日本語入力 @ ウィキ - アットウィキ
- NSTextInputからNSTextInputClientへのメソッドの変化について書かれてある
- Bug-org 875674 Implement NSTextInputClient protocol on Mac - WebStudio