├── README.md
├── dynamic-spaces.el
└── test
├── dynamic-spaces-test-setup.el
└── dynamic-spaces-test.el
/README.md:
--------------------------------------------------------------------------------
1 | # dynamic-spaces - Don't move text separated by multiple spaces
2 |
3 | *Author:* Anders Lindgren
4 | *Version:* 0.0.1
5 | *URL:* [https://github.com/Lindydancer/dynamic-spaces](https://github.com/Lindydancer/dynamic-spaces)
6 |
7 | When editing a text, and `dynamic-spaces-mode` is enabled, text
8 | separated by more than one space doesn't move, if possible.
9 | Concretely, end-of-line comments stay in place when you edit the
10 | code and you can edit a field in a table without affecting other
11 | fields.
12 |
13 | For example, this is the content of a buffer before an edit (where
14 | `*` represents the cursor):
15 |
16 | alpha*gamma delta
17 | one two three
18 |
19 | When inserting "beta" without dynamic spaces, the result would be:
20 |
21 | alphabeta*gamma delta
22 | one two three
23 |
24 | However, with `dynamic-spaces-mode` enabled the result becomes:
25 |
26 | alphabeta*gamma delta
27 | one two three
28 |
29 | ## Usage
30 |
31 | To enable *dynamic spaces* for all supported modes, add the
32 | following to a suitable init file:
33 |
34 | (dynamic-spaces-global-mode 1)
35 |
36 | Or, activate it for a specific major mode:
37 |
38 | (add-hook 'example-mode-hook 'dynamic-spaces-mode)
39 |
40 | Alternatively, use M-x customize-group RET dynamic-spaces RET.
41 |
42 | ## Space groups
43 |
44 | Two pieces of text are considered different (and
45 | `dynamic-spaces-mode` tries to keep then in place) if they are
46 | separated by a "space group". The following is, by default,
47 | considered space groups:
48 |
49 | * A TAB character.
50 | * Two or more whitespace characters.
51 |
52 | However, the following are *not* considered space groups:
53 |
54 | * whitespace in a quoted string.
55 | * Two spaces, when preceded by a punctuation character and
56 | `sentence-end-double-space` is non-nil.
57 | * Two spaces, when preceded by a colon and `colon-double-space` is
58 | non-nil.
59 |
60 | ## Configuration
61 |
62 | You can use the following variables to modify the behavior or
63 | `dynamic-spaces-mode`:
64 |
65 | * `dynamic-spaces-mode-list` - List of major modes where
66 | dynamic spaces mode should be enabled by the global mode.
67 | * `dynamic-spaces-avoid-mode-list` - List of major modes where
68 | dynamic spaces mode should not be enabled.
69 | * `dynamic-spaces-global-mode-ignore-buffer` - When non-nil in a
70 | buffer, `dynamic-spaces-mode` will not be enabled in that buffer
71 | when `dynamic-spaces-global-mode` is enabled.
72 | * `dynamic-spaces-commands` - Commands that dynamic spaces mode
73 | should adjust spaces for.
74 | * `dynamic-spaces-keys` - Keys, in `kbd` format, that dynamic
75 | spaces mode should adjust spaces for. (This is needed as many
76 | major modes define electric command and bind them to typical edit
77 | keys.)
78 | * `dynamic-spaces-find-next-space-group-function` - A function that
79 | would find the next dynamic space group.
80 |
81 | ## Notes
82 |
83 | By default, this is disabled for org-mode since it interferes with
84 | the org mode table edit system.
85 |
86 |
87 | ---
88 | Converted from `dynamic-spaces.el` by [*el2markdown*](https://github.com/Lindydancer/el2markdown).
89 |
--------------------------------------------------------------------------------
/dynamic-spaces.el:
--------------------------------------------------------------------------------
1 | ;;; dynamic-spaces.el --- Don't move text separated by multiple spaces -*- lexical-binding: t; -*-
2 |
3 | ;; Copyright (C) 2015-2017,2025 Anders Lindgren
4 |
5 | ;; Author: Anders Lindgren
6 | ;; Created: 2015-09-10
7 | ;; Version: 0.0.1
8 | ;; Keywords: convenience
9 | ;; URL: https://github.com/Lindydancer/dynamic-spaces
10 |
11 | ;; This program is free software; you can redistribute it and/or modify
12 | ;; it under the terms of the GNU General Public License as published by
13 | ;; the Free Software Foundation, either version 3 of the License, or
14 | ;; (at your option) any later version.
15 |
16 | ;; This program is distributed in the hope that it will be useful,
17 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
18 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 | ;; GNU General Public License for more details.
20 |
21 | ;; You should have received a copy of the GNU General Public License
22 | ;; along with this program. If not, see .
23 |
24 | ;;; Commentary:
25 |
26 | ;; When editing a text, and `dynamic-spaces-mode' is enabled, text
27 | ;; separated by more than one space doesn't move, if possible.
28 | ;; Concretely, end-of-line comments stay in place when you edit the
29 | ;; code and you can edit a field in a table without affecting other
30 | ;; fields.
31 | ;;
32 | ;; For example, this is the content of a buffer before an edit (where
33 | ;; `*' represents the cursor):
34 | ;;
35 | ;; alpha*gamma delta
36 | ;; one two three
37 | ;;
38 | ;; When inserting "beta" without dynamic spaces, the result would be:
39 | ;;
40 | ;; alphabeta*gamma delta
41 | ;; one two three
42 | ;;
43 | ;; However, with `dynamic-spaces-mode' enabled the result becomes:
44 | ;;
45 | ;; alphabeta*gamma delta
46 | ;; one two three
47 |
48 | ;; Usage:
49 | ;;
50 | ;; To enable *dynamic spaces* for all supported modes, add the
51 | ;; following to a suitable init file:
52 | ;;
53 | ;; (dynamic-spaces-global-mode 1)
54 | ;;
55 | ;; Or, activate it for a specific major mode:
56 | ;;
57 | ;; (add-hook 'example-mode-hook 'dynamic-spaces-mode)
58 | ;;
59 | ;; Alternatively, use `M-x customize-group RET dynamic-spaces RET'.
60 |
61 | ;; Space groups:
62 | ;;
63 | ;; Two pieces of text are considered different (and
64 | ;; `dynamic-spaces-mode' tries to keep then in place) if they are
65 | ;; separated by a "space group". The following is, by default,
66 | ;; considered space groups:
67 | ;;
68 | ;; * A TAB character.
69 | ;;
70 | ;; * Two or more whitespace characters.
71 | ;;
72 | ;; However, the following are *not* considered space groups:
73 | ;;
74 | ;; * whitespace in a quoted string.
75 | ;;
76 | ;; * Two spaces, when preceded by a punctuation character and
77 | ;; `sentence-end-double-space' is non-nil.
78 | ;;
79 | ;; * Two spaces, when preceded by a colon and `colon-double-space' is
80 | ;; non-nil.
81 |
82 | ;; Configuration:
83 | ;;
84 | ;; You can use the following variables to modify the behavior or
85 | ;; `dynamic-spaces-mode':
86 | ;;
87 | ;; * `dynamic-spaces-mode-list' - List of major modes where
88 | ;; dynamic spaces mode should be enabled by the global mode.
89 | ;;
90 | ;; * `dynamic-spaces-avoid-mode-list' - List of major modes where
91 | ;; dynamic spaces mode should not be enabled.
92 | ;;
93 | ;; * `dynamic-spaces-global-mode-ignore-buffer' - When non-nil in a
94 | ;; buffer, `dynamic-spaces-mode' will not be enabled in that buffer
95 | ;; when `dynamic-spaces-global-mode' is enabled.
96 | ;;
97 | ;; * `dynamic-spaces-commands' - Commands that dynamic spaces mode
98 | ;; should adjust spaces for.
99 | ;;
100 | ;; * `dynamic-spaces-keys' - Keys, in `kbd' format, that dynamic
101 | ;; spaces mode should adjust spaces for. (This is needed as many
102 | ;; major modes define electric command and bind them to typical edit
103 | ;; keys.)
104 | ;;
105 | ;; * `dynamic-spaces-find-next-space-group-function' - A function that
106 | ;; would find the next dynamic space group.
107 |
108 | ;; Notes:
109 | ;;
110 | ;; By default, this is disabled for org-mode since it interferes with
111 | ;; the org mode table edit system.
112 |
113 | ;;; Code:
114 |
115 | ;; -------------------------------------------------------------------
116 | ;; Configuration variables.
117 | ;;
118 |
119 | (defgroup dynamic-spaces nil
120 | "Adapt spaces when editing automatically."
121 | :group 'convenience)
122 |
123 |
124 | (defcustom dynamic-spaces-mode-list
125 | '(prog-mode
126 | text-mode)
127 | "List of major modes where Dynamic-Spaces mode should be enabled.
128 |
129 | When Dynamic-Spaces Global mode is enabled, Dynamic-Spaces mode is
130 | enabled for buffers whose major mode is a member of this list, or
131 | is derived from a member in the list.
132 |
133 | See also `dynamic-spaces-avoid-mode-list'."
134 | :group 'dynamic-spaces
135 | :type '(repeat (symbol :tag "Major mode")))
136 |
137 |
138 | (defcustom dynamic-spaces-avoid-mode-list
139 | '(org-mode)
140 | "List of major modes in which Dynamic-Spaces mode should not be enabled.
141 |
142 | When Dynamic-Spaces Global mode is enabled, Dynamic-Spaces mode
143 | is not enabled for buffers whose major mode is a member of this
144 | list, or is derived from a member in the list.
145 |
146 | This variable take precedence over `dynamic-spaces-mode-list'."
147 | :group 'dynamic-spaces
148 | :type '(repeat (symbol :tag "Major mode")))
149 |
150 |
151 | (defvar dynamic-spaces-global-mode-ignore-buffer nil
152 | "When non-nil, stop global Dynamic-Spaces mode from enabling the mode.
153 | This variable becomes buffer local when set in any fashion.")
154 | (make-variable-buffer-local 'dynamic-spaces-global-mode-ignore-buffer)
155 |
156 |
157 | (defcustom dynamic-spaces-commands '(delete-backward-char
158 | delete-char
159 | self-insert-command
160 | dabbrev-expand
161 | yank)
162 | "Commands that Dynamic-Spaces mode should adjusts spaces for."
163 | :group 'dynamic-spaces
164 | :type '(repeat function))
165 |
166 |
167 | (defun dynamic-spaces-normalize-key-vector (v)
168 | "Normalize the key vector V to the form used by `this-single-command-keys'.
169 |
170 | Currently, this replaces `escape' with the ASCII code 27."
171 | (let ((i 0))
172 | (while (< i (length v))
173 | (when (eq (elt v i) 'escape)
174 | (aset v i 27))
175 | (setq i (+ i 1)))))
176 |
177 |
178 | (defun dynamic-spaces-convert-keys-to-vector (strs)
179 | "Return a list of vectors corresponding to the keys in STRS."
180 | (mapcar (lambda (s)
181 | (let ((key (read-kbd-macro s t)))
182 | (dynamic-spaces-normalize-key-vector key)
183 | key))
184 | strs))
185 |
186 |
187 | (defcustom dynamic-spaces-keys '("C-d"
188 | ""
189 | "M-d"
190 | " d"
191 | "M-DEL"
192 | " ")
193 | "Keys that, in Dynamic-Spaces mode, adjust spaces.
194 |
195 | This must be set before Dynamic-Spaces mode is enabled.
196 |
197 | The main reason to allow keys, rather than commands, is to handle
198 | the myriad of electric commands various major modes provide."
199 | :group 'dynamic-spaces
200 | :type '(repeat string))
201 |
202 |
203 | (defvar dynamic-spaces-key--vectors '()
204 | "Keys that, in Dynamic-Spaces mode, adjust spaces, in vector format.
205 |
206 | The format should be the same as returned by `this-single-command-keys'.
207 |
208 | Do not set this variable manually, it is initialized from
209 | `dynamic-spaces-key' when Dynamic-Spaces mode is enabled.")
210 |
211 |
212 | (defcustom dynamic-spaces-pre-filter-functions
213 | '(dynamic-spaces-reject-in-string
214 | dynamic-spaces-reject-double-spaces)
215 | "List of functions that can reject a space group.
216 |
217 | The functions are called without arguments with the point at the
218 | end of the potential space group. They should return non-nil
219 | if the group should be rejected. The functions should not move
220 | the point."
221 | :group 'dynamic-spaces
222 | :type '(repeat function))
223 |
224 |
225 | (defcustom dynamic-spaces-post-filter-once-functions
226 | '(dynamic-spaces-reject-in-string)
227 | "List of functions that can reject a space group after an edit.
228 |
229 | The functions in the list are called once, before the space group
230 | is adjusted.
231 |
232 | The functions are called without arguments with the point at the
233 | end of the space group. They should return non-nil if the
234 | group should be rejected. The functions should not move the
235 | point.
236 |
237 | Use `add-hook' to add functions to this variable."
238 | :group 'dynamic-spaces
239 | :type '(repeat function))
240 |
241 |
242 | (defcustom dynamic-spaces-post-filter-functions
243 | '(dynamic-spaces-reject-double-spaces)
244 | "List of functions that can reject a space group when shrinking spaces.
245 |
246 | The functions in the list can be called multiple times, while the
247 | space group is being adjusted.
248 |
249 | The functions are called without arguments with the point at the
250 | end of the space group. They should return non-nil if the
251 | group should be rejected. The functions should not move the
252 | point.
253 |
254 | Use `add-hook' to add functions to this variable."
255 | :group 'dynamic-spaces
256 | :type '(repeat function))
257 |
258 |
259 | (defcustom dynamic-spaces-find-next-space-group-function
260 | #'dynamic-spaces-find-next-space-group
261 | "A function used to find the next space group on the line.
262 |
263 | The function is called without arguments and should place point
264 | at the end of the space group and return non-nil when a group is
265 | found."
266 | :group 'dynamic-spaces
267 | :type 'function)
268 |
269 |
270 | (defcustom dynamic-spaces-find-next-space-group-alist '()
271 | "Alist from MODE to function to find next space group on line.
272 |
273 | The first entry where `major-mode' is MODE, or derived from MODE,
274 | is selected. If no such mode exists use the value of
275 | `dynamic-spaces-find-next-space-group-function'."
276 | :group 'dynamic-spaces
277 | :type '(repeat (cons (symbol :tag "Major mode")
278 | function)))
279 |
280 |
281 | ;; -------------------------------------------------------------------
282 | ;; The modes
283 | ;;
284 |
285 | ;;;###autoload
286 | (define-minor-mode dynamic-spaces-mode
287 | "Minor mode that adapts surrounding spaces when editing."
288 | :group 'dynamic-spaces
289 | (if dynamic-spaces-mode
290 | (progn
291 | (add-hook 'pre-command-hook 'dynamic-spaces-pre-command-hook t t)
292 | (add-hook 'post-command-hook 'dynamic-spaces-post-command-hook nil t)
293 | (set (make-local-variable 'dynamic-spaces-key--vectors)
294 | (dynamic-spaces-convert-keys-to-vector dynamic-spaces-keys))
295 | (let ((tail dynamic-spaces-find-next-space-group-alist))
296 | (while tail
297 | (let ((pair (pop tail)))
298 | (when (derived-mode-p (car pair))
299 | (set (make-local-variable
300 | 'dynamic-spaces-find-next-space-group-function)
301 | (cdr pair))
302 | ;; Break the loop.
303 | (setq tail '()))))))
304 | (remove-hook 'pre-command-hook 'dynamic-spaces-pre-command-hook t)
305 | (remove-hook 'post-command-hook 'dynamic-spaces-post-command-hook t)))
306 |
307 |
308 | (defun dynamic-spaces-activate-if-applicable ()
309 | "Turn on Dynamic-Spaces mode, if applicable.
310 |
311 | Don't turn it on if `dynamic-spaces-global-mode-ignore-buffer' is non-nil."
312 | (when (and (not dynamic-spaces-global-mode-ignore-buffer)
313 | (apply #'derived-mode-p dynamic-spaces-mode-list)
314 | (not (apply #'derived-mode-p dynamic-spaces-avoid-mode-list)))
315 | (dynamic-spaces-mode 1)))
316 |
317 |
318 | ;;;###autoload
319 | (define-global-minor-mode dynamic-spaces-global-mode dynamic-spaces-mode
320 | dynamic-spaces-activate-if-applicable
321 | :group 'dynamic-spaces)
322 |
323 |
324 | ;; -------------------------------------------------------------------
325 | ;; Pre- and post-command hooks.
326 | ;;
327 |
328 |
329 | (defvar dynamic-spaces--space-groups nil
330 | "List of (MARKER . COLUMN) of space groups after point on current line.
331 |
332 | MARKER and COLUMN refers to the first character after a group of spaces.
333 |
334 | Information passed from Dynamic Spaces pre- to post command hook.")
335 |
336 |
337 | (defvar dynamic-spaces--bol nil
338 | "Position of the beginning of a line before the command.
339 |
340 | Information passed from Dynamic Spaces pre- to post command hook.")
341 |
342 |
343 | (defvar dynamic-spaces--inside-space-group nil
344 | "Original point if point was inside a space group, or nil.
345 |
346 | Information passed from Dynamic Spaces pre- to post command hook.")
347 |
348 |
349 | (defvar dynamic-spaces--middle-of-two-space-group nil
350 | "Non-nil if point was in the middle of a space group of two spaces.")
351 |
352 |
353 | ;; (defvar dynamic-spaces--log nil)
354 |
355 |
356 | (defun dynamic-spaces-line-end-position ()
357 | "Like `line-end-position' but aware of selective display."
358 | (if selective-display
359 | (save-excursion
360 | (if (search-forward "\r" (line-end-position) t)
361 | (- (point) 1)
362 | (line-end-position)))
363 | (line-end-position)))
364 |
365 |
366 | (defun dynamic-spaces-middle-of-two-spaces ()
367 | "Non-nil if point is in the middle of space group consisting of two spaces."
368 | (and (eq (char-after) ?\s)
369 | (not (memq (char-after (+ (point) 1)) '(?\s ?\t)))
370 | (eq (char-before) ?\s)
371 | (not (memq (char-before (- (point) 1)) '(?\s ?\t)))))
372 |
373 |
374 | (defun dynamic-spaces-pre-command-hook ()
375 | "Record the start of each space group.
376 |
377 | This is typically attached to a local `pre-command-hook' and is
378 | executed before each command."
379 | ;; Clear out old information, and recycle markers. (This isn't done
380 | ;; in `dynamic-spaces-post-command-hook' for the benefit if
381 | ;; `dynamic-spaces-view-space-group-print-source'.)
382 | (dynamic-spaces-recycle-marker-list dynamic-spaces--space-groups)
383 | (setq dynamic-spaces--space-groups '())
384 | (when (or (memq this-command dynamic-spaces-commands)
385 | (let ((keys (this-single-command-keys)))
386 | ;; (push keys dynamic-spaces--log)
387 | (or (member keys dynamic-spaces-key--vectors)
388 | (and (eq (length keys) 1)
389 | (let ((key (aref keys 0)))
390 | ;; Printable characters and backspace.
391 | (and (numberp key)
392 | (>= key 32)
393 | (<= key 127)))))))
394 | (setq dynamic-spaces--bol (line-beginning-position))
395 | (setq dynamic-spaces--space-groups (dynamic-spaces-find-space-groups))
396 | (setq dynamic-spaces--inside-space-group
397 | (and dynamic-spaces--space-groups
398 | (= (save-excursion
399 | (skip-chars-forward " \t")
400 | (point))
401 | (car (car dynamic-spaces--space-groups)))
402 | (point)))
403 | (setq dynamic-spaces--middle-of-two-space-group
404 | (and dynamic-spaces--inside-space-group
405 | (dynamic-spaces-middle-of-two-spaces)))))
406 |
407 |
408 | (defun dynamic-spaces-post-command-hook ()
409 | "Update space groups after a command."
410 | (when dynamic-spaces--space-groups
411 | (unless buffer-read-only
412 | (when (eq (line-beginning-position) dynamic-spaces--bol)
413 | (when (or
414 | ;; When deleting chars after the point inside a space
415 | ;; group it should shrink.
416 | (eq dynamic-spaces--inside-space-group
417 | (point))
418 | ;; Special case: Point was in the middle of two spaces,
419 | ;; and text to the left has been deleted. Don't adjust
420 | ;; the space group, as this situation often occurs in
421 | ;; normal editing.
422 | (and dynamic-spaces--middle-of-two-space-group
423 | (< (point) dynamic-spaces--inside-space-group)))
424 | (dynamic-spaces-recycle-marker (pop dynamic-spaces--space-groups)))
425 | (dynamic-spaces-adjust-space-groups-to
426 | dynamic-spaces--space-groups)))))
427 |
428 |
429 | ;; -------------------------------------------------------------------
430 | ;; The engine
431 | ;;
432 |
433 | (defun dynamic-spaces-find-next-space-group ()
434 | "Go to end of next space group.
435 |
436 | Return non-nil if one is found.
437 |
438 | A space group starts either two whitespace characters or with a tab.
439 |
440 | Ignore space groups rejected by `dynamic-spaces-pre-filter-functions'.
441 |
442 | The point may move even if no group was found."
443 | (let (res)
444 | (when (memq (following-char) '(?\s ?\t))
445 | (skip-chars-backward " \t"))
446 | (while (and
447 | (setq res (re-search-forward "\\( \\|\t\\)[ \t]*"
448 | (dynamic-spaces-line-end-position) t))
449 | ;; Reject cases like when two spaces are preceded by a
450 | ;; punctuation character and `sentence-end-double-space'
451 | ;; is non-nil.
452 | (run-hook-with-args-until-success
453 | 'dynamic-spaces-pre-filter-functions)))
454 | res))
455 |
456 |
457 | (defun dynamic-spaces-reject-in-string ()
458 | "Reject space group at point when in strings."
459 | (nth 3 (syntax-ppss)))
460 |
461 |
462 | (defun dynamic-spaces-reject-double-spaces ()
463 | "Maybe reject double-space space groups when preceded by special characters.
464 |
465 | If `sentence-end-double-space' is non-nil, reject space groups
466 | preceded by `.', `!', or `?'.
467 |
468 | If `colon-double-space' is non-nil, reject space groups preceded
469 | by `:'."
470 | (and (eq (char-before (point)) ?\s)
471 | (eq (char-before (- (point) 1)) ?\s)
472 | (let ((ch (char-before (- (point) 2))))
473 | (and (or (and sentence-end-double-space
474 | (memq ch '(?. ?! ??)))
475 | (and colon-double-space
476 | (eq ch ?:)))))))
477 |
478 |
479 | (defvar dynamic-spaces--unused-markers '()
480 | "List or unused markers, used to avoid reallocation of markers.")
481 |
482 |
483 | (defun dynamic-spaces-find-space-groups ()
484 | "List of (MARKER . COLUMN):s with space groups after the point."
485 | (save-excursion
486 | (let ((res '()))
487 | (while (funcall dynamic-spaces-find-next-space-group-function)
488 | (let ((marker (if dynamic-spaces--unused-markers
489 | (pop dynamic-spaces--unused-markers)
490 | (make-marker))))
491 | (set-marker marker (point))
492 | (push (cons marker (current-column)) res)))
493 | (nreverse res))))
494 |
495 |
496 | (defun dynamic-spaces-space-group-before-p (pos)
497 | "True if POS is preceded by a space group."
498 | (and (or (eq (char-before pos) ?\t)
499 | (and (eq (char-before pos) ?\s)
500 | (memq (char-before (- pos 1)) '(?\s ?\t))))
501 | (save-excursion
502 | (goto-char pos)
503 | (not (run-hook-with-args-until-success
504 | 'dynamic-spaces-post-filter-functions)))))
505 |
506 |
507 | (defun dynamic-spaces-adjust-space-groups-to (space-groups)
508 | "Adjust space groups to the right of the point to match SPACE-GROUPS."
509 | (save-excursion
510 | (dolist (pair space-groups)
511 | (goto-char (car pair))
512 | (unless (run-hook-with-args-until-success
513 | 'dynamic-spaces-post-filter-once-functions)
514 | ;; Note: There following does not check if the code has moved
515 | ;; to the right, but must be padded to retain a space group.
516 | ;; This ensures that inserting "x" in "alpha * beta" result in
517 | ;; "alpha x* beta" and not "alpha x* beta".
518 | (while (let ((diff (- (cdr pair) (current-column))))
519 | (cond ((< diff 0)
520 | ;; The text has moved to the right, delete
521 | ;; whitespace to compensate, and redo.
522 | (cond ((eq (char-before) ?\t)
523 | ;; Delete the \t and, possibly, insert
524 | ;; two spaces to ensure that a space
525 | ;; group is preserved.
526 | ;;
527 | ;; Ensure that markers before the tab
528 | ;; (e.g. from the `save-excursion'
529 | ;; above) and after (e.g. from
530 | ;; `dynamic-spaces--space-groups') are
531 | ;; preserved.
532 | (backward-char)
533 | (unless (dynamic-spaces-space-group-before-p
534 | (point))
535 | (insert " "))
536 | (delete-char 1)
537 | t)
538 | ((and (eq (char-before) ?\s)
539 | (dynamic-spaces-space-group-before-p
540 | (- (point) 1)))
541 | (backward-delete-char 1)
542 | t)
543 | (t
544 | nil)))
545 | ((> diff 0)
546 | ;; The text has moved to the left. Insert more
547 | ;; space to compensate.
548 | (insert (make-string diff ?\s))
549 | nil)
550 | (t
551 | nil))))))))
552 |
553 |
554 | (defun dynamic-spaces-recycle-marker-list (space-group-list)
555 | "Save markers in SPACE-GROUP-LIST to avoid excessive allocation."
556 | (dolist (space-group space-group-list)
557 | (dynamic-spaces-recycle-marker space-group)))
558 |
559 |
560 | (defun dynamic-spaces-recycle-marker (space-group)
561 | "Save markers in SPACE-GROUP to avoid excessive allocation."
562 | (push (set-marker (car space-group) nil) dynamic-spaces--unused-markers))
563 |
564 |
565 | ;; -------------------------------------------------------------------
566 | ;; Debug support.
567 | ;;
568 |
569 | (defun dynamic-spaces-view-space-group-print-source ()
570 | "Print information about current line and existing space groups."
571 | (save-excursion
572 | (princ dynamic-spaces--space-groups)
573 | (terpri)
574 | ;; `princ' doesn't preserve syntax highlighting.
575 | (let ((src (buffer-substring
576 | (line-beginning-position) (dynamic-spaces-line-end-position))))
577 | (with-current-buffer standard-output
578 | (insert src)))
579 | (terpri)
580 | ;; --------------------
581 | ;; Visualize point as "*"
582 | (princ (make-string (current-column) ?\s))
583 | (princ "*\n")
584 | ;; --------------------
585 | ;; Visualize space group. ("-" is a space, "T" a tab, and ">"
586 | ;; the extra space inserted by the tab.)
587 | (let ((col 0))
588 | (dolist (group dynamic-spaces--space-groups)
589 | (goto-char (car group))
590 | (skip-chars-backward " \t")
591 | (princ (make-string (- (current-column) col) ?\s))
592 | (setq col (current-column))
593 | (while (< (point) (car group))
594 | (if (eq (char-after) ?\s)
595 | (princ "-")
596 | (princ "T")
597 | (setq col (+ col 1))
598 | (princ (make-string (- (save-excursion
599 | (forward-char)
600 | (current-column))
601 | col)
602 | ?>)))
603 | (forward-char)
604 | (setq col (current-column)))))
605 | (terpri)))
606 |
607 |
608 | (defun dynamic-spaces-view-space-groups (key-sequence)
609 | "Display dynamic-space information about the current line.
610 |
611 | Execute the command bound to KEY-SEQUENCE and displays the
612 | current line (with meta information) before the command, after
613 | the command but before adjustments, and after adjustments.
614 |
615 | When executed interactively, the user is prompted for a key
616 | sequence."
617 | (interactive
618 | (list (read-key-sequence "Press keys to run command")))
619 | (let ((dynamic-spaces-commands
620 | (cons 'dynamic-spaces-view-space-groups
621 | dynamic-spaces-commands)))
622 | (dynamic-spaces-pre-command-hook)
623 | (with-output-to-temp-buffer "*DynamicSpaces*"
624 | (princ (format "Beginning of line: %d\n" dynamic-spaces--bol))
625 | (princ (format "Is point inside space group: %s\n"
626 | (if dynamic-spaces--inside-space-group
627 | "yes"
628 | "no")))
629 | (princ "\nSource before:\n")
630 | (dynamic-spaces-view-space-group-print-source)
631 | ;; --------------------
632 | ;; Run command.
633 | (let ((post-command-hook nil))
634 | (execute-kbd-macro key-sequence))
635 | (princ "\nSource after command, before adjustment:\n")
636 | (dynamic-spaces-view-space-group-print-source)
637 | ;; --------------------
638 | ;; After adjustment.
639 | (dynamic-spaces-post-command-hook)
640 | (princ "\nSource after adjustment:\n")
641 | (dynamic-spaces-view-space-group-print-source)
642 | ;; --------------------
643 | (display-buffer standard-output))))
644 |
645 |
646 | ;; -------------------------------------------------------------------
647 | ;; The end
648 | ;;
649 |
650 | (provide 'dynamic-spaces)
651 |
652 | ;;; dynamic-spaces.el ends here
653 |
--------------------------------------------------------------------------------
/test/dynamic-spaces-test-setup.el:
--------------------------------------------------------------------------------
1 | ;;; dynamic-spaces-test-setup.el --- Execute dynaminc-spaces tests. -*- lexical-binding: t; -*-
2 |
3 | ;; Copyright (C) 2017,2025 Anders Lindgren
4 |
5 | ;; Author: Anders Lindgren
6 | ;; Keywords: faces
7 |
8 | ;; This program is free software: you can redistribute it and/or modify
9 | ;; it under the terms of the GNU General Public License as published by
10 | ;; the Free Software Foundation, either version 3 of the License, or
11 | ;; (at your option) any later version.
12 |
13 | ;; This program is distributed in the hope that it will be useful,
14 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
15 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 | ;; GNU General Public License for more details.
17 |
18 | ;; You should have received a copy of the GNU General Public License
19 | ;; along with this program. If not, see .
20 |
21 | ;;; Commentary:
22 |
23 | ;; This package sets up a suitable enviroment for testing
24 | ;; dynamic-spaces, and executes the tests.
25 | ;;
26 | ;; Usage:
27 | ;;
28 | ;; emacs -Q -l dynamic-spaces-test-setup.el
29 | ;;
30 | ;; Note that this package assumes that some packages are located in
31 | ;; specific locations.
32 |
33 | ;;; Code:
34 |
35 | (setq inhibit-startup-screen t)
36 | (prefer-coding-system 'utf-8)
37 |
38 | (defvar dynamic-spaces-test-setup-directory
39 | (if load-file-name
40 | (file-name-directory load-file-name)
41 | default-directory))
42 |
43 | (dolist (dir '("." ".." "../../faceup"))
44 | (add-to-list 'load-path (concat dynamic-spaces-test-setup-directory dir)))
45 |
46 | (require 'dynamic-spaces)
47 | (require 'dynamic-spaces-test)
48 |
49 | (if noninteractive
50 | (ert-run-tests-batch-and-exit)
51 | (ert t))
52 |
53 | ;;; dynamic-spaces-test-setup.el ends here
54 |
--------------------------------------------------------------------------------
/test/dynamic-spaces-test.el:
--------------------------------------------------------------------------------
1 | ;;; dynamic-spaces-test.el --- Tests for dynamic-spaces. -*- lexical-binding: t; -*-
2 |
3 | ;; Copyright (C) 2015,2025 Anders Lindgren
4 |
5 | ;; Author: Anders Lindgren
6 |
7 | ;; This program is free software; you can redistribute it and/or modify
8 | ;; it under the terms of the GNU General Public License as published by
9 | ;; the Free Software Foundation, either version 3 of the License, or
10 | ;; (at your option) any later version.
11 |
12 | ;; This program is distributed in the hope that it will be useful,
13 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | ;; GNU General Public License for more details.
16 |
17 | ;; You should have received a copy of the GNU General Public License
18 | ;; along with this program. If not, see .
19 |
20 | ;;; Commentary:
21 |
22 | ;; Tests for dynamic-spaces.
23 |
24 | ;; Implementation notes:
25 | ;;
26 | ;; Normally, a test case can call a function to check if it performs a
27 | ;; specific task. Unfortunately, for `dynamic-spaces', what a function
28 | ;; does depends on what was bound to a specific key.
29 | ;;
30 | ;; This code test this by queing up a number of key events and then
31 | ;; exit to the main command-loop using recursive edit. By queing
32 | ;; `exit-recursive-edit' as the last event, control is immediately
33 | ;; handed back to the test program.
34 |
35 | ;; Dependencies:
36 | ;;
37 | ;; This package rely in the `faceup' extension to `ert'. The feature
38 | ;; that is used is the ability to define a test function to be it's
39 | ;; own "explainer" function. Also, the multi-line aware
40 | ;; `faceup-test-equal' is used.
41 |
42 | ;;; Code:
43 |
44 | (require 'dynamic-spaces)
45 | (require 'ert)
46 | ;; For `faceup-test-equal'.
47 | (require 'faceup)
48 |
49 |
50 | (defun dynamic-spaces-keys-to-events (keys)
51 | (mapcar (lambda (key)
52 | ;; `kbd' returns "" when applied to " ".
53 | (let ((k (if (equal key " ")
54 | 32
55 | (kbd key))))
56 | (cons t
57 | (cond ((stringp k)
58 | (string-to-char k))
59 | ((vectorp k)
60 | (aref k 0))
61 | (t
62 | k)))))
63 | keys))
64 |
65 | (defun dynamic-spaces-check (before after &rest keys)
66 | "Check that BEFORE is transformed to AFTER if KEYS are pressed."
67 | ;; Que up a number of event, end with `exit-recursive-edit'. Exit
68 | ;; the function to the command-loop (by calling recursive-edit).
69 | ;; Since the last qued event is `exit-recursive-edit', control
70 | ;; immediately returns here.
71 | (let ((unread-command-events
72 | (append
73 | (dynamic-spaces-keys-to-events keys)
74 | ;; Uncomment the following line to stay in recursive edit.
75 | ;;
76 | ;; This is a vector, but after being appled to `append' only
77 | ;; the elements remain.
78 | (kbd "C-M-c") ; Exit recursive edit.
79 | unread-command-events)))
80 | (with-temp-buffer
81 | (dynamic-spaces-mode 1)
82 | (insert before)
83 | ;; Move point to "*", and remove the star.
84 | (goto-char (point-min))
85 | (when (search-forward "*" nil t)
86 | (goto-char (match-beginning 0))
87 | (delete-char 1))
88 | (save-window-excursion
89 | (select-window (display-buffer (current-buffer)))
90 | (recursive-edit))
91 | (insert "*")
92 | (faceup-test-equal after (buffer-substring (point-min)
93 | (point-max))))))
94 | (faceup-defexplainer dynamic-spaces-check)
95 |
96 | (ert-deftest dynamic-spaces-basics ()
97 | ;; ----------------------------------------
98 | ;; Insert
99 |
100 | ;; ----------
101 | ;; Plain inserts
102 | (should (dynamic-spaces-check
103 | "alpha* beta"
104 | "alphax* beta"
105 | "x"))
106 | (should (dynamic-spaces-check
107 | "alpha* beta"
108 | "alpha * beta"
109 | " "))
110 |
111 | (should (dynamic-spaces-check
112 | "alpha* beta"
113 | "alphax* beta"
114 | "x"))
115 | (should (dynamic-spaces-check
116 | "alpha* beta"
117 | "alpha * beta"
118 | " "))
119 |
120 | (should (dynamic-spaces-check
121 | "alpha* beta"
122 | "alphax* beta"
123 | "x"))
124 | (should (dynamic-spaces-check
125 | "alpha* beta"
126 | "alpha * beta"
127 | " "))
128 |
129 | ;; Special case: Inserting a character in the middle of a space
130 | ;; group should not be compensated.
131 | (should (dynamic-spaces-check
132 | "alpha * beta"
133 | "alpha x* beta"
134 | "x"))
135 | ;; Special case: It should be possible to press space up to the next
136 | ;; space group.
137 | (should (dynamic-spaces-check
138 | "alpha * beta"
139 | "alpha *beta"
140 | " "))
141 |
142 |
143 | ;; ----------
144 | ;; Inserts with two groups.
145 | (should (dynamic-spaces-check
146 | "alpha* beta gamma"
147 | "alphax* beta gamma"
148 | "x"))
149 | (should (dynamic-spaces-check
150 | "alpha* beta gamma"
151 | "alphax* beta gamma"
152 | "x"))
153 | (should (dynamic-spaces-check
154 | "alphax* beta gamma"
155 | "alphaxx* beta gamma"
156 | "x"))
157 | ;; ----------
158 | ;; Inserts a space and more text.
159 | (should (dynamic-spaces-check
160 | "alpha* beta"
161 | "alpha x* beta"
162 | " "
163 | "x"))
164 | ;; ----------
165 | ;; Inserts a character that splits a space group, with one space at
166 | ;; the end should not insert extra space.
167 | (should (dynamic-spaces-check
168 | "alpha * beta"
169 | "alpha x* beta"
170 | "x"))
171 |
172 | (should (dynamic-spaces-check
173 | "alpha * beta"
174 | "alpha xxxx* beta"
175 | "C-u"
176 | "x"))
177 |
178 | (should (dynamic-spaces-check
179 | "alpha * beta"
180 | "alpha xxxx* beta"
181 | "C-u"
182 | "x"))
183 |
184 | (should (dynamic-spaces-check
185 | "alpha * beta"
186 | "alpha xxxx* beta"
187 | "C-u"
188 | "x"))
189 |
190 | (should (dynamic-spaces-check
191 | "alpha * beta"
192 | "alpha xxxx* beta"
193 | "C-u"
194 | "x"))
195 |
196 | (should (dynamic-spaces-check
197 | "alpha * beta"
198 | "alpha xxxx* beta"
199 | "C-u"
200 | "x"))
201 |
202 | (should (dynamic-spaces-check
203 | "alpha * beta"
204 | "alpha xxxx* beta"
205 | "C-u"
206 | "x"))
207 |
208 | ;; Here, no extra space should be inserted.
209 | (should (dynamic-spaces-check
210 | "alpha * beta"
211 | "alpha xxxx* beta"
212 | "C-u"
213 | "x"))
214 |
215 | ;; ----------------------------------------
216 | ;; Delete
217 |
218 | ;; ----------
219 | ;; Delete backward
220 | (should (dynamic-spaces-check
221 | "alpha* beta"
222 | "alph* beta"
223 | "DEL"))
224 | ;; ----------
225 | ;; Delete backward at bol (ensure that last space is retained).
226 | (should (dynamic-spaces-check
227 | "alpha beta\n*gamma delta"
228 | "alpha beta*gamma delta"
229 | "DEL")))
230 |
231 |
232 | (ert-deftest dynamic-spaces-delete-beginning-end ()
233 | (should (dynamic-spaces-check
234 | "1234* 5678"
235 | "1234* 5678"
236 | "C-d"))
237 |
238 | (should (dynamic-spaces-check
239 | "* 5678"
240 | "* 5678"
241 | "C-d"))
242 |
243 | (should (dynamic-spaces-check
244 | " * 5678"
245 | " * 5678"
246 | "C-d"))
247 |
248 | (should (dynamic-spaces-check
249 | "123*4 5678"
250 | "123* 5678"
251 | "C-d"))
252 |
253 | (should (dynamic-spaces-check
254 | "12*34 5678"
255 | "12*4 5678"
256 | "C-d"))
257 |
258 | (should (dynamic-spaces-check
259 | "alpha* beta gamma delta"
260 | "alpha* gamma delta"
261 | "M-d"))
262 |
263 | (should (dynamic-spaces-check
264 | "1234 *5678"
265 | "1234 *5678"
266 | "DEL"))
267 |
268 | (should (dynamic-spaces-check
269 | "1234 * 5678"
270 | "1234 * 5678"
271 | "DEL"))
272 |
273 | (should (dynamic-spaces-check
274 | "1234 *5678"
275 | "*5678"
276 | "M-DEL"))
277 |
278 | (should (dynamic-spaces-check
279 | "1234 * 5678"
280 | "* 5678"
281 | "M-DEL"))
282 |
283 | nil)
284 |
285 |
286 | (ert-deftest dynamic-spaces-two-space-groups ()
287 | "Check that some small space groups are ignored.
288 |
289 | In normal editing, such as when deleting a word, small space
290 | groups tend to be created. Check that these are ignored."
291 | ;; Deleting words.
292 | (should (dynamic-spaces-check
293 | "alpha beta gamma* delta"
294 | "alpha * delta"
295 | "M-DEL"
296 | "M-DEL"))
297 | (should (dynamic-spaces-check
298 | "alpha * beta"
299 | "* beta"
300 | "M-DEL"))
301 |
302 | nil)
303 |
304 |
305 | (ert-deftest dynamic-spaces-tab ()
306 | ;; ----------------------------------------
307 | ;; Insert
308 |
309 | ;; Inserting a character before a tab (so that it doesn't push it
310 | ;; over a tab stop), the spaces doesn't have to be adjusted.
311 | (should (dynamic-spaces-check
312 | "12*\tbeta"
313 | "12x*\tbeta"
314 | "x"))
315 | ;; When inserting a charcter so that a tab is pushed over a tab
316 | ;; stop, the tab character must be removed.
317 | (should (dynamic-spaces-check
318 | "1234567*\t\tbeta"
319 | "1234567x*\tbeta"
320 | "x"))
321 | ;; Special case: A single column tab character counts as a space group.
322 | ;; Ensure that it's replace with two spaces when inserting before it.
323 | (should (dynamic-spaces-check
324 | "1234567*\tbeta"
325 | "1234567x* beta"
326 | "x"))
327 |
328 | ;; ----------------------------------------
329 | ;; Delete
330 | (should (dynamic-spaces-check
331 | "12*\tbeta"
332 | "1*\tbeta"
333 | "DEL"))
334 | ;; Deleting a character so that a tab moves past a tab stop.
335 | ;;
336 | ;; (Note; In this case, the package could also have inserted another
337 | ;; tab character, but the effect is the same.)
338 | (should (dynamic-spaces-check
339 | "12345678*\tbeta"
340 | "1234567*\t beta"
341 | "DEL"))
342 |
343 | )
344 |
345 |
346 | (ert-deftest dynamic-spaces-strings ()
347 | ;; Spaces inside strings should never be adjusted.
348 | (should (dynamic-spaces-check
349 | "abc* \"inside string\""
350 | "abcx* \"inside string\""
351 | "x"))
352 | ;; Spaces after strings should be adjusted.
353 | (should (dynamic-spaces-check
354 | "abc* \"inside string\" def"
355 | "abcx* \"inside string\" def"
356 | "x"))
357 | ;; Spaces after strings should be adjusted, even if point is in string.
358 | (should (dynamic-spaces-check
359 | "\"inside* string\" def"
360 | "\"insidex* string\" def"
361 | "x"))
362 | (should (dynamic-spaces-check
363 | " *alpha beta"
364 | " \"*alpha beta"
365 | "\""))
366 | ;; Deleting a quote makes the rest of the line a string, so it
367 | ;; should not be adjusted.
368 | (should (dynamic-spaces-check
369 | "alpha \"\"* beta"
370 | "alpha \"* beta"
371 | "DEL"))
372 | ;; Deleting the leading quote should not readjust the spaces inside
373 | ;; the origibal string. Not should it adjust spaces after the
374 | ;; string, as they are part of a new string.
375 | (should (dynamic-spaces-check
376 | "*\"inside string\" def"
377 | "*inside string\" def"
378 | ""))
379 | (should (dynamic-spaces-check
380 | "\"*inside string\" def"
381 | "*inside string\" def"
382 | "DEL"))
383 | ;; Ditto, when inserting a quote.
384 | (should (dynamic-spaces-check
385 | "*inside string\" def"
386 | "\"*inside string\" def"
387 | "\"")))
388 |
389 |
390 | (ert-deftest dynamic-spaces-sentence-end ()
391 | "Double spaces at the end of a sentence doesn't count as a space group."
392 | (let ((sentence-end-double-space t))
393 | ;; Two spaces after a space should not be considered a space group.
394 | (should (dynamic-spaces-check
395 | "a*bc. def"
396 | "*bc. def"
397 | "DEL"))
398 | ;; Three spaces should.
399 | (should (dynamic-spaces-check
400 | "a*bc. def"
401 | "*bc. def"
402 | "DEL"))
403 | ;; A tab should.
404 | (should (dynamic-spaces-check
405 | "a*bc.\tdef"
406 | "*bc.\tdef"
407 | "DEL"))
408 | ;; A tab should (on a tab boundary).
409 | (should (dynamic-spaces-check
410 | "1*234567.\tdef"
411 | "*234567.\t def"
412 | "DEL"))
413 | ;; A space and a tab should.
414 | (should (dynamic-spaces-check
415 | "a*bc. \tdef"
416 | "*bc. \tdef"
417 | "DEL"))
418 | ;; A space and a tab should (on a tab boundary).
419 | (should (dynamic-spaces-check
420 | "1*23456. \tdef"
421 | "*23456. \t def"
422 | "DEL"))))
423 |
424 |
425 | (ert-deftest dynamic-spaces-insert-sentence-end ()
426 | "Test inserting before a sentence end."
427 | (let ((sentence-end-double-space nil))
428 | (should (dynamic-spaces-check
429 | "a*bc. def"
430 | "ax*bc. def"
431 | "x")))
432 | (let ((sentence-end-double-space t))
433 | (should (dynamic-spaces-check
434 | "a*bc. def"
435 | "ax*bc. def"
436 | "x"))))
437 |
438 |
439 |
440 | (provide 'dynamic-spaces-test)
441 |
442 | ;;; dynamic-spaces-test.el ends here
443 |
--------------------------------------------------------------------------------