├── 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 | --------------------------------------------------------------------------------