summaryrefslogtreecommitdiffstats
path: root/emacs.d/lisp/rect-mark.el
blob: 6373230be1d499c6bd4fb37027de48c2b8326f72 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
;;; rect-mark.el --- Mark a rectangle of text with highlighting.

;;; Copyright (C) 1994, 1995 Rick Sladkey <jrs@world.std.com>

;;; This file is not part of GNU Emacs but it is distributed under the
;;; same conditions as GNU Emacs.

;;; This is free software.
;;; GNU Emacs is free software; you can redistribute it and/or modify
;;; it under the terms of the GNU General Public License as published
;;; by the Free Software Foundation; either version 2, or (at your
;;; option) any later version.

;;; GNU Emacs is distributed in the hope that it will be useful, but
;;; WITHOUT ANY WARRANTY; without even the implied warranty of
;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
;;; General Public License for more details.

;;; You should have received a copy of the GNU General Public License
;;; along with GNU Emacs; see the file COPYING.  If not, write to the
;;; Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.

;; Author: Rick Sladkey <jrs@world.std.com>
;; Version: 1.4

;;; Commentary:

;; If you use both transient-mark-mode and picture-mode, you will
;; probably realize how convenient it would be to be able to highlight
;; the region between point and mark as a rectangle.  Have you ever
;; wished you could see where exactly those other two corners fell
;; before you operated on a rectangle?  If so, then this program is
;; for you.

;; For example, you can set the mark in preparation for a rectangle
;; command with `C-x r C-SPC', watch the highlighted rectangle grow as
;; you move the cursor to the other corner, and then issue the command
;; and the rectangle disappears.  Or if point and mark are already set
;; but you want to see what the region would look like as a rectangle,
;; try `C-x r C-x' which exchanges point and mark and makes the
;; highlighted region rectangular.

;; The default Emacs key-bindings put `point-to-register' on
;; `C-x r C-SPC' but since that command it is already on `C-x r SPC'
;; and since it is irresistably intuitive to put `rm-set-mark' on
;; `C-x r C-SPC', I have taken the liberty of recommending that you
;; override the default key-bindings.

;; You can also kill or copy rectangles onto the kill ring which is
;; convenient for yanking rectangles into ordinary buffers (i.e.  ones
;; not in picture mode) and for pasting rectangles into other window
;; system programs (e.g. xterm).  These keys are by default bound to
;; `C-x r C-w' and `C-x r M-w' by analogy to the normal kill and copy
;; counterparts.

;; Finally, there is mouse support for rectangle highlighting by
;; dragging the mouse while holding down the shift key.  The idea is
;; that this behaves exactly like normal mouse dragging except that
;; the region is treated as a rectangle.

;;; Usage:

;; Use this section in your "~/.emacs" when rect-mark isn't included
;; as an integral part of Emacs.  Don't forget to remove the first
;; three columns.

;; ;; Support for marking a rectangle of text with highlighting.
;; (define-key ctl-x-map "r\C-@" 'rm-set-mark)
;; (define-key ctl-x-map [?r ?\C-\ ] 'rm-set-mark)
;; (define-key ctl-x-map "r\C-x" 'rm-exchange-point-and-mark)
;; (define-key ctl-x-map "r\C-w" 'rm-kill-region)
;; (define-key ctl-x-map "r\M-w" 'rm-kill-ring-save)
;; (define-key global-map [S-down-mouse-1] 'rm-mouse-drag-region)
;; (autoload 'rm-set-mark "rect-mark"
;;   "Set mark for rectangle." t)
;; (autoload 'rm-exchange-point-and-mark "rect-mark"
;;   "Exchange point and mark for rectangle." t)
;; (autoload 'rm-kill-region "rect-mark"
;;   "Kill a rectangular region and save it in the kill ring." t)
;; (autoload 'rm-kill-ring-save "rect-mark"
;;   "Copy a rectangular region to the kill ring." t)
;; (autoload 'rm-mouse-drag-region "rect-mark"
;;   "Drag out a rectangular region with the mouse." t)

;; Use this section in your "~/.emacs" to modify picture mode so that
;; it automatically uses the rect-mark equivalents of many commands.

;; ;; One vision of a better picture mode.
;; (add-hook 'picture-mode-hook 'rm-example-picture-mode-bindings)
;; (autoload 'rm-example-picture-mode-bindings "rect-mark"
;;   "Example rect-mark key and mouse bindings for picture mode.")

;;; Code:


;;;###autoload (define-key ctl-x-map "r\C-@" 'rm-set-mark)
;;;###autoload (define-key ctl-x-map [?r ?\C-\ ] 'rm-set-mark)
;;;###autoload (define-key ctl-x-map "r\C-x" 'rm-exchange-point-and-mark)
;;;###autoload (define-key ctl-x-map "r\C-w" 'rm-kill-region)
;;;###autoload (define-key ctl-x-map "r\M-w" 'rm-kill-ring-save)
;;;###autoload (define-key global-map [S-down-mouse-1] 'rm-mouse-drag-region)

;; Our state variables, each internal and buffer local.
(defvar rm-mark-active nil)
(defvar rm-overlay-list)
(defvar rm-old-transient-mark-mode)
(defvar rm-force)
(defvar rm-old-global-variables)

;; A list of our buffer local variables.
(defconst rm-our-local-variables
  '(rm-mark-active
    rm-overlay-list
    rm-old-transient-mark-mode
    rm-force
    rm-old-global-variables))

;; System variables which must temorarily be buffer local.
(defconst rm-temporary-local-variables
  '(transient-mark-mode
    ;; Alas, we can no longer uninstall a post command hook from a post
    ;; command hook (as of 19.28 at least) so we must leave it installed
    ;; globally.
    ;post-command-hook
    deactivate-mark-hook))

;; Those commands which don't necessarily deactivate the mark but
;; should.  This is a partial list as of Emacs 19.22.  Most problems
;; are the result of the pathological case of a zero-width rectangle.
(defconst rm-deactivate-mark-commands
  '(clear-rectangle
    copy-rectangle
    copy-rectangle-to-register
    kill-rectangle
    open-rectangle
    string-rectangle
    yank-rectangle
    keyboard-quit))

;;; Quiet the byte-compiler.
(defvar killed-rectangle)
(defvar picture-mode-map)
(defvar deactivate-mark-hook)


;;;###autoload
(defun rm-example-picture-mode-bindings ()
  "Example rect-mark keyboard and mouse bindings for picture mode."
  (define-key picture-mode-map "\C-@" 'rm-set-mark)
  (define-key picture-mode-map [?\C-\ ] 'rm-set-mark)
  (define-key picture-mode-map [down-mouse-1] 'rm-mouse-drag-region)
  (define-key picture-mode-map "\C-x\C-x" 'rm-exchange-point-and-mark)
  (define-key picture-mode-map "\C-w" 'rm-kill-region)
  (define-key picture-mode-map "\M-w" 'rm-kill-ring-save)
  (define-key picture-mode-map "\C-y" 'yank-rectangle)
  ;; Prevent `move-to-column-force' from deactivating the mark.
  (defun move-to-column-force (column)
    (let ((deactivate-mark deactivate-mark))
      (move-to-column (max column 0) t)
      (hscroll-point-visible))))

;;;###autoload
(defun rm-set-mark (force)
  "Set mark like `set-mark-command' but anticipates a rectangle.
This arranges for the rectangular region between point and mark
to be highlighted using the same face that is used to highlight
the region in `transient-mark-mode'.  This special state lasts only
until the mark is deactivated, usually by executing a text-modifying
command like \\[kill-rectangle], by inserting text, or by typing \\[keyboard-quit].

With optional argument FORCE, arrange for tabs to be expanded and
for spaces to inserted as necessary to keep the region perfectly
rectangular.  This is the default in `picture-mode'."
  (interactive "P")
  (rm-activate-mark force)
  (push-mark nil nil t))

;;;###autoload
(defun rm-exchange-point-and-mark (force)
  "Like `exchange-point-and-mark' but treats region as a rectangle.
See `rm-set-mark' for more details.

With optional argument FORCE, tabs are expanded and spaces are
inserted as necessary to keep the region perfectly rectangular.
This is the default in `picture-mode'."
  (interactive "P")
  (rm-activate-mark force)
  (exchange-point-and-mark))

;;;###autoload
(defun rm-kill-region (start end)
  "Like kill-rectangle except the rectangle is also saved in the kill ring.
Since rectangles are not ordinary text, the killed rectangle is saved
in the kill ring as a series of lines, one for each row of the rectangle.
The rectangle is also saved as the killed rectangle so it is available for
insertion with yank-rectangle."
  (interactive "r")
  (rm-kill-ring-save start end)
  (delete-rectangle start end)
  (and (interactive-p)
       rm-mark-active
       (rm-deactivate-mark)))

;;;###autoload
(defun rm-kill-ring-save (start end)
  "Copies the region like rm-kill-region would but the rectangle isn't killed."
  (interactive "r")
  (setq killed-rectangle (extract-rectangle start end))
  (kill-new (mapconcat (function
			(lambda (row)
			  (concat row "\n")))
		       killed-rectangle ""))
  (and (interactive-p)
       rm-mark-active
       (rm-deactivate-mark)))

;;;###autoload
(defun rm-mouse-drag-region (start-event)
  "Highlight a rectangular region of text as the the mouse is dragged over it.
This must be bound to a button-down mouse event."
  (interactive "e")
  (let* ((start-posn (event-start start-event))
	 (start-point (posn-point start-posn))
	 (start-window (posn-window start-posn))
	 (start-frame (window-frame start-window))
	 (bounds (window-edges start-window))
	 (top (nth 1 bounds))
	 (bottom (if (window-minibuffer-p start-window)
		     (nth 3 bounds)
		   ;; Don't count the mode line.
		   (1- (nth 3 bounds))))
	 (click-count (1- (event-click-count start-event))))
    (setq mouse-selection-click-count click-count)
    (mouse-set-point start-event)
    (rm-activate-mark)
    (let (end-event
	  end-posn
	  end-point
	  end-window)
      (track-mouse
	(while (progn
		 (setq end-event (read-event)
		       end-posn (event-end end-event)
		       end-point (posn-point end-posn)
		       end-window (posn-window end-posn))
		 (or (mouse-movement-p end-event)
		     (eq (car-safe end-event) 'switch-frame)))
	  (cond
	   ;; Ignore switch-frame events.
	   ((eq (car-safe end-event) 'switch-frame)
	    nil)
	   ;; Are we moving within the original window?
	   ((and (eq end-window start-window)
		 (integer-or-marker-p end-point))
	    (goto-char end-point)
	    (rm-highlight-rectangle start-point end-point))
	   ;; Are we moving on a different window on the same frame?
	   ((and (windowp end-window)
		 (eq (window-frame end-window) start-frame))
	    (let ((mouse-row (+ (nth 1 (window-edges end-window))
				(cdr (posn-col-row end-posn)))))
	      (cond
	       ((< mouse-row top)
		(mouse-scroll-subr (- mouse-row top)
				   nil start-point))
	       ((and (not (eobp))
		     (>= mouse-row bottom))
		(mouse-scroll-subr (1+ (- mouse-row bottom))
				   nil start-point)))))
	   (t
	    (let ((mouse-y (cdr (cdr (mouse-position))))
		  (menu-bar-lines (or (cdr (assq 'menu-bar-lines
						 (frame-parameters)))
				      0)))
	      ;; Are we on the menu bar?
	      (and (integerp mouse-y) (< mouse-y menu-bar-lines)
		   (mouse-scroll-subr (- mouse-y menu-bar-lines)
				      nil start-point)))))))
      (and (eq (get (event-basic-type end-event) 'event-kind) 'mouse-click)
	   (eq end-window start-window)
	   (numberp end-point)
	   (if (= start-point end-point)
	       (setq deactivate-mark t)
	     (push-mark start-point t t)
	     (goto-char end-point)
	     (rm-kill-ring-save start-point end-point)))
      )))


(defun rm-activate-mark (&optional force)
  ;; Turn on rectangular marking mode by temporarily (and in a buffer
  ;; local way) disabling transient mark mode and manually handling
  ;; highlighting from a post command hook.
  (setq rm-force (and (not buffer-read-only)
		      (or force
			  (eq major-mode 'picture-mode))))
  ;; Be careful if we are already marking a rectangle.
  (if rm-mark-active
      nil
    ;; Make each of our state variables buffer local.
    (mapcar (function make-local-variable) rm-our-local-variables)
    (setq rm-mark-active t
	  rm-overlay-list nil
	  rm-old-transient-mark-mode transient-mark-mode)
    ;; Remember which system variables weren't buffer local.
    (setq rm-old-global-variables
	  (apply (function nconc)
		 (mapcar (function
			  (lambda (variable)
			    (and (not (assoc variable
					     (buffer-local-variables)))
				 (list variable))))
			 rm-temporary-local-variables)))
    ;; Then make them all buffer local too.
    (mapcar (function make-local-variable) rm-temporary-local-variables)
    ;; Making transient-mark-mode buffer local doesn't really work
    ;; correctly as of 19.22: the current buffer's value affects all
    ;; displayed buffers.
    (setq transient-mark-mode nil)
    (add-hook 'post-command-hook 'rm-post-command)
    (add-hook 'deactivate-mark-hook 'rm-deactivate-mark)))

(defun rm-post-command ()
  ;; An error in a post-command function can be fatal if it re-occurs
  ;; on each call, thus the condition-case safety nets.
  ;; We have to do things this way because deactivate-mark doesn't
  ;; (in general) get called if transient-mark-mode isn't turned on.
  (if rm-mark-active
      (if (or (not mark-active)
	      deactivate-mark
	      (memq this-command rm-deactivate-mark-commands))
	  (condition-case nil
	      (rm-deactivate-mark)
	    (error nil))
	(condition-case info
	    (rm-highlight-rectangle (mark) (point))
	  (error
	   (ding)
	   (message "rect-mark trouble: %s" info)
	   (condition-case nil
	       (rm-deactivate-mark)
	     (error nil)))))
    (and (boundp 'rm-overlay-list)
	 (condition-case nil
	     (rm-deactivate-mark)
	   (error nil)))))

(defun rm-highlight-rectangle (start end)
  ;; This function is used to highlight the rectangular region from
  ;; START to END.  We do this by putting an overlay on each line
  ;; within the rectangle.  Each overlay extends across all the
  ;; columns of the rectangle.  We try to reuse overlays where
  ;; possible because this is more efficient and results in less
  ;; flicker.  If rm-force is nil and the buffer contains tabs or
  ;; short lines, the higlighted region may not be perfectly
  ;; rectangular.
  (save-excursion
    ;; Calculate the rectangular region represented by point and mark,
    ;; putting start in the north-west corner and end in the
    ;; south-east corner.  We can't effectively use
    ;; operate-on-rectangle because it doesn't work for zero-width
    ;; rectangles as of 19.22.
    (and (> start end)
	 (setq start (prog1
			 end
		       (setq end start))))
    (let ((start-col (save-excursion
		       (goto-char start)
		       (current-column)))
	  (end-col (save-excursion
		     (goto-char end)
		     (current-column)))
	  (deactivate-mark deactivate-mark))
      (and (> start-col end-col)
	   (setq start-col (prog1
			       end-col
			     (setq end-col start-col))
		 start (save-excursion
			 (goto-char start)
			 (move-to-column start-col rm-force)
			 (point))
		 end (save-excursion
		       (goto-char end)
		       (move-to-column end-col rm-force)
		       (point))))
      ;; Force a redisplay so we can do reliable window start/end
      ;; calculations.
      (sit-for 0)
      (let ((old rm-overlay-list)
	    (new nil)
	    overlay
	    (window-start (max (window-start) start))
	    (window-end (min (window-end) end)))
	;; Iterate over those lines of the rectangle which are visible
	;; in the currently selected window.
	(goto-char window-start)
	(while (< (point) window-end)
	  (let ((row-start (progn
			     (move-to-column start-col rm-force)
			     (point)))
		(row-end (progn
			   (move-to-column end-col rm-force)
			   (point))))
	    ;; Trim old leading overlays.
	    (while (and old
			(setq overlay (car old))
			(< (overlay-start overlay) row-start)
			(/= (overlay-end overlay) row-end))
	      (delete-overlay overlay)
	      (setq old (cdr old)))
	    ;; Reuse an overlay if possible, otherwise create one.
	    (if (and old
		     (setq overlay (car old))
		     (or (= (overlay-start overlay) row-start)
			 (= (overlay-end overlay) row-end)))
		(progn
		  (move-overlay overlay row-start row-end)
		  (setq new (cons overlay new)
			old (cdr old)))
	      (setq overlay (make-overlay row-start row-end))
	      (overlay-put overlay 'face 'region)
	      (setq new (cons overlay new)))
	    (forward-line 1)))
	;; Trim old trailing overlays.
	(mapcar (function delete-overlay) old)
	(setq rm-overlay-list (nreverse new))))))

(defun rm-deactivate-mark ()
  ;; This is used to clean up after `rm-activate-mark'.
  ;; Alas, we can no longer uninstall a post command hook from a post
  ;; command hook (as of 19.28 at least) so we must leave it installed
  ;; globally.
  ;(setq post-command-hook (delq 'rm-post-command post-command-hook))
  (setq deactivate-mark-hook (delq 'rm-deactivate-mark deactivate-mark-hook))
  (setq transient-mark-mode rm-old-transient-mark-mode)
  (mapcar (function delete-overlay) rm-overlay-list)
  (mapcar (function kill-local-variable) rm-old-global-variables)
  (mapcar (function kill-local-variable) rm-our-local-variables)
  (and transient-mark-mode
       mark-active
       (deactivate-mark)))

(provide 'rect-mark)

;;; rect-mark.el ends here