branch: externals/matlab-mode
commit 5500471a1dda65855be8145bd89bd51aa9d2cf51
Author: John Ciolfi <[email protected]>
Commit: John Ciolfi <[email protected]>
matlab-ts-mode: add on save fixes, correct defcustom names
---
contributing/treesit-mode-how-to.org | 3 +
matlab-ts-mode.el | 121 ++++++++++++++++++---
tests/test-matlab-ts-mode-font-lock.el | 8 +-
.../on_save_fix_class.m | 12 ++
.../on_save_fix_class_expected.org | 28 +++++
.../on_save_fix_fcn.m | 12 ++
.../on_save_fix_fcn_expected.org | 28 +++++
.../on_save_no_fix_script.m | 15 +++
.../on_save_no_fix_script_expected.org | 15 +++
tests/test-matlab-ts-mode-on-save-fixes.el | 58 ++++++++++
10 files changed, 280 insertions(+), 20 deletions(-)
diff --git a/contributing/treesit-mode-how-to.org
b/contributing/treesit-mode-how-to.org
index cf81b9f734..1cb68a7808 100644
--- a/contributing/treesit-mode-how-to.org
+++ b/contributing/treesit-mode-how-to.org
@@ -1694,3 +1694,6 @@ well worth writing a tree-sitter mode.
13. Added support for =M-x outline-minor-mode=. Outline headings are
=function='s, =classdef='s,
and "%% heading" comments.
+ 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.
diff --git a/matlab-ts-mode.el b/matlab-ts-mode.el
index 3767d1f054..c942513ea5 100644
--- a/matlab-ts-mode.el
+++ b/matlab-ts-mode.el
@@ -41,26 +41,26 @@
(defgroup matlab-ts nil
"MATLAB(R) tree-sitter mode."
- :prefix "matlab-ts-"
+ :prefix "matlab-ts-mode-"
:group 'languages)
-(defface matlab-ts-pragma-face
+(defface matlab-ts-mode-pragma-face
'((t :inherit font-lock-comment-face
:bold t))
"*Face to use for pragma %# lines.")
-(defface matlab-ts-string-delimiter-face
+(defface matlab-ts-mode-string-delimiter-face
'((t :inherit font-lock-string-face
:bold t))
"*Face to use for \\='single quote\\=' and \"double quote\" string
delimiters.")
-(defface matlab-ts-comment-heading-face
+(defface matlab-ts-mode-comment-heading-face
'((t :inherit font-lock-comment-face
:overline t
:bold t))
"Face for \"%% code section\" headings when NOT in
matlab-sections-minor-mode.")
-(defface matlab-ts-comment-to-do-marker-face
+(defface matlab-ts-mode-comment-to-do-marker-face
'((((class color) (background light))
:inherit font-lock-comment-face
:background "yellow"
@@ -80,13 +80,22 @@ Guidelines:
source repository. TO" "DO markers should reflect improvements are are
not problems with the existing code."))
-(defcustom matlab-ts-font-lock-level 4
+(defcustom matlab-ts-mode-font-lock-level 4
"*Level of font lock, 1 for minimal syntax highlighting and 4 for maximum."
:type '(choice (const :tag "Minimal" 1)
(const :tag "Low" 2)
(const :tag "Standard" 3)
(const :tag "Standard plus parse errors" 4)))
+(defcustom matlab-ts-mode-on-save-fixes
+ '(matlab-ts-mode-on-save-fix-name)
+ "List of function symbols which offer to fix *.m files on save.
+During save these functions are called and will prompt to fix issues in
+*.m files. Each function gets no arguments, and returns nothing. They
+can move point, but it will be restored for them."
+ :type '(repeat (choice :tag "Function: "
+ (matlab-ts-mode-on-save-fix-name))))
+
;;; Global variables used in multiple code ";;; sections"
(defvar matlab-ts-mode--comment-heading-re
@@ -347,7 +356,7 @@ start-point and end-point."
(let ((heading-start (match-beginning 1))
(heading-end (match-end 1)))
(treesit-fontify-with-override heading-start heading-end
- 'matlab-ts-comment-heading-face
+ 'matlab-ts-mode-comment-heading-face
override start end)))))
(defun matlab-ts-mode--comment-to-do-capture (comment-node override start end
&rest _)
@@ -370,7 +379,7 @@ than the COMMENT-NODE start-point and end-point."
(let ((keyword-start (match-beginning 1))
(keyword-end (match-end 1)))
(treesit-fontify-with-override keyword-start keyword-end
-
'matlab-ts-comment-to-do-marker-face
+
'matlab-ts-mode-comment-to-do-marker-face
override start end))
(goto-char comment-end))))))
@@ -387,13 +396,14 @@ than the COMMENT-NODE start-point and end-point."
:language 'matlab
:feature 'comment
:override t
- '(((comment) @matlab-ts-pragma-face (:match "^%#.+$"
@matlab-ts-pragma-face)) ;; %#pragma's
+ '(((comment) @matlab-ts-mode-pragma-face
+ (:match "^%#.+$" @matlab-ts-mode-pragma-face)) ;; %#pragma's
((comment) @matlab-ts-mode--comment-heading-capture) ;; %% comment heading
(function_definition (comment) @matlab-ts-mode--doc-comment-capture) ;;
doc help comments
(class_definition (comment) @matlab-ts-mode--doc-comment-capture)) ;; doc
help comments
- ;; F-Rule: fix-me, etc. comment keywords
+ ;; F-Rule: to do, fix me, triple-x marker comment keywords
:language 'matlab
:feature 'comment
:override t
@@ -437,8 +447,8 @@ than the COMMENT-NODE start-point and end-point."
:language 'matlab
:feature 'string
'((string_content) @font-lock-string-face
- ((string_content) ["\"" "'"]) @matlab-ts-string-delimiter-face
- (string ["\"" "'"] @matlab-ts-string-delimiter-face))
+ ((string_content) ["\"" "'"]) @matlab-ts-mode-string-delimiter-face
+ (string ["\"" "'"] @matlab-ts-mode-string-delimiter-face))
;; F-Rule: transpose uses "'" after an identifier, e.g. for matrix A we
tranpose it via: A'
;; since "'" is also used as a string, we use a different face for
transpose and put it under
@@ -878,11 +888,89 @@ Returns t if tree-sitter NODE defines an outline heading."
(beginning-of-line)
(looking-at matlab-ts-mode--comment-heading-re))))))
+;;; Save hooks
+
+(defun matlab-ts-mode--highlight-ask (begin end prompt)
+ "Highlight from BEGIN to END while asking PROMPT as a yes-no question."
+ (let ((mo (make-overlay begin end (current-buffer)))
+ (show-paren-mode nil) ;; this will highlight things we often ask
about. disable.
+ ans)
+ (condition-case nil
+ (progn
+ (overlay-put mo 'face 'matlab-region-face)
+ (setq ans (y-or-n-p prompt))
+ (delete-overlay mo))
+ (quit (delete-overlay mo)
+ (error "Quit")))
+ ans))
+
+(defun matlab-ts-mode-on-save-fix-name (&optional no-prompt)
+ "If file name and function/classdef name are different, offer to fix.
+If optional NO-PROMPT is t, fix the name if needed without prompting."
+ (interactive)
+ (when (or no-prompt (not noninteractive)) ;; can only prompt if in
interactive mode
+ (let* ((root (treesit-buffer-root-node))
+ (children (treesit-node-children root)))
+ (cl-loop for child in children do
+ (let ((child-type (treesit-node-type child)))
+ (cond
+ ;; Case: function_definition or class_definition
+ ((string-match-p (rx bol (or "function_definition"
"class_definition") eol)
+ child-type)
+ (let* ((def-name-node (treesit-node-child-by-field-name
child "name"))
+ (def-name (treesit-node-type def-name-node))
+ (file-name (file-name-nondirectory (or
(buffer-file-name) (buffer-name))))
+ (base-name-no-ext (replace-regexp-in-string
"\\.[^.]+\\'" "" file-name)))
+ ;; When base-name-no-ext is a valid name, MATLAB will use
that.
+ ;; Invalid file names result in an error in MATLAB, so
don't try to fix
+ ;; the function/classdef name in that case.
+ (when (and (string-match-p "\\`[a-zA-Z][a-zA-Z0-9_]*\\'"
base-name-no-ext)
+ (not (string= def-name base-name-no-ext)))
+ (let ((start-pt (treesit-node-start def-name-node))
+ (end-pt (treesit-node-end def-name-node)))
+ (when (or no-prompt
+ (matlab-ts-mode--highlight-ask
+ start-pt end-pt
+ (concat (if (string= child-type
"function_definition")
+ "Function"
+ "Classdef")
+ " name and file names are
different. Fix?")))
+ (save-excursion
+ (goto-char start-pt)
+ (delete-region start-pt end-pt)
+ (insert base-name-no-ext))))))
+ (cl-return))
+
+ ;; Case: anthing except a comment
+ ((not (string= "comment" child-type))
+ (cl-return))))))))
+
+(defun matlab-ts-mode--write-file-callback ()
+ "Called from `write-contents-functions'.
+When `matlab-verify-on-save-flag' is true, run `matlab-mode-verify-fix-file'.
+Enable/disable `matlab-sections-minor-mode' based on file content."
+ (mapc (lambda (fix-function)
+ (funcall fix-function))
+ matlab-ts-mode-on-save-fixes)
+ ;; `write-contents-functions' expects this callback to return nil to
continue with other hooks and
+ ;; the final save. See `run-hook-with-args-until-success'.
+ nil)
+
;;; matlab-ts-mode
;;;###autoload
(define-derived-mode matlab-ts-mode prog-mode "MATLAB:ts"
- "Major mode for editing MATLAB files with tree-sitter."
+ "Major mode for editing MATLAB files with tree-sitter.
+
+This mode is independent from the classic matlab-mode.el, `matlab-mode',
+so configuration variables of that mode, like do not affect this mode.
+
+If you have the MATLAB tree-sitter grammar installed,
+ (treesit-ready-p \\='matlab)
+is t, add the following to an Init File (e.g. `user-init-file' or
+`site-run-file') to enter the MATLAB tree-sitter mode by default:
+
+ (add-to-list \\='major-mode-remap-alist \\='(matlab-mode . matlab-ts-mode))"
(when (treesit-ready-p 'matlab)
(treesit-parser-create 'matlab)
@@ -905,7 +993,7 @@ Returns t if tree-sitter NODE defines an outline heading."
(setq-local page-delimiter "^\\(?:\f\\|%%\\(?:\\s-\\|\n\\)\\)")
;; Font-lock. See: ./tests/test-matlab-ts-mode-font-lock.el
- (setq-local treesit-font-lock-level matlab-ts-font-lock-level)
+ (setq-local treesit-font-lock-level matlab-ts-mode-font-lock-level)
(setq-local treesit-font-lock-settings matlab-ts-mode--font-lock-settings)
(setq-local treesit-font-lock-feature-list '((comment definition)
(keyword string type)
@@ -941,11 +1029,12 @@ Returns t if tree-sitter NODE defines an outline
heading."
;; See: ./tests/test-matlab-ts-mode-outline.el
(setq-local treesit-outline-predicate #'matlab-ts-mode--outline-predicate)
+ ;; Save hook
+ (add-hook 'write-contents-functions #'matlab-ts-mode--write-file-callback)
+
;; TODO Highlight parens OR if/end type blocks
;; TODO Electric pair mode
- ;; TODO function/classdef name vs file name prompt to fix
;; TODO what about syntax table and electric keywords?
- ;; TODO function / end match like matlab-mode
;; 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.
diff --git a/tests/test-matlab-ts-mode-font-lock.el
b/tests/test-matlab-ts-mode-font-lock.el
index 7a8b937867..b73b24b91d 100644
--- a/tests/test-matlab-ts-mode-font-lock.el
+++ b/tests/test-matlab-ts-mode-font-lock.el
@@ -60,13 +60,13 @@ after validating it, rename it to
("D" . font-lock-delimiter-face)
("f" . font-lock-function-name-face)
("h" . font-lock-doc-face) ;; function doc help
comment
- ("H" . matlab-ts-comment-heading-face) ;; %%
comment heading
+ ("H" . matlab-ts-mode-comment-heading-face) ;; %%
comment heading
("k" . font-lock-keyword-face)
- ("M" . matlab-ts-comment-to-do-marker-face)
+ ("M" . matlab-ts-mode-comment-to-do-marker-face)
("n" . font-lock-constant-face) ;; numbers
("s" . font-lock-string-face)
- ("S" . matlab-ts-string-delimiter-face)
- ("p" . matlab-ts-pragma-face)
+ ("S" . matlab-ts-mode-string-delimiter-face)
+ ("p" . matlab-ts-mode-pragma-face)
("P" . font-lock-property-name-face)
("t" . font-lock-type-face)
("v" . font-lock-variable-name-face)
diff --git a/tests/test-matlab-ts-mode-on-save-fixes-files/on_save_fix_class.m
b/tests/test-matlab-ts-mode-on-save-fixes-files/on_save_fix_class.m
new file mode 100644
index 0000000000..d22315e08e
--- /dev/null
+++ b/tests/test-matlab-ts-mode-on-save-fixes-files/on_save_fix_class.m
@@ -0,0 +1,12 @@
+% -*- matlab-ts -*-
+
+% (t-utils-xr (rename-buffer "tmp__on_save_fix_class.m")
(matlab-ts-mode-on-save-fix-name t))
+
+classdef foo
+ methods
+
+ function bar
+ disp('bar')
+ end
+ end
+end
diff --git
a/tests/test-matlab-ts-mode-on-save-fixes-files/on_save_fix_class_expected.org
b/tests/test-matlab-ts-mode-on-save-fixes-files/on_save_fix_class_expected.org
new file mode 100644
index 0000000000..e695bf970c
--- /dev/null
+++
b/tests/test-matlab-ts-mode-on-save-fixes-files/on_save_fix_class_expected.org
@@ -0,0 +1,28 @@
+#+startup: showall
+
+* Executing commands from on_save_fix_class.m:3:2:
+
+ (t-utils-xr (rename-buffer "tmp__on_save_fix_class.m")
(matlab-ts-mode-on-save-fix-name t))
+
+- Invoking : (rename-buffer "tmp__on_save_fix_class.m")
+ Start point : 115
+ No point movement
+ No buffer modifications
+
+- Invoking : (matlab-ts-mode-on-save-fix-name t)
+ Start point : 115
+ No point movement
+ Buffer modified:
+ #+begin_src diff
+--- start_contents
++++ end_contents
+@@ -2,7 +2,7 @@
+
+ % (t-utils-xr (rename-buffer "tmp__on_save_fix_class.m")
(matlab-ts-mode-on-save-fix-name t))
+
+-classdef foo
++classdef tmp__on_save_fix_class
+ methods
+
+ function bar
+ #+end_src diff
diff --git a/tests/test-matlab-ts-mode-on-save-fixes-files/on_save_fix_fcn.m
b/tests/test-matlab-ts-mode-on-save-fixes-files/on_save_fix_fcn.m
new file mode 100644
index 0000000000..6aa160f3d5
--- /dev/null
+++ b/tests/test-matlab-ts-mode-on-save-fixes-files/on_save_fix_fcn.m
@@ -0,0 +1,12 @@
+% -*- matlab-ts -*-
+
+% (t-utils-xr (rename-buffer "tmp__on_save_fix_fcn.m")
(matlab-ts-mode-on-save-fix-name t))
+
+function foo
+ disp('foo')
+ bar;
+end
+
+function bar
+ disp('bar')
+end
diff --git
a/tests/test-matlab-ts-mode-on-save-fixes-files/on_save_fix_fcn_expected.org
b/tests/test-matlab-ts-mode-on-save-fixes-files/on_save_fix_fcn_expected.org
new file mode 100644
index 0000000000..0e5351a92f
--- /dev/null
+++ b/tests/test-matlab-ts-mode-on-save-fixes-files/on_save_fix_fcn_expected.org
@@ -0,0 +1,28 @@
+#+startup: showall
+
+* Executing commands from on_save_fix_fcn.m:3:2:
+
+ (t-utils-xr (rename-buffer "tmp__on_save_fix_fcn.m")
(matlab-ts-mode-on-save-fix-name t))
+
+- Invoking : (rename-buffer "tmp__on_save_fix_fcn.m")
+ Start point : 113
+ No point movement
+ No buffer modifications
+
+- Invoking : (matlab-ts-mode-on-save-fix-name t)
+ Start point : 113
+ No point movement
+ Buffer modified:
+ #+begin_src diff
+--- start_contents
++++ end_contents
+@@ -2,7 +2,7 @@
+
+ % (t-utils-xr (rename-buffer "tmp__on_save_fix_fcn.m")
(matlab-ts-mode-on-save-fix-name t))
+
+-function foo
++function tmp__on_save_fix_fcn
+ disp('foo')
+ bar;
+ end
+ #+end_src diff
diff --git
a/tests/test-matlab-ts-mode-on-save-fixes-files/on_save_no_fix_script.m
b/tests/test-matlab-ts-mode-on-save-fixes-files/on_save_no_fix_script.m
new file mode 100644
index 0000000000..b5699d16c4
--- /dev/null
+++ b/tests/test-matlab-ts-mode-on-save-fixes-files/on_save_no_fix_script.m
@@ -0,0 +1,15 @@
+% -*- matlab-ts -*-
+
+% (t-utils-xr (rename-buffer "tmp__on_save_fix_script.m")
(matlab-ts-mode-on-save-fix-name t))
+
+% Calling foo makes this a script and which uses local functions.
+foo
+
+function foo
+ disp('foo')
+ bar;
+end
+
+function bar
+ disp('bar')
+end
diff --git
a/tests/test-matlab-ts-mode-on-save-fixes-files/on_save_no_fix_script_expected.org
b/tests/test-matlab-ts-mode-on-save-fixes-files/on_save_no_fix_script_expected.org
new file mode 100644
index 0000000000..417281a583
--- /dev/null
+++
b/tests/test-matlab-ts-mode-on-save-fixes-files/on_save_no_fix_script_expected.org
@@ -0,0 +1,15 @@
+#+startup: showall
+
+* Executing commands from on_save_no_fix_script.m:3:2:
+
+ (t-utils-xr (rename-buffer "tmp__on_save_fix_script.m")
(matlab-ts-mode-on-save-fix-name t))
+
+- Invoking : (rename-buffer "tmp__on_save_fix_script.m")
+ Start point : 116
+ No point movement
+ No buffer modifications
+
+- Invoking : (matlab-ts-mode-on-save-fix-name t)
+ Start point : 116
+ No point movement
+ No buffer modifications
diff --git a/tests/test-matlab-ts-mode-on-save-fixes.el
b/tests/test-matlab-ts-mode-on-save-fixes.el
new file mode 100644
index 0000000000..c663e2a164
--- /dev/null
+++ b/tests/test-matlab-ts-mode-on-save-fixes.el
@@ -0,0 +1,58 @@
+;;; test-matlab-ts-mode-on-save-fixes.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-on-save-fixes-files/NAME.m comparing against
+;; ./test-matlab-ts-mode-on-save-fixes-files/NAME_expected.org
+;;
+
+;;; Code:
+
+(require 't-utils)
+(require 'matlab-ts-mode)
+
+(cl-defun test-matlab-ts-mode-on-save-fixes (&optional m-file)
+ "Test defun movement using ./test-matlab-ts-mode-on-save-fixes-files/NAME.m.
+Using ./test-matlab-ts-mode-on-save-fixes-files/NAME.m, compare defun
+movement against
+./test-matlab-ts-mode-on-save-fixes-files/NAME_expected.org. If M-FILE is
+not provided, loop comparing all
+./test-matlab-ts-mode-on-save-fixes-files/NAME.m files.
+
+To add a test, create
+ ./test-matlab-ts-mode-on-save-fixes-files/NAME.m
+and run this function. The baseline is saved for you as
+ ./test-matlab-ts-mode-on-save-fixes-files/NAME_expected.org~
+after validating it, rename it to
+ ./test-matlab-ts-mode-on-save-fixes-files/NAME_expected.org"
+
+ (let ((test-name "test-matlab-ts-mode-on-save-fixes"))
+
+ (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-on-save-fixes)
+;;; test-matlab-ts-mode-on-save-fixes.el ends here