branch: externals/shift-number
commit a71f5c3c77affc27a3d0bf16886a8a3b03c05044
Author: Campbell Barton <[email protected]>
Commit: Campbell Barton <[email protected]>

    Re-write: replace the existing logic with evil-numbers
    
    - Support hex/binary/octal numbers, subscript and superscript numbers.
    - Add incremental shifting (within region).
    - Add option to increment directional (based on the region).
    - Add tests.
    - Bump minimum supported emacs version to 28.1
---
 readme.rst                  |   95 +++-
 shift-number.el             | 1082 +++++++++++++++++++++++++++++++++----------
 tests/shift-number-tests.el |  776 ++++++++++++++++++++++++++++++-
 tests/shift-number-tests.sh |    2 +-
 4 files changed, 1682 insertions(+), 273 deletions(-)

diff --git a/readme.rst b/readme.rst
index ae952992e0..d5f3832c56 100644
--- a/readme.rst
+++ b/readme.rst
@@ -9,6 +9,15 @@ About
 This Emacs package provides commands to increase and decrease the number
 at point (or the next number on the current line).
 
+Supported number formats:
+
+- Decimal: ``42``, ``-17``, ``+5``, ``007``
+- Binary: ``0b101``, ``0B1010``
+- Octal: ``0o42``, ``0O755``
+- Hexadecimal: ``0xBEEF``, ``0xcafe``
+- Superscript: ``⁴²``, ``⁻¹``
+- Subscript: ``₄₂``, ``₋₁``
+
 
 Installation
 ============
@@ -32,6 +41,8 @@ For the manual installation, clone the repository, add the 
directory to
    (add-to-list 'load-path "/path/to/shift-number-dir")
    (autoload 'shift-number-up "shift-number" nil t)
    (autoload 'shift-number-down "shift-number" nil t)
+   (autoload 'shift-number-up-incremental "shift-number" nil t)
+   (autoload 'shift-number-down-incremental "shift-number" nil t)
 
 
 Usage
@@ -42,7 +53,7 @@ As you can see in the gif demonstration:
 - ``M-x shift-number-up`` increases the current number.
 
   If there is no number at point, the first number between the current 
position and the end of line is increased.
-  With a numeric prefix ARG, the number is increased for this ARG.
+  With a numeric prefix ARG, the number is increased by ARG.
 
 - ``M-x shift-number-down`` decreases the current number.
 
@@ -54,38 +65,96 @@ You may bind some keys to these commands in a usual manner, 
for example:
    (global-set-key (kbd "M-_") 'shift-number-down)
 
 
+Region and Rectangle Support
+----------------------------
+
+When a region is active, all numbers in the region are modified.
+Rectangle selections (via ``rectangle-mark-mode``) are also supported.
+
+
+Incremental Mode
+----------------
+
+For incrementing multiple numbers with progressively increasing values,
+use the incremental commands:
+
+- ``M-x shift-number-up-incremental`` increases each number progressively
+  (first by 1, second by 2, etc.).
+
+- ``M-x shift-number-down-incremental`` decreases each number progressively.
+
+These commands operate on all numbers between point and mark.
+The mark must be set, but the region does not need to be active.
+
+This is useful for creating sequences. For example, with ``0 0 0`` between
+point and mark, running ``shift-number-up-incremental`` produces ``1 2 3``.
+
+The incremental count continues across lines and rectangle cells, so with
+three lines of ``0 0 0`` between point and mark, incrementing produces::
+
+   1 2 3
+   4 5 6
+   7 8 9
+
+
 Custom Variables
 ----------------
 
-``shift-number-motion``: ``nil``
-   When non-nil, move the cursor to the end of the number,
-   set the ``mark`` to the beginning.
 ``shift-number-negative``: ``t``
    When non-nil, support negative numbers.
 
+``shift-number-motion``: ``nil``
+   Control cursor movement after modifying a number. Options are:
+
+   - ``nil``: cursor stays at beginning of number, mark unchanged.
+   - ``t``: cursor moves to end of number, mark unchanged.
+   - ``mark``: cursor moves to end of number, mark set to beginning.
+
+``shift-number-pad-default``: ``nil``
+   When non-nil, preserve the number's width when it shrinks
+   (e.g., ``10`` becomes ``09`` when decremented).
 
-Similar packages
+``shift-number-separator-chars``: ``nil``
+   A string of separator characters allowed in numeric literals for visual
+   grouping. For example, set to ``"_"`` to support numbers like ``1_000_000``.
+
+``shift-number-case``: ``nil``
+   Case to use for hexadecimal numbers. Options are:
+
+   - ``nil``: preserve current case
+   - ``upcase``: use upper case (A-F)
+   - ``downcase``: use lower case (a-f)
+
+``shift-number-incremental-direction-from-region``: ``t``
+   When non-nil, reverse incremental direction when point is before mark.
+   With point before mark, ``shift-number-up-incremental`` on ``0 0 0``
+   produces ``3 2 1`` instead of ``1 2 3``.
+
+
+Related packages
 ================
 
-There are other packages for the same task (modifying the number at
-point):
+Other packages for a similar task (modifying the number at point):
 
 - `operate-on-number <https://github.com/knu/operate-on-number.el>`__
 - `number <https://github.com/chrisdone/number>`__
-- `evil-numbers <https://github.com/cofi/evil-numbers>`__
 
 Comparing with them, ``shift-number`` has the following distinctions:
 
 - If there is no number at point, it operates on the next number on the
   current line.
 
-- The point does not move anywhere when a number is modified.
+- The point does not move anywhere when a number is modified
+  (unless ``shift-number-motion`` is enabled).
 
 - If a number has leading zeros (for example ``007``), they are preserved
   during shifting.
 
-- It is simple: only shifting up/down is available, no multiplication or
-  other more complex stuff.
+- Supports multiple number formats: decimal, binary, octal, hexadecimal,
+  superscript, and subscript.
+
+- Supports region and rectangle selections.
 
-- It does not prompt for any additional input: you just press a key
-  bound to ``shift-number-{up/down}`` command and the number is changing.
+`evil-numbers <https://github.com/juliapath/evil-numbers>`__
+   Uses ``shift-number`` as its backend, providing Evil (Vim emulation)
+   integration with operators and visual state support.
diff --git a/shift-number.el b/shift-number.el
index b20d83c872..2de5f36c04 100644
--- a/shift-number.el
+++ b/shift-number.el
@@ -8,8 +8,8 @@
 ;; URL: https://codeberg.org/ideasman42/emacs-shift-number
 ;; Keywords: convenience
 ;; Created: 12 Apr 2016
-;; Version: 0.1
-;; Package-Requires: ((emacs "24.1"))
+;; Version: 0.2
+;; Package-Requires: ((emacs "28.1"))
 
 ;;; Commentary:
 
@@ -23,10 +23,13 @@
 ;; (autoload 'shift-number-down "shift-number" nil t)
 
 ;; For more verbose description and a gif demonstration, see
-;; <https://github.com/alezost/shift-number.el>.
+;; <https://codeberg.org/ideasman42/emacs-shift-number>.
 
 ;;; Code:
 
+(eval-when-compile
+  ;; For `pcase-dolist'.
+  (require 'pcase))
 
 ;; ---------------------------------------------------------------------------
 ;; Compatibility
@@ -44,6 +47,16 @@
       (gv-letplace (getter setter) place
         (funcall setter `(- ,getter ,(or delta 1)))))))
 
+(when (version< emacs-version "29.1")
+  (defsubst pos-bol (&optional n)
+    "Return the position at the line beginning.
+N specifies which line (default 1, the current line)."
+    (line-beginning-position n))
+  (defsubst pos-eol (&optional n)
+    "Return the position at the line end.
+N specifies which line (default 1, the current line)."
+    (line-end-position n)))
+
 
 ;; ---------------------------------------------------------------------------
 ;; Custom Variables
@@ -57,219 +70,643 @@
   :type 'boolean)
 
 (defcustom shift-number-motion nil
-  "If non-nil, move the point to the end of the number.
-The `mark' is set the the beginning of the number."
+  "Control cursor movement after modifying a number.
+- nil: cursor stays at beginning of number, mark unchanged.
+- t: cursor moves to end of number, mark unchanged.
+- `mark': cursor moves to end of number, mark set to beginning."
+  :type
+  '(choice
+    (const :tag "No motion" nil) (const :tag "Motion" t) (const :tag "Motion 
with mark" mark)))
+
+(defcustom shift-number-pad-default nil
+  "Whether to preserve number width when it shrinks.
+When non-nil, decrementing 10 gives 09 instead of 9."
+  :type 'boolean)
+
+(defcustom shift-number-separator-chars nil
+  "Separator characters allowed in numeric literals for visual grouping.
+
+This value is a string containing separator characters,
+typically \"_\" or \",\" which are allowed in numeric literals in some systems.
+
+Set to nil to disable this functionality."
+  :type '(choice (const nil) string))
+
+(defcustom shift-number-case nil
+  "Case to use for hexadecimal numbers."
+  :type
+  '(choice
+    (const :tag "Current Case" nil)
+    (const :tag "Upper Case" upcase)
+    (const :tag "Lower Case" downcase)))
+
+(defcustom shift-number-incremental-direction-from-region t
+  "When non-nil, reverse incremental direction when point is before mark.
+With point before mark, `shift-number-up-incremental' on \"0 0 0\"
+produces \"3 2 1\" instead of \"1 2 3\"."
   :type 'boolean)
 
 (declare-function apply-on-rectangle "rect")
 
+
 ;; ---------------------------------------------------------------------------
-;; Private Variables/Constants
+;; Internal Constants
+
+(defconst shift-number--chars-superscript "⁰¹²³⁴⁵⁶⁷⁸⁹"
+  "String containing superscript digit characters 0-9.")
+(defconst shift-number--chars-subscript "₀₁₂₃₄₅₆₇₈₉"
+  "String containing subscript digit characters 0-9.")
+
+;; Helper for building script alists below.
+(defun shift-number--build-script-alist (minus-char plus-char digit-chars)
+  "Build an alist mapping ASCII characters to script equivalents.
+MINUS-CHAR is the script minus sign.
+PLUS-CHAR is the script plus sign.
+DIGIT-CHARS is a 10-character string of script digits 0-9."
+  (cons
+   (cons ?- minus-char)
+   (cons
+    (cons ?+ plus-char)
+    (mapcar
+     (lambda (i)
+       (cons (string-to-char (number-to-string i)) (aref digit-chars i)))
+     (number-sequence 0 9)))))
+
+(defconst shift-number--superscript-alist
+  (shift-number--build-script-alist ?⁻ ?⁺ shift-number--chars-superscript)
+  "Alist mapping regular characters to superscript equivalents.")
+
+(defconst shift-number--subscript-alist
+  (shift-number--build-script-alist ?₋ ?₊ shift-number--chars-subscript)
+  "Alist mapping regular characters to subscript equivalents.")
+
+(defconst shift-number--superscript-alist-decode
+  (mapcar (lambda (x) (cons (cdr x) (car x))) shift-number--superscript-alist)
+  "Alist mapping superscript characters to regular equivalents.")
+
+(defconst shift-number--subscript-alist-decode
+  (mapcar (lambda (x) (cons (cdr x) (car x))) shift-number--subscript-alist)
+  "Alist mapping subscript characters to regular equivalents.")
 
-;; Regexp for `shift-number' function.
-;; The first parenthesized expression must match the number.
-(defconst shift-number--regexp "\\([[:digit:]]+\\)")
 
 ;; ---------------------------------------------------------------------------
-;; Private Functions
+;; Internal String Separator Utilities
+
+(defun shift-number--strip-chars (str sep-chars)
+  "Remove SEP-CHARS from STR."
+  (dotimes (i (length sep-chars))
+    (let ((ch (char-to-string (aref sep-chars i))))
+      (setq str (replace-regexp-in-string (regexp-quote ch) "" str t t))))
+  str)
+
+(defun shift-number--strip-chars-apply (str-src str-dst sep-chars)
+  "Restore SEP-CHARS positions from STR-SRC into STR-DST."
+  (let ((sep-chars-list (append sep-chars nil))
+        ;; Convert strings to lists.
+        (str-src-rev (nreverse (append str-src nil)))
+        (str-dst-rev (nreverse (append str-dst nil)))
+        (result (list)))
+    (while str-dst-rev
+      (let ((ch-src (pop str-src-rev)))
+        (cond
+         ((and ch-src (memq ch-src sep-chars-list))
+          (push ch-src result))
+         (t
+          (push (pop str-dst-rev) result)))))
+    (apply #'string result)))
 
-(defmacro shift-number--swap-vars (i j)
-  "Swap the value of I & J."
-  `(setq ,i
-         (prog1 ,j
-           (setq ,j ,i))))
 
+;; ---------------------------------------------------------------------------
+;; Internal Utilities
+
+(defun shift-number--case-category (str default)
+  "Categorize the case of STR.
+Return DEFAULT if STR has no case (e.g., digits only), otherwise:
+-   1: Upper case.
+-  -1: Lower case.
+- nil: Mixed case."
+  (let ((str-dn (downcase str))
+        (str-up (upcase str)))
+    (cond
+     ((string-equal str str-dn)
+      (cond
+       ((string-equal str str-up)
+        default)
+       (t
+        -1)))
+     (t
+      (cond
+       ((string-equal str str-up)
+        1)
+       (t
+        nil))))))
+
+(defun shift-number--format-binary (number &optional width fillchar)
+  "Format NUMBER as binary.
+Fill up to WIDTH with FILLCHAR (defaults to ?0) if binary
+representation of NUMBER is smaller than WIDTH."
+  (let ((nums (list))
+        (fillchar (or fillchar ?0)))
+    (while (> number 0)
+      (push (number-to-string (% number 2)) nums)
+      (setq number (truncate number 2)))
+    (let ((len (length nums)))
+      (apply #'concat
+             (cond
+              ((and width (< len width))
+               (make-string (- width len) fillchar))
+              (t
+               ""))
+             nums))))
+
+(defun shift-number--format (num width base)
+  "Format NUM with at least WIDTH digits in BASE."
+  (cond
+   ((= base 2)
+    (shift-number--format-binary num width))
+   ((= base 8)
+    (format (format "%%0%do" width) num))
+   ((= base 16)
+    (format (format "%%0%dX" width) num))
+   ((= base 10)
+    (format (format "%%0%dd" width) num))
+   (t
+    "")))
+
+(defun shift-number--skip-chars-impl (ch-skip ch-sep-optional dir ch-num limit)
+  "Wrapper for `skip-chars-forward' and `skip-chars-backward'.
+
+CH-SKIP: Characters to skip.
+CH-SEP-OPTIONAL: Separator characters (single instances are stepped over).
+DIR: Direction to step in (1 or -1).
+CH-NUM: Number of characters to step.
+LIMIT: Point which will not be stepped past."
+  (let* ((is-forward (< 0 dir))
+         (skip-chars-fn
+          (cond
+           (is-forward
+            #'skip-chars-forward)
+           (t
+            #'skip-chars-backward)))
+         (clamp-fn
+          (cond
+           (is-forward
+            #'min)
+           (t
+            #'max)))
+         (skipped
+          (abs
+           (funcall skip-chars-fn
+                    ch-skip
+                    ;; Limit.
+                    (funcall clamp-fn (+ (point) (* ch-num dir)) limit)))))
+
+    ;; Step over single separators, as long as there is a number after them.
+    ;; Allow '100,123' and '16_777_216' to be handled as single numbers.
+    (when ch-sep-optional
+      (let ((point-next nil)
+            (skipped-next 0))
+        (setq ch-num (- ch-num skipped))
+        (while (and (not (zerop ch-num))
+                    (save-excursion
+                      (and (eq 1 (shift-number--skip-chars-impl 
ch-sep-optional nil dir 1 limit))
+                           (progn
+                             ;; Not counted towards 'skipped'
+                             ;; as this character is to be ignored entirely.
+                             (setq skipped-next
+                                   (shift-number--skip-chars-impl ch-skip nil 
dir ch-num limit))
+                             (unless (zerop skipped-next)
+                               (setq point-next (point))
+                               ;; Found (apply `point-next').
+                               t)))))
+          ;; Step over the separator and contents found afterwards.
+          (when point-next
+            (goto-char point-next)
+            (setq skipped (+ skipped skipped-next))
+            (setq ch-num (- ch-num skipped-next))
+            t))))
+
+    skipped))
+
+(defun shift-number--match-from-skip-chars (match-chars dir limit do-check 
do-match)
+  "Match MATCH-CHARS in DIR (-1 or 1), until LIMIT.
+
+When DO-CHECK is non-nil, any failure to match returns nil.
+When DO-MATCH is non-nil, match data is set.
+
+Each item in MATCH-CHARS is a list of (CH-SKIP CH-NUM CH-SEP-OPTIONAL).
+- CH-SKIP is the argument to pass to
+  `skip-chars-forward' or `skip-chars-backward'.
+- CH-NUM specifies how many characters to match.
+  Valid values:
+  - Symbol `+' one or more.
+  - Symbol `*' zero or more.
+  - An integer: match exactly this many.
+- CH-SEP-OPTIONAL specifies optional separator characters."
+  (catch 'result
+    (let* ((is-forward (< 0 dir))
+           (point-init (point))
+           ;; Fill when `do-match' is set.
+           (match-list (list)))
+
+      ;; Sanity check.
+      (when (cond
+             (is-forward
+              (> (point) limit))
+             (t
+              (< (point) limit)))
+        (error "Limit is on wrong side of point (internal error)"))
+
+      (unless is-forward
+        (setq match-chars (reverse match-chars)))
+
+      (pcase-dolist (`(,ch-skip ,ch-num ,ch-sep-optional) match-chars)
+        ;; Beginning of the match.
+        (when do-match
+          (push (point) match-list))
 
-(defun shift-number--replace-in-region (str beg end)
-  "Utility to replace region from BEG to END with STR.
-Return the region replaced."
-  (declare (important-return-value nil))
+        (cond
+         ((integerp ch-num)
+          (let ((skipped (shift-number--skip-chars-impl ch-skip 
ch-sep-optional dir ch-num limit)))
+            (when do-check
+              (unless (eq skipped ch-num)
+                (throw 'result nil)))))
+         ((eq ch-num '+)
+          (let ((skipped
+                 (shift-number--skip-chars-impl
+                  ch-skip ch-sep-optional dir most-positive-fixnum limit)))
+            (when do-check
+              (unless (>= skipped 1)
+                (throw 'result nil)))))
+
+         ;; No length checking needed as zero is acceptable.
+         ;; Skip these characters if they exist.
+         ((eq ch-num '*)
+          (shift-number--skip-chars-impl ch-skip ch-sep-optional dir 
most-positive-fixnum limit))
+         ((eq ch-num '\?)
+          (shift-number--skip-chars-impl ch-skip ch-sep-optional dir 1 limit))
+         (t
+          (error "Unknown type %S (internal error)" ch-num)))
+
+        ;; End of the match.
+        (when do-match
+          (push (point) match-list)))
+
+      ;; Add match group 0 (full match) at the beginning of the list.
+      (when do-match
+        (setq match-list
+              (cond ; `point-init' `point' `match-list'.
+               (is-forward
+                (cons point-init (cons (point) (nreverse match-list))))
+               (t ; `point' `point-init' `match-list'.
+                (cons (point) (cons point-init match-list)))))
+
+        (set-match-data match-list)))
+    t))
 
-  (let ((len (length str))
-        (i-beg nil)
-        (i-end nil)
-        (i-end-ofs nil))
 
-    ;; Check for skip end.
-    (let ((i 0))
-      (let ((len-test (min (- end beg) len)))
-        (while (< i len-test)
-          (let ((i-next (1+ i)))
-            (cond
-             ((eq (aref str (- len i-next)) (char-after (- end i-next)))
-              (setq i i-next))
-             (t ; Break.
-              (setq len-test i))))))
-      (unless (zerop i)
-        (setq i-end (- len i))
-        (decf len i)
-        (decf end i)
-        (setq i-end-ofs i)))
-
-    ;; Check for skip start.
-    (let ((i 0))
-      (let ((len-test (min (- end beg) len)))
-        (while (< i len-test)
-          (cond
-           ((eq (aref str i) (char-after (+ beg i)))
-            (incf i))
-           (t ; Break.
-            (setq len-test i)))))
-      (unless (zerop i)
-        (setq i-beg i)
-        (incf beg i)))
-
-    (when (or i-beg i-end)
-      (setq str (substring str (or i-beg 0) (or i-end len))))
-
-    (goto-char beg)
-
-    (unless (eq beg end)
-      (delete-region beg end))
-    (unless (string-empty-p str)
-      (insert str))
-
-    (when i-end-ofs
-      ;; Leave the cursor where it would be if the end wasn't clipped.
-      (goto-char (+ (point) i-end-ofs)))
-    (cons beg (+ beg (length str)))))
-
-(defun shift-number--in-regexp-p (regexp pos limit-beg limit-end)
-  "Return non-nil, if POS is inside REGEXP on the current line."
-  ;; The code originates from `org-at-regexp-p'.
-  (save-excursion
-    (let ((found nil)
-          (exit nil))
-      (goto-char limit-beg)
-      (while (and (null (or exit found)) (re-search-forward regexp limit-end 
t))
-        (cond
-         ((> (match-beginning 0) pos)
-          (setq exit t))
-         ((>= (match-end 0) pos)
-          (setq found t))))
-      found)))
+;; ---------------------------------------------------------------------------
+;; Internal Script Translation
+
+(defun shift-number--translate-with-alist (alist string)
+  "Translate every character in STRING using ALIST."
+  (funcall (cond
+            ((stringp string)
+             #'concat)
+            (t
+             #'identity))
+           (mapcar (lambda (c) (cdr (assoc c alist))) string)))
+
+(defun shift-number--encode-super (x)
+  "Convert string X to superscript."
+  (shift-number--translate-with-alist shift-number--superscript-alist x))
+(defun shift-number--decode-super (x)
+  "Convert string X from superscript to regular characters."
+  (shift-number--translate-with-alist shift-number--superscript-alist-decode 
x))
+
+(defun shift-number--encode-sub (x)
+  "Convert string X to subscript."
+  (shift-number--translate-with-alist shift-number--subscript-alist x))
+(defun shift-number--decode-sub (x)
+  "Convert string X from subscript to regular characters."
+  (shift-number--translate-with-alist shift-number--subscript-alist-decode x))
 
-(defun shift-number--impl (n pos limit-beg limit-end)
-  "Change the number at point by N.
-If there is no number at point, search forward till the end of
-the current line and change it.
-
-Search backwards from LIMIT-BEG for a number overlapping POS.
-Otherwise search forward limited by LIMIT-END."
-  ;; The whole number is removed and a new number is inserted in its
-  ;; place, so `save-excursion' is not used, as it will put the point at
-  ;; the beginning of the number.  Instead, the point is saved and
-  ;; restored later.
-  (let ((num-bounds nil)
-        (has-sign nil)
-        ;; Allow numbers to become negative.
-        (use-sign shift-number-negative))
 
-    (save-match-data
-      (when (or (and (< limit-beg pos)
-                     (shift-number--in-regexp-p shift-number--regexp pos 
limit-beg limit-end))
-                (re-search-forward shift-number--regexp limit-end t))
-        (let ((beg (match-beginning 1))
-              (end (match-end 1)))
-          (setq num-bounds (cons beg end))
-
-          ;; Only detect a sign when negative numbers are supported.
-          (when (and use-sign (< limit-beg beg))
-            (let ((ch (char-before beg)))
+;; ---------------------------------------------------------------------------
+;; Internal Implementation
+
+(defun shift-number--inc-at-pt-impl-with-match-chars
+    (match-chars
+     ;; Numeric & other options.
+     sign-group num-group base beg end padded do-case
+     ;; Callbacks.
+     range-check-fn number-xform-fn decode-fn encode-fn)
+  "Perform the increment/decrement within BEG and END.
+
+For MATCH-CHARS docs see `shift-number--match-from-skip-chars'.
+NUM-GROUP is the match group used to evaluate the number.
+SIGN-GROUP is the match group used for the sign ('-' or '+').
+
+When PADDED is non-nil,
+the number keeps its current width (with leading zeroes).
+When PADDED is the symbol `auto', detect leading zeros and preserve width.
+
+When RANGE-CHECK-FN is non-nil, it's called with the match beginning & end.
+A nil result causes this function to skip this match.
+
+When DO-CASE is non-nil, apply case handling for hexadecimal numbers.
+
+DECODE-FN converts the matched string to regular characters for parsing.
+ENCODE-FN converts the result back for replacement.
+
+When all characters are found in sequence, evaluate the number in BASE,
+replacing it by the result of NUMBER-XFORM-FN.
+Return (OLD-BEG . OLD-END) on success, nil on failure."
+  (save-match-data
+    (when (and (save-excursion
+                 ;; Skip backwards (as needed), there may be no
+                 ;; characters to skip back, so don't check the result.
+                 (shift-number--match-from-skip-chars match-chars -1 beg nil 
nil)
+                 ;; Skip forwards from the beginning, setting match data.
+                 (shift-number--match-from-skip-chars match-chars 1 end t t))
+
+               ;; Either there is no range checking or the range must
+               ;; be accepted by the caller.
+               (or (null range-check-fn)
+                   (funcall range-check-fn (match-beginning 0) (match-end 0))))
+
+      ;; Capture old bounds before any modification.
+      (let* ((old-beg (match-beginning 0))
+             (old-end (match-end 0))
+             (sep-char (nth 2 (nth (1- num-group) match-chars)))
+             (num-str-raw (match-string num-group))
+             (sign-str (match-string sign-group))
+             ;; Check if sign is preceded by a decimal digit (e.g., "123-456").
+             ;; If so, ignore the sign to avoid treating it as a negative 
number.
+             ;; Only check for 0-9 to allow "1e-10" scientific notation to 
work.
+             (sign-preceded-by-digit
+              (and sign-str
+                   (not (string-empty-p sign-str)) (> (match-beginning 
sign-group) beg)
+                   (let ((ch-before (char-before (match-beginning 
sign-group))))
+                     (and ch-before (>= ch-before ?0) (<= ch-before ?9)))))
+             (use-sign (and shift-number-negative (not 
sign-preceded-by-digit)))
+             (str-prev
+              (funcall decode-fn
+                       (concat
+                        (cond
+                         (use-sign
+                          sign-str)
+                         (t
+                          ""))
+                        num-str-raw)))
+
+             (str-prev-strip
               (cond
-               ((eq ?- ch)
-                (setq has-sign t))
-               ((eq ?+ ch)
-                (setq has-sign t)))
-
-              ;; Ignore the sign when immediately preceded by a number, e.g. 
`123-456'.
-              (when (and has-sign (< limit-beg (1- beg)))
-                (save-excursion
-                  (goto-char (1- beg))
-                  (unless (zerop (skip-chars-backward "0-9" (- beg 2)))
-                    ;; Don't allow negative numbers otherwise
-                    ;; `1-0' would subtract zero to make `1--0'.
-                    (setq use-sign nil)
-                    (setq has-sign nil)))))))))
+               (sep-char
+                (shift-number--strip-chars str-prev sep-char))
+               (t
+                str-prev)))
 
-    (cond
-     (num-bounds
-      (let* ((beg (car num-bounds))
-             (end (cdr num-bounds))
-             ;; Take care, nil when negative unsupported.
-             (old-sign
-              (and use-sign
-                   (cond
-                    ((and has-sign (eq ?- (char-before beg)))
-                     -1)
-                    (t
-                     1))))
-             (old-bounds
-              (cons
+             ;; Auto-detect leading zeros: if number starts with 0 and has 
more digits.
+             (has-leading-zeros (and (> (length num-str-raw) 1) (eq (aref 
num-str-raw 0) ?0)))
+             (use-padding
+              (cond
+               ((eq padded 'auto)
+                has-leading-zeros)
+               (t
+                padded)))
+
+             (num-prev (string-to-number str-prev-strip base))
+             (num-next
+              (let ((result (funcall number-xform-fn num-prev)))
+                ;; When negative numbers are disabled, clamp to 0.
+                (cond
+                 (shift-number-negative
+                  result)
+                 (t
+                  (max 0 result)))))
+             (str-next
+              (shift-number--format
+               (abs num-next)
                (cond
-                (has-sign
-                 (1- beg))
+                (use-padding
+                 (- (match-end num-group) (match-beginning num-group)))
                 (t
-                 beg))
-               end))
+                 1))
+               base)))
 
-             (old-num-str (buffer-substring-no-properties beg end))
-             (old-num (string-to-number old-num-str))
-             (new-num
+        ;; Maintain case.
+        (when do-case
+          ;; Upper case (already set), no need to handle here.
+          (cond
+           ;; Keep current case.
+           ((null shift-number-case)
+            (when (eq -1 (or (shift-number--case-category str-prev -1) -1))
+              (setq str-next (downcase str-next))))
+           ((eq shift-number-case 'downcase)
+            (setq str-next (downcase str-next)))))
+
+        (when sep-char
+          ;; This is a relatively expensive operation,
+          ;; only apply separators back if any were found to begin with.
+          (unless (string-equal str-prev str-prev-strip)
+            (setq str-next (shift-number--strip-chars-apply str-prev str-next 
sep-char))))
+
+        ;; Replace number then sign to work around Emacs bug #74666.
+        ;; Order doesn't affect correctness, but this order avoids the bug.
+
+        ;; Replace the number.
+        (replace-match (funcall encode-fn str-next) t t nil num-group)
+
+        ;; Replace the sign (as needed).
+        (when use-sign
+          (cond
+           ;; From negative to positive.
+           ((and (< num-prev 0) (not (< num-next 0)))
+            (replace-match "" t t nil sign-group))
+           ;; From positive to negative.
+           ((and (not (< num-prev 0)) (< num-next 0))
+            (replace-match (funcall encode-fn "-") t t nil sign-group))))
+
+        (goto-char (match-end num-group))
+
+        ;; Return old bounds for cursor positioning.
+        (cons old-beg old-end)))))
+
+(defun shift-number--inc-at-pt-impl (beg end padded range-check-fn 
number-xform-fn)
+  "Increment/decrement the number at point, limited by BEG and END.
+
+Keep padding when PADDED is non-nil.  Use `auto' for automatic detection.
+
+See `shift-number--inc-at-pt-impl-with-match-chars' for details on
+RANGE-CHECK-FN and NUMBER-XFORM-FN.
+
+Return (OLD-BEG . OLD-END) on success, nil on failure.
+Point is left at the end of the modified number."
+  (or
+   ;; Find binary literals:
+   ;; 0[bB][01]+, e.g. 0b101 or 0B0.
+   (shift-number--inc-at-pt-impl-with-match-chars
+    `(("+-" \?) ("0" 1) ("bB" 1) ("01" + ,shift-number-separator-chars))
+    ;; Sign, number groups & base.
+    1 4 2
+    ;; Other arguments.
+    beg end padded nil range-check-fn number-xform-fn
+    ;; Decode & encode callbacks.
+    #'identity #'identity)
+
+   ;; Find octal literals:
+   ;; 0[oO][0-7]+, e.g. 0o42 or 0O5.
+   (shift-number--inc-at-pt-impl-with-match-chars
+    `(("+-" \?) ("0" 1) ("oO" 1) ("0-7" + ,shift-number-separator-chars))
+    ;; Sign, number groups & base.
+    1 4 8
+    ;; Other arguments.
+    beg end padded nil range-check-fn number-xform-fn
+    ;; Decode & encode callbacks.
+    #'identity #'identity)
+
+   ;; Find hex literals:
+   ;; 0[xX][0-9a-fA-F]+, e.g. 0xBEEF or 0Xcafe.
+   (shift-number--inc-at-pt-impl-with-match-chars
+    `(("+-" \?) ("0" 1) ("xX" 1) ("[:xdigit:]" + 
,shift-number-separator-chars))
+    ;; Sign, number groups & base.
+    1 4 16
+    ;; Other arguments.
+    beg end padded t range-check-fn number-xform-fn
+    ;; Decode & encode callbacks.
+    #'identity #'identity)
+
+   ;; Find decimal literals:
+   ;; [0-9]+, e.g. 42 or 23.
+   (shift-number--inc-at-pt-impl-with-match-chars
+    `(("+-" \?) ("0123456789" + ,shift-number-separator-chars))
+    ;; Sign, number groups & base.
+    1 2 10
+    ;; Other arguments.
+    beg end padded nil range-check-fn number-xform-fn
+    ;; Decode & encode callbacks.
+    #'identity #'identity)
+
+   ;; Find decimal literals (superscript).
+   (shift-number--inc-at-pt-impl-with-match-chars
+    `(("⁺⁻" \?) (,shift-number--chars-superscript + nil))
+    ;; Sign, number groups & base.
+    1 2 10
+    ;; Other arguments.
+    beg end padded nil range-check-fn number-xform-fn
+    ;; Decode & encode callbacks.
+    #'shift-number--decode-super #'shift-number--encode-super)
+
+   ;; Find decimal literals (subscript).
+   (shift-number--inc-at-pt-impl-with-match-chars
+    `(("₊₋" \?) (,shift-number--chars-subscript + nil))
+    ;; Sign, number groups & base.
+    1 2 10
+    ;; Other arguments.
+    beg end padded nil range-check-fn number-xform-fn
+    ;; Decode & encode callbacks.
+    #'shift-number--decode-sub #'shift-number--encode-sub)))
+
+(defconst shift-number--number-chars-regexp
+  (concat "[" "[:xdigit:]" shift-number--chars-superscript 
shift-number--chars-subscript "]")
+  "Regexp matching characters that may be part of a number.")
+
+(defun shift-number--inc-at-pt-with-search (amount beg end padded 
range-check-fn dir)
+  "Change the number at point by AMOUNT, limited by BEG and END.
+
+Keep padding when PADDED is non-nil.  Use `auto' for automatic detection.
+
+DIR specifies search direction: 1 for forward, -1 for backward.
+
+See `shift-number--inc-at-pt-impl-with-match-chars' for details on
+RANGE-CHECK-FN.
+
+Return (OLD-BEG . OLD-END) on success, nil on failure.
+Point is left at the end of the modified number."
+  (let ((result nil))
+    (save-match-data
+      ;; Search for any text that might be part of a number,
+      ;; if `shift-number--inc-at-pt-impl' cannot parse it - that's fine,
+      ;; keep searching until the limit.
+      ;; This avoids doubling up on number parsing logic.
+      ;;
+      ;; Note that the while body is empty.
+      (while (and
+              ;; Found item, exit the loop.
+              (null
+               (when (setq result
+                           (shift-number--inc-at-pt-impl
+                            ;; Clamp limits to line bounds.
+                            ;; The caller may use a range that spans lines to
+                            ;; allow searching and finding items across
+                            ;; multiple lines (currently used for selection).
+                            (max beg (pos-bol))
+                            (min end (pos-eol))
+                            padded
+                            range-check-fn
+                            (lambda (n) (+ n amount))))
+                 t))
+
+              ;; No more matches found, exit the loop.
               (cond
-               (use-sign
-                (+ old-num (* old-sign n)))
+               ((< dir 0)
+                (re-search-backward shift-number--number-chars-regexp beg t))
                (t
-                ;; It doesn't make sense to add a "sign" if further increments 
ignore it.
-                (max 0 (+ old-num n)))))
+                (re-search-forward shift-number--number-chars-regexp end 
t))))))
+    result))
 
-             (new-sign old-sign)
-             (new-num-sign-str "")
-             (new-num-leading-str "")
-             (new-num-str (number-to-string (abs new-num))))
 
-        ;; Handle sign flipping & negative numbers.
-        (when use-sign
-          (cond
-           ((< new-num 0)
-            (setq new-sign (- old-sign)))
-           ;; Without this check -1 would increase to -0.
-           ;; While technically correct, it's not desirable.
-           ((zerop new-num)
-            (when (eq old-sign -1)
-              (setq new-sign 1))))
+;; ---------------------------------------------------------------------------
+;; Private Functions
 
-          (cond
-           ((eq new-sign -1)
-            (setq new-num-sign-str "-"))
-           ((and has-sign (eq old-sign 1))
-            ;; If a literal `+' was present, don't remove it.
-            (setq new-num-sign-str "+"))))
-
-        ;; If there are leading zeros, preserve them keeping the same
-        ;; length of the original number.
-        (when (string-match-p "\\`0" old-num-str)
-          (let ((len-diff (- (length old-num-str) (length new-num-str))))
-            (when (> len-diff 0)
-              (setq new-num-leading-str (make-string len-diff ?0)))))
-
-        ;; Prefer this over delete+insert so as to reduce the undo overhead
-        ;; when numbers are mostly the same.
-        (let* ((new-num-str-full (concat new-num-sign-str new-num-leading-str 
new-num-str))
-               (new-bounds (cons (car old-bounds) (+ (car old-bounds) (length 
new-num-str-full)))))
-
-          (shift-number--replace-in-region new-num-str-full (car old-bounds) 
(cdr old-bounds))
-
-          ;; Result.
-          (cons old-bounds new-bounds))))
-     (t
-      nil))))
+(defmacro shift-number--swap-vars (i j)
+  "Swap the value of I & J."
+  `(setq ,i
+         (prog1 ,j
+           (setq ,j ,i))))
+
+(defun shift-number--impl (n pos limit-beg limit-end dir)
+  "Change the number at point by N.
+If there is no number at point, search in the direction DIR
+and change the first number found.
+
+DIR specifies direction: 1 for forward, -1 for backward.
+Search is limited by LIMIT-BEG and LIMIT-END.
+
+Return (OLD-BOUNDS . NEW-BOUNDS) on success, nil on failure."
+  (save-excursion
+    (goto-char pos)
+    (let ((old-bounds
+           (shift-number--inc-at-pt-with-search
+            n limit-beg limit-end (or shift-number-pad-default 'auto) nil 
dir)))
+      (when old-bounds
+        (let* ((num-chars
+                (concat
+                 "[:xdigit:]oOxXbB" shift-number--chars-superscript 
shift-number--chars-subscript))
+               (old-beg (car old-bounds))
+               (old-end (cdr old-bounds))
+               (new-beg
+                (save-excursion
+                  (skip-chars-backward num-chars limit-beg)
+                  (when (and shift-number-negative (memq (char-before) '(?- ?+ 
?⁻ ?⁺ ?₋ ?₊)))
+                    (backward-char))
+                  (point)))
+               (new-end
+                (cond
+                 ((< dir 0)
+                  (save-excursion
+                    (skip-chars-forward num-chars limit-end)
+                    (point)))
+                 (t
+                  ;; Forward search leaves point at end of number.
+                  (point)))))
+          (cons old-bounds (cons new-beg new-end)))))))
 
 (defun shift-number--on-line (n)
   "Adjust the number N on the current line."
   (let* ((old-pos (point))
-         (bounds-pair
-          (shift-number--impl n old-pos (line-beginning-position) 
(line-end-position))))
+         (bounds-pair (shift-number--impl n old-pos (pos-bol) (pos-eol) 1)))
 
     (unless bounds-pair
       (error "No number on the current line"))
@@ -292,19 +729,26 @@ Otherwise search forward limited by LIMIT-END."
       (goto-char old-pos)
 
       (when shift-number-motion
-        (set-mark new-beg)
+        (when (eq shift-number-motion 'mark)
+          (set-mark new-beg))
         (goto-char new-end))
 
       new-end)))
 
-(defun shift-number--on-region-impl (n region-beg region-end)
+(defun shift-number--on-region-impl (n region-beg region-end incremental dir 
start-count)
   "Shift the numbers N in the region defined.
-REGION-BEG & REGION-END define the region."
+REGION-BEG & REGION-END define the region.
+When INCREMENTAL is non-nil, each successive number is changed by an
+additional N (first by 1*N, second by 2*N, etc.).
+DIR specifies the direction: 1 for forward, -1 for backward.
+START-COUNT specifies the initial count for incremental mode.
+Returns the final count value."
   (let* ((pos-beg-old (mark))
          (pos-beg-new nil)
          (pos-end-old (point))
          (pos-end-new nil)
-         (point-is-first nil))
+         (point-is-first nil)
+         (count start-count))
 
     ;; Ensure order, mark then point.
     (when (< pos-end-old pos-beg-old)
@@ -312,9 +756,16 @@ REGION-BEG & REGION-END define the region."
       (shift-number--swap-vars pos-end-old pos-beg-old))
 
     (save-excursion
-      (let ((bounds-pair nil))
-        (goto-char region-beg)
-        (while (and (setq bounds-pair (shift-number--impl n region-beg 
region-beg region-end)))
+      (let ((bounds-pair nil)
+            (search-pos
+             (cond
+              ((< dir 0)
+               region-end)
+              (t
+               region-beg))))
+        (goto-char search-pos)
+        (while (and (setq bounds-pair
+                          (shift-number--impl (* n count) search-pos 
region-beg region-end dir)))
           (let* ((old-bounds (car bounds-pair))
                  (new-bounds (cdr bounds-pair))
                  (old-beg (car old-bounds))
@@ -323,66 +774,150 @@ REGION-BEG & REGION-END define the region."
                  (new-end (cdr new-bounds))
                  (delta (- new-end old-end)))
 
-            (when shift-number-motion
-              ;; Clamp the mark to the number beginning.
-              (cond
-               ((<= pos-beg-old old-beg)) ; NOP.
-               ((> pos-beg-old old-end)
-                (incf pos-beg-old delta))
-               (t
-                (setq pos-beg-new new-beg)))
+            ;; Always update pos-end-new to track the last modified number.
+            (setq pos-end-new
+                  (cond
+                   (shift-number-motion
+                    new-end)
+                   (t
+                    new-beg)))
 
-              ;; Clamp the point to the number end.
+            (cond
+             ((< dir 0)
+              ;; For backward mode, we process from end to beginning.
+              ;; Positions before the modified number are unaffected.
+              ;; Continue searching from before the modified number.
+              (setq search-pos new-beg)
+              (setq region-end new-beg))
+             (t
+              ;; For forward mode, adjust cursor if after the modified number.
               (cond
-               ((<= pos-end-old old-beg)) ; NOP.
+               ((< pos-end-old old-beg)) ; NOP - cursor is before the number.
                ((> pos-end-old old-end)
-                (incf pos-end-old delta))
-               (t
-                (setq pos-end-new new-end))))
-
-            ;; Keep contracting the region forward & updating it's end-points.
-            (setq region-beg new-end)
-            (incf region-end delta)))))
-
-    (when point-is-first
-      (shift-number--swap-vars pos-end-new pos-beg-new))
+                (incf pos-end-old delta)))
+
+              ;; Track mark position when motion with mark is enabled.
+              (when (eq shift-number-motion 'mark)
+                (cond
+                 ((< pos-beg-old old-beg)) ; NOP - mark is before the number.
+                 ((> pos-beg-old old-end)
+                  (incf pos-beg-old delta))
+                 (t
+                  (setq pos-beg-new new-beg))))
+
+              ;; Continue from after the modified number.
+              (setq search-pos new-end)
+              (setq region-beg new-end)
+              (incf region-end delta)))
+
+            ;; Increment count for incremental mode.
+            (when incremental
+              (incf count))))))
+
+    ;; Always position cursor at the last modified number.
+    ;; Only swap mark position (for motion mode) based on selection direction.
     (when pos-end-new
       (goto-char pos-end-new))
     (when pos-beg-new
-      (set-mark pos-beg-new)))
-
-  region-end)
-
-(defun shift-number--on-region (n)
-  "Shift the numbers N on the current region."
-  (shift-number--on-region-impl n (region-beginning) (region-end)))
-
-(defun shift-number--on-rectangle (n)
-  "Shift the numbers N on the current region."
-  (let ((shift-fn `(lambda (beg end) (save-excursion 
(shift-number--on-region-impl ,n beg end)))))
-    (apply-on-rectangle
-     ;; Make the values global.
-     `(lambda (col-beg col-end)
-        (let ((beg nil)
-              (end nil))
-          (save-excursion
-            (move-to-column col-beg)
-            (setq beg (point))
-            (move-to-column col-end)
-            (setq end (point)))
-          (funcall ,shift-fn beg end)))
-     (region-beginning) (region-end))))
+      (when point-is-first
+        (shift-number--swap-vars pos-end-new pos-beg-new))
+      (set-mark pos-beg-new))
+
+    ;; Return final count value (for chaining in rectangle mode).
+    count))
+
+(defun shift-number--on-region (n incremental)
+  "Shift the numbers N on the current region.
+When INCREMENTAL is non-nil, use progressive amounts."
+  (shift-number--on-region-impl n (region-beginning) (region-end) incremental 
1 1))
+
+(defun shift-number--on-rectangle (n incremental dir)
+  "Shift the numbers N in the current rectangle selection.
+When INCREMENTAL is non-nil, use progressive amounts across all cells.
+DIR specifies the direction: 1 for forward, -1 for backward."
+  (let ((final-point nil)
+        (final-mark nil)
+        (count 1))
+    ;; For backward mode, we need to process lines in reverse order.
+    ;; Collect line info first, then process.
+    (cond
+     ((< dir 0)
+      (let ((line-info nil))
+        ;; Collect line positions and column info.
+        (apply-on-rectangle
+         (lambda (col-beg col-end)
+           (let ((beg nil)
+                 (end nil))
+             (save-excursion
+               (move-to-column col-beg)
+               (setq beg (point))
+               (move-to-column col-end)
+               (setq end (point)))
+             (push (list beg end) line-info)))
+         (region-beginning) (region-end))
+        ;; Process in reverse order (line-info is already reversed from push).
+        (dolist (info line-info)
+          (let ((beg (car info))
+                (end (cadr info)))
+            (goto-char end)
+            (setq count (shift-number--on-region-impl n beg end incremental 
dir count))
+            (setq final-point (point))
+            (when (mark)
+              (setq final-mark (mark)))))))
+     (t
+      (apply-on-rectangle
+       (lambda (col-beg col-end)
+         (let ((beg nil)
+               (end nil))
+           (save-excursion
+             (move-to-column col-beg)
+             (setq beg (point))
+             (move-to-column col-end)
+             (setq end (point)))
+           ;; Move point to region start so cursor tracking works correctly.
+           (goto-char beg)
+           ;; Don't use save-excursion here - we want to capture final 
position.
+           ;; Chain count across lines for incremental mode.
+           (setq count (shift-number--on-region-impl n beg end incremental dir 
count))
+           ;; Capture cursor/mark from last line processed.
+           (setq final-point (point))
+           (when (mark)
+             (setq final-mark (mark)))))
+       (region-beginning) (region-end))))
+    ;; Apply final cursor position.
+    (when final-point
+      (goto-char final-point))
+    (when final-mark
+      (set-mark final-mark))))
 
 (defun shift-number--on-context (n)
   "Manipulate numbers in the current region or line by N."
   (cond
    ((bound-and-true-p rectangle-mark-mode)
-    (shift-number--on-rectangle n))
+    (shift-number--on-rectangle n nil 1))
    ((region-active-p)
-    (shift-number--on-region n))
+    (shift-number--on-region n nil))
    (t
     (shift-number--on-line n))))
 
+(defun shift-number--on-context-incremental (n)
+  "Incrementally change numbers between point and mark by N.
+The first number is changed by N, the second by 2*N, etc.
+Works with rectangle selection when `rectangle-mark-mode' is active."
+  (unless (mark)
+    (user-error "The mark is not set"))
+  (let ((dir
+         (cond
+          ((and shift-number-incremental-direction-from-region (< (point) 
(mark)))
+           -1)
+          (t
+           1))))
+    (cond
+     ((bound-and-true-p rectangle-mark-mode)
+      (shift-number--on-rectangle n t dir))
+     (t
+      (shift-number--on-region-impl n (region-beginning) (region-end) t dir 
1)))))
+
 
 ;; ---------------------------------------------------------------------------
 ;; Public Functions
@@ -399,6 +934,75 @@ REGION-BEG & REGION-END define the region."
   (interactive "*p")
   (shift-number--on-context (- arg)))
 
+;;;###autoload
+(defun shift-number-up-incremental (&optional arg)
+  "Incrementally increase numbers between point and mark by ARG.
+The first number is increased by ARG, the second by 2*ARG, etc.
+Works with rectangle selection when `rectangle-mark-mode' is active."
+  (interactive "*p")
+  (shift-number--on-context-incremental arg))
+
+;;;###autoload
+(defun shift-number-down-incremental (&optional arg)
+  "Incrementally decrease numbers between point and mark by ARG.
+The first number is decreased by ARG, the second by 2*ARG, etc.
+Works with rectangle selection when `rectangle-mark-mode' is active."
+  (interactive "*p")
+  (shift-number--on-context-incremental (- arg)))
+
+;;;###autoload
+(defun shift-number-increment-at-point-with-search (&rest args)
+  "Change the number at point, with keyword arguments in ARGS.
+
+Required keywords:
+  :amount       - The amount to increment.
+  :range        - A cons cell (BEG . END) specifying search bounds.
+
+Optional keywords:
+  :dir          - Search direction: 1 for forward, -1 for backward (default 1).
+  :range-check-fn - See `shift-number--inc-at-pt-impl-with-match-chars'.
+
+Optional keywords to override customization variables:
+  :pad-default    - Override `shift-number-pad-default'.
+  :negative       - Override `shift-number-negative'.
+  :separator-chars - Override `shift-number-separator-chars'.
+  :case           - Override `shift-number-case'.
+  :motion         - Override `shift-number-motion'.
+
+Return (OLD-BEG . OLD-END) on success, nil on failure.
+Point is left at the end of the modified number.
+
+This function is only exposed for use by `evil-numbers'."
+  (let ((amount nil)
+        (range nil)
+        (dir 1)
+        (range-check-fn nil)
+        (shift-number-pad-default shift-number-pad-default)
+        (shift-number-negative shift-number-negative)
+        (shift-number-separator-chars shift-number-separator-chars)
+        (shift-number-case shift-number-case)
+        (shift-number-motion shift-number-motion))
+    (while args
+      (let ((key (pop args))
+            (value (pop args)))
+        (pcase key
+          (:amount (setq amount value))
+          (:range (setq range value))
+          (:dir (setq dir value))
+          (:range-check-fn (setq range-check-fn value))
+          (:pad-default (setq shift-number-pad-default value))
+          (:negative (setq shift-number-negative value))
+          (:separator-chars (setq shift-number-separator-chars value))
+          (:case (setq shift-number-case value))
+          (:motion (setq shift-number-motion value))
+          (_ (error "Unknown keyword: %S" key)))))
+    (unless amount
+      (error "Missing required keyword: :amount"))
+    (unless range
+      (error "Missing required keyword: :range"))
+    (shift-number--inc-at-pt-with-search
+     amount (car range) (cdr range) (or shift-number-pad-default 'auto) 
range-check-fn dir)))
+
 (provide 'shift-number)
 ;; Local Variables:
 ;; fill-column: 99
diff --git a/tests/shift-number-tests.el b/tests/shift-number-tests.el
index b8ffd98527..2980e24cd6 100644
--- a/tests/shift-number-tests.el
+++ b/tests/shift-number-tests.el
@@ -568,7 +568,7 @@ The minus sign is ignored, so -5 is treated as 5, 
decrementing gives 4, result i
   "Check that only numbers within the region are modified.
 The 1 is unchanged, 2 becomes 3, 3 becomes 4, original 4 and 5 unchanged."
   (let ((text-initial "1 2 3 4 5")
-        (text-expected "1 3 4 |4 5"))
+        (text-expected "1 3 |4 4 5"))
     (with-shift-number-test text-initial
       (transient-mark-mode 1)
       (goto-char 3) ; Position before '2'.
@@ -594,7 +594,7 @@ The 1 is unchanged, 2 becomes 3, 3 becomes 4, original 4 
and 5 unchanged."
 (ert-deftest region-mixed-signs ()
   "Check that region with mixed positive and negative numbers works."
   (let ((text-initial "-5 0 +5")
-        (text-expected "-4 1 +|6"))
+        (text-expected "-4 1 |+6"))
     (with-shift-number-test text-initial
       (transient-mark-mode 1)
       (goto-char (point-min))
@@ -626,9 +626,10 @@ The 1 is unchanged, 2 becomes 3, 3 becomes 4, original 4 
and 5 unchanged."
       (should (equal text-expected (buffer-string))))))
 
 (ert-deftest region-reversed ()
-  "Check that region works when mark is after point."
+  "Check that region works when mark is after point.
+Cursor ends on last modified number regardless of selection direction."
   (let ((text-initial "1 2 3")
-        (text-expected "|2 3 4"))
+        (text-expected "2 3 |4"))
     (with-shift-number-test text-initial
       (transient-mark-mode 1)
       (goto-char (point-max))
@@ -676,7 +677,7 @@ The 1 is unchanged, 2 becomes 3, 3 becomes 4, original 4 
and 5 unchanged."
          ;; format-next-line: off
          (concat "-4 0\n"
                  "-4 0\n"
-                 "-|4 0")))
+                 "|-4 0")))
     (with-shift-number-test text-initial
       (transient-mark-mode 1)
       (goto-char (point-min))
@@ -762,15 +763,257 @@ The 1 is unchanged, 2 becomes 3, 3 becomes 4, original 4 
and 5 unchanged."
       (cursor-marker)
       (should (equal text-expected (buffer-string))))))
 
+(ert-deftest rectangle-with-motion ()
+  "Check rectangle mode works with shift-number-motion enabled."
+  (let ((text-initial
+         ;; format-next-line: off
+         (concat "1 0 0\n"
+                 "1 0 0\n"
+                 "1 0 0"))
+        (text-expected
+         ;; format-next-line: off
+         (concat "2 0 0\n"
+                 "2 0 0\n"
+                 "2| 0 0"))
+        (shift-number-motion t))
+    (with-shift-number-test text-initial
+      (transient-mark-mode 1)
+      (goto-char (point-min))
+      (set-mark (point))
+      (goto-char (point-max))
+      (backward-char 4) ; Position at column 0 of last line.
+      (rectangle-mark-mode 1)
+      (shift-number-up 1)
+      (rectangle-mark-mode 0)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+;; ---------------------------------------------------------------------------
+;; Incremental Mode Tests
+
+(ert-deftest incremental-no-mark-error ()
+  "Check that incremental mode signals error when mark is not set."
+  (with-shift-number-test "0 0 0"
+    (should-error-with-message
+        (shift-number-up-incremental 1)
+      'user-error
+      "The mark is not set")))
+
+(ert-deftest incremental-without-active-region ()
+  "Check that incremental mode works without an active region.
+Only the mark needs to be set."
+  (let ((text-initial "0 0 0")
+        (text-expected "1 2 |3"))
+    (with-shift-number-test text-initial
+      ;; Set mark but don't activate region.
+      (goto-char (point-min))
+      (set-mark (point))
+      (goto-char (point-max))
+      ;; Region is not active, but incremental should still work.
+      (shift-number-up-incremental 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest incremental-region-by-amount ()
+  "Check that incremental mode respects the amount argument."
+  (let ((text-initial "0 0 0")
+        (text-expected "2 4 |6"))
+    (with-shift-number-test text-initial
+      (goto-char (point-min))
+      (set-mark (point))
+      (goto-char (point-max))
+      (shift-number-up-incremental 2)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest incremental-region-down ()
+  "Check that incremental decrement works."
+  (let ((text-initial "10 10 10")
+        (text-expected "9 8 |7"))
+    (with-shift-number-test text-initial
+      (goto-char (point-min))
+      (set-mark (point))
+      (goto-char (point-max))
+      (shift-number-down-incremental 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest incremental-region-multiline ()
+  "Check that incremental mode works across multiple lines."
+  (let ((text-initial
+         ;; format-next-line: off
+         (concat "0 0 0\n"
+                 "0 0 0\n"
+                 "0 0 0"))
+        (text-expected
+         ;; format-next-line: off
+         (concat "1 2 3\n"
+                 "4 5 6\n"
+                 "7 8 |9")))
+    (with-shift-number-test text-initial
+      (goto-char (point-min))
+      (set-mark (point))
+      (goto-char (point-max))
+      (shift-number-up-incremental 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest incremental-rectangle ()
+  "Check that incremental mode works with rectangle selection."
+  (let ((text-initial
+         ;; format-next-line: off
+         (concat "0 0 0\n"
+                 "0 0 0\n"
+                 "0 0 0"))
+        (text-expected
+         ;; format-next-line: off
+         (concat "1 0 0\n"
+                 "2 0 0\n"
+                 "|3 0 0")))
+    (with-shift-number-test text-initial
+      (transient-mark-mode 1)
+      (goto-char (point-min))
+      (set-mark (point))
+      (goto-char (point-max))
+      (backward-char 4) ; Position at column 0 of last line.
+      (rectangle-mark-mode 1)
+      (shift-number-up-incremental 1)
+      (rectangle-mark-mode 0)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest incremental-rectangle-multicolumn ()
+  "Check that incremental count continues across rectangle cells.
+Cursor ends on the last number modified (highest increment)."
+  (let ((text-initial
+         ;; format-next-line: off
+         (concat "0 0\n"
+                 "0 0\n"
+                 "0 0"))
+        (text-expected
+         ;; format-next-line: off
+         (concat "1 2\n"
+                 "3 4\n"
+                 "5 |6")))
+    (with-shift-number-test text-initial
+      (transient-mark-mode 1)
+      (goto-char (point-min))
+      (set-mark (point))
+      (goto-char (point-max))
+      (rectangle-mark-mode 1)
+      (shift-number-up-incremental 1)
+      (rectangle-mark-mode 0)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest incremental-direction-reversed ()
+  "Check that incremental direction reverses when point is before mark.
+With point before mark, 0 0 0 becomes 3 2 1 instead of 1 2 3."
+  (let ((text-initial "0 0 0")
+        (text-expected "|3 2 1")
+        (shift-number-incremental-direction-from-region t))
+    (with-shift-number-test text-initial
+      (goto-char (point-min))
+      (set-mark (point-max))
+      (shift-number-up-incremental 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest incremental-direction-forward ()
+  "Check that incremental direction is forward when point is after mark.
+With point after mark, 0 0 0 becomes 1 2 3 (standard behavior)."
+  (let ((text-initial "0 0 0")
+        (text-expected "1 2 |3")
+        (shift-number-incremental-direction-from-region t))
+    (with-shift-number-test text-initial
+      (goto-char (point-min))
+      (set-mark (point))
+      (goto-char (point-max))
+      (shift-number-up-incremental 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest incremental-direction-disabled ()
+  "Check that direction is always forward when option is disabled."
+  (let ((text-initial "0 0 0")
+        (text-expected "1 2 |3")
+        (shift-number-incremental-direction-from-region nil))
+    (with-shift-number-test text-initial
+      (goto-char (point-min))
+      (set-mark (point-max))
+      ;; Point is before mark, but option is disabled so direction is forward.
+      (shift-number-up-incremental 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest incremental-direction-reversed-down ()
+  "Check that reversed direction works with shift-number-down-incremental."
+  (let ((text-initial "10 10 10")
+        (text-expected "|7 8 9")
+        (shift-number-incremental-direction-from-region t))
+    (with-shift-number-test text-initial
+      (goto-char (point-min))
+      (set-mark (point-max))
+      (shift-number-down-incremental 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest incremental-direction-reversed-multiline ()
+  "Check that reversed direction works across multiple lines."
+  (let ((text-initial
+         ;; format-next-line: off
+         (concat "0 0 0\n"
+                 "0 0 0\n"
+                 "0 0 0"))
+        (text-expected
+         ;; format-next-line: off
+         (concat "|9 8 7\n"
+                 "6 5 4\n"
+                 "3 2 1"))
+        (shift-number-incremental-direction-from-region t))
+    (with-shift-number-test text-initial
+      (goto-char (point-min))
+      (set-mark (point-max))
+      (shift-number-up-incremental 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest incremental-direction-reversed-rectangle ()
+  "Check that reversed direction works with rectangle selection."
+  (let ((text-initial
+         ;; format-next-line: off
+         (concat "0 0 0\n"
+                 "0 0 0\n"
+                 "0 0 0"))
+        (text-expected
+         ;; format-next-line: off
+         (concat "|3 0 0\n"
+                 "2 0 0\n"
+                 "1 0 0"))
+        (shift-number-incremental-direction-from-region t))
+    (with-shift-number-test text-initial
+      (transient-mark-mode 1)
+      ;; Set mark at end of last line (column 0), point at start (column 0).
+      ;; Point is before mark, so direction is reversed.
+      (goto-char (point-max))
+      (backward-char 4) ; Position at column 0 of last line.
+      (set-mark (point))
+      (goto-char (point-min))
+      (rectangle-mark-mode 1)
+      (shift-number-up-incremental 1)
+      (rectangle-mark-mode 0)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
 ;; ---------------------------------------------------------------------------
 ;; Motion Tests
 
 (ert-deftest motion-enabled ()
-  "Check that shift-number-motion moves cursor and sets mark at number start."
+  "Check that shift-number-motion mark moves cursor and sets mark at number 
start."
   (let ((text-initial "abc 123 def")
         (text-expected "abc 124| def")
         (mark-expected 5) ; Position of "124".
-        (shift-number-motion t))
+        (shift-number-motion 'mark))
     (with-shift-number-test text-initial
       (shift-number-up 1)
       (cursor-marker)
@@ -778,11 +1021,11 @@ The 1 is unchanged, 2 becomes 3, 3 becomes 4, original 4 
and 5 unchanged."
       (should (equal mark-expected (mark))))))
 
 (ert-deftest motion-enabled-down ()
-  "Check that shift-number-motion works with shift-number-down."
+  "Check that shift-number-motion mark works with shift-number-down."
   (let ((text-initial "abc 123 def")
         (text-expected "abc 122| def")
         (mark-expected 5) ; Position of "122".
-        (shift-number-motion t))
+        (shift-number-motion 'mark))
     (with-shift-number-test text-initial
       (shift-number-down 1)
       (cursor-marker)
@@ -790,11 +1033,11 @@ The 1 is unchanged, 2 becomes 3, 3 becomes 4, original 4 
and 5 unchanged."
       (should (equal mark-expected (mark))))))
 
 (ert-deftest motion-enabled-region ()
-  "Check that shift-number-motion works with region operations."
+  "Check that shift-number-motion mark works with region operations."
   (let ((text-initial "1 2 3")
         (text-expected "2 3 4|")
         (mark-expected 1) ; Position of "2" (first number in region).
-        (shift-number-motion t))
+        (shift-number-motion 'mark))
     (with-shift-number-test text-initial
       (transient-mark-mode 1)
       (goto-char (point-min))
@@ -1007,15 +1250,6 @@ The 1 is unchanged, 2 becomes 3, 3 becomes 4, original 4 
and 5 unchanged."
       (cursor-marker)
       (should (equal text-expected (buffer-string))))))
 
-(ert-deftest hexadecimal-prefix ()
-  "Check that hexadecimal prefix is treated as separate number."
-  (let ((text-initial "0x10")
-        (text-expected "|1x10"))
-    (with-shift-number-test text-initial
-      (shift-number-up 1)
-      (cursor-marker)
-      (should (equal text-expected (buffer-string))))))
-
 (ert-deftest multiple-decimal-points ()
   "Check behavior with multiple decimal points like version numbers."
   (let ((text-initial "1.2.3")
@@ -1092,5 +1326,507 @@ The 1 is unchanged, 2 becomes 3, 3 becomes 4, original 
4 and 5 unchanged."
       (cursor-marker)
       (should (equal text-expected (buffer-string))))))
 
+;; ---------------------------------------------------------------------------
+;; Binary Literal Tests
+
+(ert-deftest binary-increment ()
+  "Check that binary literal increments correctly."
+  (let ((text-initial "0b101")
+        (text-expected "|0b110"))
+    (with-shift-number-test text-initial
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest binary-decrement ()
+  "Check that binary literal decrements correctly."
+  (let ((text-initial "0b110")
+        (text-expected "|0b101"))
+    (with-shift-number-test text-initial
+      (shift-number-down 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest binary-uppercase-prefix ()
+  "Check that uppercase 0B prefix is preserved."
+  (let ((text-initial "0B101")
+        (text-expected "|0B110"))
+    (with-shift-number-test text-initial
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest binary-padding-preserved ()
+  "Check that binary padding is preserved."
+  (let ((text-initial "0b0001")
+        (text-expected "|0b0010"))
+    (with-shift-number-test text-initial
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest binary-to-zero ()
+  "Check that binary can decrement to zero."
+  (let ((text-initial "0b1")
+        (text-expected "|0b0"))
+    (with-shift-number-test text-initial
+      (shift-number-down 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest binary-negative ()
+  "Check that negative binary literal works."
+  (let ((text-initial "-0b101")
+        (text-expected "|-0b110"))
+    (with-shift-number-test text-initial
+      (shift-number-down 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest binary-to-negative ()
+  "Check that binary can decrement below zero."
+  (let ((text-initial "0b0")
+        (text-expected "|-0b1"))
+    (with-shift-number-test text-initial
+      (shift-number-down 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+;; ---------------------------------------------------------------------------
+;; Octal Literal Tests
+
+(ert-deftest octal-increment ()
+  "Check that octal literal increments correctly."
+  (let ((text-initial "0o42")
+        (text-expected "|0o43"))
+    (with-shift-number-test text-initial
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest octal-decrement ()
+  "Check that octal literal decrements correctly."
+  (let ((text-initial "0o43")
+        (text-expected "|0o42"))
+    (with-shift-number-test text-initial
+      (shift-number-down 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest octal-uppercase-prefix ()
+  "Check that uppercase 0O prefix is preserved."
+  (let ((text-initial "0O755")
+        (text-expected "|0O756"))
+    (with-shift-number-test text-initial
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest octal-wrap-digit ()
+  "Check that octal wraps from 7 to 10."
+  (let ((text-initial "0o7")
+        (text-expected "|0o10"))
+    (with-shift-number-test text-initial
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest octal-padding-preserved ()
+  "Check that octal padding is preserved."
+  (let ((text-initial "0o007")
+        (text-expected "|0o010"))
+    (with-shift-number-test text-initial
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest octal-negative ()
+  "Check that negative octal literal works."
+  (let ((text-initial "-0o77")
+        (text-expected "|-0o100"))
+    (with-shift-number-test text-initial
+      (shift-number-down 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest octal-to-negative ()
+  "Check that octal can decrement below zero."
+  (let ((text-initial "0o0")
+        (text-expected "|-0o1"))
+    (with-shift-number-test text-initial
+      (shift-number-down 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+;; ---------------------------------------------------------------------------
+;; Hexadecimal Literal Tests
+
+(ert-deftest hex-increment ()
+  "Check that hexadecimal literal increments correctly."
+  (let ((text-initial "0xBEEF")
+        (text-expected "|0xBEF0"))
+    (with-shift-number-test text-initial
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest hex-decrement ()
+  "Check that hexadecimal literal decrements correctly."
+  (let ((text-initial "0xBEF0")
+        (text-expected "|0xBEEF"))
+    (with-shift-number-test text-initial
+      (shift-number-down 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest hex-lowercase ()
+  "Check that lowercase hex is preserved."
+  (let ((text-initial "0xcafe")
+        (text-expected "|0xcaff"))
+    (with-shift-number-test text-initial
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest hex-uppercase-prefix ()
+  "Check that uppercase 0X prefix is preserved."
+  (let ((text-initial "0X10")
+        (text-expected "|0X11"))
+    (with-shift-number-test text-initial
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest hex-padding-preserved ()
+  "Check that hex padding is preserved."
+  (let ((text-initial "0x00FF")
+        (text-expected "|0x0100"))
+    (with-shift-number-test text-initial
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest hex-wrap-f-to-10 ()
+  "Check that hex F wraps to 10."
+  (let ((text-initial "0xF")
+        (text-expected "|0x10"))
+    (with-shift-number-test text-initial
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest hex-case-upcase ()
+  "Check that shift-number-case upcase forces uppercase."
+  (let ((text-initial "0xcafe")
+        (text-expected "|0xCAFF")
+        (shift-number-case 'upcase))
+    (with-shift-number-test text-initial
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest hex-case-downcase ()
+  "Check that shift-number-case downcase forces lowercase."
+  (let ((text-initial "0xCAFE")
+        (text-expected "|0xcaff")
+        (shift-number-case 'downcase))
+    (with-shift-number-test text-initial
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest hex-case-nil-preserves ()
+  "Check that shift-number-case nil preserves original case."
+  (let ((text-initial "0xCaFe")
+        (text-expected "|0xcaff")
+        (shift-number-case nil))
+    (with-shift-number-test text-initial
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest hex-negative ()
+  "Check that negative hex literal works."
+  (let ((text-initial "-0xFF")
+        (text-expected "|-0x100"))
+    (with-shift-number-test text-initial
+      (shift-number-down 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest hex-to-negative ()
+  "Check that hex can decrement below zero."
+  (let ((text-initial "0x0")
+        (text-expected "|-0x1"))
+    (with-shift-number-test text-initial
+      (shift-number-down 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+;; ---------------------------------------------------------------------------
+;; Superscript Number Tests
+
+(ert-deftest superscript-increment ()
+  "Check that superscript number increments correctly."
+  (let ((text-initial "x⁴²")
+        (text-expected "x|⁴³"))
+    (with-shift-number-test text-initial
+      (forward-char 1)
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest superscript-decrement ()
+  "Check that superscript number decrements correctly."
+  (let ((text-initial "x⁴³")
+        (text-expected "x|⁴²"))
+    (with-shift-number-test text-initial
+      (forward-char 1)
+      (shift-number-down 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest superscript-negative ()
+  "Check that negative superscript works."
+  (let ((text-initial "x⁻¹")
+        (text-expected "x|⁻²"))
+    (with-shift-number-test text-initial
+      (forward-char 1)
+      (shift-number-down 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest superscript-to-negative ()
+  "Check that superscript can become negative."
+  (let ((text-initial "x⁰")
+        (text-expected "x|⁻¹"))
+    (with-shift-number-test text-initial
+      (forward-char 1)
+      (shift-number-down 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest superscript-single-digit ()
+  "Check that single digit superscript works."
+  (let ((text-initial "²")
+        (text-expected "|³"))
+    (with-shift-number-test text-initial
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest superscript-positive-sign ()
+  "Check that positive superscript sign works."
+  (let ((text-initial "x⁺¹")
+        (text-expected "x|⁺²"))
+    (with-shift-number-test text-initial
+      (forward-char 1)
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+;; ---------------------------------------------------------------------------
+;; Subscript Number Tests
+
+(ert-deftest subscript-increment ()
+  "Check that subscript number increments correctly."
+  (let ((text-initial "H₂O")
+        (text-expected "H|₃O"))
+    (with-shift-number-test text-initial
+      (forward-char 1)
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest subscript-decrement ()
+  "Check that subscript number decrements correctly."
+  (let ((text-initial "H₃O")
+        (text-expected "H|₂O"))
+    (with-shift-number-test text-initial
+      (forward-char 1)
+      (shift-number-down 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest subscript-negative ()
+  "Check that negative subscript works."
+  (let ((text-initial "x₋₁")
+        (text-expected "x|₋₂"))
+    (with-shift-number-test text-initial
+      (forward-char 1)
+      (shift-number-down 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest subscript-multi-digit ()
+  "Check that multi-digit subscript works."
+  (let ((text-initial "C₁₂H₂₂O₁₁")
+        (text-expected "C|₁₃H₂₂O₁₁"))
+    (with-shift-number-test text-initial
+      (forward-char 1)
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest subscript-positive-sign ()
+  "Check that positive subscript sign works."
+  (let ((text-initial "x₊₁")
+        (text-expected "x|₊₂"))
+    (with-shift-number-test text-initial
+      (forward-char 1)
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest subscript-to-negative ()
+  "Check that subscript can decrement below zero."
+  (let ((text-initial "x₀")
+        (text-expected "x|₋₁"))
+    (with-shift-number-test text-initial
+      (forward-char 1)
+      (shift-number-down 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+;; ---------------------------------------------------------------------------
+;; Separator Characters Tests
+
+(ert-deftest separator-underscore-enabled ()
+  "Check that underscore separator is treated as part of number when enabled."
+  (let ((text-initial "1_000_000")
+        (text-expected "|1_000_001")
+        (shift-number-separator-chars "_"))
+    (with-shift-number-test text-initial
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest separator-underscore-large-increment ()
+  "Check that underscore-separated number increments by large amount."
+  (let ((text-initial "1_000_000")
+        (text-expected "|2_000_000")
+        (shift-number-separator-chars "_"))
+    (with-shift-number-test text-initial
+      (shift-number-up 1000000)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest separator-comma-enabled ()
+  "Check that comma separator is treated as part of number when enabled."
+  (let ((text-initial "1,000,000")
+        (text-expected "|1,000,001")
+        (shift-number-separator-chars ","))
+    (with-shift-number-test text-initial
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest separator-disabled ()
+  "Check that separator is not recognized when disabled."
+  (let ((text-initial "1_000")
+        (text-expected "|2_000")
+        (shift-number-separator-chars nil))
+    (with-shift-number-test text-initial
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest separator-hex-underscore ()
+  "Check that underscore separator works with hex literals.
+Separator position is preserved relative to the end of the number."
+  (let ((text-initial "0xFF_FF")
+        (text-expected "|0x100_00")
+        (shift-number-separator-chars "_"))
+    (with-shift-number-test text-initial
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest separator-binary-underscore ()
+  "Check that underscore separator works with binary literals."
+  (let ((text-initial "0b1111_1111")
+        (text-expected "|0b10000_0000")
+        (shift-number-separator-chars "_"))
+    (with-shift-number-test text-initial
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest separator-octal-underscore ()
+  "Check that underscore separator works with octal literals."
+  (let ((text-initial "0o77_77")
+        (text-expected "|0o100_00")
+        (shift-number-separator-chars "_"))
+    (with-shift-number-test text-initial
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+;; ---------------------------------------------------------------------------
+;; Padding Default Option Tests
+
+(ert-deftest pad-default-enabled ()
+  "Check that shift-number-pad-default preserves width when number shrinks."
+  (let ((text-initial "10")
+        (text-expected "|09")
+        (shift-number-pad-default t))
+    (with-shift-number-test text-initial
+      (shift-number-down 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest pad-default-disabled ()
+  "Check that shift-number-pad-default disabled doesn't pad."
+  (let ((text-initial "9")
+        (text-expected "|8")
+        (shift-number-pad-default nil))
+    (with-shift-number-test text-initial
+      (shift-number-down 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+(ert-deftest pad-default-leading-zeros-always-pad ()
+  "Check that leading zeros are preserved regardless of pad-default."
+  (let ((text-initial "09")
+        (text-expected "|08")
+        (shift-number-pad-default nil))
+    (with-shift-number-test text-initial
+      (shift-number-down 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
+;; ---------------------------------------------------------------------------
+;; Region Cursor Position Tests
+
+(ert-deftest region-cursor-with-motion ()
+  "Check that shift-number-motion t positions cursor at end without mark."
+  (let ((text-initial "1 2 3")
+        (text-expected "2 3 4|")
+        (shift-number-motion t))
+    (with-shift-number-test text-initial
+      (transient-mark-mode 1)
+      (goto-char (point-min))
+      (set-mark (point))
+      (goto-char (point-max))
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string)))
+      ;; Mark should not be changed to number position.
+      (should (equal 1 (mark))))))
+
+(ert-deftest region-cursor-without-motion ()
+  "Check that cursor is at beginning of last number when motion is nil."
+  (let ((text-initial "1 2 3")
+        (text-expected "2 3 |4")
+        (shift-number-motion nil))
+    (with-shift-number-test text-initial
+      (transient-mark-mode 1)
+      (goto-char (point-min))
+      (set-mark (point))
+      (goto-char (point-max))
+      (shift-number-up 1)
+      (cursor-marker)
+      (should (equal text-expected (buffer-string))))))
+
 (provide 'shift-number-tests)
 ;;; shift-number-tests.el ends here
diff --git a/tests/shift-number-tests.sh b/tests/shift-number-tests.sh
index fc0617e2ab..b487ba330e 100755
--- a/tests/shift-number-tests.sh
+++ b/tests/shift-number-tests.sh
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
 SCRIPT_PATH="$(dirname "${BASH_SOURCE[0]}")"
 
 ${EMACS:-emacs} \

Reply via email to