ダラダラと長いメモ

 何を考えて、居たかとかのメモ。
 最近見た他のエディタの機能に、使う言語に合わせて補完するというのがあった。オブジェクト指向な奴だと必然的に補完される部分がある程度決まったりしていて、そこを補完している感じなんだということが何となく分かったので、試しにlispの補完を考えてみた。lispはオブジェクトは使っているのを見たことないので、応用はしにくいけど、変わりに括弧は大量に打たなければならないので、ソレを何とかできないかなぁとか。
 結局考えて色々試してみたが、中々難しい。取り合えず閉じ括弧を補完するのが直ぐに浮かんだので色々試したので其の過程とか中身とか。
 (を打って閉じ括弧を自動入力するというのは前に見かけて使ってみたけど、実際は邪魔なときのが多い。特に一度書いたのを修正する時は勝手に)が入力されてしまうので(let ((hoge (point)))とかでhogeの前の(を修正したらいきなり)とかが出ちゃって(let (()(hoge (point)))とか分けの分からないことになって、果てしなく邪魔臭い。しかし適切な所に、)を出現させるというのを考えてみたけど、それって結局今書いている辺りの文字を全部読み込んで文脈を理解させないと駄目だし、かなり難しいなぁとか。
 少し考えて作ったのがキャレットの位置の前にある文字列を読み込んで、それに応じて)を入れ込むという奴。例えば、直前も文字が関数ならば)を必要とする奴と必要ない関数かを調べて、)を入れるか考えるというもの。

(defun lisp-paren-complete ()
  ;"ハッシュに補完対象となる文字列と関数名を入れてfuncall"
  (interactive"*")
  (let ((end (point))
		(start (save-excursion (pre-checker))))
	(if (= start end)
		(return-from lisp-paren-complete nil))
	(let ((pre (buffer-substring start end))
		  (htable (make-hash-table
				   :test (if *dabbrevs-case-fold* #'equalp #'equal))))
	  (setf (gethash "(if " htable) 'create-if)
	  (setf (gethash "(cond " htable) 'create-cond)
	  (setf (gethash "(defun " htable) 'create-defun)
	  (setf (gethash "(provide " htable) 'create-provide)
	  (setf (gethash "(dotimes " htable) 'create-dotimes)
	  (setf (gethash "(lambda " htable) 'create-lambda)
	  (setf (gethash "(in-package " htable) 'create-in-package)
	  (setf (gethash "(insert " htable) 'create-double-quotation)
	  (setf (gethash "(plain-error " htable) 'create-double-quotation)
	  (setf (gethash "(looking-back " htable) 'create-double-quotation-ex)
	  (setf (gethash "(looking-for " htable) 'create-double-quotation-ex)
	  (setf (gethash "(interactive " htable) 'create-double-quotation)
	  (setf (gethash "(message " htable) 'create-double-quotation)
	  (setf (gethash "(require " htable) 'create-double-quotation)
	  (setf (gethash "(when " htable) 'create-simple-paren)
	  (setf (gethash "(let " htable) 'create-simple-paren)
	  ;(setf (gethash "" htable) 'create-close)
	  ;この辺は引数を持たない関数を閉じるような仕様に変えた方が良さそう。
	  (if (gethash pre htable)
		  (funcall (gethash pre htable)))
	  )))

 始めはハッシュとか使わずに全部condで文字列を比較して書いていたけど、大量にやったら重たくなりそうなのでハッシュにしてみた。余り使い方が分かっていないので、酷い勘違いをしているかもしれないが。まあいい事にした。これ関数呼び出すときにハッシュを作っているけど、補完候補を大量に溜め込むグローバル変数*1にしておいた方が動きは速そう。というか、そういうのはリストを使うのかもしれないけどリストとかよく使い方が分からないので余り触りたくない。
 で、適当に良く使う関数の後ろに来て欲しい括弧とかを自分でイチイチ関数にして試してみたんだけど、便利なのは便利なんだけど、使ってみると予想以上に適切な候補を補完するのが難しい。殆どがゴミのような関数になってしまうし。手動なので、自分が使わない関数には機能が割り当てられないという。規則性がある関数は比較的便利なんじゃないかと思うのも作ったけど。

(defun create-provide()
  (insert "\"")
  (let* ((buf (buffer-name (selected-buffer)))
		 (type (concat "." (pathname-type buf)))
		 (name (string-right-trim type buf)))
	(insert name "\")"))
  (backward-char 2)
  (while (not (looking-back "\""))
	(selection-backward-char)))

 こんなんとか、provide打ったら、ほぼ現在のファイル名から拡張子抜いた分を入れると思うので、ソレが予め表示されるとか、そんな感じ。唯の補完よりはやや賢い感じに。何個か作ったけど殆どはゴミのような補完しかしないけど。この辺は単純なの(殆どキーマクロみたいなので)良さそうなので自分で勝手に変えると良さそう。
 あと、)を閉じるだけの場合は手動で登録とか面倒すぎてありえないなぁと思ったので、少し考えて関数の引数を調べて引数がない関数なら殆ど単純に括弧を閉じてもいい事に気づいたのでそれを試すことに、ldocの機能でステータスバーに確か表示していたからそれをパクって何とかできないかと思って調べたら、それらしきものは出来た。しかし、使ってみると意外と使えない。ldoc-func-get-argsはそのままldocの中に入っていたので、ldoc入れてる人ならed::ldoc-func-get-args見たいにしたらいいかも。

(defun paren-closer()
  "直前のシンボルに引数がなければ括弧を閉じる"
  (interactive)
  (let ((symbol (find-symbol (save-excursion (pre-word)))))
	(when (fboundp symbol)
	  (unless (ldoc-func-get-args symbol)
		(insert ")")
		(sleep-for .1)
		(quit))
	  )))

 どの変に問題があるかと言うと。例えばpointを補完するとすと、poi暗いまで打って補完すればpoint)とか成ったらいい感じと、思ったわけだけど。poiでpoint)になってしまうと、point-maxとかしたいときには逆に邪魔だったりして。同じ文字列で複数候補があるときは微妙に使えないという。(eolp)とか被らないようなのだと。(eol位まで打って補完すれば、(eolp)とか成ってくれるけど2文字くらいだとありがたみがないというか、微妙な感じ。まあ、役に立つ場合もあるけど。
 で、まあ、これを前作っていた適当補完機能を作り変えて、色々くっ付けて見たら、それらしきもは出来上がった。(依然として問題だらけだけど。)補完のほうは、)の奴を考えていて気づいたのを試したら之が結構便利だった。)の閉じるタイミングがあるように(を打つタイミングもあるなぁと、まず、前が関数じゃないと(がでてこない。変数だと付いても'とかだし。*2で、(を打つと次が関数なら(を打ったらlispのシンボルを補完とかするようにしてみた。

(defun paren-starter()
  (interactive "*")
  (insert "(")
  (refresh-screen)
  (let ((key (read-char *keyboard*)))
	(unless
		(and (eq (lookup-key-command key) 'self-insert-command)
			 (or (syntax-word-p key)
				 (syntax-symbol-p key)))
	  (unread-char key)
	  (return-from paren-starter))
	(insert key))
  (my-lisp-indent-complete-symbol))
;キー割り当ては(の予定

補完はインクリメンタルに。って前に書いた奴を適当に改造。してみたけど、ここの出来が良くないので色々不具合がある。

;(インクリメンタルに補完)
(defun my-lisp-indent-complete-symbol()
  (interactive "*")
  (lisp-indent-line)
  (if (looking-at "\\_>")
	  (unless (expand-abbrev)
		(let ((key nil) re)
		  (loop
			(setq re (lisp-complete-symbol))
			(refresh-screen)
			(cond ((or (eq :no-match re) (eq :no-completions re))
				   ;*do-completion completion-message辺りを参考に
				   (return))
				  ((eq :solo-match re)
				   (paren-closer)
				   (return))
				  ((or (eq t re)(eq :not-unique re))
				   (setq key (read-char ed:*keyboard*))
				   (unless
					   (and (eq (lookup-key-command key) 'self-insert-command)
							;入力されたキーがコマンドでないか確かめる↑
							(or (syntax-word-p key)
								(syntax-symbol-p key)))
					 (unread-char key);上の条件に合わなければ、keyを削除して
					 (return))
				   (insert key)
				   (lisp-complete-symbol))
				  (t
				   (return))
				  ))
		  ))))

書いていたら長くなったのでファイルにして関数が無駄に多いのでpackageとかにしようかと思ったときにldoc-func-get-argsが人の奴だったと思い出したので辞めて、ダラダラとメモしておくことにした。

*1:lispの変数の型は呼び方が何と呼んでいいのかよく分かって居ないことに気づいた

*2:これ何か上手くできないかなぁと今思った