Subject: bug#69097: [PATCH] Add 'kill-region-or-word' command

From: Philip Kaludercic <[email-masked]>
Date: Tue, 03 Sep 2024 16:32:46 +0000

Eli Zaretskii <[email-masked]> writes:

>> From: Philip Kaludercic <[email-masked]>
>> Cc: Eli Zaretskii <[email-masked]>,  Stefan Kangas <[email-masked]>,
>>   Andrea Corallo <[email-masked]>,  [email-masked],  [email-masked],
>>   [email-masked]
>> Date: Mon, 02 Sep 2024 21:12:01 +0000
>> 
>> I had misremembered the last state of this patch.  It is easier to just
>> have a tristate option.  Here is the updated proposal:
>
> Thanks.
>
>> +(defcustom kill-word-if-no-region nil
>
> I would call this 'kill-region-dwim' instead.

Can do.

>> +  "Behaviour when `kill-region' is invoked without an active region.
>> +If set to nil (default), then an error occurs and nothing is killed.  If
>> +set to `emacs-word', then kill a the last word as defined by the current
>> +major mode.  If set to `unix-word', then kill the last word in the style
>> +of a shell like Bash, disregarding the major mode."
>> +  :type '(choice (const :tag "Kill a word like `backward-kill-word'" emacs-word)
>> +                 (const :tag "Kill a word like Bash would" unix-word)
>> +                 (const :tag "Do not kill anything" nil))
>> +  :group 'killing)
>
> :version tag is missing.

Whoops, added it.

>> -Lisp programs should use this function for killing text.
>> - (To delete text, use `delete-region'.)
>> -Supply two arguments, character positions BEG and END indicating the
>> - stretch of text to be killed.  If the optional argument REGION is
>> - non-nil, the function ignores BEG and END, and kills the current
>> - region instead.  Interactively, REGION is always non-nil, and so
>> - this command always kills the current region."
>> +Lisp programs should use this function for killing text.  (To delete
>> +text, use `delete-region'.)  Supply two arguments, character positions
>> +BEG and END indicating the stretch of text to be killed.  If the
>> +optional argument REGION is non-nil, the function ignores BEG and END,
>> +and kills the current region instead.  If REGION has the special value
>
> Not sure why you decided to reformat this part.  Its formatting was
> not random.

I think this was just an accidental M-q.

> This also needs a NEWS entry.

I've added the NEWS entry from the last iteration of the patch (now
actually as a patch, not just a diff):

>From 981331cfe0757b21f529be02d848a3ef7bcc4295 Mon Sep 17 00:00:00 2001
From: Philip Kaludercic <[email-masked]>
Date: Tue, 3 Sep 2024 18:29:56 +0200
Subject: [PATCH] Allow 'kill-region' kill the last word when there is no
 region

* etc/NEWS: Document the new user option.
* lisp/simple.el (kill-region-dwim): Add new option.
(kill-region): Respect 'kill-region-dwim'.  (Bug#69097)
---
 etc/NEWS       |  6 ++++++
 lisp/simple.el | 50 ++++++++++++++++++++++++++++++++++++++++++--------
 2 files changed, 48 insertions(+), 8 deletions(-)

diff --git a/etc/NEWS b/etc/NEWS
index 1e66f084117..7fadf52a6cf 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -123,6 +123,12 @@ When using 'visual-wrap-prefix-mode' in buffers with variable-pitch
 fonts, the wrapped text will now be lined up correctly so that it's
 exactly below the text after the prefix on the first line.
 
+---
+** New user option 'kill-word-if-no-region'.
+This option will modify the fall-back behaviour of 'kill-region' if no
+region is active, and will kill the last word instead of raising an
+error.
+
 
 * Changes in Specialized Modes and Packages in Emacs 31.1
 
diff --git a/lisp/simple.el b/lisp/simple.el
index eedc5768fe2..3b4453c7a8f 100644
--- a/lisp/simple.el
+++ b/lisp/simple.el
@@ -5817,6 +5817,17 @@ kill-read-only-ok
   :type 'boolean
   :group 'killing)
 
+(defcustom kill-region-dwim nil
+  "Behaviour when `kill-region' is invoked without an active region.
+If set to nil (default), then an error occurs and nothing is killed.  If
+set to `emacs-word', then kill a the last word as defined by the current
+major mode.  If set to `unix-word', then kill the last word in the style
+of a shell like Bash, disregarding the major mode."
+  :type '(choice (const :tag "Kill a word like `backward-kill-word'" emacs-word)
+                 (const :tag "Kill a word like Bash would" unix-word)
+                 (const :tag "Do not kill anything" nil))
+  :group 'killing)
+
 (defun kill-region (beg end &optional region)
   "Kill (\"cut\") text between point and mark.
 This deletes the text from the buffer and saves it in the kill ring.
@@ -5843,21 +5854,44 @@ kill-region
  (To delete text, use `delete-region'.)
 Supply two arguments, character positions BEG and END indicating the
  stretch of text to be killed.  If the optional argument REGION is
- non-nil, the function ignores BEG and END, and kills the current
+ `region', the function ignores BEG and END, and kills the current
  region instead.  Interactively, REGION is always non-nil, and so
- this command always kills the current region."
+ this command always kills the current region.  It is possible to
+ override this behaviour by customising the user option
+ `kill-region-dwim'."
   ;; Pass mark first, then point, because the order matters when
   ;; calling `kill-append'.
   (interactive (progn
                  (let ((beg (mark))
                        (end (point)))
-                   (unless (and beg end)
-                     (user-error "The mark is not set now, so there is no region"))
-                   (list beg end 'region))))
+                   (cond
+                    ((and beg end (use-region-p))
+                     (list beg end 'region))
+                    (kill-region-dwim
+                     (list beg end kill-region-dwim))
+                    ((user-error "The mark is not set now, so there is no region"))))))
+
   (condition-case nil
-      (let ((string (if region
-                        (funcall region-extract-function 'delete)
-                      (filter-buffer-substring beg end 'delete))))
+      (let ((string (cond
+                     ((eq region 'emacs-word)
+                      (let ((end (point)))
+                        (save-excursion
+                          (forward-word -1)
+                          (filter-buffer-substring (point) end 'delete))))
+                     ((eq region 'unix-word)
+                      (let ((end (point)))
+                        (save-excursion
+                          (skip-chars-backward "[:space:]")
+                          (skip-chars-backward "^[:space:]")
+                          (filter-buffer-substring
+                           (if (get-char-property (point) 'read-only)
+                               (next-single-char-property-change
+                                (point) 'read-only nil end)
+                             (point))
+                           end 'delete))))
+                     (region
+                      (funcall region-extract-function 'delete))
+                     ((filter-buffer-substring beg end 'delete)))))
 	(when string			;STRING is nil if BEG = END
 	  ;; Add that string to the kill ring, one way or another.
 	  (if (eq last-command 'kill-region)
-- 
2.46.0

Robert Pluim <[email-masked]> writes:

>>>>>> On Tue, 03 Sep 2024 15:21:54 +0300, Eli Zaretskii <[email-masked]> said:
>
>     >> From: Philip Kaludercic <[email-masked]>
>     >> Cc: Eli Zaretskii <[email-masked]>,  Stefan Kangas <[email-masked]>,
>     >> Andrea Corallo <[email-masked]>,  [email-masked],  [email-masked],
>     >> [email-masked]
>     >> Date: Mon, 02 Sep 2024 21:12:01 +0000
>     >> 
>     >> I had misremembered the last state of this patch.  It is easier to just
>     >> have a tristate option.  Here is the updated proposal:
>
>     Eli> Thanks.
>
>     >> +(defcustom kill-word-if-no-region nil
>
>     Eli> I would call this 'kill-region-dwim' instead.
>
>     >> +  "Behaviour when `kill-region' is invoked without an active region.
>     >> +If set to nil (default), then an error occurs and nothing is killed.  If
>     >> +set to `emacs-word', then kill a the last word as defined by the current
>     >> +major mode.  If set to `unix-word', then kill the last word in the style
>     >> +of a shell like Bash, disregarding the major mode."
>     >> +  :type '(choice (const :tag "Kill a word like `backward-kill-word'" emacs-word)
>     >> +                 (const :tag "Kill a word like Bash would" unix-word)
>     >> +                 (const :tag "Do not kill anything" nil))
>     >> +  :group 'killing)
>
>     Eli> :version tag is missing.
>
> Is it worth allowing a user-specified function?

That would be possible as well, but to make it manageable with the
current approach the function would have to be one that moves the point.
So we could rewrite the patch with two default options `backward-word'
and `unix-backward-word' (which we would have to add), but I am not sure
it would have much use.  Perhaps killing the current word, instead of
just killing to the beginning?

> Robert

-- 
	Philip Kaludercic on peregrine