diff --git a/ChangeLog b/ChangeLog index c082a0cf..1192cb65 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,16 @@ +2026-01-19 Mats Lidell + +* 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 * 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 31eef3c8..76f4b08d 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 93d3636a..2672d56e 100644 --- a/test/kotl-mode-tests.el +++ b/test/kotl-mode-tests.el @@ -3,7 +3,7 @@ ;; Author: Mats Lidell ;; ;; 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