Subject: [PATCH] Add symbol-mode
From: Gabriel Santos <[email-masked]>
To: [email-masked]
Cc: Gabriel Santos <[email-masked]>
Date: Thu, 27 Mar 2025 19:44:21 -0300
List-Id: <debbugs-submit.debbugs.gnu.org>

Greetings,

This patch provides a new package for interacting with symbols, based on
the current word commands and the built-in package "subword".

A minor mode that replaces the word commands is also provided, also
inspired by the subword package, but additionally on the usage of
defvar-keymap I made in a PR I opened for the puni package[1].

- Points of discussion

Would the mention of this mode be a good addition to the documentation?

I don't know how to contribute to the docs, but I would look into doing
so if you think it's a good idea

[1] <https://github.com/AmaiKinono/puni/pull/75>

P.S.: This is being sent from a different e-mail from the patch because
my e-mail provider, viz. Disroot, is having issues with spam, and that
was blocking my messages, so I decided to create a Gmail account to go
around that.

Regards,

-- 
Gabriel Santos

From ea08a5251878e5de1b379becfcef401a4393fa49 Mon Sep 17 00:00:00 2001
From: Gabriel Santos <[email-masked]>
Date: Mon, 24 Mar 2025 20:18:06 -0300
Subject: [PATCH] Add symbol-mode

* lisp/progmodes/symbol.el: New file.
* test/lisp/progmodes/symbol-tests.el: New test file.
* etc/NEWS: Announce symbol-mode.
---
 etc/NEWS                            |   9 ++
 lisp/progmodes/symbol.el            | 222 ++++++++++++++++++++++++++++
 test/lisp/progmodes/symbol-tests.el |  84 +++++++++++
 3 files changed, 315 insertions(+)
 create mode 100644 lisp/progmodes/symbol.el
 create mode 100644 test/lisp/progmodes/symbol-tests.el

diff --git a/etc/NEWS b/etc/NEWS
index d90836b8c6d..8c70bf2e648 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -1651,6 +1651,15 @@ A major mode based on the tree-sitter library for editing "go.work"
 files.  If tree-sitter is properly set-up by the user, it can be
 enabled for files named "go.work".
 
+---
+** New minor mode 'symbol-mode'.
+This minor mode replaces word related commands such as 'upcase-word'
+with commands better suited for symbols.  A symbol is any combination
+of characters not separated by a space.  This is primarily useful for
+programming modes, where one can think of variables such as
+"this_is_a_word" as an actual word in the codebase, as it is used to
+refer to a value.
+
 
 * Incompatible Lisp Changes in Emacs 31.1
 
diff --git a/lisp/progmodes/symbol.el b/lisp/progmodes/symbol.el
new file mode 100644
index 00000000000..26ce9d70878
--- /dev/null
+++ b/lisp/progmodes/symbol.el
@@ -0,0 +1,222 @@
+;;; symbol.el --- Keymap for interacting with symbols instead of words -*- lexical-binding: t -*-
+
+;; Copyright (C) 2025 Free Software Foundation, Inc.
+
+;; Author: Gabriel Santos <[email-masked]>
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This package provides the `symbol' minor mode which replaces the
+;; regular commands for word movement and interaction, such as
+;; `backward-word' and `capitalize-word' with ones specialized for
+;; symbols (namely `backward-symbol' and `capitalize-symbol').
+
+;; A symbol is any sequence of characters, including words, that can
+;; compose a single "word" in a programming sense.  Thus, most
+;; combinations of words and signs should be valid.
+
+;; To make this clearer, the following are all symbols:
+;; - symbol
+;; - this-is-a-symbol
+;; - this_is_also_a_symbol
+;; - my/symbol
+;; - 1010-symbol
+;; - alsoSomeSymbol
+;; - 孙/武
+
+;; To enable `symbol-mode' automatically for a specific mode, use
+;; hooks:
+;;
+;; (add-hook 'lisp-mode-hook 'symbol-mode)
+;;
+;; Or, for the use-package inclined:
+;;
+;; (use-package symbol
+;;   :hook
+;;   (lisp-mode . symbol-mode))
+
+;;; Code:
+
+(defun backward-symbol (arg)
+  "Move point to the previous position that is the start of a symbol.
+For the definition of a symbol, see `symbol-mode'.
+With prefix argument ARG, do it ARG times if positive, or move
+forwards ARG times if negative."
+  (interactive "^p")
+  (forward-symbol (- arg)))
+
+(defun kill-symbol (arg)
+  "Kill characters forward until encountering the end of a symbol.
+For the definition of a symbol, see `symbol-mode'.
+With prefix argument ARG, do it ARG times if positive, or do it
+backwards ARG times if negative."
+  (interactive "p")
+  (kill-region (point) (progn (forward-symbol arg) (point))))
+
+(defun backward-kill-symbol (arg)
+  "Kill characters backwards until encountering the start of a symbol.
+For the definition of a symbol, see `symbol-mode'.
+With prefix argument ARG, do it ARG times if positive, or do it
+forwards ARG times if negative."
+  (interactive "p")
+  (kill-symbol (- arg)))
+
+(defun mark-symbol (&optional arg allow-extend)
+  "Set mark ARG symbols from point or move mark one symbol.
+For the definition of a symbol, see `symbol-mode'.
+When called from Elisp with ALLOW-EXTEND omitted or nil, mark is
+set ARG symbols from point.
+
+With ARG and ALLOW-EXTEND both non-nil (interactively with prefix
+argument), the place to which mark goes is the same place
+\\[forward-symbol] would move to with the same argument; if the mark is
+active, it moves ARG symbols from its current position, otherwise if is
+set ARG symbols from point.
+When invoked interactively without a prefix argument and no active
+region, mark moves one symbol forward.
+When invoked interactively without a prefix argument, and region is
+active, mark moves one symbol away of point (i.e., forward if mark is
+at or after point, back if mark is before point), thus extending the
+region by one symbol.  Since the direction of region depends on the
+relative position of mark and point, you can change the direction
+by \\[exchange-point-and-mark]."
+  (interactive "P\np")
+  (cond ((and allow-extend
+              (or (and (eq last-command this-command) (mark t))
+                  (region-active-p)))
+         (setq arg (if arg (prefix-numeric-value arg)
+                     (if (< (mark) (point)) -1 1)))
+         (set-mark
+          (save-excursion
+            (goto-char (mark))
+            (forward-symbol arg)
+            (point))))
+        (t
+         (push-mark
+          (save-excursion
+            (forward-symbol (prefix-numeric-value arg))
+            (point))
+          nil t))))
+
+(defun right-symbol (&optional n)
+  "Move point N symbols to the right (to the left if N is negative).
+for the definition of a symbol, see `symbol-mode'.
+
+Depending on the bidirectional context, this may move either backward
+or forward in the buffer.  This is in contrast with \\[forward-symbol]
+and \\[backward-symbol], which see."
+  (interactive "^p")
+  (if (eq (current-bidi-paragraph-direction) 'left-to-right)
+      (forward-symbol n)
+    (backward-symbol n)))
+
+(defun left-symbol (&optional n)
+  "Move point N symbols to the left (to the right if N is negative).
+for the definition of a symbol, see `symbol-mode'.
+
+Depending on the bidirectional context, this may move either backward
+or forward in the buffer.  This is in contrast with \\[forward-symbol]
+and \\[backward-symbol], which see."
+  (interactive "^p")
+  (if (eq (current-bidi-paragraph-direction) 'left-to-right)
+      (backward-symbol n)
+    (forward-symbol n)))
+
+(defun transpose-symbols (arg)
+  "Interchange symbols around point, leaving point at the end of them.
+for the definition of a symbol, see `symbol-mode'.
+With prefix argument ARG, the symbol before or around point is taken and
+dragged forward past ARG or other symbols (backwards if ARG is negative).
+If ARG is unspecified, the symbols around or after point and around or
+after mark are interchanged."
+  (interactive "*p")
+  (transpose-subr 'forward-symbol arg))
+
+(defun downcase-symbol (arg)
+  "Convert to lower case from point to the end of a symbol, moving over.
+For the definition of a symbol, see `symbol-mode'.
+With a positive prefix argument ARG, downcase that many symbols forward.
+Note that calling this function in the middle of a symbol doesn't
+affect it.  If ARG is negative, previous symbols, including the current
+one are converted, but the point won't move."
+  (interactive "p")
+  (let ((start (point)))
+    (if (minusp arg)
+        (progn
+          (forward-symbol arg)
+          (downcase-region start (point))
+          (goto-char start))
+      (downcase-region (point) (forward-symbol arg)))))
+
+
+(defun upcase-symbol (arg)
+  "Convert to upper case from point to the end of a symbol, moving over.
+For the definition of a symbol, see `symbol-mode'.
+With a positive prefix argument ARG, upcase that many symbols forward.
+Note that calling this function in the middle of a symbol doesn't
+affect it.  If ARG is negative, previous symbols, including the current
+one are converted, but the point won't move."
+  (interactive "p")
+  (let ((start (point)))
+    (if (minusp arg)
+        (progn
+          (forward-symbol arg)
+          (upcase-region start (point))
+          (goto-char start))
+      (upcase-region (point) (forward-symbol arg)))))
+
+(defun capitalize-symbol (arg)
+  "Capitalize from point to the end of symbol, moving over.
+For the definition of a symbol, see `symbol-mode'.
+With a positive prefix arugment ARG, capitalize that many symbols forward.
+Note that calling this function in the middle of a symbol doesn't
+affect it.  If ARG is negative, previous symbols, including the current
+one are converted, but the point won't move."
+  (interactive "p")
+  (let ((start (point)))
+    (if (minusp arg)
+        (progn
+          (forward-symbol arg)
+          (capitalize-region start (point))
+          (goto-char start))
+      (capitalize-region (point) (forward-symbol arg)))))
+
+(defvar-keymap symbol-mode-map
+  :doc "Keymap used for `symbol-mode'."
+  "<remap> <forward-word>" #'forward-symbol
+  "<remap> <backward-word>" #'backward-symbol
+  "<remap> <kill-word>" #'kill-symbol
+  "<remap> <mark-word>" #'mark-symbol
+  "<remap> <backward-kill-word>" #'backward-kill-symbol
+  "<remap> <left-word>" #'left-symbol
+  "<remap> <right-word>" #'right-symbol
+  "<remap> <transpose-words>" #'transpose-symbols
+  "<remap> <downcase-word>" #'downcase-symbol
+  "<remap> <upcase-word>" #'upcase-symbol
+  "<remap> <capitalize-word>" #'capitalize-symbol)
+
+;;;###autoload
+(define-minor-mode symbol-mode
+  "Replace word-related commands with symbols-related ones.
+A symbol is a word-like construct composed of a combination of words and
+signs, with no separating space."
+  :lighter " λ")
+
+(provide 'symbol)
+
+;;; symbol.el ends here
diff --git a/test/lisp/progmodes/symbol-tests.el b/test/lisp/progmodes/symbol-tests.el
new file mode 100644
index 00000000000..da334fdb792
--- /dev/null
+++ b/test/lisp/progmodes/symbol-tests.el
@@ -0,0 +1,84 @@
+;;; symbol-tests.el --- Testing the symbol-related commands -*- lexical-bint -*-
+
+;; Copyright (C) 2025 Free Software Foundation, Inc.
+
+;; Author: Gabriel Santos <[email-masked]>
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This file is used for testing the functionality of the
+;; `symbol' package.  It's pretty close to the regular word commands,
+;; but it's always nice ot have tests.
+
+;;; Code:
+
+(require 'ert)
+(require 'symbol)
+
+(defconst symbol-tests-strings
+  '("foo^" "foo-bar^" "foo_bar^" "foo/bar^" "孙/武^"))
+
+(ert-deftest symbol-tests ()
+  "Test the `symbol-mode' rules."
+  (with-temp-buffer
+    (dolist (str symbol-tests-strings)
+      (erase-buffer)
+      (insert str)
+      (goto-char (point-min))
+      (while (search-forward "^" nil t)
+        (replace-match ""))
+      (goto-char (point-min))
+      (while (not (eobp))
+        (forward-symbol 1)
+        (insert "^"))
+      (should (equal (buffer-string) str)))))
+
+(ert-deftest symbol-tests2 ()
+  "Test that motion in `symbol-mode' stops at the right places."
+
+  (let* ((line "foo foo-bar foo_bar foo/bar 365-symbol")
+         (fwrd "*  *       *       *       *          *")
+         (bkwd "*   *       *       *       *         *"))
+
+    (with-temp-buffer
+      (symbol-mode 1)
+      (insert line)
+
+      ;; Test forward motion.
+
+      (goto-char (point-min))
+      (let ((stops (make-string (length fwrd) ?\ )))
+        (while (progn
+                 (aset stops (1- (point)) ?\*)
+                 (not (eobp)))
+          (forward-symbol 1))
+        (should (equal stops fwrd)))
+
+      ;; Test backwards motion.
+
+      (goto-char (point-max))
+      (let ((stops (make-string (length bkwd) ?\ )))
+        (while (progn
+                 (aset stops (1- (point)) ?\*)
+                 (not (bobp)))
+          (backward-symbol 1))
+        (should (equal stops bkwd))))))
+
+
+(provide 'symbol-tests)
+;;; symbol-tests.el ends here
-- 
2.49.0