`(Hello ,world)

ツッコミ、添削大歓迎です。いろいろ教えてください。

2010-07-28

オートインデントはどうやってるのか

テキストエディタでLispの自動インデントをどうやっているのか知りたい。あわよくばJavaScriptに持っていってブラウザのフォームに組み込んで、BiwaSchemeで使えるようにしたい。

xyzzyのソースを追ってみる。

リターンキーにバインドされた関数を調べる

M-x describe-bindings

...

TAB lisp-indent-line

RET lisp-newline-and-indent

lisp-newline-and-indent

lispmode.l:262

(defun lisp-newline-and-indent (&optional (arg 1))
  (interactive "*p")
  (delete-trailing-spaces)
  (insert #\LFD arg)
  (lisp-indent-line))
  • lisp-indent-lineがインデントする関数
lisp-indent-line

lispmode.l:229

(defun lisp-indent-line ()
  (interactive "*")
  (if (or (not (interactive-p))
          *lisp-tab-always-indent*
          (save-excursion
            (skip-chars-backward " \t")
            (bolp)))
        (smart-indentation (save-excursion
                             (goto-bol)
                             (if (protect-match-data
                                   (looking-at "[ \t]*;;;"))
                                 0
                               (max 0 (calc-lisp-indent (point))))))
    (insert "\t"))
  t)
  • TABのバインディングもこの関数
  • タブを押したときに、行の頭のほうだったらsmart-indentaion、そうじゃなかったらタブ挿入
  • インデントの位置は(calc-lisp-indent (point))で計算
  • goto-bol 行頭に移動します
    • bol = begining of line?
  • save-excursion 処理の前後でカレントバッファとポイントを保存します。
calc-lisp-indent

lispmode.l:159

(defun calc-lisp-indent (opoint)
  (protect-match-data
    (let ((begin-paren (and lisp-indent-close-paren
                            (looking-at "[ \t]*)"))))
      (goto-bol)
      (when (and (looking-at "\\s(")
                 (forward-char -1))
        (skip-white-backward)
        (forward-char 1))
      (or (up-list -1 t)
          (return-from calc-lisp-indent 0))
      (cond (begin-paren
             (+ (current-column) lisp-paren-imaginary-offset))
            ((or (looking-back "#")
                 (and (not (looking-back "#'"))
                      (looking-back "'")))
             (+ (current-column) 1))
            (t
             (let ((package (or (and (stringp *buffer-package*)
                                     (find-package *buffer-package*))
                                *package*)))
               (when (save-excursion
                       (when (and (up-list -1 t)
                                  (looking-for "((")
                                  (up-list -1 t))
                         (forward-char 1)
                         (multiple-value-bind (symbol found)
                             (calc-lisp-indent-current-symbol package)
                           (and found (get symbol 'lisp-indent-flet)))))
                 (return-from calc-lisp-indent (+ (current-column) *lisp-body-indention*)))
               (let ((column (progn
                               (forward-char 1)
                               (current-column))))
                 (multiple-value-bind (symbol found pkg-marker-p)
                     (calc-lisp-indent-current-symbol package)
                   (when pkg-marker-p
                     (return-from calc-lisp-indent column))
                   (let ((method (when found
                                   (or (let ((method (get symbol 'lisp-indent-handler)))
                                         (and method
                                              (save-excursion
                                                (and (up-list -1 t)
                                                     (forward-list -1 t)
                                                     (up-list -1 t)
                                                     (progn
                                                       (forward-char 1)
                                                       (multiple-value-bind (symbol found)
                                                           (calc-lisp-indent-current-symbol package)
                                                         (eq symbol 'handler-case)))))
                                              method))
                                       (get symbol 'lisp-indent-hook)))))
                     (cond ((numberp method)
                            (let ((count -1))
                              (while (< (point) opoint)
                                (skip-white-forward)
                                (setq count (+ count 1))
                                (or (forward-sexp 1 t)
                                    (return)))
                              (+ column -1 (if (< count method)
                                               (* *lisp-body-indent* 2)
                                             *lisp-body-indent*))))
                           (method
                            (+ column -1 *lisp-body-indention*))
                           (t (skip-chars-forward " \t")
                              (if (or (eolp) (looking-for ";"))
                                  (if *lisp-indent-offset*
                                      (+ column *lisp-indent-offset*)
                                    column)
                                (current-column)))))))))))))
  • うへ~!読めない!計算してるのはここなんだろうけど
  • looking-at: 現在のカーソル位置で前方向に正規表現でマッチしたらt、しなかったらnilを返します。
  • looking-for: 現在のカーソル位置から前方向にマッチしたらt、しなかったらnilを返します。
    • こっちは正規表現じゃなくて、現在の場所のテキストがマッチするかどうか
  • up-list: カーソルを ARG 個外側の括弧の後ろに移します。
    • こいつはbuiltin関数...
  • lisp-indent-hook シンボルにプロパティを設定することで lisp-indent-line でのインデント量を制御します。
    • 説明に書いてあることが、calc-lisp-indent関数の後半でやってること
    • シンボルに'lisp-indent-hookがsetされてると、それでhookされる
  • forward-sexp lisp-modeでS式を1つ進めます。[ESC C-f]
  • forward-list 前方のリストの終端へ移動します。[ESC C-n]
  • (defvar *lisp-body-indention* 2) ; lispmode.l:25
  • 最初の(up-list -1 t)でインデントしたい位置の1つ上に上がって、begin-parenじゃなくてコメント行じゃなかったら calc-lisp-indent-current-symbol で、そのポイントのシンボルを得る
    • シンボルに数値が設定されていたら(if=2, when=1, など)その数だけ(* *lisp-body-indent* 2)インデント
(if (predicate)  ; <- (get 'if 'lisp-indent-hook) = 2 なので
    ここ         ; 2個目の引数までは (* *lisp-body-indent* 2)
  ここ           ; それ以降は *lisp-body-indent*
    • 最初の引数が同じ行に書いてあったら、その位置
(hoge fuga  ; <- 最初の引数 fuga があるので
      ここ) ; <- そこと同じ位置
    • そうでなければ開始括弧の位置+*lisp-indent-offset*
(hoge  ; <- 最初の引数がないので
 ここ) ; <- 括弧の次の位置
calc-lisp-indent-current-symbol
(defun calc-lisp-indent-current-symbol (package)
  (skip-syntax-spec-forward " ")
  (if (looking-for ":")
      (values nil nil t)
    (multiple-value-bind (symbol found maybe-symbol)
        (lookup-symbol (point)
                       (progn
                         (skip-syntax-spec-forward "w_")
                         (point))
                       package)
      (values symbol (and found maybe-symbol)))))
  • キーワード(":" で始まってる)だったら (nil nil t)
  • skip-syntax-spec-forward: シンタックステーブルのカテゴリ基づいて文字を前方方向にスキップします。
    • "w_" ワードの区切りに移動・英字
  • lookup-symbol: ビルトイン関数
    • 1番目の引数のポイントから2番目の引数のポイントまでの文字を3番目のパッケージから探して、(シンボルか?、数値じゃないか?)の多値を返す?
store-match-data
(defmacro protect-match-data (&body body)
  `(let* ((#1=#:match-data (match-data (car *match-data-pool*)))
          (*match-data-pool* (cdr *match-data-pool*)))
     (unwind-protect
         (progn ,@body)
       (store-match-data #1#))))
  • match-data: scan-buffer で検索時点の状態を保持します。複数の検索をした後で、元の検索の結果で match-string / match-beginning / match-end / replace-match を行うことが可能です。
  • store-match-data: match-data で退避しておいた検索時点の状態を戻します。
結局
  • よくわからなかった。まあでもLispはS式なんで、ソースのオートインデントは他の言語のを行うより楽かもしれない。
  • Lispのソース読むの辛い
トラックバック - http://cadr.g.hatena.ne.jp/mokehehe/20100728