2011-02-27

日本語混じりの文章で hippie-expand を使う

Emacs にはデフォルトで M-/ に dabbrev-expand が割り当てられていますが、 hippie-expand の方がファイルのパスや Elisp のシンボルも補完してくれる等、機能が豊富です。

hippie-expand の問題点

日本語Nihongo日本語。にほんごNiho-!-

上の例文でカーソル位置は -!- にあります。 M-x hippie-expand としても Nihongo とは補完されません。
理由として2点あります。

(1) Nihongo という単語の両端にスペースがない。
(2) Niho という単語の頭にスペースがない。

半角文字は常にスペースで囲う癖をつければいいのですが、やや面倒です。

(1) の問題を解決するには関数 he-dabbrev-search の再定義をします。

(2) の問題を解決する方法はここの he-dabbrev-beg の定義で示されています。
http://ko.meadowy.net/~shirai/diary/?date=20060125

(1) と (2) の問題を解決したものが以下で、
半角英数、全角英数、漢字、ひらがな、カタカナ、等、同一カテゴリの文字のみを補完するようになります。

(defun my-char-category-regexp (char)
  (let ((c (char-category-set char)))
    (cond
     ((aref c ?a) "\\ca")               ; ASCII
     ((aref c ?j)                       ; Japanese
      (cond
       ((aref c ?K) "\\cK")             ; katakana
       ((aref c ?H) "\\cH")             ; hiragana
       ((aref c ?k) "\\ck")             ; hankaku-kana
       ((aref c ?C) "\\cC")             ; kanji
       ((aref c ?A) "\\cA")             ; 2byte alphanumeric
       ((aref c ?r) "\\cr")             ; Japanese roman ?
       (t "\\cj")))
     ((aref c ?l) "\\cl")               ; Latin
     (t "."))))

;; このコードから借用
;; http://ko.meadowy.net/~shirai/diary/?date=20060125
;; syntax と hippie-expand-dabbrev-as-symbol を使うよう変更。
(defadvice he-dabbrev-beg
  (around modify-regexp-for-japanese activate compile)
  "Dynamically for Japanese words."
  (if (bobp)
      ad-do-it
    (when hippie-expand-dabbrev-skip-space
      (skip-syntax-backward ". "))
    (let ((category-regexp (my-char-category-regexp (char-before)))
          (syntax-regexp (if hippie-expand-dabbrev-as-symbol
                             "\\sw\\|\\s_" "\\sw")))
      (save-excursion
        (when (> (point) (minibuffer-prompt-end))
          (forward-char -1)
          (while (and (looking-at syntax-regexp)
                      (looking-at category-regexp)
                      (> (point) (minibuffer-prompt-end))
                      (not (= (point) (field-beginning (point) nil
                                                       (1- (point))))))
            (forward-char -1))
          (or (and (looking-at category-regexp)
                   (looking-at syntax-regexp))
              (forward-char 1)))
        (setq ad-return-value (point))))))

;; syntax と category 両方合致するものを検索する
(defun my-he-dabbrev-search-1 (pattern regpat reverse limit)
  (let ((category-regexp (concat (my-char-category-regexp (aref pattern 0))
                                 "+")))
    (when (if reverse
              (re-search-backward regpat limit t)
            (re-search-forward regpat limit t))
      (if reverse
          (save-excursion
            (goto-char (match-beginning 0))
            (re-search-forward category-regexp (match-end 0) t))
        (goto-char (match-beginning 0))
        (re-search-forward category-regexp (match-end 0) t)))))

;; he-dabbrev-search の再定義。変更個所は [1] [2] [3]
;; 以下の3つの関数から呼ばれる
;; try-expand-dabbrev-visible
;; try-expand-dabbrev
;; try-expand-dabbrev-all-buffers
(defun my-he-dabbrev-search (pattern &optional reverse limit)
  (let ((result ())
        (regpat (cond ((not hippie-expand-dabbrev-as-symbol)
                       (concat "\\<" (regexp-quote pattern) "\\sw+"))
                      ((eq (char-syntax (aref pattern 0)) ?_)
                       (concat (regexp-quote pattern) "\\(\\sw\\|\\s_\\)+"))
                      (t
                       (concat "\\<" (regexp-quote pattern)
                               "\\(\\sw\\|\\s_\\)+")))))
    (while (and (not result)
                ;; [1]
                (my-he-dabbrev-search-1 pattern regpat reverse limit))
      (setq result (buffer-substring-no-properties (match-beginning 0)
                                                   (match-end 0)))
      (if (or (and hippie-expand-dabbrev-as-symbol
                   (> (match-beginning 0) (point-min))
                   (memq (char-syntax (char-after (1- (match-beginning 0))))
                         '(?_ ?w))
                   ;; [2]
                   (eq (char-charset (char-after (1- (match-beginning 0))))
                       'ascii))
              ;; [3]
              (<= (length result) (length pattern))
              (he-string-member result he-tried-table t))
          (setq result nil))) ; ignore if bad prefix or already in table
    result))
(fset 'he-dabbrev-search 'my-he-dabbrev-search)

動作確認:GNU Emacs 22

2011-03-28 追記
関数 my-he-dabbrev-search-1 の引数 reverse が non-nil の時うまく動作していなかったのを修正。

2011-02-23

Lisp mode のカーソル移動 forward-list

Elisp を書くようになると、括弧関連のカーソル移動のコマンドも多用するようになります。

C-M-f, C-M-b S式単位で前後に移動
C-M-a, C-M-e 関数単位で移動
C-M-d, C-M-u 括弧の内側外側へ移動
C-M-p, C-M-n 括弧単位で前後に移動

この中で、唯一 C-M-n (M-x forward-list) だけ、挙動に納得がいかなかった。

(-0- list (list)-1-
          (list)-2-)

-0- がスタート地点で、 -1- が C-M-n の1回目の実行後。-2- が2回目の地点。
カーソルが括弧の後ろに移動します。この動きなら C-M-f で代用可能です。

期待したのは以下のように、括弧の先頭へのカーソル移動。

(-0- list -1-(list)
          -2-(list))

ということで、自分でコマンドを作るとこんな感じに。

(defun my-forward-list (&optional arg)
  "Do `forward-list' and `backward-sexp'."
  (interactive "p")
  (or arg (setq arg 1))
  (let ((start (point))
        end)
    (if (and (= arg 1)
             (= start (scan-sexps (scan-lists start 1 0) -1)))
        (setq end (scan-lists start 2 0))
      (setq end (scan-lists start arg 0)))
    (goto-char (or (and end (scan-sexps end -1))
                   (buffer-end arg)))))

(eval-after-load "lisp-mode"
  '(define-key lisp-mode-shared-map "\C-\M-n" 'my-forward-list)) ; forward-list

動作確認: GNU Emacs 22

2011-02-21

Mac OS X の拡張属性を消す removexattr

Mac OS X の拡張属性 (EA, Extended Attributes) はリソースフォーク、クリエータタイプ、不可視属性、等の HFS+ 固有のレガシーなものから、 com.apple.quarantine のような最近追加されたものまで色々あります。

拡張属性を確認する場合。
ls -l@ file
xattr file


普通は拡張属性を消す必要はないんですが、Windows の FAT32 のボリュームに Mac から拡張属性の付いたファイルをコピーすると、 file と ._file のように2つのファイルとしてコピーされます。できれば ._file のようなファイルは作りたくない。

消す場合は xattr コマンドで -d オプション。 com.apple.quarantine のような名前を指定する必要あり。
xattr -d com.apple.quarantine file

一つのファイルから全ての拡張属性を消すにはシェルスクリプトを書く必要があります。
しかしながら、 xattr (中身は python スクリプト) が妙に遅かったので、 C で書いたものが以下。

removexattr.c
/*
 * Require Mac OS X 10.4 or above.
 * man 2 listxattr
 * man 2 removexattr
 * gcc -O2 -o removexattr removexattr.c
 */

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdint.h>

#include <sys/xattr.h>

#define BUFSIZE 0x8000

int main(int argc, const char *argv[])
{
    int i, len;
    uint8_t *namebuf, *p;
    ssize_t size;

    if (argc <= 1) {
        fprintf(stderr,
                "Remove all extended attributes from given files or directories.\n"
                "Usage: %s [file ...]\n", argv[0]);
        exit(1);
    }

    namebuf = (uint8_t *)malloc(BUFSIZE);
    if (namebuf == NULL) {
        fprintf(stderr, "malloc() failed\n");
        exit(1);
    }

    for (i = 1; i < argc; i++) {
        size = listxattr(argv[i], namebuf, BUFSIZE, XATTR_NOFOLLOW);
        if (size <= 0)
            continue;

        p = namebuf;
        len = 0;
        while (len < size) {
            removexattr(argv[i], p, XATTR_NOFOLLOW);
            len = len + strlen(p) + 1;
            p = namebuf + len;
        }
    }

    free(namebuf);

    return 0;
}

コンパイルは (要Xcode)
gcc -O2 -o removexattr removexattr.c
使い方は
./removexattr file

find の -exec で指定ディレクトリ以下全てのファイル、フォルダの拡張属性を消すこともできますが、それはさすがに勇気がいります。

動作確認: Mac OS X 10.6

2011-02-20

Emacs で HTTP レスポンスヘッダを表示

emacs-w3m で = 打鍵してもヘッダは確認できますが、 URL をウェブブラウザで開く前に HEAD メソッドで手軽に確認したい、というのが今回の話。

以下のような時に使います。

  • Twitter 等でよく使われる短縮 URL の転送先の確認
  • ウェブページの最終更新日の確認
  • ウェブにアップロードされたファイルのサイズの確認

コマンドラインで
curl -I http://www.example.com/
でもいいのですが、 Emacs 使いならもっと手軽にやりたい。

Show HTTP response header

(defvar my-show-http-header-history nil
  "History list.")
(defvar my-show-http-header-config nil
  "Window configuration.")
(make-variable-buffer-local 'my-show-http-header-config)
(defvar my-show-http-header-name "*HTTP response header*"
  "Buffer name.")

(defun my-show-http-header-read ()
  (let ((default (progn
                   (unless (fboundp 'ffap-url-at-point)
                     (require 'ffap))
                   (ffap-url-at-point))))
    (read-string (if default
                     (format "URL (default %s): " default)
                   "URL: ")
                 nil 'my-show-http-header-history default)))

(defun my-show-http-header-colorize ()
  (save-excursion
    (goto-char (point-min))
    (while (re-search-forward
            "\\(^[Ll]ocation: \\|^[Ll]ast-[Mm]odified: \\)" nil t)
      (add-text-properties (point)
                           (line-end-position)
                           '(face font-lock-keyword-face)))))

(defun my-show-http-header (url &optional use-url)
  "Show HTTP response header of URL.
With prefix arg USE-URL, use `url-retrieve-synchronously'
instead of curl."
  (interactive
   (list (my-show-http-header-read) current-prefix-arg))
  (when (or (not (stringp url))
            (not (string-match "^https?://" url)))
    (error "No URL"))
  (let (window-config)
    ;; Save the window state before pop-to-buffer.
    (when (and (null (get-buffer-window my-show-http-header-name))
               (not (window-minibuffer-p (selected-window))))
      (setq window-config (current-window-configuration)))
    (pop-to-buffer (get-buffer-create my-show-http-header-name))
    (let (buffer-read-only)
      (unwind-protect
          (progn
            (erase-buffer)
            (insert (concat "URL: " url "\n\n"))
            (if use-url  ; or (null use-url) to inverse the prefix arg
                (insert-buffer (let ((url-max-redirections 0)
                                     (url-privacy-level 'paranoid)
                                     (url-request-method "HEAD"))
                                 (url-retrieve-synchronously url))))
            (call-process "curl" nil t nil
                          "-I" "-s" "-k"
                          "-A" "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)"
                          url))
        (goto-char (point-min))
        (save-excursion
          (while (re-search-forward "\r\n" nil t)
            (replace-match "\n")))
        (my-show-http-header-colorize)))
      (when window-config
        (setq my-show-http-header-config window-config))
      (setq view-exit-action (lambda (buffer)
                               (when my-show-http-header-config
                                 (set-window-configuration
                                  my-show-http-header-config))
                               (kill-buffer buffer)))
      (view-mode-enter)))

;; (global-set-key "\C-cH" 'my-show-http-header)

特徴

  • カーソル位置の URL を読み取ってくれるので、手入力する手間が省けます。
  • q 打鍵一発で元のウィンドウの状態に戻ります。
  • Location ヘッダと Last-Modified ヘッダは色付けされます。
  • デフォルトでは外部プログラムの curl を使いますが、 C-u 付きで実行すれば Emacs に含まれる url.el ライブラリを使います。

サーバーによっては User-Agent や Referer 等のリクエストヘッダを見てレスポンスヘッダを変えてきたりしますが、その辺は curl のオプション等を適当にいじってみてください。

使い方
M-x my-show-http-header RET http://tinyurl.com/1awl RET

結果

URL: http://tinyurl.com/1awl

HTTP/1.1 301 Moved Permanently
X-Powered-By: PHP/5.3.5
Location: http://www.google.co.jp/
X-tiny: cache 0.00046396255493164
Content-type: text/html
Connection: close
Date: Wed, 18 Feb 2011 15:03:09 GMT
Server: TinyURL/1.6




動作確認: GNU Emacs 22, Mac OS X 10.6

2011-02-19

Finder で最前面に表示されているフォルダを Emacs (Dired) で開く

Finder に切り替えて、フォルダを Dock の Emacs アイコンにドラッグ&ドロップすればいいのですが、もっと手軽にやりたい。

do-applescript 関数は互換性に関して面倒が多いので、 osascript を使います。

(defun my-dired-finder-window ()
  "Open the front most window of Finder in dired."
  (interactive)
  (let (file
        (script (concat
                 "tell application \"Finder\"\n"
                 "    if ((count Finder windows) >= 1) then\n"
                 "        get POSIX path of (target of window 1 as alias)\n"
                 "    else\n"
                 "        get POSIX path of (desktop as alias)\n"
                 "    end if\n"
                 "end tell\n")))
    (setq file (with-temp-buffer
                 (call-process "osascript" nil t nil "-e" script)
                 (buffer-substring-no-properties (point-min) (1- (point-max)))))
    (if (file-directory-p file)
        (dired file)
      (error "Not a directory: %s" file))))

(global-set-key "\C-cf" 'my-dired-finder-window)

動作確認: GNU Emacs 22, Mac OS X 10.6

2011-02-18

Dired からゴミ箱に捨てる、情報を見る、 Finder で表示

AppleScript でやるわけですが、 Emacs の do-applescript 関数は以下の3つの環境でのみ使用できるのを確認しています。X11 版は未確認です。

・ Cocoa Emacs の GUI 起動
・ Carbon Emacs の GUI 起動
・ Carbon Emacs の -nw 起動

3つそれぞれ微妙に違ったコードを書かなければならず面倒なので、 do-applescript は使わず外部コマンドの osascript を呼ぶ方法を紹介します。

キーバインドの例では、
M-DEL で Finder の「ゴミ箱に入れる」
M-i で Finder の「情報を見る」
M-r で Finder の「オリジナルを表示」
に割り当ててますが、適当に変えてください。 (特に M-r)

Dired で Finder の「ゴミ箱に入れる」
(defun my-dired-do-trash-1 (async &rest files)
  (let ((script
         (concat
          "on run argv\n"
          "    set itemArray to {}\n"
          "    repeat with i in argv\n"
          "        set itemArray to itemArray & (i as POSIX file as Unicode text)\n"
          "    end repeat\n"
          "    tell application \"Finder\"\n"
          "        delete itemArray\n"
          "    end tell\n"
          "end run\n")))
    (if async
        (apply 'start-process "osascript-trash" nil "osascript" "-e" script files)
      (apply 'call-process  "osascript" nil nil nil "-e" script files))))

;; Trash files
(defun my-dired-do-trash (&optional arg)
  "Trash the marked files.
If no files are marked or a specific numeric prefix arg is given,
the next ARG files are used.  Just \\[universal-argument] means the current file."
  (interactive "P")
  (let ((files (dired-get-marked-files nil arg))
        point)
    (message "Trashing...")
    ;; Position of the first mark
    (save-excursion
      (goto-char (point-min))
      (setq point (re-search-forward dired-re-mark nil t)))
    (unless point
      (setq point (point)))
    (apply 'my-dired-do-trash-1 nil files)
    (revert-buffer)
    ;; Restore the position
    (goto-char (point-min))
    (forward-line (line-number-at-pos point))
    (forward-line -1)
    (dired-move-to-filename)
    (message "Trashing...done")
    ;; Display the result
    (if (null (cdr files))
        (message "Trashed 1 item: %s" (file-name-nondirectory (car files)))
      (display-message-or-buffer
       (format "Trashed %d items:\n%s"
               (length files)
               (mapconcat 'file-name-nondirectory files "\n"))))))

(eval-after-load "dired"
  '(define-key dired-mode-map "\M-\C-?" 'my-dired-do-trash)) ; M-DEL, dired-unmark-all-files, use U or * ? RET

Dired で Finder の「情報を見る」
;; Get info
(defun my-dired-do-getinfo (&optional arg)
  "Show Finder's information window of the marked files.
If no files are marked or a specific numeric prefix arg is given,
the next ARG files are used.  Just \\[universal-argument] means the current file."
  (interactive "P")
  (let ((files (dired-get-marked-files nil arg))
        (script
         (concat
          "on run argv\n"
          "    tell application \"Finder\"\n"
          "        activate\n"
          "    end tell\n"
          "    repeat with i in argv\n"
          "        set i to (i as POSIX file as Unicode text)\n"
          "        tell application \"Finder\"\n"
          "            open information window of item i\n"
          "        end tell\n"
          "    end repeat\n"
          "end run\n")))
    (apply 'start-process "osascript-getinfo" nil "osascript" "-e" script files)))

(eval-after-load "dired"
  '(define-key dired-mode-map "\M-i" 'my-dired-do-getinfo)) ; tab-to-tab-stop

Dired で Finder の「オリジナルを表示」
;; Reveal in Finder
(defun my-dired-do-reveal (&optional arg)
  "Reveal the marked files in Finder.
If no files are marked or a specific numeric prefix arg is given,
the next ARG files are used.  Just \\[universal-argument] means the current file."
  (interactive "P")
  (let ((files (dired-get-marked-files nil arg))
        (script
         (concat
          "on run argv\n"
          "    set itemArray to {}\n"
          "    repeat with i in argv\n"
          "        set itemArray to itemArray & (i as POSIX file as Unicode text)\n"
          "    end repeat\n"
          "    tell application \"Finder\"\n"
          "        activate\n"
          "        reveal itemArray\n"
          "    end tell\n"
          "end run\n")))
    (apply 'start-process "osascript-reveal" nil "osascript" "-e" script files)))

(eval-after-load "dired"
  '(define-key dired-mode-map "\M-r" 'my-dired-do-reveal)) ; move-to-window-line

動作確認: GNU Emacs 22, Mac OS X 10.6

日本語や濁点の付いたファイル名に対しても動作しています。
手元の GNU Emacs 22 (Carbon) では「名\称\\未\\\設"定""フ"""ォルダ」のような悪魔的フォルダ名に対しては Dired がファイル名の取得に失敗します。これは Dired の問題です。
osascript の引数処理の関係で Mac OS X 10.4 以上が必要と思われます。


Terminal からゴミ箱に捨てる方法はこちら。 コマンドラインからゴミ箱に捨てるシェルスクリプト

2011-02-17

コマンドラインからゴミ箱に捨てるシェルスクリプト

Mac OS X 用のゴミ箱スクリプトは検索すれば結構出てくるものの、どれもファイルを直接 ~/.Trash 等に移動するものが多いような気がする。
外付け HDD の場合はゴミ箱のパスが違うし、移動先に同じファイル名がある場合の処理とか、結構面倒くさい。

面倒くさいので全部 Finder に丸投げ、ということで AppleScript 経由でゴミ箱に捨てるスクリプトがこちら。

Finder で「ゴミ箱に入れる」
trash
#!/bin/sh

# Trash items.
# Usage: trash [files]

# Backslashes and doublequotes must be escaped.
# replace  \  -->  \\
# replace  "  -->  \"
CWD=`pwd | sed -E -e 's/\\\\/\\\\\\\\/g' -e 's/"/\\\\"/g'`

osascript - "$@" <<EOF
on run argv
    set itemArray to {}

    repeat with i in argv
        if first character of i is not "/" then
            set i to "$CWD" & "/" & i
        end if
        set itemArray to itemArray & (i as POSIX file as Unicode text)
    end repeat

    tell application "Finder"
        delete itemArray
    end tell
end run
EOF

動作確認:Mac OS X 10.6 (osascript の引数処理の関係で Mac OS X 10.4 以上が必要と思われます。)

使う場合、実行権限を付けて PATH の通った所へファイルを置いてください。
使い方の例: hoge.txt fuga.txt を捨てる
trash hoge.txt fuga.txt

AppleScript を使う利点としては、このスクリプトでゴミ箱に捨てた直後ならば、 Finder で「取り消す Cmd-Z」ができる。 Mac OS X 10.6 からはゴミ箱内の項目を「戻す」ことができるので、それにも対応している。欠点としては動作が遅い。


以下はおまけの Finder で「オリジナルを表示」と「情報を見る」のスクリプト。

Finder で「オリジナルを表示」
reveal
#!/bin/sh

# Reveal in Finder.
# Usage: reveal [files]

CWD=`pwd | sed -E -e 's/\\\\/\\\\\\\\/g' -e 's/"/\\\\"/g'`

osascript - "$@" <<EOF
on run argv
    set itemArray to {}

    repeat with i in argv
        if first character of i is not "/" then
            set i to "$CWD" & "/" & i
        end if
        set itemArray to itemArray & (i as POSIX file as Unicode text)
    end repeat

    tell application "Finder"
        activate
        reveal itemArray
    end tell
end run
EOF

Finder で「情報を見る」
getinfo
#!/bin/sh

# Show information window in Finder.
# Usage: getinfo [files]

CWD=`pwd | sed -E -e 's/\\\\/\\\\\\\\/g' -e 's/"/\\\\"/g'`

osascript - "$@" <<EOF
on run argv
    repeat with i in argv
        if first character of i is not "/" then
            set i to "$CWD" & "/" & i
        end if
        set i to (i as POSIX file as Unicode text)

        tell application "Finder"
            open information window of item i
        end tell
    end repeat

    tell application "Finder"
        activate
    end tell
end run
EOF

Emacs の Dired からゴミ箱に捨てる方法はこちら。 Dired からゴミ箱に捨てる、情報を見る、 Finder で表示

2011-02-16

EmacsのバッファやDiredでカーソル位置のファイルを Quick Look

Mac OS X には qlmanage という便利なコマンドがあるのでそれを使う。

C-c y で Quick Look。もう一度 C-c y で、Quick Look を閉じる。
call-process を使って C-g で閉じるのもいいですが、やや反応が鈍いので下の例では start-process を使っています。

(defun my-quicklook-at-point ()
  "Preview a file at point with Quick Look."
  (interactive)
  (require 'ffap)
  (let ((url (ffap-url-at-point))
        (file (ffap-file-at-point))
        (process (get-process "qlmanage_ps")))
    (when url
      (if (string-match "\\`file://\\(.*\\)\\'" url)
          (setq file (match-string 1 url))
        (setq file nil)))
    (when (or (not (stringp file))
              (not (file-exists-p (setq file (expand-file-name file)))))
      (when process
        (kill-process process))
      (error "No file found"))
    (if process
        (kill-process process)
      (message "Quick Look: %s" file)
      (start-process "qlmanage_ps" nil "qlmanage" "-p" file))))

(global-set-key "\C-cy" 'my-quicklook-at-point)

Dired の設定。

(defun my-dired-do-quicklook ()
  "In dired, preview with Quick Look."
  (interactive)
  (let ((file (dired-get-filename))
        (process (get-process "qlmanage_ps")))
    (if process
        (kill-process process)
      (start-process "qlmanage_ps" nil "qlmanage" "-p" file))))

(eval-after-load "dired"
  '(define-key dired-mode-map "\C-cy" 'my-dired-do-quicklook))

キーバインドは適当に変えてください。

動作確認:GNU Emacs 22, Mac OS X 10.6

EmacsのバッファやDiredでカーソル位置のファイル(URL)をFinder(ウェブブラウザ)で開く

Finder でダブルクリックした時と同じ動作をさせたい場合、 Mac OS X には open という便利なコマンドがあるのでそれを使う。

;; カーソル位置のファイルや URL を open で開く
(defun my-open-at-point ()
  "Ask /usr/bin/open to open the thing at or before point."
  (interactive)
  (require 'ffap)
  (let ((file (or (ffap-url-at-point)
                  (ffap-file-at-point))))
    (unless (stringp file)
      (error "No file or URL found"))
    (when (file-exists-p (expand-file-name file))
      (setq file (expand-file-name file)))
    (message "Open: %s" file)
    (start-process "open_ps" nil "open" file)))

(global-set-key "\C-co" 'my-open-at-point)
;; double click
(global-set-key [double-mouse-1] 'my-open-at-point)
(global-set-key [double-down-mouse-1] 'ignore) ; mouse-drag-region

キーバインドは一例ですが、ダブルクリックにも割り当ててます。
以下のようなURLやファイルに対応します。

http://www.google.co.jp/
~/Library
/usr/include/stdio.h
#include <stdio.h>

注意点として、 (require 'hoge) のような文字列に対して実行すると、 hoge.el.gz というファイルが開き、 .gz が解凍されてしまう場合があります。この辺の挙動は y-or-n-p とか使って確認してから開く、という風に変えてもいいかもしれない。

以下 Dired の設定。

;; カーソル位置のファイルを open で開く
(defun my-dired-do-open (&optional arg)
  "In dired, invoke /usr/bin/open on the marked files.
If no files are marked or a specific numeric prefix arg is given,
the next ARG files are used.  Just \\[universal-argument] means the current file."
  (interactive "P")
  (let ((files (dired-get-marked-files nil arg)))
    (apply 'start-process "open_ps" nil "open" files)))

(eval-after-load "dired"
  '(define-key dired-mode-map "\C-co" 'my-dired-do-open))

動作確認: Mac OS X 10.6, GNU Emacs 22

2011-02-15

highlight-parentheses.el を使ってみる

対応する括弧に色を付けてくれる
HighlightParentheses
http://nschum.de/src/emacs/highlight-parentheses/

設定はこんな感じに
(require 'highlight-parentheses)
(setq hl-paren-colors '("red" "blue" "yellow" "green" "magenta" "peru" "cyan"))
(set-face-attribute 'hl-paren-face nil :weight 'bold)
(add-hook 'emacs-lisp-mode-hook 'highlight-parentheses-mode)

結果はこんな風
(defun hl-paren-highlight ()
  "Highlight the parentheses around point."
  (unless (= (point) hl-paren-last-point)
    (setq hl-paren-last-point (point))
    (let ((overlays hl-paren-overlays)
          pos1 pos2
          (pos (point)))
      (save-excursion
        (condition-case err
            (while (and (setq pos1 (cadr (syntax-ppss pos1)))
                        (car overlays))
              (move-overlay (pop overlays) pos1 (1+ pos1))
              (when (setq pos2 (scan-sexps pos1 1))
                (move-overlay (pop overlays) (1- pos2) pos2)))
          (error nil)))
      (dolist (ov overlays)
        (move-overlay ov 1 1)))))


バージョン 2009-03-19 (1.0.1) の時点では、 hl-paren-colors に設定した最後の色が有効にならないので関数 hl-paren-highlight に以下のパッチを当てる。
--- highlight-parentheses.el.old
+++ highlight-parentheses.el
@@ -100,7 +100,7 @@
       (save-excursion
         (condition-case err
             (while (and (setq pos1 (cadr (syntax-ppss pos1)))
-                        (cddr overlays))
+                        (car overlays))
               (move-overlay (pop overlays) pos1 (1+ pos1))
               (when (setq pos2 (scan-sexps pos1 1))
                 (move-overlay (pop overlays) (1- pos2) pos2)

2011-02-14

矩形範囲に連番を挿入する my-number-rectangle

使い方は、範囲選択して、

3桁で1から開始、1増加
M-x my-number-rectangle RET %03d RET 1 RET
001
002
003
004

3桁で1から開始、10増加
C-u 10 M-x my-number-rectangle RET %03d RET 1 RET
001
011
021
031

16進数の8桁で 128開始、16増加
C-u C-u M-x my-number-rectangle RET %08x RET 128 RET
00000080
00000090
000000a0
000000b0

%03d や %08x 等の書式については format を参照してください。
M-x describe-function RET format RET

my-number-rectangle
(defvar my-number-rectangle-format-history nil
  "History list for `my-number-rectangle'.")
(defvar my-number-rectangle-number nil
  "For internal use.")
(defvar my-number-rectangle-initial-number 1
  "*Default initial number for `my-number-rectangle'.")

(defun my-number-rectangle-read-format ()
  (let (default)
    (if (car my-number-rectangle-format-history)
        (setq default (car my-number-rectangle-format-history))
      (setq default "%03d"))
    (read-string (format "Format of number (default %s): " default)
                 nil 'my-number-rectangle-format-history default)))

(defun my-number-rectangle-line (startcol endcol fmt inc)
  (delete-rectangle-line startcol endcol t)
  (insert (format fmt my-number-rectangle-number))
  (setq my-number-rectangle-number (+ my-number-rectangle-number (or inc 1))))

(defun my-number-rectangle (start end fmt initial-number &optional inc)
  "Replace rectangle contents with numbers on each line.
Number is formatted with a string FMT and starts from INITIAL-NUMBER.
The increment value is 1 unless specified by prefix arg INC.

Example of FMT with number 0 and 31.
%d              \"0\"             \"31\"
% 5d            \"    0\"         \"   31\"
%-5d            \"0    \"         \"31   \"
%+5d            \"   +0\"         \"  +31\"
%05d            \"00000\"         \"00031\"
%x              \"0\"             \"1f\"
%X              \"0\"             \"1F\"
%05x            \"00000\"         \"0001f\"
%#05x           \"00000\"         \"0x01f\"

See `format' for details."
  (interactive
   (progn
     (barf-if-buffer-read-only)
     (unless (and transient-mark-mode mark-active)
       (signal 'mark-inactive nil))
     (let ((fmt (my-number-rectangle-read-format))
           (initial-number (read-number "Initial number: "
                                        my-number-rectangle-initial-number)))
       (list (region-beginning)
             (region-end)
             fmt
             initial-number
             (prefix-numeric-value current-prefix-arg)))))
  (require 'rect)
  (setq my-number-rectangle-number initial-number)
  (apply-on-rectangle 'my-number-rectangle-line start end fmt inc))

;; (global-set-key "\C-xrN" 'my-number-rectangle)

動作確認は GNU Emacs 22 。

wdired でファイル名に連番付ける時に、と思って書いてみたもののあまり使わないですね。
多分他にも似たようなコードは探せば見つかるでしょう。 車輪の再発明をすることで Elisp の経験値は上がっていきます。

余談ですが、GNU Emacs 22 以降の M-x replace-regex はかなり強力です。
行頭に連番挿入する程度ならば
M-x replace-regex RET ^ RET \,(format "%03d" (+ \# 1)) RET
等で十分でしょう。

2011-02-13

Mac OS X で Dired の日本語ファイル名の並び順がおかしい場合

Emacs の環境変数 LANG が UTF-8 の場合、 Dired の日本語ファイル名の並び順がおかしくなります。
これは Mac OS X の ls が LANG の値を見てランダムな並び順を出力するために起こります。
Emacs の設定ファイルに (setenv "LANG") や (setenv "LANG" "C") などと書けば正常な並び順になります。

普段は LANG=ja_JP.UTF-8 で、 Dired から ls を呼ぶ時だけ LANG=C にするには、以下のように設定します。

(setenv "LANG" "ja_JP.UTF-8")

(defadvice insert-directory
  (around setenv-LANG activate compile)
  "Set environment variable LANG to C."
  (let ((process-environment (copy-sequence process-environment)))
    (setenv "LANG" "C")
    ad-do-it))

動作確認は Mac OS X 10.6 と GNU Emacs 22 でしています。

2011-02-12

delete-selection-mode が効かない場合

delete-selection-mode は選択範囲がある時に文字を入力すれば、その範囲を消すというマイナーモード。
(delete-selection-mode 1)
と ~/.emacs.el や ~/.emacs.d/init.el に書けば有効になる。

ただし、文字列を入力する自作のコマンドを定義した場合、プロパティを追加しないと選択範囲を消してくれません。
プロパティについてはこちら プロパティリストを調べる describe-plist

例えば insert-lambda という文字列入力コマンドを定義した場合、このように設定します。
(defun insert-lambda ()
  (interactive "*")
  (insert "lambda"))

(put 'insert-lambda 'delete-selection t)

t 以外に 'yank, 'supersede, 'kill 等を設定できる。
詳しくはソースを参照。
M-x find-function RET delete-selection-mode RET

2011-02-11

Emacs Lisp モードで M-x find-* を使う

Emacs Lisp モードでシンボルが定義されているソースファイルを直接開くコマンドは以下の4つがあります。

M-x find-library
M-x find-variable
M-x find-face-definition
M-x find-function


M-x find-* とタイプするのは面倒だし、キーバインドを4つも定義したくない。
そこで、カーソル位置のシンボルから自動で上の4つを選択してくれたら、というのがここで紹介する方法。

変数、関数、フェイス等、複数の値を持つシンボルに関しては変数を優先するようになってます。
シンボルが値を持たない場合、 M-x find-tag を実行するので、下の例では M-. に割り当てています。

(defun my-find-tag ()
  "In Emacs Lisp mode, one of these functions is called:
`find-library', `find-variable', `find-face-definition',
`find-function' and `find-tag'."
  (interactive)
  (call-interactively
   (let ((symbol (variable-at-point t))
         (variable (variable-at-point))
         (function (function-called-at-point)))
     (cond
      ((or (eq function 'require)
           (eq function 'featurep))
       'find-library)
      ((and (symbolp variable)
            (boundp variable))
       'find-variable)
      ((and (symbolp symbol)
            (facep symbol))
       'find-face-definition)
      ((and (fboundp function)
            (eq symbol function))
       'find-function)
      (t
       'find-tag)))))

(eval-after-load "lisp-mode"
  '(define-key lisp-mode-shared-map "\M-." 'my-find-tag)) ; find-tag

2011-02-10

Emacs でバイナリ形式の .plist を編集する

Mac OS X の .plist はテキスト形式とバイナリ形式があります。
Xcode 付属の Property List Editor で編集できますが、できれば Emacs を使いたい。

元ネタ
Edit and save binary plist files
http://hints.macworld.com/article.php?story=2005061422012079

auto-compression-mode で対処する方法が紹介されてますが、これだとバイナリ形式ではない .plist でもバイナリ形式で保存してしまったりと不具合があります。
その記事のコメントに別の方法が投稿されていますが、それでもまだ若干問題があります。

  • バイナリ形式ではない .plist を開くと xml-mode にならない
  • 書き込み権限のないバイナリ形式の .plist を開くことができない
  • 日本語等が文字化けする (Emacs22)

その問題点を改良したものがこちら。

;; Edit and save binary plist files
;; http://hints.macworld.com/article.php?story=2005061422012079
(add-to-list 'auto-mode-alist '("\\.plist\\'" . visit-bplist))
(add-to-list 'auto-mode-alist '("\\.nib\\'" . visit-bplist))

(add-to-list 'magic-mode-alist '("\\`bplist" . visit-bplist))
(add-to-list 'auto-coding-regexp-alist '("\\`bplist" . utf-8))

(defvar plist-converted-binary nil
  "Buffer local variable indicating if file came from binary-plist.")
(make-variable-buffer-local 'plist-converted-binary)

(defun visit-bplist ()
  (let (bplist)
    (when (string-match "\\`bplist" (buffer-string))
      (setq bplist t)
      (save-excursion
        (let (buffer-read-only)
          (message "Reading in binary plist")
          (erase-buffer)
          (let ((process-coding-system-alist '(("plutil" . utf-8))))
            (call-process "plutil" nil t nil
                          "-convert" "xml1" "-o" "-" (buffer-file-name))))))
    (xml-mode)
    (when bplist
      (set-buffer-modified-p nil)
      (setq buffer-undo-list nil)
      (setq plist-converted-binary t))))

(defadvice save-buffer (after convert-plist (&optional args))
  (when plist-converted-binary
    (shell-command
     (format "/usr/bin/plutil -convert binary1 %s"
             (shell-quote-argument (buffer-file-name))) nil nil)
    (message "Wrote bplist %s" (buffer-file-name))))
(ad-activate 'save-buffer)

動作確認は Mac OS X 10.6 と GNU Emacs 22 でしています。

2011-02-09

プロパティリストを調べる describe-plist

Emacs をもう少し詳しく理解するための関数。
Elispの基本的なことですが、シンボルは4つの値を持つことができます。

・名前
・変数
・関数
・プロパティリスト

詳しくは info を参照。
M-: (info "(elisp)Symbol Components")
変数は M-x describe-variable (C-h v) 、関数は M-x describe-function (C-h f) で調べることができますが、最後のプロパティリストについては便利なコマンドは用意されていません。
そこで、 apropos.el に apropos-describe-plist という関数があるので、それを呼び出すコマンドを作ります。

(defun describe-plist (symbol)
  "Display SYMBOL's property list."
  (interactive
   (let ((sym (variable-at-point t))
         (enable-recursive-minibuffers t)
         str)
     (when (or (not (symbolp sym))
               (not (symbol-plist sym)))
       (setq sym nil))
     (setq str (completing-read
                (if sym
                    (format
                     "Describe property list (default %s): " sym)
                  "Describe property list: ")
                obarray
                'symbol-plist
                t nil nil
                (when sym (symbol-name sym))))
     (list (if (equal str "")
               sym (intern str)))))
  (unless (symbol-plist symbol)
    (error "Symbol %s has no property list" symbol))
  (require 'apropos)
  (apropos-describe-plist symbol))

(define-key help-map "P" 'describe-plist)

キーバインドは一例です。上の例だと C-h P に割り当てています。
挙動は M-x describe-variable 等と同様に、シンボルがプロパティリストを持たない場合はミニバッファでの補完はききません。

試しに M-x describe-plist RET mark-inactive RET とすると、このように表示されます。

Symbol mark-inactive's plist is
 (error-conditions (mark-inactive error)
  error-message "The mark is not active now")

[back]

動作確認は GNU Emacs 22 でしています。

Dired で選択範囲にマークを付ける

リージョン指定してマークを付ける。
C-u 付きで選択範囲のマークを消す。

dired-mark-files-in-region という関数があるのでそれを利用した。
キーバインドは一例です。

(defun dired-mark-region (start end &optional arg)
  "Mark all files in region.
With prefix argument, unflag all those files."
  (interactive "r\nP")
  (let ((dired-marker-char (if arg ?\040 ?*))) ; \040 = SPC
    (dired-mark-files-in-region
     (save-excursion
       (goto-char start)
       (line-beginning-position))
     end)))

(eval-after-load "dired"
  '(define-key dired-mode-map "*r" 'dired-mark-region))