Open

Emacs-Lisp入門 2021

16

*scratch* バッファ

S式の評価

  • 直前のS式を評価 → C-j(結果は次行)
  • カーソルの直前のS式を評価 → C-x C-e / eval-last-sexp(結果はミニバッファ)
  • カーソルの位置か、そこ以前の行頭で始まるS式を評価 → M-C-x / eval-defun
  • バッファ全体を評価 → M-x eval-buffer
  • 選択範囲を評価 → M-x eval-region

各種定義の確認

  • 関数定義の確認 → (symbol-function '関数名)

  • interactiveじゃない関数をEval → M-: / eval-expression

  • 入力履歴は M-: からの M-p / M-n で辿れる

    • C-x M-: / C-x M-ESC で直前のコマンドを再実行 / repeat-complex-command
  • 入力シンボルの補完 → M-Tab (emacs-list-mode)

  • 関数(名)の検索 → M-x apropos / ドキュメントも含む apropos-documentation / C-h f 関数名

  • 関数のマニュアル表示 → M-x describe-function

  • 定義ジャンプ → M-x find-function

    • 定義ジャンプ(xref) → M-x xref-find-definitions / M-.
  • キーバインドされた関数名の確認 → M-x describe-key / C-h k

  • 関数にバインドされたキーの確認 → M-x where-is / C-h w

  • 現在のキーバインド全体の確認 → M-x describe-bindings / C-h b

新規バッファ → C-x b バッファ名

特定条件でループを抜ける → catch~throw

  • while (true) { if(cond) break; ..} のような動作
(catch 'found
  (while foo-list
    ... 何か処理
    (if (condition)
      (throw 'found t) ; found を t で返す
      ...)))
  • コンスセル
; 以下は等価
(+ 1 2)
(+ . (1 . (2 . nil))
  • おまけ → (zone)

メジャーモード

メジャーモードとファイル名の関連付け変数 → auto-mode-alist

メジャーモードの条件

  • モード名
    • (setq major-mode 'my-mode)(モード名)
    • (setq mode-name "My Mode")(モードラインに表示される名前)
  • キーマップ(ローカルマップ)の設定
    • モード用のローカルキーマップ変数の定義
      • (setq my-local-map (make-sparse-keymap))
        • make-sparse-keymap → 機能未割当の空のキーマップを生成する関数
    • キー割り当て
      • (define-key my-local-map "h" 'backword-char)
      • (define-key my-local-map "j" 'previous-line)
    • ローカルマップを有効にする
      • (use-local-map my-local-map
  • 必要な変数定義
    • TODO...

上記をまとめると以下がメジャーモードの最低限のひな型

(defun my-mode ()
  (interactive)
  (setq major-mode 'my-mode
        mode-name "My Mode")
  (setq my-local-map (make-sparse-keymap))
  (define-key my-local-map "h" 'backword-char)
  (define-key my-local-map "j" 'next-line)
  (define-key my-local-map "k" 'previous-char)
  (define-key my-local-map "l" 'forward-char)
  (use-local-map my-local-map))

関数まわり

基本

(defun 関数名 (引数)
  "説明文"
  定義...)

オプション引数 → (a &optional foo bar) / 省略時は nil
可変長引数   → (a &rest b) / (1 2 3) のとき a=1, b=(2 3)

インタラクティブ関数

  • C-nM-x info のようにキーボード操作で読み出せる関数
  • 関数定義に (interactive) が必須
    • 引数(省略可能)は対話的呼び出し時に引数をどう受け取るか、プロンプトの指定
    • sから開始すると文字列を受け取る、という変わった仕様
    • \n が引数の区切り
      • sHogeFuga\nnFooBar なら最初の引数は文字列、次が数値
(defun interactive-function (a b)
  (interactive "sInput string a:\nsInput string b:")
  (message (concat a b)))

ラムダ式

(lambda (args) 定義...)

fset を使うと

(defun plus1 (x) "add 1" (1+ x))
; 以下と等価
(fset 'plus1 '(lambda (x) "add 1" (1+ x)))

シャープクオート(#'

以下の回答が詳しい

https://ja.stackoverflow.com/a/29187

とりあえずは以下の通り

  • 関数シンボルをクオートしたいときは #' でクオートしておけばよい
    • クオートしているものが関数シンボルだと明示することもできるかな
  • (let ((...)) #'(lambda (arg))) のようなクロージャを作りたいときは#'

変数

  • 変数はシンボル('foo)として存在
    • fooのように quote なしの場合、参照先の値を取得する
      • foo → int* pointer(C言語でいうとポインタ変数)
      • 'foo → *pointer(C言語でいうとアドレスの指す先の値)

名前空間

  • Emacs-Lisp の名前空間は一つのみ!
    • C言語と同じ
      • Prefixを付けて変数名の衝突に気を付けること

動的スコープ

Emacs 27.1 からデフォルトで lexical-binding

実行時の設定が事前定義された変数の値を書き換える。

(setq x 5)
(defun subfunc ()
  (insert (format "x is [%d] in subfunc\n" x)))

(defun main ()
  (let ((x 3))
    (insert (format "x is [%d] in main\n" x))
    (subfunc)))

(main)
;-> x is [3] in main
;-> x is [3] in subfunc  (xの値が5から3に代わっている)

定義

  • グローバル変数 → (defvar foo 初期値 "説明文")
    • 初期値が代入されるのは未定義変数のときのみ

代入(束縛)

  • 確保したメモリ領域に値をセット(代入)するのではなく、シンボルに対して値を紐づけるので束縛。
(set 'foo 5)
; もしくは
(setq foo 5) ; 糖衣構文のようなもの(マクロ)

int *pointer;
*pointer = 5;

局所変数(let, let*

  • (let ...) の中だけで有効な変数
(let ((var1 "val") var2)
  (message var1 var2))

var1="val"var2=nil

  • let* だと束縛後からすぐ参照可能になる
(let* ((foo 3) (bar foo))
  bar)

foo=bar=3

編集関数

ポイント(カーソル位置)

バッファの先頭か? → (bobp)
バッファの末尾か? → (eobp)
行頭か? → (bolp)
行末か? → (eolp)
n文字進む/戻る → (forward-char n) / (backward-char n)
1行進める → (forward-line) / elisp なら next-line よりこちらの関数を使う
n行へ移動する → (goto-line n)
行頭/行末へ移動 → (beginning-of-line) / (end-of-line)
S式単位で進む/戻る → (forward-sexp) / (backward-sexp)
現在のポイント位置の取得 → (point)
ポイントの開始と末尾の位置 → (point-min) / (point-max)
選択範囲の取得 → (region-biginning) / (region-end)
指定位置へ移動 → (goto-char p) / (goto-char (point-end)) とかする

現在位置を保存して処理後に戻る → save-excursion

; ただしバッファ切り替えの場合は戻らない
(save-excursion
 (goto-char (point-min))
 (insert "foo"))

ウィンドウに表示されている領域でのn行名へ移動する → (move-to-window-line n)

  • バッファ全体ではなく現在表示されている画面で何行目か?ということ
  • (move-to-window-line 0) なら表示画面内の先頭行へ移動する。

行頭からn文字目へ移動 → (move-to-column n &optional 強制フラグ)

  • 強制フラグが non-nil のとき行末を超えたときに空白文字が追加されて指定桁またで移動する
    現在の桁位置 → (current-column)

Emacs の文字列操作では、C言語のファイルポインタの read/seek にあたるものが ポイント(カーソル)になる。

よって、以下のような思考フローとなる

  1. 編集したい位置へポイントを移動する(ポイント移動関数)
    • バッファ全体なら(point-min) などバッファ先頭へ移動する
  2. 編集したいものを探す
    • search-*/re-search-* などの検索系で編集したい位置のポイントを探す
  3. 任意のコードで編集処理を行う
    • この時に save-excursion などで編集前後の状態を保存したり戻したりする

カーソルを移動させるので、後処理をしっかししないと関数実行後に画面位置が知らない場所に移動したりするので注意。

Emacs-Lisp講座のサンプルプログラムを例に

(defun my-caclurator ()
  (interactive)
  (save-excursion ; カーソル位置の保存(後処理)
    (let (col (sum 0) n) ; 局所変数の準備
      (goto-char (point-min)) ; カーソル位置を先頭に戻す(バッファ全体が対象)
      (re-search-forward "^--BEGIN$") ; 処理したい位置まで移動する(ファイルポインタの seek にあたる)
      (while (re-search-forward "\\(^--END\\)\\|\\([0-9]+$\\)" nil t) ; 処理したい対象を正規表現で検索 
        (cond ; 見つかった行ごとに分岐して処理
         ((match-beginning 2) ; `[0-9]+$` の場合
          (setq n (match-string 2))
          (setq n (string-to-number n))
          (goto-char (match-beginning 2))
          (skip-chars-backward " \t")
          (delete-region
           (point)
           (progn (end-of-line) (point)))
          (move-to-column 70 t)
          (insert (format "%5d" n))
          (setq sum (+ sum n)))

         ((match-beginning 1) ; `^--END$` の場合
          (delete-region
           (point)
           (progn (end-of-line) (point)))
          (move-to-column 70 t)
          (insert (format "%5d" sum))))))))

マーカー

ポイント → バッファ内の絶対アドレス(整数)
マーカー → 任意の文字に付与する目印(マーカーオブジェクト)

(goto-char ..) などの移動系関数にはマーカーオブジェクトも引数に与えられる

違い

  • ポイント位置が指す文字はバッファ内容が変化すれば当然変わる
  • マーカーは特定の文字に付与した標識なので、バッファ内容が変化しても変わらない

マーカー利用手順

  1. マーカーを生成し、setq
  2. バッファの所定の位置を set-marker
  3. バッファの別の位置で任意の処理(多くはマーカーより手前)
  4. set-marker した位置で任意の処理
  5. マーカーに setq nil してクリア(しないとEmacsの動作が重くなる)

コマンドでマーカー

  • C-SPC / C-@ → set-mark-command

ナローイング

バッファ内の移動可能な領域を制限し、特定領域だけ編集できるようにする。
ナローイングすると (point-min), (point-max) の値も変化する。

ナローイング作成      → (narrow-to-region pos1 pos2)
ナローイング領域の一時保持 → (save-restriction 領域)
ナローイング解除(最終手段)→ (widen) / バッファ全体を処理するという命令なので普通は save-restriction を使うべき

サンプル

(defun narrowing-test ()
  (interactive)
  (goto-char (point-min))
  (save-restriction ; 処理内のみナローイングを保持
    (save-excursion
      (search-forward "--END")
      (goto-char (match-beginning 0))
      (narrow-to-region (point-min) (point)))
    (while (not (eobp))
      (insert "foo!")
      (forward-line 1))))

elisp のデバッグ

printデバッグ

messagesit-for でミニバッファに値を表示する

(progn
  (setq x 123)
  (message "x = [\%d]" x)
  (sit-for 3))

Edebug

基本的なフロー

  1. (setq debug-on-error t)
  2. エラーが発生したらスタックトレースを確認する
  3. エラー発生関数を M-x edebug-defun
  4. 再度実行し関数の動きをトレース
(defun fac (n)
  (if (< 0 n)
      (* n (fac (1- n)))
    1))
; ここで M-x edebug-defun する(fac 3) ; フリンジにデバッグ対象である目印が付く

  • 5文字で折り返す関数をデバッグする例
(defun fold-within5 ()
  (interactive)
  (save-excursion
    (goto-char (point-min))
    (while (not (eobp))
      (move-to-column 5)
      (newline 1)
      (next-line 1))))
; M-x edebug-defun

C-x b temp などでバッファを移動して M-x fold-within5 などすると Edebug が起動する。

aaaaaaaaaaaaaa
bbbbbbbbbbbbbb
; M-x fold-within5 と入力すると Edebug が起動

基本コマンドは以下

  • スペースキーでステップ実行
  • カーソルを移動して h でその箇所まで実行
  • e でミニバッファでS式を実行
  • q で終了

データ構造

連想リスト(alist)

`'(("key1" . "val1") ("key2" . "val2") ("key3" . "val3") ... )`
`'(("foo") ("bar") ("buzz") ... )` ; value がなくても alist

要素の追加 → (cons (list 'key 'val) alist)
連想リストを使った補完関数 → (completing-read プロンプト alist &optional 検査関数 要マッチ 初期入力値)

  • (completion-ignore-case t) で大文字小文字無視

  • 検査関数 → alistの単語の中から補完候補を絞りこむための関数

    • (lambda (input) string-match "^S" (car input)) → 先頭Sから始まるのだけに絞る
  • alist サンプル

(defun my-completing-buffer-word ()
  (save-excursion
    (goto-char (point-min)) ; ポイントをバッファ先頭へ
    (let (word word-alist)
      (while (re-search-forward "\\w+" nil t) ; 英単語を検索し続ける
        (setq word (match-string 0))
        (if (not (assoc word word-alist)) ; 連想リストを検索
            (setq word-alist (cons (list word) word-alist)))) ; 連想リストへ word を登録
      (completing-read "Word? " word-alist)))) ; 単語を補完する

プロパティリスト(plist)

key-value 形式を保持するは alist と同じだが、保存形式にコンスセルを使用しない。
key-value の順にリストにデータを保存する(なので必ず偶数個数のリスト)

; alist
'(("富士山" . 3776) ("雲取山" . 2017) ("高尾山" . 699))
; plist
'("富士山" 3776 "雲取山" 2017 "高尾山" 699)

alist より変更に弱いのでデータストア的な使用には不適。
plist は様々なオブジェクトに結び付けて付加的な情報を保持する使用方法を想定(普通は短いplistをまとめて処理する)

追加 → (setq foo-plist (plist-put foo-plist '塔ノ岳 1491))
取得 → (plist-get foo-plist '塔ノ岳)

キーワードシンボル

: から始まるシンボル → :foo, :bar など
keywordpt を返す
調べるときは keyword symbol でググる。

(keywordp :foo) ; -> t
(keywordp 'foo) ; -> nil

配列

固定長データタイプ

  • 新規作成
    • (make-vector length initial-value)
    • (vector &rest 初期値たち)
  • 指定した値で全フィル → (fillarray vec value)
  • 値の取得  → (aref vec 添字)
  • 値のセット → (aset vec 添字 val)

obarray(シンボルテーブル)

  • Emacsが内部で利用するシンボルを格納する素数長のVector
  • シンボル名(文字列)のハッシュ値からobarrayでの添え字を決定し値を格納する
    • intern時にシンボルが存在しないときは obarray に値を追加する
    • soft-intern 時は存在しなくても obarray を変更しない

バッファ

https://www.gnu.org/software/emacs/manual/html_node/emacs/Buffers.html#Buffers

バッファ → 編集作業領域
Emacsの編集作業 → バッファへの操作。
バッファオブジェクト → バッファを管理するEmacs内のデータ

基本操作

作成 → (generate-new-buffer name)
取得 → (get-buffer name) / (get-buffer-create name)
削除 → (kill-buffer name-or-obj)
バッファ名の取得 → (buffer-name &optional buf-obj)
ファイル名の取得 → (buffer-file-name buf-obj)
ファイル名からバッファの取得  → (get-file-buffer file-name)
バッファの切り替え

  • 編集対象だけ内部切り替え → (set-buffer name-or-obj)
  • 表示画面も含めて切り替え → (switch-to-buffer name-or-obj)

編集済みフラグ → (buffer-modified-p buf-obj)
編集済みフラグのセット → (set-buffer-modified-p フラグ)

バッファローカル変数

ローカル変数一覧 → (buffer-local-variables &optional target-buffer)
既存変数のローカル化  → (make-local-variable name) / 新規作成ではない
恒常ローカル変数の作成 → (make-variable-buffer-local) / 全バッファに作成されるバッファローカル変数

  • 恒常的バッファローカル変数の初期値 → set-default / setq-default

TIPS

一時編集用のバッファ名は先頭にスペースを挿入する → " *temp*" など

  • 補完対象から外れる

テキスト装飾

https://www.gnu.org/software/emacs/manual/html_node/emacs/Faces.html#Faces

確認方法

  • カーソル上のface確認 → (describe-face)
  • Emacsで定義されているface一覧 → (list-faces-display)
  • Emacs上の色一覧 → (list-colors-display)

装飾手順

  1. 色指定などの情報を保持するfaceを作成
  2. faceへスタイル情報(プロパティ)を設定
  3. faceを「指定した範囲」へ適用
; 1. face の作成
(make-face 'checksheet-face-hide)
; 2. faceへスタイルを設定
(set-face-foreground 'checksheet-face-hide "red")  ; 前景を赤
(set-face-background 'checksheet-face-hide "red") ; 背景を赤
; 3. 指定した範囲へ face 適用
(put-text-property 6 10 'face 'checksheet-face-hide)

テキストプロパティ

  • 文字装飾

    • 文字単位での範囲指定(後から変更不可)
      • バッファ内のテキストそのものに与えるplist
    • テキストデータのメタ情報を変更するのでバッファ内に修正フラグが立つ
  • 実は文字列(elisp内の"...")にも付与可能

    • テキストプロパティが反映された状態でバッファに挿入される

プロパティ付与 → (put-text-property 始点 終点 name value &optional bufobj-or-string)
プロパティ複数付与 → (add-text-property 始点 終点 plist &optional bufobj-or-string)
指定位置にプロパティ取得 → (text-properties-at point &optional bufobj-or-string)
プロパティ削除 → (remove-text-properties 始点 終点 plist &optional bufobj-or-string)

テキストプロパティは文字列にメタ情報を与えるということが本質。
バッファ内のプログラムテキストに(装飾とは無関係な)任意のメタ情報を付与して解析関数で利用するなどの使い方も可能。

オーバーレイプロパティ

  • 文字の上に被せるように装飾をほどこす
    • 装飾範囲は自由(後から変更可能)
  1. 特定の範囲を覆うoverlayの作成
    • (make-overlay 始点 終点 &optional buffer 視点移動許可 終点移動許可)
  2. overlay にプロパティを設定する
; 1行名を非表示にする
(save-excursion
  (goto-char (point-min))
  (let ((ov (make-overlay (point) (progn (end-of-line) (point))))) ; overlay の作成
    (overlay-put ov 'invisible t))) ; overlay にプロパティを設定

オーバーレイプロパティ

  • 不可視化 → '(invisible foo)
    • (add-to-invisibility-spec 'foo) / (remove-from-invisibility-spec 'foo) で操作可能
  • カーソル移動不可 → intangible
  • オーバーレイの前後に文字追加 → before-string / after-string
  • キーマップ設定 → keymap
  • ローカルキーマップ置き換え → local-map
  • 表示するものを指定 → display
  • オーバーレイ内テキスト編集時のフック設定 → modification-hooks
  • オーバーレイの始点・終点の文字列挿入時フック設定 → insert-in-front-hooks / insert-behind-hooks
  • フェイスの設定 → face

フェイス(face)

視覚的な属性をまとめて保持するオブジェクト

  • 新規作成 → (make-face シンボル)
  • コピー → (copy-face OLD NEW)

Emacsで使える色

(find-file (expand-file-name "rgb.txt" data-directory)) で表示されるテキストファイルの色名が使える。

ヘルプ

  • 標準ドキュメント → M-x info
  • へプルコマンド → C-h / <F1>
    • C-h C-h でどんなヘルプが用意されているかの一覧表示

キーバインド

キーバインドについて最初に一読するとよい記事

https://www.masteringemacs.org/article/mastering-key-bindings-emacs
  • キーバインドの設定自体は簡単
    • 難しいのは設定する キーマップ を正しく選択すること

モードマップ一覧を表示するコマンド。

C-u M-x apropos-variable RET -mode-map$ RET

モードフックの一覧

C-u M-x apropos-variable RET -mode-hook$ RET

python-modeになったとき、そのバッファで有効なキーバインドを設定

(defun my-add-python-keys ()
  (local-set-key (kbd "C-c q") 'shell))

(add-hook 'python-mode-hook 'my-add-python-keys)

ユーザーが自由に利用できるプレフィックスキー

  • <F5> 以降のファンクションキー
  • C-c
  • super / hyper キー(s-*, H-*
    • super/hyper は自分で設定しないと使用不可
    • Xmodmap など。
(setq w32-apps-modifier 'hyper)
(setq w32-lwindow-modifier 'super)
(setq w32-rwindow-modifier 'hyper)

キーマップ

  1. すべてのバッファで有効 → global-map
  2. 特定のバッファで有効 → current-local-map
  3. 特定の{メジャー, マイナー}モードで有効 → 〇〇-mode-map

キーバインドの検索順

マイナーモード(〇〇-mode-map
  ↓
バッファローカルマップ(current-local-map)
  ↓
メジャーモードマップ(〇〇-mode-map
  ↓
グローバルマップ(global-map
  ↓
未割り当て(エラー)

より詳細なキーマップ検索

https://ayatakesi.github.io/emacs/24.5/elisp_html/Searching-Keymaps.html
(or (if overriding-terminal-local-map ; 1 -> overriding-terminal-local-map
        (find-in overriding-terminal-local-map))
    (if overriding-local-map ; 2 -> overriding-local-map 
        (find-in overriding-local-map)
      (or (find-in (get-char-property (point) 'keymap)) ; 3 -> テキストプロパティに付与された keymap
          (find-in-any emulation-mode-map-alists) ; 4 -> 
          (find-in-any minor-mode-overriding-map-alist)
          (find-in-any minor-mode-map-alist)
          (if (get-text-property (point) 'local-map)
              (find-in (get-char-property (point) 'local-map))
            (find-in (current-local-map)))))
    (find-in (current-global-map)))
  1. overriding-terminal-local-map / 現在の端末で有効なキーマップ
  2. overriding-local-map / すべてのローカルマップを上書きするキーマップ(使用注意!)
  3. 以下のいずれかのキーマップにキー定義が存在していれば、それを使う
    • カーソル位置の文字のkeymapプロパティ / yasnipet などで使われている
    • emulation-mode-map-alists中のアクティブなmode-map
    • minor-mode-overriding-map-alist中のアクティブなmode-map
    • minor-mode-map-alist中のアクティブなmode-map / いわゆるマイナーモード
    • カーソル位置の文字のlocal-mapプロパティ
  4. current-local-map / カレントバッファで有効なローカルマップ
  5. current-global-map

上記のダミー関数に出現する変数は以下参照

https://www.gnu.org/software/emacs/manual/html_node/elisp/Controlling-Active-Maps.html

引用元: https://tarao.hatenablog.com/entry/20130304/evil_config

キーバインドの割り当て関連

  • キー入力指定 → (kbd "C-m") / (kbd "<home>") / (kbd "<f8>")
  • キーバインドの置換 → (define-key key-translation-map (kbd "C-h") (kbd "<DEL>"))
  • キーバインドの指定 → (define-key KEYMAP KEY COMMAND)
    • global-set-key → (define-key global-map ...) の糖衣関数
    • ローカルバッファ限定
      • 設定   → (local-set-key KEY COMMAND)
      • 設定解除 → (local-unset-key KEY)
  • 既存コマンドのキーバインドを別のものコマンドにリマップする
; kill-line 関数を自作の my-kill-line に置き換える
(define-key (current-global-map) [remap kill-line] 'my-kill-line)

local-set-keycurrent-local-map はマイナーモードのキーバインドより後に検索される点を注意。

ログインするとコメントできます