Subject: bug#60623: 30.0.50; Add forward-sentence with tree sitter support

From: Theodor Thornhill <[email-masked]>
Date: Sat, 07 Jan 2023 12:54:13 +0100

Hi all!

This patch tweaks the forward-sentence function to be usable with
tree-sitter.

It follows the same style as the recent change in transpose-sexps, so I
hope it isn't too controversial.

What exact node types do you consider useful for sentence movement?

I added an example value in java-ts-mode and c-ts-mode, but that is
merely a suggestion.  Let's decide on some heuristic to decide the
proper nodes to use.

What do you think?

Theo

>From 32eb3c3401f2232c349843e21269ff88e982945e Mon Sep 17 00:00:00 2001
From: Theodor Thornhill <[email-masked]>
Date: Sat, 7 Jan 2023 12:44:14 +0100
Subject: [PATCH] Add forward-sentence with tree sitter support

* etc/NEWS: Mention the new changes.
* lisp/progmodes/c-ts-mode.el (c-ts-base-mode): Set the variable.
* lisp/progmodes/java-ts-mode.el (java-ts-mode): Set the variable.
* lisp/textmodes/paragraphs.el (forward-sentence-function): Move old
implementation to a new defvar.
(forward-sentence): Use the variable in this function unconditionally.
* lisp/treesit.el (treesit-sentence-type-regexp): New defvar.
(treesit-forward-sentence): New defun.
(treesit-major-mode-setup): Conditionally set
forward-sentence-function.
---
 etc/NEWS                       | 12 ++++++
 lisp/progmodes/c-ts-mode.el    |  8 ++++
 lisp/progmodes/java-ts-mode.el |  6 +++
 lisp/textmodes/paragraphs.el   | 71 +++++++++++++++++++---------------
 lisp/treesit.el                | 18 +++++++++
 5 files changed, 83 insertions(+), 32 deletions(-)

diff --git a/etc/NEWS b/etc/NEWS
index a2924201267..5b4533b0522 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -57,6 +57,18 @@ treesit.el now unconditionally sets 'transpose-sexps-function' for all
 Tree-sitter modes.  This functionality utilizes the new
 'transpose-sexps-function'.
 
+** New defvar-local forward-sentence-function.
+The previous implementation of 'forward-sentence' is moved into this
+variable, which can be set to customize the sentece movement behavior.
+
+** New defvar-local 'treesit-sentence-type-regexp.
+Similarly to 'treesit-defun-type-regexp', this variable is used to
+navigate sentences in Tree-sitter enabled modes.
+
+** New function 'treesit-forward-sentence'.
+treesit.el now conditionally sets 'forward-sentence-function' for all
+Tree-sitter modes that sets 'treesit-sentence-type-regexp'.
+
 
 * Changes in Specialized Modes and Packages in Emacs 30.1
 ---
diff --git a/lisp/progmodes/c-ts-mode.el b/lisp/progmodes/c-ts-mode.el
index e76966e7660..3794286162e 100644
--- a/lisp/progmodes/c-ts-mode.el
+++ b/lisp/progmodes/c-ts-mode.el
@@ -735,6 +735,14 @@ c-ts-base-mode
   :syntax-table c-ts-mode--syntax-table
 
   ;; Navigation.
+  (setq-local treesit-sentence-type-regexp
+              (regexp-opt '("statement"
+                            "expression"
+                            "definition"
+                            "specifier"
+                            "declaration"
+                            "comment"
+                            "preproc")))
   (setq-local treesit-defun-type-regexp
               (cons (regexp-opt '("function_definition"
                                   "type_definition"
diff --git a/lisp/progmodes/java-ts-mode.el b/lisp/progmodes/java-ts-mode.el
index 87a4e2b90f8..0e3381e219a 100644
--- a/lisp/progmodes/java-ts-mode.el
+++ b/lisp/progmodes/java-ts-mode.el
@@ -296,6 +296,12 @@ java-ts-mode
               (append "{}():;," electric-indent-chars))
 
   ;; Navigation.
+  (setq-local treesit-sentence-type-regexp
+              (regexp-opt '("statement"
+                            "expression"
+                            "parameters"
+                            "list"
+                            "comment")))
   (setq-local treesit-defun-type-regexp
               (regexp-opt '("method_declaration"
                             "class_declaration"
diff --git a/lisp/textmodes/paragraphs.el b/lisp/textmodes/paragraphs.el
index 73abb155aaa..5bbb77c1cb6 100644
--- a/lisp/textmodes/paragraphs.el
+++ b/lisp/textmodes/paragraphs.el
@@ -441,44 +441,51 @@ end-of-paragraph-text
 	  (if (< (point) (point-max))
 	      (end-of-paragraph-text))))))
 
-(defun forward-sentence (&optional arg)
+(defvar forward-sentence-function
+  (lambda (&optional arg)
+    (let ((opoint (point))
+          (sentence-end (sentence-end)))
+      (while (< arg 0)
+        (let ((pos (point))
+	      par-beg par-text-beg)
+	  (save-excursion
+	    (start-of-paragraph-text)
+	    ;; Start of real text in the paragraph.
+	    ;; We move back to here if we don't see a sentence-end.
+	    (setq par-text-beg (point))
+	    ;; Start of the first line of the paragraph.
+	    ;; We use this as the search limit
+	    ;; to allow sentence-end to match if it is anchored at
+	    ;; BOL and the paragraph starts indented.
+	    (beginning-of-line)
+	    (setq par-beg (point)))
+	  (if (and (re-search-backward sentence-end par-beg t)
+		   (or (< (match-end 0) pos)
+		       (re-search-backward sentence-end par-beg t)))
+	      (goto-char (match-end 0))
+	    (goto-char par-text-beg)))
+        (setq arg (1+ arg)))
+      (while (> arg 0)
+        (let ((par-end (save-excursion (end-of-paragraph-text) (point))))
+	  (if (re-search-forward sentence-end par-end t)
+	      (skip-chars-backward " \t\n")
+	    (goto-char par-end)))
+        (setq arg (1- arg)))
+      (let ((npoint (constrain-to-field nil opoint t)))
+        (not (= npoint opoint)))))
   "Move forward to next end of sentence.  With argument, repeat.
 When ARG is negative, move backward repeatedly to start of sentence.
 
 The variable `sentence-end' is a regular expression that matches ends of
-sentences.  Also, every paragraph boundary terminates sentences as well."
+sentences.  Also, every paragraph boundary terminates sentences as well.")
+
+(defun forward-sentence (&optional arg)
+  "Move forward to next end of sentence.  With argument, repeat.
+When ARG is negative, move backward repeatedly to start of sentence.
+Delegates its work to `forward-sentence-function'."
   (interactive "^p")
   (or arg (setq arg 1))
-  (let ((opoint (point))
-        (sentence-end (sentence-end)))
-    (while (< arg 0)
-      (let ((pos (point))
-	    par-beg par-text-beg)
-	(save-excursion
-	  (start-of-paragraph-text)
-	  ;; Start of real text in the paragraph.
-	  ;; We move back to here if we don't see a sentence-end.
-	  (setq par-text-beg (point))
-	  ;; Start of the first line of the paragraph.
-	  ;; We use this as the search limit
-	  ;; to allow sentence-end to match if it is anchored at
-	  ;; BOL and the paragraph starts indented.
-	  (beginning-of-line)
-	  (setq par-beg (point)))
-	(if (and (re-search-backward sentence-end par-beg t)
-		 (or (< (match-end 0) pos)
-		     (re-search-backward sentence-end par-beg t)))
-	    (goto-char (match-end 0))
-	  (goto-char par-text-beg)))
-      (setq arg (1+ arg)))
-    (while (> arg 0)
-      (let ((par-end (save-excursion (end-of-paragraph-text) (point))))
-	(if (re-search-forward sentence-end par-end t)
-	    (skip-chars-backward " \t\n")
-	  (goto-char par-end)))
-      (setq arg (1- arg)))
-    (let ((npoint (constrain-to-field nil opoint t)))
-      (not (= npoint opoint)))))
+  (funcall forward-sentence-function arg))
 
 (defun count-sentences (start end)
   "Count sentences in current buffer from START to END."
diff --git a/lisp/treesit.el b/lisp/treesit.el
index a7f453a8899..ec4d711bba8 100644
--- a/lisp/treesit.el
+++ b/lisp/treesit.el
@@ -1783,6 +1783,22 @@ treesit-end-of-defun
     (when treesit-defun-skipper
       (funcall treesit-defun-skipper))))
 
+(defvar-local treesit-sentence-type-regexp ""
+  "A regexp that matches the node type of sentence nodes.
+
+A sentence node is a node that is bigger than a sexp, and
+delimits larger statements in the source code.  It is, however,
+smaller in scope than defuns.  This is used by
+`treesit-forward-sentence' and friends.")
+
+(defun treesit-forward-sentence (&optional arg)
+  "Tree-sitter `forward-sentence-function' function.
+ARG is the same as in `forward-sentence'.  Behavior of this
+function depends on `treesit-sentence-type-regexp'."
+  (funcall
+   (if (> arg 0) #'treesit-end-of-thing #'treesit-beginning-of-thing)
+   treesit-sentence-type-regexp (abs arg)))
+
 (defvar-local treesit-text-type-regexp "\\`comment\\'"
   "A regexp that matches the node type of textual nodes.
 
@@ -2256,6 +2272,8 @@ treesit-major-mode-setup
                 #'treesit-add-log-current-defun))
 
   (setq-local transpose-sexps-function #'treesit-transpose-sexps)
+  (when treesit-sentence-type-regexp
+    (setq-local forward-sentence-function #'treesit-forward-sentence))
 
   ;; Imenu.
   (when treesit-simple-imenu-settings
-- 
2.34.1