Emacsで指定範囲の色を変える

この記事は ウェブクルー Advent Calendar 2017の4日目の記事です。
昨日は@wc_moriyamaさんの「わが社のSMS活用事例と5分でできる配信トライアル – Qiita」でした。


いま作っているEmacsプラグインで必要になった機能が、わりと実装の面倒くさいものだったのでメモ。
なおEmacs Lispは初心者なので、変なことやっているかもしれない(可能性大)。
Emacs 25.3以降で動くことは確認した。

やりたいこと

こうしたい。テキストは青空文庫から。

やりたいこと

指定した矩形範囲の色を変えたい。
ポイントは以下の2点。

  1. 色を変える範囲をウィンドウ上の行番号・列番号で指定したい(複数行にわたっても)
  2. 指定範囲に文字(空白も含む)がなくても色を変えたい

なお等幅フォントを使っていない場合はきれいに矩形状に色が変わらないが、考慮しない。
(でも等幅でも変になることがあるようだ?)

基本方針: オーバーレイ

今回のように色を変えるには、Emacsではオーバーレイというのを使うようだ。これを駆使して実現してみる。
基本的な使い方は以下。

;; make-overlayでオーバーレイの範囲を指定(後で操作するため、 ovr という変数に入れておく)
(setq ovr (make-overlay 2 12))

;; オーバーレイで背景色を指定
(overlay-put ovr 'face '(t :background "red"))

;; オーバーレイで、指定範囲の後に文字列を追加
(overlay-put ovr 'after-string "あいうえお")

;; オーバーレイを削除
(delete-overlay ovr)

ここではオーバーレイの属性として faceafter-string を使った。他にもいろいろある

オーバーレイの基本的な使い方

難しいこと

make-overlay は範囲をポイントで指定する。つまりそのバッファでの何文字目から何文字目という形で指定する。

今回やりたいのはウィンドウ上での行番号・列番号で範囲を指定すること。なので行番号・列番号をポイントに変換する処理が必要。

悩んだのは、文字のないところにどうやってオーバーレイをかけるかということ。文字のないところにポイントはないので、これはEmacsの機能として不可能なのでは…と思った。

実現しているelisp

調べたところ、文字のないところの色を変えているelispをいくつか見つけた。なので工夫すればできるよう。

  • vline.el
    現在カーソルがある列をハイライトする。主にこれを参考にした。
  • Emacsの矩形選択
    C-x SPC (rectangle-mark-mode)でできるもの。 rect.el に実装がある。
    Emacs 24.4から標準搭載らしい。
  • popup.el
    コンテキストメニュー的なものやツールチップを表示できる。

文字のないところにオーバーレイをかける方法

文字のあるところには普通に face で背景色を指定する。
文字のないところには after-string で空白を追加すればいい。

以下では example-put-ovrs が現在行の列番号 bgn-column から end-column の色を変える。
example-put-ovrs-rectangle はこれを何度も使って、矩形状に色を変える。

コードと実行画像

(defvar example-ovrs-list nil)

(defun example-make-add-string (line-end-column bgn-column end-column face)
  "`line-end-column'の後に追加する文字列を返す。
色を変える範囲は`bgn-column'と`end-column'で指定。色は`face'で指定する。"
  (let* ((transparent-blanks
          (make-string
           (max
            0
            (- bgn-column line-end-column))
           ? ))
         (colored-blanks
          (propertize
           (make-string
            (max
             0
             (- end-column (max bgn-column line-end-column)))
            ? ) 'face face)))
    (concat transparent-blanks colored-blanks)))

(defun example-make-ovrs (line-end-column bgn-column end-column)
  "現在行にかける2つのオーバーレイをコンスセルの形で返す。
文字があるところにかけるオーバーレイと、文字がないところに加えるオーバーレイの2つ。"
  (let* ((before-p (< line-end-column bgn-column))
         (after-p (> line-end-column end-column))
         (line-end-point (save-excursion
                           (example-move-to-column line-end-column) (point)))
         (bgn-point (save-excursion
                      (example-move-to-column bgn-column) (point)))
         (end-point (save-excursion
                      (example-move-to-column end-column) (point)))
         (text-ovr (cond (before-p
                          (make-overlay line-end-point line-end-point))
                         (after-p
                          (make-overlay bgn-point end-point))
                         (t
                          (make-overlay bgn-point line-end-point))))
         (add-string-ovr (make-overlay line-end-point line-end-point)))
    `(,text-ovr . ,add-string-ovr)))

(defun example-move-to-column (column)
  "現在行の列番号`column'に移動する。"
  (vertical-motion `(,column . 0)))

(defun example-get-line-end-column ()
  "現在行(物理行)の末尾の列番号を返す。"
  (save-excursion
    (vertical-motion `(,(1- (window-body-width)) . 0))
    (% (current-column) (window-body-width))))

(defun example-put-ovrs (bgn-column end-column face)
  "現在行の、列番号で指定した範囲`bgn-column'から`end-column'の色を`face'で指定した色にする。"
  (let* ((line-end-column (example-get-line-end-column))
         (ovrs (example-make-ovrs line-end-column bgn-column end-column))
         (text-ovr (car ovrs))
         (add-string-ovr (cdr ovrs))
         (add-string (example-make-add-string
                      line-end-column bgn-column end-column face)))
    (overlay-put text-ovr 'face face)
    (overlay-put add-string-ovr 'after-string add-string)
    (push text-ovr example-ovrs-list)
    (push add-string-ovr example-ovrs-list)))

(defun example-put-ovrs-rectangle (left-top right-bottom face)
  "矩形状にオーバーレイをかける。
矩形は左上の点`left-top'と右下の点`right-bottom'で指定する。"
  (let* ((bgn-line (car left-top))
         (end-line (car right-bottom))
         (lines (number-sequence bgn-line end-line))
         (bgn-column (cdr left-top))
         (end-column (cdr right-bottom)))
    (save-excursion
      (dolist (line lines)
        (move-to-window-line line)
        (example-put-ovrs bgn-column end-column face)))))

(defun example-clear-ovrs ()
  "オーバーレイを全消去。"
  (dotimes (x (length example-ovrs-list))
    (delete-overlay (pop example-ovrs-list))))

;; 使用例
"
列番号の目盛:
0         1         2         3         4         5         6         7         8         9         0         1         2         3
012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901
"
(example-put-ovrs 10 30 '(t :background "red"))
(example-put-ovrs 100 120 '(t :background "blue"))
(example-put-ovrs 10 100 '(t :background "white"))
(example-clear-ovrs)


(example-put-ovrs-rectangle '(0 . 0) '(10 . 40) '(t :background "red"))
(example-put-ovrs-rectangle '(11 . 80) '(20 . 100) '(t :background "yellow"))
(example-put-ovrs-rectangle '(21 . 20) '(30 . 90) '(t :background "green"))
(example-clear-ovrs)

文字のないところにオーバーレイをかける方法

example-make-add-string

追加する空白文字列を作っている。
この関数は行末 line-end-columnbgn-columnend-column の関係で返却値を変える。

  • 行末が bgn-column より前にあるとき
    行末から bgn-column までの色なし空白 + bgn-column から end-column までの色付き空白
  • 行末が bgn-columnend-column の間にあるとき
    行末から end-column までの色付き空白
  • 行末が end-column より後にあるとき
    空文字

example-make-ovrs

オーバーレイを2つ、コンスセルにして返す。 (text-ovr . add-string-ovr) の形。
text-ovr は文字のあるところにかかり、色を変えるためのオーバーレイ。
add-string-ovr は文字のないところに文字を追加するためのオーバーレイ。
text-ovr は行末 line-end-columnbgn-columnend-column の関係で範囲が変わる。
add-string-ovr は常に行末。

text-ovr の範囲は以下のようになる。

  • 行末が bgn-column より前にあるとき
    行末が指定される。ここに文字はないので色は変わらない。
  • 行末が bgn-columnend-column の間にあるとき
    bgn-column から line-end-column までが指定される。
  • 行末が end-column より後にあるとき
    bgn-column から end-column が指定される。

これから

  • 設定(?)によってうまくいかないことがある気がするので、できるだけいつでも使えるようにする
  • これだと同じ行に複数のオーバーレイを追加すると崩壊するので何か考える
  • 本題のプラグインを早く…

明日は@asukameさんです。よろしくお願いします!

Emacs/Vimで、ただの削除をする方法

Vimでは "_ をつけてから削除系のコマンドを打つと、削除したものがクリップボード的なものに入らない。つまり「切り取り」ではなく完全な「削除」になる。とても便利。
だがEmacsでこれをやる方法がわからず、地味に困っていたので調べた。ついでにVimについても書く。

何がしたいのか

単語 very_long_long_long_word をコピーし、別の単語 another_long_long_long_word をこれで置き換えたいとする。(置換コマンドを打てというのはなしで。)
another_long_long_long_wordを普通に削除(Vimでdiw、EmacsでM-dみたいに)して、very_long_long_long_wordを貼り付けようとしても、クリップボード的なものにはたったいま削除したanother_long_long_long_wordが入ってしまっているので、貼り付けられない。「削除」と言いつつ、知る限りのコマンドはすべて「切り取り」でしかないから。
another_long_long_long_wordを削除するとき、クリップボード的なものにこれを入れないようにしたい。

Vim

レジスタ

普通 ddci" など削除系コマンドを打つと、削除した内容がクリップボード的なもの(レジスタ)に入っていく。レジスタの指定をするときはダブルクオーテーションを前につける。
レジスタは0+などいろいろあり、たとえば3のレジスタに入っている内容を貼り付けたいなら"3pのように、ダブルクオーテーション + レジスタのあとに貼り付けコマンドを打てばいい。逆に"3ddのように削除内容を入れるレジスタを指定することもできる。これは削除系(dとかcとか)だけでなくヤンクyでも同じ。
レジスタの内容は:di:regで確認できる。

_レジスタを使うといい

特殊なレジスタとして_がある。Vimで:h "_とコマンドを打てばこのレジスタのヘルプが見れる。そこには”Black hole register”と書いてある。このレジスタを指定して削除系のコマンドを打てば、レジスタには残らない。ただの削除が行えるということだ。"_ddとすれば行の削除、"_di{とすれば{}内の削除が、レジスタを汚さずにできる。

これを使えば、上のような例の場合、another_long_long_long_wordを"_diwで消せばレジスタに残らないので、pでvery_long_long_long_wordが貼り付けられる。

まあdiwで削除しても"0pとすればvery_long_long_long_wordが貼り付けられるが。

Emacs

キルリング

Emacsでのクリップボード的なものはキルリングという。C-yのあとにM-yを何度も押すと、キルリングの内容が新しいものから次々に出てきて、過去にコピー・削除したものを貼り付けられる。(M-x helm-show-kill-ringを使えばもっと便利。helm入れてたら使えるはず。)

リージョン指定してBackspaceすればいい

これはEmacs: how to delete text without kill ring? – Stack Overflowに書いてある。Vimのように、どの削除コマンドにも応用できる方法はないようだ。
削除したいリージョンを選択してからDeleteキーなりBackspaceキーを押せば、キルリングを汚さずに、ただの削除ができる。
これは変数delete-active-regiontになっているとき(デフォルト)、BackspaceやDeleteがそのリージョン全体に対してBackspaceやDeleteの働きをするためのようだ(Backspaceなどはキルリングを汚さず、ただの削除をするので)。リージョン選択後にM-x delete-regionでもいける。

参考

Emacs/Vimで、現在編集中のファイルフルパスをクリップボードにコピーする

FirefoxにVimperator入れて使ってるが、yだけで現在開いているサイトのURLをコピーできるのが便利。
(copy.js使うともっと便利。)
同じことをエディタでもできればいいと思ったので設定書いてみた。
Emacs LispもVim scriptも初心者なのでいろいろ調べた。

環境

特にバージョンとかは関係なく動くと思うが一応。
+ OS: ArchLinux
+ Emacs: 25.1.1
+ Vim: 8.0

Emacs

(define-key global-map (kbd "C-z y")
  '(lambda ()
     (interactive)
     (let ((fullpath (buffer-file-name)))
       (message "copied fullpath: %s" fullpath)
       (kill-new fullpath))))

C-z y で、現在開いているファイルのフルパスをコピーする。エコー領域にも表示される。

メモ

  • エコー領域
    これは「ミニバッファ」と言うと思ってたが、
    入力を待っているときはミニバッファと言い、
    出力だけのときはエコー領域と言うらしい。
    EmacsWiki: Echo Areaによれば

    Short messages to the user appear commonly in the echo area. This area is shared with the minibuffer. The space occupied by the minibuffer for input is thus also used by the echo area for output. There are two names for this space because the behavior is different.

  • buffer-file-name
    関数。ファイルのフルパスを返す。

  • default-directory
    変数。ディレクトリのパスを返す。
    今回は使わなかったが。

  • kill-new
    関数。文字列をkill-ringに追加。

  • なぜ(interactive)をつけるのか
    これはこういうものらしい。
    define-keyとかglobal-set-keyでキーバインドを設定するときは
    インタラクティブコマンドにしないといけないそう。
    (interactive)をつけない場合、”Wrong type argument: commandp”という
    エラーメッセージが出る。

    M-x describe-function RET global-set-keyには以下のように書いてある。

    COMMAND is the command definition to use; usually it is
    a symbol naming an interactively-callable function.

参考

Vim

nnoremap <Space><C-g> :<C-u>echo "copied fullpath: " . expand('%:p') \| let @+=expand('%:p')<CR>

Space + C-g で、現在開いているファイルのフルパスをコピーする。エコー領域にも表示される。
Emacsで言うエコー領域を、Vimでは何と言うのかわからないが…。

メモ

  • expand(‘%:p’)
    これでフルパスを取得。
  • let @+=
    これで+レジスタ(クリップボード)に、=の右側を入れる。
  • .
    Vim scriptでの文字列連結。
  • <C-u>
    bashでCtrl-uすればカーソルの左を全削除するが、あれと同じ。Vimでもコマンド打つときはそうなる。
    ノーマルモードで数字打ったあとにコロン押してコマンドを打つと、
    はじめに範囲指定が入って、余計なことをする。
    <C-u>をやればその数字が消える。
  • 1 C-g
    おまけ。これはデフォルトで使えるコマンド。
    このコマンドを打てばファイルフルパスが表示される。
    2 C-g ならバッファ番号まで表示される。
    C-g だけなら相対パス。

参考