key-combo v1.4.1:複数行の文字列挿入時に自動でインデント。キーが二つの場合の設定簡略化

key-combo v1.4.1をリリースします。変更点は以下です

  1. 複数行の文字列を挿入する時に自動でインデントするようにしました
  2. 登録するキーが二つの場合の設定を簡略化しました
  3. (実験的)関数型言語用設定の追加

変更詳細

1つめは複数文字列への対応です。smartchrの設定例 - pogin’s blogをみて、c言語などで「{」を入れたら改行してカーソルをうまいこと移動してくれるのはいいなと思ってました。ただ、文字ごとにmy-smartchr-bracesやmy-smartchr-commentなどと関数が増えていくのは嫌なので、挿入する際に複数行なら自動でインデントとカーソル位置の調整をするようにしました(yasnippetと同じですね)。
設定例は

(key-combo-define-global (kbd "{ RET") "{\n`!!'\n}")

のようにすれば、

{
    !! //←!!の場所にカーソルがインデントした状態で配置
}

となります。改行する方が多いモードなら

(key-combo-define-global (kbd "{")  ("{\n`!!'\n}" "{`!!'}"))

とかの方が便利かもしれません(デフォルトは上の設定のみです)
2つめはキーが二つの場合の設定簡略化です。v1.3までは登録するキーが二つの場合、先行するキーもkey-comboに登録する必要がありました。例えば「->」を打ったら「 -> 」を挿入したい時、一文字目の「-」を打ったらそのまま「-」を挿入する設定もする必要がありました。例

(key-combo-define-global (kbd "->")  " -> ")
(key-combo-define-global (kbd "-")  "-");;自分で設定する必要がある

ですが2行目の設定を忘れやすく、後から読んだ時に何のために設定したのかわかりにくいです。そこで、

(key-combo-define-global (kbd "->")  " -> ")

としただけで、一文字目の設定が無ければ自動で設定するようにしました。(この場合"-"をそのまま挿入して、">"一文字としてkey-comboで処理したいケースは少ないと判断しました)
3つ目はid:poginさんからhaskell-modeで使う場合の質問かきたので、

  1. key-combo-functional-default

関数型っぽい言語で共通して使える設定

  1. key-combo-functional-mode-hooks

key-combo-functional-defaultが有効になるモードのフック
を足しました。私はそこらへんをさっぱり触ったことがないのでわかっている人はpull requestを送るか設定を公開してみてください。

インストール方法

ソースがgithubにおいてあるので

(auto-install-from-url "https://raw.github.com/uk-ar/key-combo/8cddaec7d76901ae92678ca987fb4d086fad98c5/key-combo.el")

を評価するか、
marmaladeから
M-x package-install key-combo
でインストール可能です。

その他

今回の設定簡略化について、登録するキーが二つの場合に限ったのは理由があります。それは、三つになった場合("abc")に一文字目("a")をそのまま挿入して、のこり("bc")としてkey-comboで処理したいケースはあり得るかと思ったからです(しばらく使ってみないとわかりませんが)。

追記(2012/4/4)

c-mode他で"="を押したときに" = "ではなく"="になってしまうバグがあったため、1.4.1として更新しました。1.4を入れてしまった方は、申し訳ありませんが1.4.1に更新してみて下さい。テストはすべて通していたのですが、テストケースに漏れがあったので気づきませんでした(新しくテストを追加済みです)

括弧の自動挿入の挙動をオレオレ設定できるflex-autopair.elで夢を叶える

空気を読まずに4/1なのに本気エントリの投入です。今までいくつ作られてきたのか分からない、括弧を自動挿入する系のelispを作ったのでリリースします。
私はこれを使い始めてから、閉じ括弧とスペースを打つ回数が激減しました!さらに、怖いぐらい宝くじも当たり、長年の持病だった痔と水虫も治ったらいいなぁ。まずはデフォルト設定を一週間試してみてください。

紹介のためのスクリーンキャストを撮影しましたので、とりあえずご覧下さい。

試しに使ってみる!

スクリーンキャストを見て、面白いと思ったはずなので使ってみましょう。
インストールは

(auto-install-from-url "https://raw.github.com/uk-ar/flex-autopair/master/flex-autopair.el")

を評価するか、
marmaladeから M-x package-install flex-autopair でOKです。
インストール後に

(require 'flex-autopair)
(flex-autopair-mode 1)

とすると使えます。

手を加えず使ってみる

デフォルトの設定では以下の4つの動きになります。

  1. 開き括弧やクオートを打つとペアになる文字を自動挿入します
  2. 自動挿入が思い通りにいかなかったら、undoでキャンセルできます
  3. 間違って自分でペアを閉じようとした場合にカーソルを移動するだけにします
  4. 何か(urlや単語)の先頭で開き括弧やクオートを打つとその何かを囲います

c系のモードやlisp系のモードではより激しい挙動をしますが、詳しくは後述します。

条件によって動きを変えてみる

一週間のお試し期間が終わる頃にはデフォルトの設定ではいろいろと不満も出てくることでしょう。逆にそうなってからがスタートです。
このプラグインの特徴は括弧やクオートを押したときに何をするか(action)とどんな条件でするか(condition)を細かく設定できることです。
具体的にはそれぞれ、flex-autopair-actionsとflex-autopair-conditionsという変数を使って設定します。

flex-autopair-conditions

説明の都合で条件から先に説明します。この変数ではどんな条件で何をするかを(条件式 . アクション名)を並べた連想リスト(alist)を設定します。デフォルト設定から簡略化して抜き出すと

(setq flex-autopair-conditions
      `(;; Insert matching pair.
        (openp . pair)
        ;; Skip self.
        ((and closep
              (eq (char-after) last-command-event)) . skip)
        (closep . self)
        ))

となります。この連想リストを上から評価して、最初に真になった要素のアクション(ここではpair,skip,self)が実行されます。この設定の場合

  • 入力が開き括弧や開きクオートの場合はpair
  • 入力が閉じ括弧や閉じクオートでカーソル位置の文字と同じ場合にはskip
  • 入力が閉じ括弧や閉じクオートでそれ以外の場合はself

みたいな動きになってます。
(openpとclosepは括弧やクオート開いたかどうか表す変数です)
では、pairやskipのようなアクションは具体的何するの?という設定がflex-autopair-actionsです。

flex-autopair-actions

この変数ではflex-autopair-conditionsで指定したアクション名と実際の動作を対応させます。具体的にはflex-autopair-conditionsと似た感じで(アクション名 . 式)を並べた連想リスト(alist)を設定します。デフォルト設定から簡略化して抜き出すと

(setq flex-autopair-actions
      '((pair . (progn (call-interactively 'self-insert-command)
                       (save-excursion
                         (insert closer))))
        (skip . (forward-char 1))
        (self . (call-interactively 'self-insert-command))
        ))

となります。この連想リスト内から、アクション名をキーに探索し、見つかった要素の式が実行されます。この設定の場合

  1. pair: 押したキーと、対になるペアを挿入
  2. skip: カーソルを右に移動
  3. self: 押したキーを挿入

となってます。
設定を追加するには、flex-autopair-conditions の場合

(setq flex-autopair-user-conditions-high
      `((openp . hoge)
        (closep . fuga)))
(flex-autopair-reload-conditions)

とすれば、flex-autopair-conditionsにflex-autopair-user-conditions-highの内容がいい感じの優先度で反映されます。
flex-autopair-actionsに設定を追加する場合

(add-to-list
 'flex-autopair-actions
 '(hoge . (message "this is hoge"))
 )

とすればいいと思います。

カップリング(?)を増やしてみる

ちまたのEmacsユーザーの間ではカップリングが流行ってるみたいなので*1やってみましょう。
flex-autopairは文字のペアを自動判別してます*2。でも普段は文字をそのまま挿入してほしいけど時々ペアにしたい文字がありますよね?例えばc言語での<です。この場合は#includeの後だけカップリングしてくれると便利デスね?そんなわがままも叶えちゃいます!
手順は

  1. 文字のペアを追加
  2. 追加したペア用の条件を追加

となります。
c-modeに<を追加する例を解説します。

文字のペアを追加

c-mode-hookで<のペア(?\< . ?\>)をflex-autopair-pairsに追加します。flex-autopair-pairsはバッファローカル変数なのでc-modeだけで有効になります。

(defun my-hook-function ()
  (add-to-list 'flex-autopair-pairs '(?\< . ?\>)))

(add-hook 'c-mode-hook 'my-hook-function)
追加したペア用の条件を追加

文字のペアを追加しただけだと、開き括弧や開きクオートを入れると無条件でペアを挿入してしまいます。なので、<の場合

  • #includeがあればpair
  • #includeがなければself

の二つの設定を追加します。

(setq flex-autopair-user-conditions-high
      `(((and
          (eq major-mode 'c-mode)
          (eq last-command-event ?<)
          (save-excursion
            (re-search-backward "#include" (point-at-bol) t)))
         . pair)
        ((and
          (eq major-mode 'c-mode)
          (eq last-command-event ?<))
         . self)
        ))
(flex-autopair-reload-conditions)

以上で

あとがき

皆さんそろそろ金運が上がって宝くじが当たったことでしょうか?私はもうすぐ府中競馬場の近くに引っ越すので楽しみでしょうがありません。また、前から一度カップリングと言ってみたかったので今回のエントリでまた一つ夢が叶いました。
さも自分で作ったったみたいに解説してますが、アイデアとコードの大半はacp.el*3とelectric-pair-mode*4盛大にパクって参考にしています。acp.elを公開してくださったid:buzztaikiさんありがとうございます。

key-combo v1.3をリリース:post-command-hookを使うelispと併用しても問題ないようにしました

key-combo v1.3をリリースしました。変更点は以下です

  1. post-command-hookを使うelispと併用しても問題ないようにしました

変更詳細

詳細を説明します。
key-comboはv1.2まで登録しているキーシーケンスが始まると2文字目以降は独自のループに入ってコマンドを処理していました。
しかし、これだと他のelispと組み合わせた時処理が微妙な時があります。
例えば、「=a」というシーケンスを打ったときpost-command-hookを使用していても、「=」を打った後ではなく「a」まで打って(aが挿入される前に)実行されます。
実行後の結果は変わらないのですがこの挙動が非常に嫌だったので「=」を打った直後に実行されるように変更しました。
他の動作や設定方法に変更はありませんが、内部実装はごっそり変わっていたりします。
(前と同じテストをがんばって通してます)

インストール方法

ソースがgithubにおいてあるので

(auto-install-from-url "https://raw.github.com/uk-ar/key-combo/c9f2bcd5dda74c1ac88d007817baec2c6b27a9fc/key-combo.el")

を評価するか、
marmaladeから
M-x package-install key-combo
でインストール可能です。

その他

内部でループを回す実装は

  • コードの見通しがよい
  • グローバルな変数がない

というメリットがあったのですがhookとの相性が致命的に悪いので諦めました。
苦労したのがundo関連でしたが、長くなるので別のエントリーで。

Emacsでキーボードイベントを扱う方法まとめ

Emacsを使い始めると、誰でもあまり意識せずにキーボードイベントを扱っていると思います。
特にdefine-keyやlocal-set-keyなどのキーバインドを設定する際には、とりあえず人のコピペでちょっと変更すればなんとかなるし、kbdマクロを使ったり使わなかったりも私の場合は適当でした。
ですが、自分でelispを書き始めるとキーボードイベントをうまく扱う必要が出てきました。

もともとEmacsではキーボードイベントを(歴史的な背景から)複数のデータ形式で扱っていて、そのため複雑になっています。
http://www.bookshelf.jp/texi/elisp-manual-20-2.5-jp/elisp_21.html#SEC303にはキーボードイベントを扱うための方針が書いてあるのですが「とりあえずベクトルで扱っておいて必要に応じてリストに変換してね」ぐらいのことしか書いてないです。

やるべきことは全くその通りなんですが

  1. どの場合に変換する必要があるのか
  2. どの関数でどの形式からどの形式に変換できるのか

とかがさっぱりわかりません。

そこで今回、備忘録もかねてまとめてみました。

イベントを表すデータ形式

一つ目は、データ形式の一覧です。
「M-C-a」と「C-a」と「a」の連続するキーボードシーケンスをそれぞれのデータ形式で表しました。
単一イベントに関しては、シーケンスではなくlast-input-eventでとれる個々のイベントを参考にしてます。

種類 実際の例 補足
文字列 "\M-\C-a\C-aa" "\C-:"や"\M-\C-:" などはエラー
文字列b "M-C-a C-a a" "C-:"や"M-C-:" などもOK
リスト (134217729 1 97)
ベクトル [134217729 1 97]
単一イベント 134217729

注意すべきは文字列としての表現が2種類存在することと、文字列aに関しては表せる範囲について、制限があるということです。
そのため、範囲外のキーを含む場合は他のデータ形式で表す必要があり、そのため扱いが複雑になっています。

イベント列を入力とする関数/変数

二つ目はイベント列を入力とする関数と変数(unread-command-events)です。
変数については本来、入力も出力も関係ないですが、setqで値をセットして意味がありそうなものという意味で挙げました。
入力の「文字列a or ベクトル」というのは文字列aとベクトルどちらでも受け付けることを表しています。

関数/変数 入力
define-key 文字列a or ベクトル
key-binding 文字列a or ベクトル
describe-bindings 文字列a or ベクトル
lookup-key 文字列a or ベクトル
unread-command-events リスト

注意すべきはunread-command-eventsで、他の関数が「文字列a」と「ベクトル」を受け付けるのに対して「リスト」しか受け付けません。
unread-command-eventsを使用する場合は、次に説明する変換系の関数を使ってリストに変換してから使用しましょう。

イベント列を入力、出力とする関数(変換系の関数)

三つ目はイベントを入力として、イベント列を出力する関数(イベント列を変換する関数)です。
出力の「文字列a or ベクトル」というのはキーボードシーケンス内にメタキーやASCIIを含まない場合は文字列、含む場合はベクトルになります。
たとえば、「C-a」と「a」のシーケンスの場合は文字列"\C-aa"(印字されると"^Aa")、「C-:」と「a」のシーケンスの場合はベクトル[67108922 97]となります。

関数 入力 出力 補足
kbd,read-kbd-macro 文字列b 文字列a or ベクトル 
key-description 文字列a or ベクトル 文字列b
listify-key-sequence 文字列a or ベクトル リスト
vconcat 文字列a or ベクトル or リスト ベクトル (vconcat "\C-aa" '(1 2))
append 文字列a or ベクトル or リスト リスト (append "\C-aa" [1 2] nil)
vector 単一イベント ベクトル
list 単一イベント リスト
single-key-description 単一イベント 文字列b

文字列aかベクトルか分からないものをベクトルに統一するには、vconcatを使うとよいです。

文字列aかベクトルか分からないものをリストに統一するには、appendもしくはlistify-key-sequenceを使うとよいです。
使い分けとして、変換だけならlistify-key-sequence、ついでに要素を加えるときにはappendを使うといいと思います。

文字列bへの変換は使いどころが想像しづらいかもしれません。
すぐに思い浮かぶのはデバッグなど人が読むための表示に便利ですが、他にも文字列からシンボルを生成するときなど確実に文字列として変換しておきたいときに便利だと思います。

たとえば、https://github.com/uk-ar/key-combo:key-comboでは通常のようにキーの登録ができない(prefixとして使用するキー自体にはコマンドは登録できない)ので、仮想的なキーマップを作ってます。
そこでは、キーシーケンスをベクトルではなく文字列bから作ったシンボルで表しています。
(特殊な使い方なので、参考になるかどうか…)

イベント列を出力とする関数/変数

四つ目はイベント列を出力とする関数と変数です。
違いが分かりやすいように「C-:」と「a」のシーケンスを入力した場合の値(またはコマンドの起動に使用した場合)と「C-a」と「a」の場合の値も記載します。

関数/変数 出力 範囲外文字あり 範囲外文字なし 補足
read-event 単一イベント 67108922 or 97 1 or 97 入力待ち、値は複数回実行した場合
read-key-sequence 文字列a or ベクトル [67108922 97] "\C-aa" 入力待ち
read-key-sequence-vector ベクトル [67108922 97] [1 97] 入力待ち
last-input-event 単一イベント 97 97 read-eventなどで値が変わる
last-command-event 単一イベント 97 97 read-eventなどでも値が不変
this-command-keys 文字列a or ベクトル [67108922 97] "\C-aa"
this-command-keys-vector ベクトル [67108922 97] [1 97]

まとめ

キーボード入力を読み込むelispではread-key-sequence-vectorやthis-command-keys-vectorを使用しておけば、ベクトルに確定できるので内部で扱いやすいです。

ユーザーの設定を読み込むelispは表現できる範囲が広いため文字列bで表現できることが重要です。
そのためには(define-keyのように)文字列aもベクトルも受け付けるようにしておいてkbdマクロを使わせるか、直接文字列bを読み込み可能にして内部でread-kbd-macroなどを使って変換する方法があります。
どちらにしても変換後は文字列aで渡ってくる可能性が高いため、要素を加えるなどベクトルで扱いたい場合はvconcatで変換する必要があります。
(read-kbd-macro-vectorが欲しい…)

簡単にまとめるつもりが、長くなってしまいました…
極力動かしながらやってますが、間違いもあるかもしれないので、ツッコミをお待ちしております。
最後に一応動作を確認するためのコードも貼ってみます。

(local-set-key (kbd "C-a a")
                (lambda ()
                  (interactive)
                  (message "lce:%S tck:%S tckv:%S lie:%S"
                           last-command-event
                           (this-command-keys)
                           (this-command-keys-vector)
                           last-input-event)
                  (let ((re (read-event)))
                    (message "lce:%S tck:%S tckv:%S lie:%S re:%S"
                             last-command-event
                             (this-command-keys)
                             (this-command-keys-vector)
                             last-input-event
                             re)
                    )))

追記(2012/2/15)

基本的にベクトルで扱う前提で書きましたが、場合によってはリストで扱う方が便利な場合もあると思います。
理由はリストの方が操作関数が多く、簡潔に書けることがあるからです。
例えばキーの重複をなくしたいときにadd-to-listを使えたり、要素を先頭に追加するときconsやpushが使えます。

key-combo v1.2をリリース:SKKと一緒に使っても問題ないようにしました

key-combo v1.2をリリースしました。変更点は以下です

  1. SKKと一緒に使っても問題ないようにしました
  2. html-mode にて objective-c 向けの設定が呼ばれるバグを修正

変更詳細

詳細を説明します。
1つめはballforestsさんによりバグの報告があり

問題ないよう対応しました。
原因はSKKが文字挿入系の関数をself-insert-commandからごそっとskk-insertに置き換えていたのを知らなかったからです。SKKを使っていないので全く気づきませんでした。

2つめはSimple-hatena-modeを使用していて設定がおかしくなっていたのに気づいて修正しました。

インストール方法

ソースがgithubにおいてあるので

(auto-install-from-url "https://raw.github.com/uk-ar/key-combo/25bc9ec548407ab59108cece2b74d316c3e380fc/key-combo.el")

を評価するか、
marmaladeから
M-x package-install key-combo
でインストール可能です。

その他

やっぱり、大きく変更したばかりなのでバグがあったりするもんですね。特に自分が使ってない組み合わせだと、全く気づかないので不具合があったらぜひ報告をおねがいします。

key-combo v1.1をリリース:メジャーモードごとに簡単にキーを割り当て可能にしました

key-combo v1.1をリリースしました。変更点は以下です

  1. メジャーモードごとに簡単にキーを割り当て可能にしました
  2. デフォルトの設定を強化しました

変更詳細

詳細を説明します。
key-comboではキーにコマンドを定義する際にkey-combo-define-globalという関数で定義するようにしてました。globalという名前から連想できるように、一度設定するとすべてのバッファで有効になってしまいます。しかし、やっぱりメジャーモード毎に設定を切り替えたいとは思ってました。例えばEmacs lispでは「define-key」のように関数名に「-」を含めることが普通なので、「 - 」のように空白を含めると不便です。(undoすれば「-」に戻りますが)
しかしv1.1でkey-combo-define-localという関数を追加したので、メジャーモードごとに設定が可能となりました!
もっと早いことできたんじゃね?と言われそうですが、技術的な問題があって…と言い訳しておきます。ページの最後に内部実装の話を書くので興味があればどうぞ。

サンプルコード

では実際の使い方です。上の例であげたemacs-lisp-modeでは「-」を直接入力、c-modeでは「 - 」と空白を開けて入力したい場合は

(add-hook 'emacs-lisp-mode-hook
          (lambda ()
            (key-combo-define-local (kbd "-") 'self-insert-command)))
(add-hook 'c-mode-common-hook
          (lambda ()
            (key-combo-define-local (kbd "-") " - ")))

といった感じで設定できます。

デフォルト設定の変更

今回、設定をメジャーモードごとにできるようになった関係でかなりの量の設定をデフォルトに取り込んでます。
これは、id:pogin さんの設定
smartchrの設定例 - pogin’s blog
をkey-combo 風にして取り込んでます。こころよくOKをくれた pogin さんありがとうございます!
デフォルト設定は今まで同様

(key-combo-load-default)

で取り込めます。
モードによって挙動が違いますが、Cのソースで「=」を何回か入れたり、スクラッチバッファで「;」を何回か入れたりして試してみて下さい。
デフォルト設定をカスタマイズしたい場合は

  • key-combo-global-default
  • key-combo-lisp-default
  • key-combo-c-default
  • key-combo-objc-default
  • key-combo-html-default
  • key-combo-org-default

にキーと対応するコマンドが書いてあります。
また、上記の設定が読み込まれるモードは

  • key-combo-lisp-mode-hooks
  • key-combo-c-mode-hooks
  • key-combo-html-mode-hooks

(global, objc, orgはモードの変更不可)
に対応するフックのリストが書いてあるので、こちらに追加していくのが楽かと思います。

インストール方法

ソースがgithubにおいてあるので

(auto-install-from-url "https://github.com/uk-ar/key-combo/raw/216ef621fdd7410e07c71569c52216705214b335/key-combo.el")

を評価するか、
marmaladeから
M-x package-install key-combo
でインストール可能です。

その他

これで、機能的には当初思っていたものがほぼ出来上がったかなと。あとはデフォルト設定の充実とドキュメント整備だと思ってます。デフォルトについて今回まだ導入しなかったのが、改行を含む系の設定です。これはyasnippet同様に自動でインデントしてあげるのがいいかなと思ってます。試してみて次回のリリースですね。

内部実装

key-combo ではkey-combo 用のキーシーケンスの始まりを監視します。v1.0まではキーシーケンスの先頭のキーをkeymapに登録していました。キーを上書き登録すると元のコマンドが何だったのか分からなくなります。また、実質keymapを2つ管理することになるので設定の組み合わせによってはよく分からない挙動をします。
そこでpre-command-hookを使ってキーシーケンスを監視するようにしたら、うまくいったのでリリースしました。

key-combo v1.0をリリース:クリーンアップ関数を指定しなくてもよくなりました

key-combo v1.0をリリースしました。大きな変更を入れて設定に互換性がなくなったため、メジャーバージョンとしました。変更点は以下です

  1. クリーンアップ関数を指定しなくてもよくなりました
  2. key-combo-describeでの関数がわかりやすくなりました

変更詳細

詳細を説明します。key-comboはv0.5でキーに関数を登録可能になりました。しかし、v0.7までは登録する際には実行用の関数とともにクリーンアップ用の関数を指定する必要がありました。v1.0からはundoをうまいこと利用するようになったため、キーの定義をスッキリ書けるようになりました。
例えば0.7までは挿入する系コマンドを登録する場合

(key-combo-define-global (kbd "=")
  '(self-insert-command . delete-backward-char))

(key-combo-define-global (kbd "==")
  '((lambda() (insert " = ")) .
    (lambda() (delete-backward-char 3))))

のようにクリーンアップをするための関数も一緒に登録する必要がありました。

しかし、v1.0からは

(key-combo-define-global (kbd "=")
  'self-insert-command)

(key-combo-define-global (kbd "==")
  (lambda() (insert " = ")))

とするだけで、クリーンアップまでしてくれます。

また、同じキーが複数回続く設定は

(key-combo-define-global
  (kbd "=")
    '(self-insert-command
     (lambda() (insert " = "))
     " == "))

のようになります。
さらに移動系のコマンドであっても、同様に書けるので

(key-combo-define-global (kbd "C-e")
  '(end-of-line
    (lambda () (goto-char (point-max)))
    key-combo-return))

のように設定するようになりました。

インストール方法

ソースがgithubにおいてあるので

(auto-install-from-url "https://raw.github.com/uk-ar/key-combo/4c439997dc2602980234ca38b530e9c901ff0290/key-combo.el")

を評価するか、
marmaladeから

M-x package-install key-combo

でインストール可能です。

感想

smartchrを使っていた上で、もう一つ不満だったのが自分で関数を登録するときにクリーンアップ用の関数を書かなければならないことでした。毎回挿入するものが変わらなければ簡単ですが、状況に応じて挿入するものが変わるような関数(comment-dwimやkey-combo v0.7をリリース:直前に空白があった場合は空白を二重に入れないように - むしゃくしゃしてやった)はクリーンアップ関数を書くのが無理だと思ってました。
undoをなんとかうまく使えないかと思ってたんですが、Advent Calendarには間に合わなかったためそのままリリースしました。今回、試行錯誤してundo-primitiveと組み合わせたらうまく動いたためundoを使った方式に切り替えました。
ただ、よくなっていることは確かなのですが一旦ちゃんと情報をまとめないとなぁと思ってます。