Windows用テキストエディタxyzzyは、柔軟性、拡張性共に高く、動作も軽快です。そのうえ、無料で配布されています。しかしながら、マニュアルが付属していないため、使いこなすには、誰しも、ネット上に散在する、有志の作成した情報ページや掲示板などを見て巡り、試行錯誤を繰り返すことになります。私もそうしています(現在進行形です)。そうやって私が得たノウハウなどを、自分なりにまとめ、公開して、後進のために資する、それがこのページの主旨です。
xyzzyのデフォルトのキーバインドはあまりにも独特です。しかも使いにくい(例えば、キーボードマクロです。「キーボードマクロ記録開始」と「キーボードマクロ記録終了」がそれぞれC-x (、C-x )なのはいいとして、「キーボードマクロ実行」がC-x eとは、どういう事か。こんなの不便に決まっています)。この独特すぎる(そして時に劣悪な)キーバインドはxyzzy入門者最初の壁です。
愚考するに、xyzzyは、というよりも、その元であるEmacsは、「各自が好きにカスタマイズできる設計なのだから、デフォルトのキーバインドは特に重要ではない。人間工学など無視して適当に割り付けてしまえ」と、テキトーなキーに割り付けされているモノが少なからずあるのではないでしょうか(笑)。ならば、デフォルトの通りに使って苦労することなど無駄以外の何物でもありません。遠慮なく、カスタマイズしてしまいましょう。せっかくの使いやすいエディタを使いにくい状態のまま使うなど、愚の骨頂です。
幸い、xyzzyには「winkey.l」や「Gates.l」が標準添付されており、これらを使えば簡単にWindows風キーバインドにすることができます。入門者はぜひともこれらを活用しましょう。
ですが、実際にはもう少しやらなければならないことがあるようです。C-xやC-cで始まる2ストロークキーの割り当ての問題です。
例えば、現在のバッファのファイルネームの変更、現在のバッファの「リードオンリー/エディット可能」切り替えは、それぞれデフォルトだとC-x C-n、C-x C-qに割り当てられています。「.xyzzy」などで(global-set-key #\C-F9 ctl-x-map)を指定すると、これらがそれぞれC-F9 C-nC-F9 C-qで出来るようになるわけですが、html+-modeでのC-x C-iもC-F9 C-iに置き換わってくれるかというと、そうはなってくれません。
この問題は、html+-modeのソースを見るとあっさりわかります。以下が、その問題の原因となっている部分です。
(define-key *html+-mode-map* '(#\C-x #\C-i) 'html+-insert-image) (define-key *html+-mode-map* '(#\C-x #\C-@) 'html+-insert-internet-shortcut) (define-key *html+-mode-map* '(#\C-x #\C-j) 'html+-insert-path) (define-key *html+-mode-map* '(#\C-x #\C-m) 'html+-select-link-dialog)
これは以下のように書き換えることで対処できます。実際には、ソースを直接書き換えるとバージョンアップの際に面倒になるので、「.xyzzy」などに書くことになると思います。
(setq *html+-mode-ctl-x-map* (make-sparse-keymap)) (setf (symbol-function 'html+-mode-ctl-x-prefix) *html+-mode-ctl-x-map*) (define-key *html+-mode-map* #\C-F9 *html+-mode-ctl-x-map*) (define-key *html+-mode-ctl-x-map* #\C-i 'html+-insert-image) (define-key *html+-mode-ctl-x-map* #\C-@ 'html+-insert-internet-shortcut) (define-key *html+-mode-ctl-x-map* #\C-j 'html+-insert-path) (define-key *html+-mode-ctl-x-map* #\C-m 'html+-select-link-dialog)
ここでは「html+-mode」を例に挙げましたが、こういったキー割り当てをしているxyzzy用Lispコードはたくさんあるので、気をつけましょう。
C-c系に於いても同様の問題が発生します。(global-set-key #\C-F10 spec-map)とやることで、C-F10をC-cの代わりに出来ますが、配布Lisp内でC-c系に決め打ちでキーが割り当てられている場合、上と同じような対処をしなければなりません。(私の知っている例だとmigemoとか)
付け加えておきますが、私はmigemoもhtml+-modeも共に愛用しています。
(symbol-function 'ctl-x-prefix)とするとベクトルが表示される
基本は黒板ですね。会社など、出先でxyzzyの設定をする必要が生じたときのために晒しておきます。
これがないとどこにいても作業に差し障る、というぐらい汎用性を確認したマクロです。先人達のコピーの域を出ていないものから自作のもの、なんだかちょっと怪しいものまで様々です。なお、設定ファイル中で参照している"myGates"は"~/site-lisp/"などに置いておく、拙作のWindows風キーコンフィグファイルです。
;;; -*- Mode: Lisp; Package: editor -*- 1行目はこれ
(setq *default-load-path* '("~/site-lisp/"))
(push "~/site-lisp/" *load-path*)
;優先的に読み込むキーワード定義ファイル置き場
(setq *keyword-load-path* '("~/"))
;;http://www1.odn.ne.jp/ymtz/tips.html#statusbar
;;ステータスバー関連
;;ステータスバーにアスキーコード/ユニコード/行番号/桁位置とか表示できます。
(setq *status-bar-format* "cupT")
;http://sugi.pobox.ne.jp/xyzzy/#SEC12
;http://www.jsdlab.co.jp/~kei/download/OChangeLog.html
(setq backup-by-copying :remote)
;;動的補完
(require "dabbrev")
(global-set-key #\M-. 'dabbrev-expand)
;;http://www1.odn.ne.jp/ymtz/tips.html#popup
;;動的補完候補をメニューでポップアップ
(global-set-key #\M-C-. 'dabbrev-popup)
(setq *popup-completion-list-default* :always)
(setq *minibuffer-popup-completion-list* :never)
;どんなファイルをオープンしてもabbrev-modeに入る
(add-hook 'editor::*find-file-hooks* #'(lambda () (abbrev-mode t) ))
;http://www.geocities.co.jp/Technopolis-Mars/8229/xyzzy/xyzzy-conf.html
;;.xyzzyもリスプモードで読み込むように情報を追加
; ("\\.l$" . lisp-mode)
;(pushnew '("\\(\\.l$\\)\\|\\(\\.xyzzy\\)" . lisp-mode) *auto-mode-alist* :test 'equal)
;参考
;http://pc2.2ch.net/test/read.cgi/software/1054141308/549-552n
;(pushnew '("^\\.[^.]+$" . lisp-mode) *auto-mode-alist* :test 'equal)
(pushnew '("^\\.[^.]+$" . lisp-mode) *auto-mode-alist* :regexp t)
;(require "isearch")
(require "migemo")
(migemo-toggle t)
;winちっくなキーバインドに
(load-library "myGates")
;; 元F3
(global-set-key #\M-F3 'show-info-viewer)
;キー表示(ラベル)変更
(set-function-bar-label #\M-F3 "InfoView")
(global-set-key #\C-y 'yank)
;word単位での移動はM-カーソルで
(global-set-key #\M-Left 'backward-word)
(global-set-key #\M-Right 'forward-word)
(global-set-key #\F1 'describe-key-briefly)
(set-function-bar-label #\F1 "Key確認")
(global-set-key #\M-F1 'describe-bindings)
(set-function-bar-label #\M-F1 "Key全部")
;現在のキーバインドで、特定の関数を割り当てられているキーを知りたいときに使う。
;引数に"forwar"とか指定して試してみれば、なんとなくわかるでしょう。
;実はM-x command-aproposの方が優れている
;他にC-u M-x aproposとかも
(defun pick-out-describe-bindings (string)
"指定した正規表現に一致する関数を割り当てられたキーを表示します"
(interactive "sKeyApropos(Regexp): \np")
(setq string (regexp-quote string))
(long-operation
(describe-bindings)
(let (mlist)
(while (scan-buffer string
:no-dup nil
:case-fold t ;大文字小文字を区別しない
:reverse nil
:tail t
:regexp t)
(goto-bol)
(push (buffer-substring (point)
(progn
(goto-eol)
(point)))
mlist))
(erase-buffer (selected-buffer))
(while mlist
(insert (car mlist))
(setq mlist (cdr mlist))
(insert "\n")
)
)))
(global-set-key #\C-M-F1 'pick-out-describe-bindings)
(set-function-bar-label #\C-M-F1 "絞Key探")
;;カレント行とその次の行の内容を入れ替え
(defun my-swap-next-line ()
"現在行と次の行の内容を入れ替えます"
(interactive)
(let (flg)
(save-excursion
(goto-eol)
(setq flg (eobp)))
(if flg
(message "最下行ではできません")
(let (line a z
(col (current-column)))
(goto-bol)
(setq a (point))
(forward-line)
(setq z (point))
(setq line (buffer-substring a z))
(delete-region a z)
(goto-eol)
(setq flg (eobp))
(if flg
(progn
(insert "\n")
(insert line)
(backward-char)
(delete-char)
)
(progn
(forward-line)
(insert line)
(previous-line)
))
(refresh-screen)
(goto-column col)
))
))
(global-set-key #\C-9 'eval-print-last-sexp)
(global-set-key #\C-2 'my-swap-next-line)
;(global-set-key #\C-2 'transpose-lines)
(global-set-key #\C-3 "……")
(global-set-key #\C-4 '"――")
;;カレント行とその前の行の内容を入れ替え
(defun my-swap-previous-line ()
"現在行と前の行の内容を入れ替えます"
(interactive)
(if (equal (current-line-number) 1)
(message "1行目では実行できません")
(let ((col (current-column)))
(previous-line)
(my-swap-next-line)
(previous-line)
(goto-column col)
)))
(global-set-key #\C-8 'my-swap-previous-line)
;行複写
(defun my-duplicate-line ()
"現在行を二重化します"
(interactive)
(let (line a z f)
(save-excursion
(goto-bol)
(setq a (point))
(goto-eol)
(setq f (eobp))
(forward-line)
(setq z (point))
(setq line (buffer-substring a z))
(if f
(insert "\n")
)
(insert line)
)))
(global-set-key #\C-5 'my-duplicate-line)
;;連続空白まとめ削除(M-\と違って、カーソル以前は無視)
;;参考:M-\はdelete-horizontal-spaces
;;参考:M-/はjust-one-space
(defun my-delete-spaces ()
"カーソル以降の空白を削除します"
(interactive)
(save-excursion
(delete-region (point)
(progn
(skip-chars-forward " \t")
(point)))
))
(global-set-key #\M-Delete 'my-delete-spaces)
;バイトコンパイル
(set-function-bar-label #\S-F8 "byte-compile")
(global-set-key #\S-F8 'byte-compile-file)
;;http://www.geocities.jp/madoinu/xyzzy/note/dialog-box.html#1
;; アプリケーションランチャのリスト
(defvar *my-launch-application-list*
(list
'(" eclipse" . "C:/userdir/Pgm/java/eclipse/eclipse.exe")
'(" kbmedia" . "C:/userdir/Pgm/browser/kbmed/kbmplay.exe")
'(" calc電卓" . "C:/WINDOWS/System32/calc.exe")
'("aya" . "C:/userdir/Pgm/game/aya/aya.exe")
'(" EdMax" . "C:/userdir/Pgm/mailer/EdMax/edmax.exe")
'(" FFFTP" . "C:/userdir/Pgm/hp作成/ffftp/FFFTP.exe")
'(" Irvine" . "C:/userdir/Pgm/netutil/irvine/irvine.exe")
))
;; アプリケーションランチャ
(defun my-application-launcher-dialog ()
"ランチャーです"
(interactive)
(multiple-value-bind (result data)
(dialog-box
; '(dialog 0 0 219 100
`(dialog 0 0 219 ,(+ (* (length *my-launch-application-list*) 10) 10)
(:caption "Application Launcher")
(:font 10 "MS Pゴシック")
(:control
; (:listbox list nil #x50b10111 7 7 150 82)
(:listbox list nil #x50b10111 7 7 150
,(* (length *my-launch-application-list*) 9) )
(:button IDOK "実行" #x50010001 162 7 50 14)
(:button IDCANCEL "やめ" #x50010000 162 24 50 14)))
(list (cons 'list *my-launch-application-list*))
nil)
(when result
(let
((cmd (cddr (assoc 'list data))))
(if cmd
; (call-process (map-slash-to-backslash cmd) :wait nil)
(shell-execute (map-slash-to-backslash cmd))
(message "何も選んでない。"))))))
(global-set-key #\S-F4 'my-application-launcher-dialog)
(set-function-bar-label #\S-F4 "AppLaunch")
(find-file "C:/userdir/main/memo/todo.txt")
ファイラを便利に使うためのもの。これも.xyzzyに書くのが基本です。ファイラで指定したファイルのフルパス取得機能は、.xyzzyの設定書き換えなどに便利なので自作しました。
;;ファイラ (require "filer") (global-set-key #\S-F12 'open-filer) (set-function-bar-label #\S-F12 "ファイラ") ;http://www1.odn.ne.jp/ymtz/tips.html#filer ;;; デフォルトのファイルマスクを変更 (setq *filer-primary-file-mask* '("*")) (setq *filer-secondary-file-mask* '("*")) (defun filer-get-full-path-filename () "filer : 目的のファイルネームをフルパスでクリップボードにコピー" (interactive) (let ((mk (editor::filer-get-mark-files)) files) (if mk (while mk (setq files (concat files (format nil "~A~%" (car mk)))) (setq mk (cdr mk))) (setq files (format nil "~A" (editor::filer-get-current-file)))) (copy-to-clipboard files) (message "done."))) (define-key filer-keymap #\C-M-p 'filer-get-full-path-filename) ;よく使うディレクトリをファイラに登録 ;http://www.uranus.dti.ne.jp/~shiro-/soft/xyzzy/filer.html#jump ;http://www.jsdlab.co.jp/~kei/xyzzy/manual/filer/filer.html (setq editor::*filer-directories* (list '(" tvchk" . "C:/userdir/Pgm/netutil/tvchk/Cache/") '("WideStudio" . "C:/userdir/Pgm/borland/WideStudio/ws/") '(" Java" . "C:/userdir/main/java/") (cons "[desktop]" (get-special-folder-location :desktop)) (cons "[sendto]" (get-special-folder-location :send-to)) (cons "[My Documents]" (get-special-folder-location :personal)) '("memo" . "C:/userdir/main/memo/") (cons "[.xyzzy置き場]" (si:getenv "XYZZYHOME") ) ;;http://www.afis.to/~start/xyzzy/textEditor/basic.html ;http://www.netlaputa.ne.jp/~henmi/lisp/xyzzy/editor/971025.html (cons "[xyzzy本体]" (si:system-root)) '("WWW" . "C:/userdir/main/www/") '("nslog" . "C:/userdir/Pgm/netutil/nsmsgs/Log/") '("down" . "C:/userdir/down/") '("extract" . "C:/userdir/down/extract/") (cons "bak置き場" (concat (si:getenv "XYZZYHOME") "/bak/")) '("Hotclip" . "C:/userdir/Pgm/env/Hotclip/") )) ;ファイラーの初期ディレクトリ ;http://www.uranus.dti.ne.jp/~shiro-/soft/xyzzy/filer.html#dir (setq *filer-primary-directory* "C:/userdir/main/memo/")
英和辞典や和英辞典をxyzzy上で引けるようにできるらしい、というんで、辞書引きモードに必要なファイル群を説明に従って入れたというのに、
などというエラーが出て途方に暮れたことはありませんか。私はこのエラーに随分長いこと悩まされてきました。「パスを指定した覚えがないのに何なんだこのエラーは」と思ってしまうのですが、わかってみれば何のことはない。これです。
ここの「辞書」という項目に、自分の環境に適合したパスを指定してやれば、この問題は解決します。
だが、ちょっと待て。設定に必要な項目は、可能な限り「.xyzzy」で一元管理しておきたいもの。そこで辞書切り替え方法のコードを見ると*edict-dictionary-path*という変数が鍵らしいとわかります。実はこの変数の内容こそが、先ほどの指定されたパス云々
というエラーメッセージの原因だったのです。
そうとわかれば話は簡単。自分の環境に合わせたパスをこの変数に代入する文を「.xyzzy」に書けばよいのです。自分の環境の場合、「xyzzy.exe」の直下にdictというディレクトリを作り、その中に辞書関連を放り込んでいるので、以下のように指定しました。
(setq *edict-dictionary-path*
(concat (si:system-root) "dict/"))
私の環境では、このようにメニューに追加を行っています。
(add-hook '*init-app-menus-hook* #'(lambda () (insert-popup-menu *app-menu* (get-menu-position *app-menu* 'ed::window) (define-popup-menu (:item nil ".&xyzzy編集" #'(lambda () (interactive) (find-file "~/.xyzzy"))) :sep (:item nil "euc-jpで開き直す" #'(lambda () (interactive) (let ((cur-file (get-buffer-file-name))) (if (and (file-readable-p cur-file) (valid-path-p cur-file) (file-exist-p cur-file)) (and (yes-or-no-p "~A" "euc-jpで開き直します") (read-file cur-file *encoding-euc-jp*)) ;*scratch*などでは無効に (error "該当のファイルがありません"))))) (:item nil "utf-8nで開き直す" #'(lambda () (interactive) (let ((cur-file (get-buffer-file-name))) (if (and (file-readable-p cur-file) (valid-path-p cur-file) (file-exist-p cur-file)) (and (yes-or-no-p "~A" "utf-8nで開き直します") (read-file cur-file *encoding-utf8n*)) ;*scratch*などでは無効に (error "該当のファイルがありません")))))) "私の(&M)")))
当サイト内の各所にあるアマゾンアソシエイトなアンカーを1キーで書くマクロ。アマゾンアソシエイトなサイトを持つ人で、html+-mode愛用者にはきっと役に立つマクロです。「.xyzzy」に書くなどして利用してください。もちろん、文字列部分は各自に合うように適切に書き換える必要がありますが、利用者には説明するまでもないでしょう。
(defun unkai-gen-amazon-associate-uri ()
"クリップボード内のamazonの個別商品URIを元にamazonアソシエイト式URIを生成してバッファに書き出す"
(interactive "*")
(let (asin ttl c
(stmd (match-data))
(clpbd (get-clipboard-data)))
(if (stringp clpbd)
(or
(progn
(string-matchp "http://.+/gp/product/\\([^/]+\\)/" clpbd)
(setq asin (match-string 1))
(match-data)
(stringp asin))
(progn
(string-matchp "http://.+/dp/\\(list/\\)?\\([^/]+\\)/" clpbd)
(setq asin (match-string 2))
(stringp asin))
(message "ASINがないみたいです")
(setq asin nil)))
; (string-matchp "http://.+/ASIN/\\([^/]+\\)/" clpbd))
(if asin
(progn
(string-matchp "Amazon.co.jp:[^\\\n]*" clpbd)
(setq ttl (match-string 0))
(if (equal (length ttl) 0)
(setq ttl "amazon.co.jp"))
(insert "<a href=\"http://www.amazon.co.jp/exec/obidos/ASIN/"
asin
"/unkainogotoku-22\" title=\""
ttl
"\">")
(setq c (point))
(insert "</a>@amazon.co.jp")
(goto-char c))
nil)
#l
; (message "クリップボードの内容がへんです"))
; (message "ASINがないみたいです"))
(store-match-data stmd)))
(insert (concat "<a href=\"http://www.amazon.co.jp/exec/obidos/ASIN/"
(match-string 1)
"/unkainogotoku-22\" title=\"amazon.co.jp\">"))
(message "ASINがないみたいです"))
l#
(store-match-data md)))
(define-key ed:*html+-mode-map* #\C-S-F5 'unkai-gen-amazon-associate-uri)
(add-hook 'ed:*html+-mode-hook*
#'(lambda ()
(set-function-bar-label #\C-S-F5 '"Amazonアソシエイト")))
サイト持ちなら毎日analog.htmlをダウンロードしていると思います。少なくとも、xrea.comユーザーはそうだと思います。そうすると、特定のフォルダにanalog.htmlのようなファイルを放り込んで整理することになると思いますが、当然、同一のフォルダに同じファイルネームが複数存在してはいけないことになるので、毎日analog.htmlをダウンロードしてフォルダに放り込んではリネームする、という作業をすることになる。毎日のことですからこれは面倒です。そこでxyzzyで自動的にリネームしてしまうようにしました。「.xyzzy」にこれを書いておくと毎日xyzzyを立ち上げるたびに自動的にリネーム処理をしてくれます。便利です。変換後の書式は(format-date-string)の書式で指定し、基本的にファイルの日付情報を元にファイルネームとすることにしてあります。
(defun unkai-matome-file-rename (path fregex fmt &optional (verbose t))
#|
path 目的のフォルダ
fregex 目的のファイル名の正規表現(拡張子も込みで)
fmt 変更後のファイル名の書式(format-date-stringの書式に準拠)
|#
(and path
(setq path
(append-trail-slash path)))
(if (check-valid-pathname path);ディレクトリが存在するか
(let ((allfn (directory path :file-only t));全ファイル名
(f #'(lambda (old)
(let ((pold (concat path old)))
(if (rename-file pold
(concat path
(format-date-string
fmt
(file-write-time pold)))
:if-exists :skip
:if-access-denied :skip)
nil;成功
old))));失敗
sifted ret good bad)
(setq sifted (remove-if-not
;正規表現を満たさないファイル名を除外
#'(lambda (arg) (string-matchp fregex arg))
allfn))
(setq ret (mapcar f sifted))
(setq good (length
(remove-if #'(lambda (arg) (if arg t ))
ret)));nil抽出リスト作成
(setq bad (length
(remove-if-not #'(lambda (arg) (if arg t))
ret)));nil以外をリストに
(message (format nil "ファイルネーム変更//成功:~D個、失敗:~D個"
good bad)))
(message (format nil "指定のフォルダ\"~A\"が見つかりません" path))))
;使用例
(unkai-matome-file-rename
"H:/userdir/main/www/www/log/" ;目的のフォルダ
"^analog[0-9.]*\\.html" ;目的のファイル名の正規表現(拡張子も込みで)
"%Y%m%d.html") ;変更後のファイル名の書式(format-date-stringの書式に準拠)
もしものときのためにバックアップファイルを指定のフォルダに残しているのですが、ある日そのフォルダの中を見たら、驚くほど多数のファイルがそこにありました。もちろん、その中には本来の目的通り、必要になるかもしれないファイルもあるでしょうが、それ以上にもう保存しておく必要性がなくなったファイルも多数存在するはずです。そこで、一定の条件を満たしたファイルのみ保存を継続し、そうでないファイルはまとめて削除する、ということにしました。その条件とはファイルのタイムスタンプ。一定の期間内に保存されたものは保存継続、それ以上の時間が経過したものはまとめて削除します。ここではその基準を30日以内と定めました。以下に示すコード内の(* 60 60 24 30)がその期間の長さを指定した箇所で、これを例えば50日に変更したければ(* 60 60 24 50)とすればいいわけです。
「.xyzzy」にこれを書いておくと毎日xyzzyを立ち上げるたびに自動的に古いバックアップファイルを削除してくれます。古いバックアップファイルの山を見てうんざりすることはもうありません。
;バックアップファイル作成場所を一箇所に
(require "backup")
(setq *backup-directory* "~/bak/")
(setq *hierarchic-backup-directory* nil)
;30日以上経過した古いバックアップファイルを自動削除
(setq stdtime (- (get-universal-time) (* 60 60 24 30))) ;30日前のuniversal-timeを算出
(dolist (onebakfile (directory *backup-directory* :absolute t))
(if (> stdtime (cadr (get-file-info onebakfile)))
(delete-file onebakfile :if-access-denied :skip)))
以下の様なマクロを定義していれば(swap x y)で内容がそれぞれの変数が保持している値が入れ替わります。変数の値を入れ替えるにはこうやって自分でマクロを定義しておくしかないのかと今まで思っていたのですが、そんな必要はなかったということを最近知りました。なぜなら(rotatef x y)とやるだけで変数 x と y の中身を入れ替えができたのです。このrotatefも「setf.l」内でdefmacroを使って定義されたマクロなのですがでした。難解なソースを見るよりリファレンスの図入り解説を見た方がわかりやすいのでリンクしておきます。
;変数の内容を入れ替える準備
(defmacro swap (a b)
`(let (tmp)
(setq tmp ,a)
(setq ,a ,b)
(setq ,b tmp)))
(setq x 1)
(setq y 2)
(swap x y);入れ替わる
(rotatef x y);入れ替わる(準備不要)