read-only と sticky

たとえば、

From: 
Mail: 
----------

という入力欄を作りたいとして "From: " とかは書きかえられたくないので、 text-property の read-only 属性を使って
;; (get-buffer-create "hoge") しておいてくださいね。

(with-current-buffer "hoge"
  (let ((inhibit-read-only t))
    (erase-buffer))
  (insert (propertize "From: " 'read-only t)
	  "\n"
	  (propertize "Mail: " 'read-only t)
	  "\n"
	  (propertize "---------" 'read-only t)
	  "\n"))

とします。 read-only 属性が t であるような文字列は変更することができません。
ところが、実際にはこうすると "From: " までいれたところで "Text is read-only" と怒られてしまいます。これは「read-only である文字列の隣に、 read-only を継承するような文字列をいれることはできない」ためです。
挿入された文字列は基本的に、前方の文字列の text-property を「継承」します。ここでは "From: " の後ろに追加される文字列("\n")が "From: " の text-property を継承し read-only 属性がつくために、"\n" を追加しようとしたところでエラーが発生します。

じゃあ、どうするのか。
property には sticky というものがあり、これが新しく追加された text に property を付けるかどうか制御しています。 先の例では `rear-nonsticky' が nil (default) なので、 "\n" が read-only を継承してしまいました。 これを t にしましょう。

(with-current-buffer "hoge"
  (let ((inhibit-read-only t))
    (erase-buffer))
  (insert (propertize "From: " 'read-only t 'rear-nonsticky t)
	  "\n"
	  (propertize "Mail: " 'read-only t 'rear-nonsticky t)
	  "\n"
	  (propertize "---------" 'read-only t 'rear-nonsticky t)
	  "\n"))

すると、今度は元の文字列を消去することはできないものも、"F" と "r" の間にいろいろ入力できてしまいますね。 `front-sticky' 属性を t にして前方に挿入される文字列に対して read-only が継承されるようにしてやりましょう。

(with-current-buffer "hoge"
  (let ((inhibit-read-only t))
    (erase-buffer))
  (insert (propertize "From: " 'read-only t 'rear-nonsticky t 'front-sticky t)
	  "\n"
	  (propertize "Mail: " 'read-only t 'rear-nonsticky t 'front-sticky t)
	  "\n"
	  (propertize "---------" 'read-only t 'rear-nonsticky t 'front-sticky t)
	  "\n"))

これで望みのものができた!と思いきや "From: " の後ろで C-k すると "From: Mail: " となってしまい、しかも "RET" やら "C-j" やら押しても "Text is read-only" となってしまいます。
"\n" も read-only にしましょう。ただし、 "\n" の前には入力することができなければいけないので、今度は front-sticky はいりません。

(with-current-buffer "hoge"
  (let ((inhibit-read-only t))
    (erase-buffer))
  (insert (propertize "From: " 'read-only t 'rear-nonsticky t 'front-sticky t)
	  (propertize "\n" 'read-only t 'rear-nonsticky t)
	  (propertize "Mail: " 'read-only t 'rear-nonsticky t 'front-sticky t)
	  (propertize "\n" 'read-only t 'rear-nonsticky t)
	  (propertize "---------\n" 'read-only t 'rear-nonsticky t 'front-sticky t)))

今度こそ完成です。

;; ちなみに inhibit-read-only を t にしておくと、 read-only 属性が無視されるのですよ。

参考

  • text-property について: (info "(elisp) Text Properties")
  • stcicky について: (info "(elisp) Sticky Properties")
  • read-only などの特殊な text-property について: (info "(elisp) Special Properties")
  • inhibit-read-only (describe-variable 'inhibit-read-only)