スクロール関係をちょっと便利(かもしれない)にしてみた。前置引数でスクロールする行数を指定できる。でも実はポイントの移動はテキトー。4を与えたときにemacsっぽく動くようにしてみたつもり。思いもよらない動きなどはしないと思うが。
(defun scroll-up (&optional arg)
(interactive "p")
(if arg
(progn
(save-excursion
(scroll-window arg))
(unless (pos-visible-in-window-p (point))
(if (plusp arg)
(goto-virtual-line (+ (get-window-start-line)
*next-screen-context-lines*))
(goto-virtual-line (+ (get-window-start-line)
(window-lines)
(- *next-screen-context-lines*))))))
(next-page)))
(defun scroll-down (&optional arg)
(interactive "p")
(if arg (scroll-up (- arg))
(previous-page)))
(defun scroll-other-window-up (&optional arg)
(interactive "p")
(other-window 1)
(if arg
(scroll-up arg)
(next-page))
(other-window -1))
(defun scroll-other-window-down (&optional arg)
(interactive "p")
(other-window 1)
(if arg
(scroll-down arg)
(previous-page))
(other-window -1))
(global-set-key #\C-v 'scroll-up)
(global-set-key #\C-z 'scroll-down)
(global-set-key #\M-C-v 'scroll-other-window-up)
(global-set-key #\M-C-z 'scroll-other-window-down)
それから前回書いたバックアップをもうちょっと使いやすくしようと思ってあれこれ考えた結果、バックアップすべきディレクトリ構造をそのまま反映したような木を変数に入れておく方向にしてみた。それでまずそれを再帰的に定義してみた。が、これを元にしてプログラムを書くのはなんか面倒臭い。
とりあえずもっと扱いやすい形に変換してから・・・と思って変換する関数を書いてみたら、それはそれでちゃんと動いたのだが、どうやら始めから変換後の形で指定するほうが使う側としてもわかりやすいのではないかという気がしてきた。
;; 変換前はこんな感じ
`(xyzzy ("~"
("usr" t)
("site-lisp"
("xrr" nil ,(compile-regexp "\\.l$" t))
nil ,(compile-regexp "\\.\\(l\\|ssn\\)$" t))
nil ,(compile-regexp "^\\.xyzzy$" t)))
;; 始めから変換後の形で書くならこうなる
`((xyzzy "~/usr" t)
(xyzzy "~/site-lisp" nil ,(compile-regexp "\\.\\(l\\|ssn\\)$" t))
(xyzzy "~/site-lisp/xrr" nil ,(compile-regexp "\\.l$" t))
(xyzzy "~" nil ,(compile-regexp "^\\.xyzzy$" t)))
こんなマクロを書いてみた。(4/14 ちょっと修正)
(defmacro backup2-copy (from to &optional regexp regexp-not (recursive t) (case-fold t))
(let ((fromv from)
(tov to)
(exp regexp)
(exp-not regexp-not)
(matchfun (if case-fold 'string-matchp 'string-match))
(fname (gensym))
(result (gensym))
body)
(when exp-not (push `(not (,matchfun ,exp-not ,fname)) body))
(when exp (push `(,matchfun ,regexp ,fname) body))
`(let (,result)
(message "ファイルをコピーしています... : ~A" ,fromv)
(dolist (,fname (directory ,fromv
:recursive ,recursive :file-only t) ,result)
(and
,@body
(file-newer-than-file-p (merge-pathnames ,fname ,fromv)
(merge-pathnames ,fname ,tov))
(copy-file (merge-pathnames ,fname ,fromv)
(merge-pathnames ,fname ,tov)
:if-exists :overwrite
:if-access-denied :error)
(push ,fname ,result))))))
使い方。 from にコピー元ディレクトリ、 to にコピー先ディレクトリを指定。 regexp, regexp-not で from からの相対パスにマッチしたりしなかったりするものだけコピー。 recursive が nil なら from 直下にあるファイルだけコピー、 case-fold が nil ならファイル名の大文字小文字を区別する。 to 以下に適当なディレクトリがないとエラーが出るのがやや問題かも。
「書いた」だけではあまりにも情報量がなさすぎというかこのページを見るほうとしても得るものがないと思ったのと、まとめておくことで後々書き直すときにやりやすいかもしれないのとで、骨組みだけ書いておくことにした。
(let (result)
(declare (special result))
(directory dir1
:recursive t
:file-info t
:callback '(lambda (file)
(and
(string-match regexp1
(first file))
(not (string-match regexp2
(first file)))
(< *last-backup* (third file))
(push (car file) result))))
(dolist (fname result)
(copy-file (concat dir1 fname)
(concat dir2
(file-namestring fname))
:if-exists :overwrite
:if-access-denied :error))
result)
dir1がコピー元、dir2コピー先、regexp1,regexp2は適当にいるファイルといらないファイルを選別するための正規表現。ちなみに (first file) はたぶんファイル file のdir1からの相対パスを返す。
だいたいこんな感じのをディレクトリの数だけ用意しておいて並べるのが基本方針。これを関数として書いておけばいいような気はするが、細かいところが結構違っていて結局あまり簡単にならないかもしれない。
圧縮処理は、最後にコピーしたファイルのリストが返ってくるので、それが non-nil なら call-process を使って行う。
結構時間かかるかなと思ったけどそうでもないのでこれでいいことにした。あんまり遅すぎるようならもうちょっと工夫しようと思ってたけど。でも改めて見てみるとあまりきれいではないような・・・。
バックアップできるようになった・・・かな?単純にファイルをコピーして圧縮するコードを必要な数だけ書いただけなので汎用性ほとんどなし。同じようなことをしてはいるんだけど。まあそのうち改良しよう。
バックアップを lisp だけでできるようにしてみようと思って少しいじる。タイピング関係の分だけはできたような気がする。最初は汎用性のあるものを書こうと思っていたが、そうすると何をすればいいのかよくわからなくなってくるのでとりあえず汎用性は考えないでファイルをコピーする関数を書いた。
format 指示子の使い方とか正規表現の書き方とか、なかなか覚えられないことがいくつかあるのでそれをポップアップメニューから選んで表示できるようにしてみた。
(defvar *help-menu*
(define-popup-menu
(:item nil "regexp(&E)"
'(lambda () (interactive) (popup-string my-help-regexp (point))))
...
))
(defun popup-help-menu () (interactive) (track-popup-menu *help-menu*))
(global-set-key #\F3 'popup-help-menu)
表示する文字列 (my-help-regexp など) を適当に定義しておいて F3 を押せばポップアップメニューから文字列を表示できる。ちなみに表示する文字列は reference から持ってきたものが多い。
かなり細かいことではあるが。
前から indent-buffer という関数を定義してバッファ全体のインデントをやっていたのだが、最近になって一番最初の行がインデントされないことに気付いたので修正した。元々は最後の一行だけだったが、それだとうまくいかなかった(理由は indent-region のソースを読めばわかる)。
(defun indent-buffer ()
(interactive)
(when mode-specific-indent-command
(save-excursion
(goto-char (point-min))
(funcall mode-specific-indent-command)
(indent-region (point-min) (point-max)))))
ダメージ分布計算関数を書き直してみたら異様に速くなった。一様な場合で計算時間が 1/30 になる例が。どうやら複数の分布をマージしようとしたときに速いらしい。でも使い道は今のところない。
100マス計算をやっていて xyzzy を再起動すると毎回同じ問題が出題されることに気付く。どうやら random で生成される乱数は起動するたびに同じ系列が繰り返されるらしい。とりあえず get-internal-real-time して得られた数の下三桁(=ms)分だけ乱数を進めてから改めて取得することで解決したつもり。
補完のおかげで get-internal-real-time が g . i C-. RET で入力できるようになった。慣れればだいぶ打鍵数少なくなるかもしれない。問題はどの程度まで入力すれば候補が絞れるのか想像つきにくいところだ。
今更ながら partial-completion というものが Emacs に存在することを知る。今やっていることとはちょっと違う(こちらは主に lisp を書いているときの補完が目的)のだが、参考になるところはしつつ、もうちょっといじってみようと思う。
今日はいくつかカスタマイズ用の変数を作ってみた。あまりカスタマイズの必要がないものもあるかもしれないが、それはあとで削ることにする。あれこれ追加したい気はするのだが、実際に使ってみて有用でない機能だったりしてもしょうがないので、使いながら少しずつ書き換えていくことにしよう。
それから昨日ぐらいに改頁コード(^L, 0x0c)を入れることを覚えた。 C-x [ と C-x ] で飛べるので .xyzzy なんかの長めのファイルには所々入れておくと便利かもしれない。
補完の続き。マッチしたシンボル名すべてに共通の prefix を自動で挿入するようにしてみた。でももともとあったものを消してしまうので str-m → string- となってくれたりしてあまり嬉しくないこともある。かも。 word ごとに補完すればいいのだがちょっと面倒かもしれない。
lisp mode で普通と違った補完をしてくれる関数を書いてみた。単語ごとにマッチさせて m-v-bi → multiple-value-bind みたいな補完をポップアップで表示してくれる。ついでに区切り文字も - だけでなく / とか . 他数十文字が使える(ただし大部分は control 文字など打つのが面倒なものなので実質その三つ、 qwerty ならそれプラス @^ ぐらい)。これがどれだけ役に立つかは不明。けどちょっと作ってみたかった。役に立つようならどっかに公開しようかな。
GNU Emacs Lispリファレンスマニュアルを読んでいると、 read-only なバッファでテキストを変更するコマンドを定義するときは let で buffer-read-only を nil に束縛するのが普通らしい。というわけでそうしてみたがうまく動かず。xyzzy ではこの方法は使えないのだろうか。代わりに toggle-read-only で一時的に解除して body を実行する簡単なマクロを定義してやることに。
ところで、日本語版のマニュアルにはなぜか「(letで)buffer-read-onlyにnilを束縛し」とある。普通は「に」と「を」を逆にして「変数を値に束縛する」という言い方をするのでは・・・と思い英語版も見てみたら同じ箇所が "bind buffer-read-only to nil (with let)" となっている。たぶんこっちがオリジナルなので今まで通りこっちが正しいと思っておくことにしよう。まあ細かいことかもしれないけど。
100マス計算がそれなりに動くようになった。ウィンドウサイズのチェックができてるのかどうかよくわからなかったり、操作性にやや難があったりとビミョーな出来ではあるがとりあえずこれで16進数の計算を練習してみようか。
そういえば kill-line とか transpose-chars とかやると相変わらず困ったことになるのだった。まあやらなきゃいいか。自分で使う分にはそう問題あるまい。
何となく100マス計算やってみたいなあとか思って、xyzzyで書こうとしていたらこんな時間に。だいたいできたけどどうなのだろうか、これは。まだちゃんとやってないのでバグが残っている可能性もある。あと、スペース打ったり kill-line したりするとわけのわからないことになるという問題が。何もしないコマンドを作ってバインドすれば一応何とかなるにはなるが・・・。
とりあえず16進のつもりで作ったけど一応2〜16進法まで対応している(と思う)。8, 10, 16ぐらいしかやる人はいないだろうけど。
ちょっと前に.xyzzyの中身がだいぶ増えてきたのでsiteinit.lに移動しようとしたら、なぜか再ダンプしても全く反映されなかった。原因がよく分からず諦めていたのだが、今日再挑戦してみたところ原因が判明。何と、~/lisp/以下にほとんど何も書いてないsiteinit.lが置いてあるではないか。どうやら昔よく分からずに書いたものが残っていたらしい。削除したらだいたいうまくいった。
一つ問題だったのが、前に書いたデフォルトのモードをlisp-modeにした関係。キーワードファイルのロードの際にバッファを生成するので *lisp-mode-hook* にキーワードのロードなどの関数を引っ掛けると無限にバッファが生成されてしまうという事態が発生していて、その部分をスタートアップの時点でやってしまって *lisp-keyword-hash-table* に始めから入れておくことで解決したのだった。ところが.xyzzyではうまく動いていたコードがsiteinit.lではうまくいかない。
具体的には下のようなものをsiteinit.lに書いた。
(defvar *lisp-keyword-file* "lisp")
(defvar *lisp-keyword-hash-table* nil)
(and *lisp-keyword-file*
(null *lisp-keyword-hash-table*)
(setq *lisp-keyword-hash-table*
(load-keyword-file *lisp-keyword-file* t)))
すると、ExplorerからS-C-RETでxyzzyを起動したときに、「"lisp"はディレクトリです」というエラーが出てダンプファイルの生成に失敗してしまう。この"lisp"は"~/etc/lisp"のつもりなのだが、確かに~/lisp/というディレクトリは存在しているからそのせいなのだろうとは考えられる。しかしなぜ.xyzzyならこれでよかったのにsiteinit.lでは駄目なのか。
調べてみるとキーワードファイルはまず *keyword-load-path* が検索され、それで見つからなければ *etc-path* (=~/etc) になるらしい。そして *keyword-load-path* は nil だった。ということは・・・これでいいんじゃないのか?と思えてしょうがないのだが実際にエラーが出ているのだから間違っているのだろう。
結局のところ上のコードの直前に明示的にこんなのを書いて何とか解決。
(setq *etc-path* (merge-pathnames "etc" (si:system-root)))
気になったのでなぜこれを書かないといけないのか調べてみたところ、どうやら次のようになっているらしい。
まず xyzzy 起動後最初に呼ばれるのがstartup.lで、この先頭に (si:*load-library "loadup") とある。ここで misc.l がロードされ *etc-path* が defvar されるが、値はnilである。misc.l 内で関数 init-misc-objects が定義され、この中に *etc-path* を先に書いた値に束縛する部分があるが、この時点では定義されるだけでまだ呼ばれていない。実際にこれが実行されるのは loadup のロードが終わってからになる。
一方 siteinit.l はというと、 loadup の最後で存在すれば読み込むことになっている。ということは、 siteinit.l がロードされた時点では変数 *etc-path* の値は nil であるということになる。そのままキーワードファイル "lisp" をロードしようとするとパスは"lisp" と nil を merge-pathnames した結果 "~/lisp" になって、エラーが発生したと。
スタートアップのあたりの流れは山本 泰三さんのページにある記述が参考になった。
それである程度分かってすっきりしたのだが、*keyword-load-path* に pushnew した方がいいような気がしてきて後で書き直してみたりした。
それからちょっと別の話。ミニバッファに入ったとき自動的にIMEをOFFにして、抜けたら元に戻してくれるlispを書いてみた。これをやりたいという人は結構いそうだし既にあるのではないかと書いた後で気付き、検索してみたところ同じようなコードを書いてる人が複数いた。まあこの程度だと同じになって然るべきか。ちなみにこんなの。
(defvar *ime-on-when-entered-p* nil)
(add-hook '*enter-minibuffer-hook*
#'(lambda (b h)
(when (buffer-ime-mode)
(toggle-ime nil)
(setq *ime-on-when-entered-p* t))))
(add-hook '*exit-minibuffer-hook*
#'(lambda (b h)
(when *minibuffer-ime-state*
(toggle-ime t)
(setq *ime-on-when-entered-p* nil))))
いつの間にかxrrがバージョンアップされていた。調べてみたところ改造した関数は以前の分から変更ないようだ。しかしソースを直接書き換えるとやっぱりこういうときに面倒なので、別ファイルを作ってそれをオートロードし、その中でxrr.lcを読むようにした。
大体こんな流れ。
- (require "xrr/xrr")
- いくつかのdefvar(必要な変数の宣言)
- いくつかのdefun(必要な関数の定義)
- さらにいくつかのdefun(改造した関数を上書き)
バイトコンパイルし忘れて更新前のxrr.lcを読んでいるのに気付かなかったせい(たぶんそうだと思う、があまりにも混乱していたのでよく分からない)でかなり手間取ったが。それにしてもテストのために何回も打って指が痛い。
そういえば以前type.lというのをちょっとだけ書きかけていたことをさっき思い出した。セッションファイルをロードしたら現れた。というかいつものセッションなのでこれまでも存在していたはずだが、気付かなかったというか忘れ去られていたというか・・・。ところでこれ、どうしよう。
ある事情から括弧を自動的に全部閉じてくれたらいいなと思い、そういうlispがないか検索してみた。いろいろ見ているとどうやら昔は「スーパー括弧」というのがあって、それを一つ書けばすべての括弧を閉じたことになったりしたらしい。書き手にとってはなかなか便利そうな気もするが、構文解析をしようとすると面倒だとか。
それで結局自動で閉じ括弧を挿入してくれるものは見つからなかったので自分で書いてみたのだが、なんか響きが気に入ったので本来の意味とは違うにもかかわらずsuper-kakkoという名前を付けてしまった。
バックアップがMS-DOSから"xyzzy -f backup"だけでできるようになった。けどまだあまり使い勝手が良くない。
*Help* とかのバッファで jump-tag とか lisp-info-F1 が使えたら便利かなと思って (apropos したときとか) .xyzzy にこんなのを書いた。
(setq *default-buffer-mode* 'lisp-mode)
そしたら起動直後にxyzzyが固まるようになった。固まった時点でC-gして様子を見ていると、*load kwd*というバッファが延々と生成されている模様。デフォルトのモードを Fundamental に戻したところ大丈夫そうだったのと、 M-x lisp-mode としても固まることから lisp-mode-hook あたりが怪しいと目を付ける。
その後 .xyzzy の lisp-mode 関係の部分をコメントアウトして起動したりいろいろやった結果、キーワードの色づけなどのために lisp-interaction-mode-hook と lisp-mode-hook に入れている関数の中にある load-keyword-file が悪さをしているとわかる。
ソースコードを見てみると (create-new-buffer "*load kwd*") という部分があった。この部分でバッファが作成された際に、デフォルトにした lisp-mode が呼ばれ lisp-mode-hook が走る。そうするとまた load-keyword-file が呼ばれて新しい *load kwd* バッファを作成し、そこでも lisp-mode になるので同様なことが起こり無限ループに陥っていた。
で、最初の *load kwd* はどう作成されたかというと、 *scratch* の lisp-interaction-mode らしい。つまりこういうこと:
*scratch* → lisp-interaction-mode → (run-hooks '*lisp-interaction-mode-hook*) → (create-new-buffer "*load kwd*") → lisp-mode → (run-hooks 'lisp-mode-hook) → (create-new-buffer "*load kwd*") → lisp-mode ・・・(二行上に戻る)
しょうがないのでフックに入れて処理していた部分を一部 .xyzzy 評価時にやってしまうことにした。
xrrでキー入力をすべて記録できるようになった。ただし記録の出力部分は作ってないのでscratchで記録の入っている変数を評価するぐらいしかなく、出力は次のようなものになる。
((("日本国憲法" "2004/12/26 14:30:23") (#\e 0 t) (#\n 188 t) (#\c 67 nil)
(#\a 144 t) (#\t 106 nil) (#\e 563 nil) (#\c 597 t) (#\t 134 t) (#\m 270 t)
(#\e 125 t) (#\n 77 t) (#\t 43 t) (#\, 149 t) (#\SPC 116 t) ...
これは日本国憲法の"enactment, "と打った部分(ミスタイプ含む)のデータ。本当はこんなのが改行なしで何百字分も出るので非常に見にくい。が、実はこのデータ自体を見る必要はないかもしれない。問題はこのデータから何を得るか。
タイピングのモニターをするlispについて。最近xrrをやってみたら美佳TEXTやTWにかなり近い感じだったのでこれを使わせてもらうことに。
以前は*pre/post-command-hook*を知らなくてあれこれと面倒なことをやっていたが、リストにキーと時間を追加する関数を書いてこれにadd-hookしておけばすむのだと気付く。*scratch*でちょっと試してみたところうまくいったのでxrrを起動して一回打ってみたが、記録用の変数を見てみると何も入っていない。
最初*pre-command-hook*がバッファローカルなのかとか思ってしまったが、考えてみたらxrrの開始から終了までが一つのコマンドだから*pre-command-hook*は走るわけがない。しょうがないのでxrrを適当に改造することにした。が、ソースがなんかよく分からない。
バックアップについて。とりあえずバッチファイルをちょっと書き換えてファイルのコピーと圧縮は一応自動化できた。あとは、変更がなかったものをわざわざ圧縮し直さないようにしたいところ。これはMS-DOSではやり方が分からない。lispでなら書けるがそうすると必然的に.xyzzy.historyは最新のものではなくなる。まあ電源を切る直前に"xyzzy -f backup"みたいなのをMS-DOSで実行することにすれば実質的に問題はないかもしれないからいいか。
それよりも、アーカイブをどこにおいておくのかが問題だな。
いろんなファイルのバックアップをもうちょっとうまくできないかと思い、それ用のlispを書きかけたが、とりあえずバッチファイルを作れば一応使い物になるのがわりとすぐにできるんじゃないかと思い直す。それでまずバッチファイルを作ったがあんまりよく分かってないのでかなり基本的なことしかできず、他のバックアップツールと組み合わせている。
やっていることは「指定されたファイルを適当なところにコピー」「それを指定されたとおりに圧縮」の二つだけで、さらにバッチファイルの役割は後半部分を自動化するだけだったりする。
lispで書くにしてもどうせ execute-shell-command あたりを使うつもりだったからあんまり変わらないかと思いつつ、でもlispの方が細かい設定がしやすそうではある。まあ自分で書いたほうが思い通りになるのは当たり前といえば当たり前。
そういえば最近、論理の存在量化子(∃)のプログラミング的な意味みたいなのが何となく分かった。それでプログラムを書くときの視点というかアプローチというか、何か変わったように感じる。