branch: externals/matlab-mode
commit 22803079c7f868931e48c0b92acbd49fab5117a9
Author: John Ciolfi <[email protected]>
Commit: John Ciolfi <[email protected]>
matlab-ts-mode: support single quoted strings in electric-pair-mode
---
contributing/treesit-mode-how-to.org | 46 +++++++
matlab-ts-mode.el | 59 ++++++++-
tests/t-utils.el | 144 +++++++++++---------
.../electric_pair_single_quote.m | 16 +++
.../electric_pair_single_quote_expected.org | 147 +++++++++++++++++++++
tests/test-matlab-ts-mode-electric-pair.el | 58 ++++++++
6 files changed, 405 insertions(+), 65 deletions(-)
diff --git a/contributing/treesit-mode-how-to.org
b/contributing/treesit-mode-how-to.org
index 1cb68a7808..e6e6f347c8 100644
--- a/contributing/treesit-mode-how-to.org
+++ b/contributing/treesit-mode-how-to.org
@@ -1110,6 +1110,45 @@ imenu-create-index-function as we did above.
TODO
+* Verify electric-pair-mode is good
+
+=M-x electric-pair-mode= for most languages will just work. However, if your
language
+uses typical characters that are paired, e.g. a single quote for a string
delimiter and
+also an operator such as a transpose, then you'll need to:
+
+#+begin_src emacs-lisp
+ (declare-function electric-pair-default-inhibit "elec-pair")
+ (defun LANGUAGE-ts-mode--electric-pair-inhibit-predicate (char)
+ "Return non-nil if `electric-pair-mode' should not pair this CHAR.
+ Do not pair the transpose operator, (\\='), but pair it when used as a
+ single quote string."
+
+ ;; (point) is just after CHAR. For example, if we type a single quote:
+ ;; x = '
+ ;; ^--(point)
+
+ (cond
+ ;; Case: Single quote
+ ((eq char ?')
+ ;; Look at the tree-sitter nodes and return t if the pairing should be
inhibited.
+ ;; <snip>
+ )
+
+ ;; Case: Not a single quote, defer to the standard electric pair handling
+ (t
+ (funcall #'electric-pair-default-inhibit char))))
+
+ ;; <snip>
+ (define-derived-mode matlab-ts-mode prog-mode "LANGUAGE:ts"
+
+ ;; <snip>
+
+ ;; Electric pair mode
+ (setq-local electric-pair-inhibit-predicate
#'matlab-ts-mode--electric-pair-inhibit-predicate)
+ )
+#+end_src
+
+
* Final version
TODO
@@ -1697,3 +1736,10 @@ well worth writing a tree-sitter mode.
14. On save fix of function/classdef name now handles buffer names that
aren't valid MATLAB
identifiers. On save fix of function/classdef name handles buffers not
associated with files
on disk. Also fixed cases where detection of scripts failed.
+
+ 15. Improved handling of single quotes for =M-x electric-pair-mode=. These
will automatically
+ pair
+ - Single quote when used to create a single-quoted string, but not when
used elsewhere,
+ e.g. a matrix transpose.
+ - Double quotes for a double-quoted string.
+ - Parenthesis =()=, Vectors, =[]=, and Cells ={}=.
diff --git a/matlab-ts-mode.el b/matlab-ts-mode.el
index 83ac1c5aa9..fd16a21b58 100644
--- a/matlab-ts-mode.el
+++ b/matlab-ts-mode.el
@@ -956,6 +956,56 @@ Enable/disable `matlab-sections-minor-mode' based on file
content."
;; the final save. See `run-hook-with-args-until-success'.
nil)
+;;; Electric Pair Mode, M-x electric-pair-mode
+
+(declare-function electric-pair-default-inhibit "elec-pair")
+(defun matlab-ts-mode--electric-pair-inhibit-predicate (char)
+ "Return non-nil if `electric-pair-mode' should not pair this CHAR.
+Do not pair the transpose operator, (\\='), but pair it when used as a
+single quote string."
+
+ ;; (point) is just after CHAR. For example, if we type a single quote:
+ ;; x = '
+ ;; ^--(point)
+
+ (cond
+ ;; Case: Single quote
+ ;; str = ' : start of single quote string => nil
+ ;; mat' : transpose operator => t
+ ;; str = " ' " : add single quote in string => t
+ ((eq char ?')
+ (let* ((node-back1 (treesit-node-at (- (point) 1)))
+ (type-back1 (treesit-node-type node-back1)))
+ (cond
+ ;; Case: in comment, return t if it looks like a transpose, e.g. A' or
similar.
+ ((string-match-p (rx bol (or "comment" "line_continuation") eol)
type-back1)
+ (save-excursion
+ (forward-char -1)
+ (looking-at "\\w\\|\\s_\\|\\.")))
+
+ ;; Case: string delimiter
+ ;; double up if starting a new string => return nil
+ ((string= "'" type-back1)
+ (not (string= "string" (treesit-node-type (treesit-node-parent
node-back1)))))
+
+ ;; Case: inside a single quote string
+ ;; s = 'foobar'
+ ;; ^ insert here
+ ;; s = 'foo''bar'
+ ((and (string= "string_content" type-back1)
+ (string= "'" (treesit-node-type (treesit-node-prev-sibling
node-back1))))
+ nil)
+
+ ;; Case: not a string delimiter, return t
+ ;; transpose: A'
+ ;; inside double quote string: "foo'bar"
+ (t
+ t))))
+
+ ;; Case: Not a single quote, defer to the standard electric pair handling
+ (t
+ (funcall #'electric-pair-default-inhibit char))))
+
;;; matlab-ts-mode
;;;###autoload
@@ -1029,19 +1079,20 @@ is t, add the following to an Init File (e.g.
`user-init-file' or
;; See: ./tests/test-matlab-ts-mode-outline.el
(setq-local treesit-outline-predicate #'matlab-ts-mode--outline-predicate)
- ;; Save hook
+ ;; Save hook. See: ./tests/test-matlab-ts-mode-on-save-fixes.el
(add-hook 'write-contents-functions #'matlab-ts-mode--write-file-callback)
+ ;; Electric pair mode. See tests/test-matlab-ts-mode-electric-pair.el
+ (setq-local electric-pair-inhibit-predicate
#'matlab-ts-mode--electric-pair-inhibit-predicate)
+
;; TODO Highlight parens OR if/end type blocks
- ;; TODO Electric pair mode
- ;; TODO what about syntax table and electric keywords?
- ;; TODO code folding
;; TODO font-lock highlight operators, *, /, +, -, ./, booleans
true/false, etc.
;; TODO face for all built-in functions such as dbstop, quit, sin, etc.
;;
https://www.mathworks.com/help/matlab/referencelist.html?type=function&category=index&s_tid=CRUX_lftnav_function_index
;;
https://stackoverflow.com/questions/51942464/programmatically-return-a-list-of-all-functions/51946257
;; Maybe use completion api and complete on each letter?
;; Maybe look at functionSignatures.json?
+ ;; TODO code folding
(treesit-major-mode-setup)))
diff --git a/tests/t-utils.el b/tests/t-utils.el
index 79241b416c..74113fa8fc 100644
--- a/tests/t-utils.el
+++ b/tests/t-utils.el
@@ -252,63 +252,77 @@ You can run `t-utils--diff-check' to debug"))))
(dolist (command commands)
(setq cmd-num (1+ cmd-num))
- (let ((start-point (point))
- (start-contents (buffer-substring-no-properties (point-min)
(point-max)))
- (key-command (when (eq (type-of command) 'string)
- ;; Keybinding, e.g. (t-utils-xr "C-M-a")
- (let ((cmd (key-binding (kbd command))))
- (when (not cmd)
- (user-error "%s:%d: Command, %s, is not a known
keybinding"
- buf-file start-line command))
- cmd))))
-
- (setq result (concat result "\n"
- (format "- Invoking : %S%s\n"
- command (if key-command
- (concat " = " (symbol-name
key-command))
- ""))
- (format " Start point : %4d\n" start-point)))
-
- (if key-command
- ;; a keybinding: (t-util-xr "C-M-a")
- (call-interactively key-command)
- ;; a command: (t-utils-xr (beginning-of-defun))
- (eval command))
-
- (let ((end-point (point))
- (end-contents (buffer-substring-no-properties (point-min)
(point-max)))
- (debug-msg (format "%d: %S, start point %s" cmd-num command
- (t-utils--get-point-for-display
start-point))))
-
- ;; Record point movement by adding what happened to result
- (if (equal start-point end-point)
- (setq result (concat result " No point movement\n")
- debug-msg (concat debug-msg ", no point movement"))
- (let* ((current-line (buffer-substring-no-properties
(line-beginning-position)
-
(line-end-position)))
- (position (format "%d:%d: " (line-number-at-pos)
(current-column)))
- (carrot (concat (make-string (+ (length position)
(current-column)) ?\s) "^")))
- (setq result (concat result (format " Moved to point: %4d\n :
%s%s\n : %s\n"
- end-point position
current-line carrot))
- debug-msg (concat debug-msg
- (format ", moved point to %s"
- (t-utils--get-point-for-display
(point)))))))
-
- ;; Record buffer modifications by adding what happened to result
- (if (equal start-contents end-contents)
- (setq result (concat result " No buffer modifications\n")
- debug-msg (concat debug-msg ", no buffer modifications"))
- (setq result (concat result
- " Buffer modified:\n"
- " #+begin_src diff\n"
- (t-utils-diff-strings start-contents
end-contents)
- " #+end_src diff\n")
- debug-msg (concat debug-msg ", buffer modified")))
-
- (when (not (t-utils--use-xr-impl-result))
- ;; Display debugging info for interactive evaluation of
(t-utils-xr COMMANDS)
- (read-string (concat debug-msg "\n" "Enter to continue:"))))))
-
+ (let ((standard-output (generate-new-buffer " *temp t-utils-xr-capture*"
t)))
+ (unwind-protect
+ (let* ((start-pt (point))
+ (start-pt-str (t-utils--get-point-for-display start-pt))
+ (start-contents (buffer-substring-no-properties (point-min)
(point-max)))
+ (key-command (when (eq (type-of command) 'string)
+ ;; Keybinding, e.g. (t-utils-xr "C-M-a")
+ (let ((cmd (key-binding (kbd command))))
+ (when (not cmd)
+ (user-error "%s:%d: Command, %s, is not
a known keybinding"
+ buf-file start-line command))
+ cmd))))
+ (setq result (concat result "\n"
+ (format "- Invoking : %S%s\n"
+ command (if key-command
+ (concat " = "
(symbol-name key-command))
+ ""))
+ (format " Start point : %4d\n"
start-pt)))
+
+ (if key-command
+ ;; a keybinding: (t-util-xr "C-M-a")
+ (call-interactively key-command)
+ ;; a command: (t-utils-xr (beginning-of-defun))
+ (eval command))
+
+ (let ((end-pt (point))
+ (end-contents (buffer-substring-no-properties (point-min)
(point-max)))
+ (debug-msg (format "%d: %S, start point %s" cmd-num
command start-pt-str)))
+
+ ;; Record point movement by adding what happened to result
+ (if (equal start-pt end-pt)
+ (setq result (concat result " No point movement\n")
+ debug-msg (concat debug-msg ", no point movement"))
+ (let* ((current-line (buffer-substring-no-properties
(line-beginning-position)
+
(line-end-position)))
+ (position (format "%d:%d: " (line-number-at-pos)
(current-column)))
+ (carrot (concat (make-string (+ (length position)
(current-column)) ?\s)
+ "^")))
+ (setq result (concat result (format " Moved to point:
%4d\n : %s%s\n : %s\n"
+ end-pt position
current-line carrot))
+ debug-msg (concat debug-msg
+ (format ", moved point to %s"
+
(t-utils--get-point-for-display (point)))))))
+
+ ;; Grab standard-output from `prin1' or `print'
+ (with-current-buffer standard-output
+ (let ((contents (string-trim (buffer-substring (point-min)
(point-max)))))
+ (when (not (string= contents ""))
+ (setq result (concat result
+ " standard-output:\n "
+ (replace-regexp-in-string "^" " "
contents)
+ "\n")))))
+
+ ;; Record buffer modifications by adding what happened to
result
+ (if (equal start-contents end-contents)
+ (setq result (concat result " No buffer modifications\n")
+ debug-msg (concat debug-msg ", no buffer
modifications"))
+ (setq result (concat result
+ " Buffer modified:\n"
+ " #+begin_src diff\n"
+ (t-utils-diff-strings start-contents
end-contents)
+ " #+end_src diff\n")
+ debug-msg (concat debug-msg ", buffer modified")))
+
+ (when (not (t-utils--use-xr-impl-result))
+ ;; Display debugging info for interactive evaluation of
(t-utils-xr COMMANDS)
+ (read-string (concat debug-msg "\n" "Enter to continue:")))))
+ ;; unwind-protect unwindforms
+ (and (buffer-name standard-output)
+ (kill-buffer standard-output)))))
+
(if (t-utils--use-xr-impl-result)
(progn
(setq t-utils--xr-impl-result result)
@@ -332,6 +346,10 @@ The commands that you can place within (t-utils-xr
COMMANDS) are
(t-utils-xr (beginning-of-defun))
2. Keybindings. For example,
(t-utils-xr \"C-M-a\")
+ 3. `standard-output' is captured. You use (prin1 OBJECT) or (print OBJECT)
+ to write `standard-output', which lets you capture the results
+ of functions in the baseline. For example,
+ (t-utils-xr (prin1 (a-buffer-query-function-special-to-your-mode)))
Multiple expressions or keybindings can be specified.
Consider ./test-defun-movement/my_test.c:
@@ -411,16 +429,20 @@ for this example is:
;; we still see 'nil' displayed, but I don't think there's much we
can do about that.
;; Note, using: (let ((standard-output (lambda (_))))
(eval-last-sexp nil))
;; still causes nil to be displayed when run from emacs --batch.
+ ;;
+ ;; One item to investigate: advising
elisp--eval-last-sexp-print-value
+ ;; to make output go to a buffer. I believe output in this
function is t, which
+ ;; means the echo area.
+ ;; Maybe just call elisp--eval-last-sexp-print-value and define
output.
(setq t-utils--xr-impl-result-active t)
- (condition-case err
+ (unwind-protect
(progn
(eval-last-sexp nil)
(setq got (concat got t-utils--xr-impl-result)
t-utils--xr-impl-result-active nil
t-utils--xr-impl-result nil))
- (error (setq t-utils--xr-impl-result-active nil
- t-utils--xr-impl-result nil)
- (signal (car err) (cdr err))))
+ (setq t-utils--xr-impl-result-active nil
+ t-utils--xr-impl-result nil))
;; look for next (t-utils-xr COMMANDS)
(goto-char xr-end-point)))
diff --git
a/tests/test-matlab-ts-mode-electric-pair-files/electric_pair_single_quote.m
b/tests/test-matlab-ts-mode-electric-pair-files/electric_pair_single_quote.m
new file mode 100644
index 0000000000..db69824a3e
--- /dev/null
+++ b/tests/test-matlab-ts-mode-electric-pair-files/electric_pair_single_quote.m
@@ -0,0 +1,16 @@
+% -*- matlab-ts -*-
+a = [1,2];
+
+
+% (t-utils-xr (re-search-forward "'") (print
(matlab-ts-mode--electric-pair-inhibit-predicate (char-before))))
+a'
+
+% (t-utils-xr (re-search-forward "'") (print
(matlab-ts-mode--electric-pair-inhibit-predicate (char-before))))
+b = "foo'bar"
+
+% (t-utils-xr (re-search-forward "foo") (insert "'") (print
(matlab-ts-mode--electric-pair-inhibit-predicate (char-before))) (delete-region
(1- (point)) (point)))
+s='foobar'
+
+% start string
+% (t-utils-xr (re-search-forward "s2") (insert " = '") (print
(matlab-ts-mode--electric-pair-inhibit-predicate (char-before))) (delete-region
(- (point) 4) (point)))
+s2
diff --git
a/tests/test-matlab-ts-mode-electric-pair-files/electric_pair_single_quote_expected.org
b/tests/test-matlab-ts-mode-electric-pair-files/electric_pair_single_quote_expected.org
new file mode 100644
index 0000000000..c606eda7cf
--- /dev/null
+++
b/tests/test-matlab-ts-mode-electric-pair-files/electric_pair_single_quote_expected.org
@@ -0,0 +1,147 @@
+#+startup: showall
+
+* Executing commands from electric_pair_single_quote.m:5:2:
+
+ (t-utils-xr (re-search-forward "'") (print
(matlab-ts-mode--electric-pair-inhibit-predicate (char-before))))
+
+- Invoking : (re-search-forward "'")
+ Start point : 144
+ Moved to point: 147
+ : 6:2: a'
+ : ^
+ No buffer modifications
+
+- Invoking : (print (matlab-ts-mode--electric-pair-inhibit-predicate
(char-before)))
+ Start point : 147
+ No point movement
+ standard-output:
+ t
+ No buffer modifications
+
+* Executing commands from electric_pair_single_quote.m:8:2:
+
+ (t-utils-xr (re-search-forward "'") (print
(matlab-ts-mode--electric-pair-inhibit-predicate (char-before))))
+
+- Invoking : (re-search-forward "'")
+ Start point : 259
+ Moved to point: 269
+ : 9:9: b = "foo'bar"
+ : ^
+ No buffer modifications
+
+- Invoking : (print (matlab-ts-mode--electric-pair-inhibit-predicate
(char-before)))
+ Start point : 269
+ No point movement
+ standard-output:
+ t
+ No buffer modifications
+
+* Executing commands from electric_pair_single_quote.m:11:2:
+
+ (t-utils-xr (re-search-forward "foo") (insert "'") (print
(matlab-ts-mode--electric-pair-inhibit-predicate (char-before))) (delete-region
(1- (point)) (point)))
+
+- Invoking : (re-search-forward "foo")
+ Start point : 437
+ Moved to point: 444
+ : 12:6: s='foobar'
+ : ^
+ No buffer modifications
+
+- Invoking : (insert "'")
+ Start point : 444
+ Moved to point: 445
+ : 12:7: s='foo'bar'
+ : ^
+ Buffer modified:
+ #+begin_src diff
+--- start_contents
++++ end_contents
+@@ -9,7 +9,7 @@
+ b = "foo'bar"
+
+ % (t-utils-xr (re-search-forward "foo") (insert "'") (print
(matlab-ts-mode--electric-pair-inhibit-predicate (char-before))) (delete-region
(1- (point)) (point)))
+-s='foobar'
++s='foo'bar'
+
+ % start string
+ % (t-utils-xr (re-search-forward "s2") (insert " = '") (print
(matlab-ts-mode--electric-pair-inhibit-predicate (char-before))) (delete-region
(- (point) 4) (point)))
+ #+end_src diff
+
+- Invoking : (print (matlab-ts-mode--electric-pair-inhibit-predicate
(char-before)))
+ Start point : 445
+ No point movement
+ standard-output:
+ nil
+ No buffer modifications
+
+- Invoking : (delete-region (1- (point)) (point))
+ Start point : 445
+ Moved to point: 444
+ : 12:6: s='foobar'
+ : ^
+ Buffer modified:
+ #+begin_src diff
+--- start_contents
++++ end_contents
+@@ -9,7 +9,7 @@
+ b = "foo'bar"
+
+ % (t-utils-xr (re-search-forward "foo") (insert "'") (print
(matlab-ts-mode--electric-pair-inhibit-predicate (char-before))) (delete-region
(1- (point)) (point)))
+-s='foo'bar'
++s='foobar'
+
+ % start string
+ % (t-utils-xr (re-search-forward "s2") (insert " = '") (print
(matlab-ts-mode--electric-pair-inhibit-predicate (char-before))) (delete-region
(- (point) 4) (point)))
+ #+end_src diff
+
+* Executing commands from electric_pair_single_quote.m:15:2:
+
+ (t-utils-xr (re-search-forward "s2") (insert " = '") (print
(matlab-ts-mode--electric-pair-inhibit-predicate (char-before))) (delete-region
(- (point) 4) (point)))
+
+- Invoking : (re-search-forward "s2")
+ Start point : 630
+ Moved to point: 633
+ : 16:2: s2
+ : ^
+ No buffer modifications
+
+- Invoking : (insert " = '")
+ Start point : 633
+ Moved to point: 637
+ : 16:6: s2 = '
+ : ^
+ Buffer modified:
+ #+begin_src diff
+--- start_contents
++++ end_contents
+@@ -13,4 +13,4 @@
+
+ % start string
+ % (t-utils-xr (re-search-forward "s2") (insert " = '") (print
(matlab-ts-mode--electric-pair-inhibit-predicate (char-before))) (delete-region
(- (point) 4) (point)))
+-s2
++s2 = '
+ #+end_src diff
+
+- Invoking : (print (matlab-ts-mode--electric-pair-inhibit-predicate
(char-before)))
+ Start point : 637
+ No point movement
+ standard-output:
+ nil
+ No buffer modifications
+
+- Invoking : (delete-region (- (point) 4) (point))
+ Start point : 637
+ Moved to point: 633
+ : 16:2: s2
+ : ^
+ Buffer modified:
+ #+begin_src diff
+--- start_contents
++++ end_contents
+@@ -13,4 +13,4 @@
+
+ % start string
+ % (t-utils-xr (re-search-forward "s2") (insert " = '") (print
(matlab-ts-mode--electric-pair-inhibit-predicate (char-before))) (delete-region
(- (point) 4) (point)))
+-s2 = '
++s2
+ #+end_src diff
diff --git a/tests/test-matlab-ts-mode-electric-pair.el
b/tests/test-matlab-ts-mode-electric-pair.el
new file mode 100644
index 0000000000..aee1fe3efd
--- /dev/null
+++ b/tests/test-matlab-ts-mode-electric-pair.el
@@ -0,0 +1,58 @@
+;;; test-matlab-ts-mode-electric-pair.el --- -*- lexical-binding: t -*-
+;;
+;; Copyright 2025 Free Software Foundation, Inc.
+;;
+;; This program 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, or (at your option)
+;; any later version.
+;;
+;; This program 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; see the file COPYING. If not, write to
+;; the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
+;;
+
+;;; Commentary:
+;;
+;; Validate matlab-ts-mode indent.
+;; Load ../matlab-ts-mode.el via require and run indent tests using
+;; ./test-matlab-ts-mode-electric-pair-files/NAME.m comparing against
+;; ./test-matlab-ts-mode-electric-pair-files/NAME_expected.org
+;;
+
+;;; Code:
+
+(require 't-utils)
+(require 'matlab-ts-mode)
+
+(cl-defun test-matlab-ts-mode-electric-pair (&optional m-file)
+ "Test defun movement using ./test-matlab-ts-mode-electric-pair-files/NAME.m.
+Using ./test-matlab-ts-mode-electric-pair-files/NAME.m, compare defun
+movement against
+./test-matlab-ts-mode-electric-pair-files/NAME_expected.org. If M-FILE is
+not provided, loop comparing all
+./test-matlab-ts-mode-electric-pair-files/NAME.m files.
+
+To add a test, create
+ ./test-matlab-ts-mode-electric-pair-files/NAME.m
+and run this function. The baseline is saved for you as
+ ./test-matlab-ts-mode-electric-pair-files/NAME_expected.org~
+after validating it, rename it to
+ ./test-matlab-ts-mode-electric-pair-files/NAME_expected.org"
+
+ (let ((test-name "test-matlab-ts-mode-electric-pair"))
+
+ (when (not (t-utils-is-treesit-available 'matlab test-name))
+ (cl-return-from test-matlab-ts-mode-font-lock))
+
+ (let ((m-files (t-utils-get-files (concat test-name "-files") "\\.m$" nil
m-file)))
+ (t-utils-test-xr test-name m-files)))
+ "success")
+
+(provide 'test-matlab-ts-mode-electric-pair)
+;;; test-matlab-ts-mode-electric-pair.el ends here