Emacs: 現在位置のSQLをよしなに選択+コピーする

EmacsSQL 書く → リージョン選択(現在位置から先頭に移動して末尾に移動) → コピー

というのを何回も繰り返す場合、

  • 手数が多くてだるい
  • カーソルを移動させると、元の位置に戻るのがだるい

というあたりがだるかったので改善してみた。

完成形

(defun my-sql:query-beginning ()
  (let (beg beg-para beg-sc)
    (save-excursion
      (backward-paragraph)
      (setq beg-para (+ (point) 1)))
    (save-excursion
      (when (search-backward-regexp ";\n" nil t)
          (setq beg-sc (+ (point) 2))))
    (if beg-sc
        (max beg-para beg-sc)
      beg-para)))

(defun my-sql:query-end ()
  (let (end end-para end-sc)
    (save-excursion
      (forward-paragraph)
      (setq end-para (- (point) 1)))
    (save-excursion
      (when (search-forward-regexp ";\n" nil t)
          (setq end-sc (- (point) 1))))
    (if end-sc
        (min end-para end-sc)
      end-para)))

(defun my-sql:copy-current-query ()
  "Copy current query."
  (interactive)
  (kill-ring-save
   (my-sql:query-beginning)
   (my-sql:query-end)))

(global-set-key (kbd "C-M-h") 'my-sql:copy-current-query)

メモ

まず、今いる位置から動かずに1キーストロークで段落選択してコピーできるようにしてみた。mark-paragraph 使えば簡単。

(defun copy-current-query ()
  "..."
  (interactive)
  (save-excursion
    (mark-paragraph)
    (kill-ring-save (region-beginning) (region-end))))

これの不満点:

  • 選択範囲の先頭に余計な改行が付く … 困るわけではないが余計なので削りたい
  • 選択範囲の末尾に余計な改行が付く … 最後のセミコロンの後に改行が付いていると、RDBMSクライアントのプロンプトにペーストしたときに即実行されてしまうのが嫌。最後の改行なしでペーストして、(場合によっては深呼吸などして)確認した後にエンター押下で実行させたい。ワンクッション置きたい。
(defun copy-current-query ()
  "..."
  (interactive)
  (let (beg end)
    (save-excursion

      (backward-paragraph)
      (setq beg (+ (point) 1))
      
      (forward-paragraph)
      (setq end (- (point) 1))

      (kill-ring-save beg end))))

まだ不満な点:

選択の単位として段落を使うと、たとえば1行ずつのSQLをこのように並べて書いていて

select * from table1 limit 10;
select * from table2 limit 10;
select * from table3 order by created_at desc limit 10;

真ん中のクエリだけコピーしたいのに3行ともコピーされてしまう。1行ごとに改行入れればよいけど、ワンライナーのために無駄に行を消費したくない。

そこで、区切りとして前方、後方の ";\n" を探すようにしてみる。
セミコロン書かない+段落で見れば良いというケースもあるので、段落境界と比較して近い方を使うようにする。

(defun copy-current-query ()
  "..."
  (interactive)
  (let (beg beg-para beg-sc
        end end-para end-sc)
    (save-excursion
      (backward-paragraph)
      (setq beg-para (+ (point) 1))
      (forward-paragraph)
      (setq end-para (- (point) 1)))

    (save-excursion
      (if (search-backward-regexp ";\n" nil t)
          (setq beg-sc (+ (point) 2))
        (setq beg-sc beg-para)))

    (save-excursion
      (if (search-forward-regexp ";\n" nil t)
          (setq end-sc (- (point) 1))
        (setq end-sc end-para)))

    (setq beg (max beg-para beg-sc))
    (setq end (min end-para end-sc))

    (kill-ring-save beg end)))

大体いい感じになった。
選択範囲の始点・終点を求める部分は使いまわせそうなので、関数を抽出する。こうしておくと、たとえば、今いるクエリをフォーマットする場合なんかに流用できる。

関数が複数になったので名前空間的に my-sql: というプレフィックスを付けてみた。

;; 上の完成形と同じなのでコード省略

関数ごとに目的がはっきりしているし、良いと思う。


以下おまけ。自分用設定的なもの。

視覚的フィードバックが何もないと不安になるので、どこをコピーしたのか分かるように一定時間ハイライトさせる。

一定時間だけ指定範囲をハイライトする Emacs Lisp | anobota を使用)

(require 'volatile-highlight)

(defun my-sql:copy-current-query ()
  "..."
  (interactive)
  (let ((beg (my-sql:query-beginning))
        (end (my-sql:query-end)))
    (kill-ring-save beg end)
    (volatile-highlight beg end 1)))

フォーマット。
参考: (書きかけ)EmacsでSQLの整形(要Ruby) | anobota

(defun my-sql:format-currenty-query ()
  "Format current query"
  (interactive)
  (shell-command-on-region
   (my-sql:query-beginning) (my-sql:query-end)
   (format "ruby %s" anbt-sql-formatter:formatter-path)
   nil t)
  (volatile-highlight (my-sql:query-beginning) (my-sql:query-end) 0.2))

(global-set-key (kbd "C-S-f") 'my-sql:format-currenty-query)