branch: externals/hyperbole
commit b8344d5987c0b75bd8118e9a4063d4c9601caa50
Author: Mats Lidell <[email protected]>
Commit: GitHub <[email protected]>
fix kotl mode delete char with active region (#842)
---
ChangeLog | 13 ++++
kotl/kotl-mode.el | 170 +++++++++++++++++++++++++++++-------------------
test/kotl-mode-tests.el | 170 +++++++++++++++++++++++++++++++++++++++++++++++-
3 files changed, 285 insertions(+), 68 deletions(-)
diff --git a/ChangeLog b/ChangeLog
index c082a0cf91..1192cb65a1 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,16 @@
+2026-01-19 Mats Lidell <[email protected]>
+
+* test/kotl-mode-tests.el (kotl-mode--delete-region)
+ (kotl-mode--delete-char): Add tests.
+
+* kotl/kotl-mode.el (kotl-mode:delete-char-acc): Accumulate deleted chars
+ to setup kill-ring.
+ (kotl-mode:delete-char, kotl-mode:delete-backward-char): Fix
+ inconsistencies with delete-char - Support transient mark mode. Use
+ kotl-mode:delete-char-acc for saving multiple chars to kill-ring when
+ called with kill-flag. (The latter is implicit when used interactively
+ with a prefix arg.)
+
2026-01-07 Mats Lidell <[email protected]>
* hywiki.el (hywiki-mode): Remove arg value :toggle, not an alternative to
diff --git a/kotl/kotl-mode.el b/kotl/kotl-mode.el
index 31eef3c8d0..76f4b08d53 100644
--- a/kotl/kotl-mode.el
+++ b/kotl/kotl-mode.el
@@ -3,7 +3,7 @@
;; Author: Bob Weiner
;;
;; Orig-Date: 6/30/93
-;; Last-Mod: 1-Jan-26 at 18:18:27 by Mats Lidell
+;; Last-Mod: 19-Jan-26 at 22:34:04 by Mats Lidell
;;
;; SPDX-License-Identifier: GPL-3.0-or-later
;;
@@ -446,14 +446,17 @@ With optional prefix arg DELETE-FLAG, delete region."
Return number of characters deleted.
Optional KILL-FLAG non-nil means save in kill ring instead of deleting.
Do not delete across cell boundaries."
- (interactive "*P")
- (when (called-interactively-p 'interactive)
- (when current-prefix-arg
- (setq kill-flag t
- arg (prefix-numeric-value current-prefix-arg))))
- (unless arg
- (setq arg 1))
- (kotl-mode:delete-char (- arg) kill-flag))
+ (interactive "*p\nP")
+ (cond ((and (use-region-p)
+ delete-active-region
+ (= arg 1))
+ ;; If a region is active, kill or delete it.
+ (if (or kill-flag
+ (eq delete-active-region 'kill))
+ (kotl-mode:kill-region (region-beginning) (region-end))
+ (kotl-mode:delete-region (region-beginning) (region-end))))
+ (t
+ (kotl-mode:delete-char (- arg) kill-flag))))
(defun kotl-mode:delete-blank-lines ()
"On blank line in a cell, delete all surrounding blank lines, leaving just
one.
@@ -478,69 +481,95 @@ whitespace at the end of the cell."
(delete-region (max start (point)) end)))
(kotl-mode:to-valid-position))
+(defvar kotl-mode:delete-char-acc nil
+ "Accumulate deleted chars to populate `kill-ring'.")
+
+(defun kotl-mode:delete-char-acc (arg kill-flag)
+ "Delete one character and accumulate in the kill ring.
+Deletes (forward if ARG > 0, backward if ARG < 0).
+First call creates a new kill ring entry, subsequent calls appends.
+With KILL-FLAG nil just call `delete-char'."
+ (if (not kill-flag)
+ (delete-char arg)
+ (let ((char (char-to-string (if (< arg 0)
+ (char-before)
+ (char-after)))))
+ (delete-char arg)
+ (if kotl-mode:delete-char-acc
+ (kill-append char (< arg 0))
+ (kill-new char)
+ (setq kotl-mode:delete-char-acc t)))))
+
(defun kotl-mode:delete-char (arg &optional kill-flag)
"Delete up to prefix ARG characters following point.
Return number of characters deleted.
Optional KILL-FLAG non-nil means save in kill ring instead of deleting.
Do not delete across cell boundaries."
- (interactive "*P")
- (when (called-interactively-p 'interactive)
- (when current-prefix-arg
- (setq kill-flag t
- arg (prefix-numeric-value current-prefix-arg))))
- (unless arg
- (setq arg 1))
-
- (if (not (and (boundp 'kotl-kview) (kview:is-p kotl-kview)))
- ;; Support use within Org tables outside of the Koutliner
- (delete-char arg kill-flag)
- (let ((del-count 0)
- (indent (kcell-view:indent))
- count start end)
- (cond ((> arg 0)
- (if (kotl-mode:eocp)
- (error "(kotl-mode:delete-char): End of cell")
- (setq end (kcell-view:end)
- arg (min arg (- end (point))))
- (while (and (> arg 0) (not (kotl-mode:eocp)))
- (if (kotl-mode:eolp)
- (if (not (eq ?\ (char-syntax (following-char))))
- (setq arg 0
- del-count (1- del-count))
- (delete-char 1 kill-flag)
- ;; There may be non-whitespace characters in the
- ;; indent area. Don't delete them.
- (setq count indent)
- (while (and (> count 0)
- (eq ?\ (char-syntax (following-char))))
- (delete-char 1)
- (setq count (1- count))))
- (delete-char 1 kill-flag))
- (setq arg (1- arg)
- del-count (1+ del-count)))))
- ((< arg 0)
- (if (kotl-mode:bocp)
- (error "(kotl-mode:delete-char): Beginning of cell")
- (setq start (kcell-view:start)
- arg (max arg (- start (point))))
- (while (and (< arg 0) (not (kotl-mode:bocp)))
- (if (kotl-mode:bolp)
- (if (not (eq ?\ (char-syntax (preceding-char))))
- (setq arg 0
- del-count (1- del-count))
- ;; There may be non-whitespace characters in the
- ;; indent area. Don't delete them.
- (setq count indent)
- (while (and (> count 0)
- (eq ?\ (char-syntax (preceding-char))))
- (delete-char -1)
- (setq count (1- count)))
- (if (zerop count)
- (delete-char -1 kill-flag)))
- (delete-char -1 kill-flag))
- (setq arg (1+ arg)
- del-count (1+ del-count))))))
- del-count)))
+ (interactive "*p\nP")
+ (unless (integerp arg)
+ (signal 'wrong-type-argument (list 'integerp arg)))
+ (cond ((and (use-region-p)
+ delete-active-region
+ (= arg 1))
+ ;; If a region is active, kill or delete it.
+ (if (or kill-flag
+ (eq delete-active-region 'kill))
+ (kotl-mode:kill-region (region-beginning) (region-end))
+ (kotl-mode:delete-region (region-beginning) (region-end))))
+ (t
+ (if (not (and (boundp 'kotl-kview) (kview:is-p kotl-kview)))
+ ;; Support use within Org tables outside of the Koutliner
+ (delete-char arg kill-flag)
+ (let ((del-count 0)
+ (indent (kcell-view:indent))
+ count start end
+ kotl-mode:delete-char-acc)
+ (cl-flet ((delete-char (arg &optional kill-flag)
+ (kotl-mode:delete-char-acc arg kill-flag)))
+ (cond ((> arg 0)
+ (if (kotl-mode:eocp)
+ (error "(kotl-mode:delete-char): End of cell")
+ (setq end (kcell-view:end)
+ arg (min arg (- end (point))))
+ (while (and (> arg 0) (not (kotl-mode:eocp)))
+ (if (kotl-mode:eolp)
+ (if (not (eq ?\ (char-syntax (following-char))))
+ (setq arg 0
+ del-count (1- del-count))
+ (delete-char 1 kill-flag)
+ ;; There may be non-whitespace characters in the
+ ;; indent area. Don't delete them.
+ (setq count indent)
+ (while (and (> count 0)
+ (eq ?\ (char-syntax
(following-char))))
+ (delete-char 1)
+ (setq count (1- count))))
+ (delete-char 1 kill-flag))
+ (setq arg (1- arg)
+ del-count (1+ del-count)))))
+ ((< arg 0)
+ (if (kotl-mode:bocp)
+ (error "(kotl-mode:delete-char): Beginning of cell")
+ (setq start (kcell-view:start)
+ arg (max arg (- start (point))))
+ (while (and (< arg 0) (not (kotl-mode:bocp)))
+ (if (kotl-mode:bolp)
+ (if (not (eq ?\ (char-syntax (preceding-char))))
+ (setq arg 0
+ del-count (1- del-count))
+ ;; There may be non-whitespace characters in the
+ ;; indent area. Don't delete them.
+ (setq count indent)
+ (while (and (> count 0)
+ (eq ?\ (char-syntax
(preceding-char))))
+ (delete-char -1)
+ (setq count (1- count)))
+ (if (zerop count)
+ (delete-char -1 kill-flag)))
+ (delete-char -1 kill-flag))
+ (setq arg (1+ arg)
+ del-count (1+ del-count)))))))
+ del-count)))))
(defun kotl-mode:delete-horizontal-space ()
"Delete all spaces and tabs around point."
@@ -797,6 +826,13 @@ If a completion is active, this aborts the completion
only."
((mark t)
(indicate-copied-region)))))))
+(defun kotl-mode:delete-region (start end)
+ "Delete region between START and END within a single kcell.
+Delegates to `kotl-mode:kill-region' but does not store killed text in
+`kill-ring'."
+ (let (kill-ring kill-ring-yank-pointer)
+ (kotl-mode:kill-region start end)))
+
(defun kotl-mode:kill-or-copy-region (start end copy-flag &optional kill-str)
(when (and start end)
(let ((indent (kcell-view:indent))
diff --git a/test/kotl-mode-tests.el b/test/kotl-mode-tests.el
index 93d3636a64..2672d56e76 100644
--- a/test/kotl-mode-tests.el
+++ b/test/kotl-mode-tests.el
@@ -3,7 +3,7 @@
;; Author: Mats Lidell <[email protected]>
;;
;; Orig-Date: 18-May-21 at 22:14:10
-;; Last-Mod: 21-Dec-25 at 23:56:18 by Mats Lidell
+;; Last-Mod: 20-Jan-26 at 00:21:25 by Mats Lidell
;;
;; SPDX-License-Identifier: GPL-3.0-or-later
;;
@@ -1021,6 +1021,7 @@ optional DEPTH the number of sub cells are created to
that depth."
(make-kotl-mode-tests--func :func #'kotl-mode:delete-forward-char :args
'(-1))
(make-kotl-mode-tests--func :func #'kotl-mode:delete-horizontal-space)
(make-kotl-mode-tests--func :func #'kotl-mode:delete-indentation)
+ (make-kotl-mode-tests--func :func #'kotl-mode:delete-region :args '(130
131))
(make-kotl-mode-tests--func :func #'kotl-mode:demote-tree :ignore t)
(make-kotl-mode-tests--func :func #'kotl-mode:down-level :ignore t)
(make-kotl-mode-tests--func :func #'kotl-mode:end-of-buffer)
@@ -1321,6 +1322,173 @@ marked with :ignore t")
(should (= (kcell-view:level) 3))
(should (string= (kcell-view:label (point)) "1a2"))))))
+(ert-deftest kotl-mode--delete-region ()
+ "Verify `kotl-mode:delete-region'does not save to `kill-ring."
+ (with-temp-buffer
+ (let (kill-ring)
+ (kotl-mode)
+ (insert "0123")
+ (kotl-mode:delete-region (kcell-view:start) (kcell-view:end-contents))
+ (should (string-empty-p (kcell-view:contents)))
+ (should-not kill-ring))))
+
+(ert-deftest kotl-mode--delete-char ()
+ "Verify `kotl-mode:delete-char' and `kotl-mode:delete-backward-char'."
+ (cl-flet ((init ()
+ (progn (erase-buffer)
+ (kotl-mode)
+ (insert "0123")
+ (kotl-mode:beginning-of-cell))))
+ (with-temp-buffer
+ (ert-info ("Delete one char forward, nothing inserted in kill ring")
+ (init)
+ (let (kill-ring)
+ (kotl-mode:delete-char 1)
+ (should (looking-at-p "123"))
+ (should-not kill-ring)))
+ (ert-info ("Delete one char forward, deleted char insert in kill ring")
+ (init)
+ (let (kill-ring)
+ (kotl-mode:delete-char 1 t)
+ (should (looking-at-p "123"))
+ (should (string= (car kill-ring) "0"))))
+ (ert-info ("Delete one char forward - interactive")
+ (init)
+ (call-interactively #'kotl-mode:delete-char)
+ (should (looking-at-p "123")))
+ (ert-info ("Delete two char forward - interactive")
+ (init)
+ (let ((current-prefix-arg 2)
+ kill-ring)
+ (call-interactively #'kotl-mode:delete-char)
+ (should (looking-at-p "23"))
+ (should (string= (car kill-ring) "01"))))
+ (ert-info ("Delete many chars forward")
+ (init)
+ (kotl-mode:delete-char 2)
+ (should (looking-at-p "23")))
+ (ert-info ("Delete one char backward")
+ (init)
+ (kotl-mode:end-of-line)
+ (kotl-mode:delete-char -1)
+ (kotl-mode:beginning-of-line)
+ (should (looking-at-p "012")))
+ (ert-info ("Delete one char backward - using backward-char")
+ (init)
+ (kotl-mode:end-of-line)
+ (kotl-mode:delete-backward-char 1)
+ (kotl-mode:beginning-of-line)
+ (should (looking-at-p "012")))
+ (ert-info ("Delete two chars backward")
+ (init)
+ (kotl-mode:end-of-line)
+ (kotl-mode:delete-char -2)
+ (kotl-mode:beginning-of-line)
+ (should (looking-at-p "01")))
+ (ert-info ("Delete two char backward - interactive")
+ (init)
+ (let ((current-prefix-arg 2)
+ kill-ring)
+ (kotl-mode:end-of-line)
+ (call-interactively #'kotl-mode:delete-backward-char)
+ (kotl-mode:beginning-of-line)
+ (should (looking-at-p "01"))
+ (should (string= (car kill-ring) "23"))))
+ ;; Error cases
+ (ert-info ("Delete at cell end")
+ (init)
+ (kotl-mode:end-of-cell)
+ (let ((err (should-error (kotl-mode:delete-char 1) :type 'error)))
+ (should (string-match "(kotl-mode:delete-char): End of cell" (cadr
err)))))
+ (ert-info ("Delete at beginning of cell backwards")
+ (init)
+ (let ((err (should-error (kotl-mode:delete-char -1) :type 'error)))
+ (should (string-match "(kotl-mode:delete-char): Beginning of cell"
(cadr err)))))
+ ;; Region
+ (dolist (func '(kotl-mode:delete-char kotl-mode:delete-backward-char))
+ ;; Same behavior forward and backward
+ (ert-info ("Transient mark mode, delete region")
+ (let ((delete-active-region t)
+ kill-ring)
+ (init)
+ (kotl-mode:forward-char 1)
+ (set-mark (point))
+ (kotl-mode:forward-char 2)
+ (transient-mark-mode)
+ (funcall func 1)
+ (kotl-mode:beginning-of-line)
+ (should (looking-at-p "03"))
+ (should-not kill-ring)))
+ (ert-info ("Transient mark mode, kill region")
+ (let ((delete-active-region t)
+ kill-ring)
+ (init)
+ (kotl-mode:forward-char 1)
+ (set-mark (point))
+ (kotl-mode:forward-char 2)
+ (transient-mark-mode)
+ (funcall func 1 t)
+ (kotl-mode:beginning-of-line)
+ (should (looking-at-p "03"))
+ (should (string= (car kill-ring) "12")))))
+ (ert-info ("Transient mark mode, delete multiple chars ignores region")
+ (let ((delete-active-region t)
+ kill-ring)
+ (init)
+ (set-mark (point))
+ (kotl-mode:forward-char 2)
+ (transient-mark-mode)
+ (kotl-mode:delete-char 2)
+ (kotl-mode:beginning-of-line)
+ (should (looking-at-p "01"))
+ (should-not kill-ring)))
+ (ert-info ("Transient mark mode, delete multiple chars ignores region,
interactive")
+ (let ((delete-active-region t)
+ kill-ring)
+ (init)
+ (set-mark (point))
+ (kotl-mode:forward-char 2)
+ (transient-mark-mode)
+ (let ((current-prefix-arg 2)
+ kill-ring)
+ (call-interactively #'kotl-mode:delete-char)
+ (kotl-mode:beginning-of-line)
+ (should (looking-at-p "01"))
+ (should (string= (car kill-ring) "23")))))
+ (ert-info ("No transient mark mode, plain delete")
+ (let ((delete-active-region t)
+ kill-ring)
+ (init)
+ (kotl-mode:forward-char 1)
+ (set-mark (point))
+ (kotl-mode:forward-char 2)
+ (transient-mark-mode -1)
+ (kotl-mode:delete-char 1)
+ (kotl-mode:beginning-of-line)
+ (should (looking-at-p "012"))
+ (should-not kill-ring)))))
+ ;; Region Error cases
+ (cl-flet ((init ()
+ (progn (erase-buffer)
+ (kotl-mode)
+ (insert "0123")
+ (kotl-mode:add-cell)
+ (insert "4567")
+ (kotl-mode:beginning-of-buffer))))
+ (with-temp-buffer
+ (ert-info ("Delete region across cells")
+ ;; Same behavior forward and backward
+ (let ((delete-active-region t))
+ (dolist (func '(kotl-mode:delete-char
kotl-mode:delete-backward-char))
+ (init)
+ (set-mark (point))
+ (kotl-mode:end-of-buffer)
+ (transient-mark-mode)
+ (let ((err (should-error (funcall func 1) :type 'error)))
+ (should (string-match
+ "(kotl-mode:kill-region): Bad region or not within a
single Koutline cell"
+ (cadr err))))))))))
+
(provide 'kotl-mode-tests)
;; This file can't be byte-compiled without the `el-mock' package