branch: externals/matlab-mode
commit e3e6952d57955764d48e9f9432247903672297dc
Author: John Ciolfi <[email protected]>
Commit: John Ciolfi <[email protected]>
Update imenu to use matlab syntax font-lock
The regular expression needed for imenu causes the regex engine to hang.
Thus, use
imenu-create-index-function and leverage matlab syntax font-lock to
identify the function names for
imenu.
Also added tip in doc/matlab-imenu.org on how add a Fcns menu to Emacs for
these.
See: https://github.com/mathworks/Emacs-MATLAB-Mode/issues/42
---
doc/matlab-imenu.org | 18 +++-
matlab.el | 127 +++++++++---------------
tests/metest-imenu-files/f0_expected.txt | 2 +
tests/metest-imenu-files/g0_expected.txt | 3 +-
tests/metest-imenu-files/oneliner.m | 1 +
tests/metest-imenu-files/oneliner_expected.txt | 1 +
tests/metest-imenu-files/symPosDef.m | 19 ++++
tests/metest-imenu-files/symPosDef_expected.txt | 3 +
tests/metest-imenu.el | 47 ++++-----
9 files changed, 113 insertions(+), 108 deletions(-)
diff --git a/doc/matlab-imenu.org b/doc/matlab-imenu.org
index 02ceb81e20..b671bf5ca6 100644
--- a/doc/matlab-imenu.org
+++ b/doc/matlab-imenu.org
@@ -5,8 +5,9 @@
# Copyright 2025 Free Software Foundation, Inc.
-matlab-mode provides
[[https://www.gnu.org/software/emacs/manual/html_node/emacs/Imenu.html][imenu]]
support which lets you jump quickly to functions in the current ~*.m~
-file you are visiting. Typing ~M-g i~ (or ~M-x imenu~) will prompt you in the
mini-buffer:
+matlab-mode and tlc-mode provides
[[https://www.gnu.org/software/emacs/manual/html_node/emacs/Imenu.html][imenu]]
support which lets you jump quickly to functions in the
+current ~*.m~ file you are visiting. Typing ~M-g i~ (or ~M-x imenu~) will
prompt you in the
+mini-buffer:
: Index item:
@@ -49,3 +50,16 @@ gives:
#+end_example
and you can select the one you'd like by clicking on it or by typing the name
with tab completion.
+
+** Fcns Menu
+
+To add a "Fcns" menu to Emacs for MATLAB and/or TLC files.
+
+ : M-x customize-variable RET matlab-mode-hook RET
+ : M-x customize-variable RET tlc-mode-hook RET
+
+and add:
+
+#+begin_src emacs-lisp
+ (lambda () (imenu-add-to-menubar "Fcns"))
+#+end_src
diff --git a/matlab.el b/matlab.el
index 4a8f391714..04580a8282 100644
--- a/matlab.el
+++ b/matlab.el
@@ -199,6 +199,47 @@ If the value is \\='guess, then we guess if a file has end
when
(make-variable-buffer-local 'matlab-functions-have-end)
(put 'matlab-functions-have-end 'safe-local-variable #'symbolp)
+(defun matlab--imenu-index ()
+ "Return index for imenu.
+This will return alist of functions in the current *.m file:
+ \\='((\"function1\" . start-point1)
+ (\"function2\" . start-point2)
+This searches for `font-lock-function-name-face' font-lock property to
+locate the functions."
+
+ ;; Handle case of font-lock being out of date.
+ (font-lock-mode 1)
+ (font-lock-fontify-region (point-min) (point-max))
+
+ (goto-char (point-min))
+ (let (match
+ (index '())
+ last-end-pt)
+ (while (setq match (text-property-search-forward
+ 'face 'font-lock-function-name-face
+ (lambda (value prop)
+ (or (eq prop value)
+ (and (listp prop)
+ (member value prop))))))
+ (let* ((start-pt (prop-match-beginning match))
+ (end-pt (prop-match-end match))
+ (function-name (buffer-substring start-pt end-pt)))
+
+ (if (and last-end-pt
+ (= last-end-pt start-pt))
+ ;; classdef get and set methods have multiple faces, so join (1)
and (2):
+ ;; function obj = set.inputMatrix(obj,val)
+ ;; ^---^----------
+ ;; (1) (2)
+ (let* ((last-el (car index))
+ (last-function-name (car last-el))
+ (last-start-pt (cdr last-el))
+ (new-el (cons (concat last-function-name function-name)
last-start-pt)))
+ (setcar index new-el))
+ (push `(,function-name . ,start-pt) index))
+ (setq last-end-pt end-pt)))
+ (reverse index)))
+
(defun matlab-toggle-functions-have-end ()
"Toggle `matlab-functions-have-end-minor-mode'."
(interactive)
@@ -406,14 +447,6 @@ This is used to generate and identify continuation lines."
:group 'matlab
:type 'string)
-(defvar matlab--ellipsis-to-eol-re
- (concat "\\.\\.\\.[[:blank:]]*\\(?:%[^\r\n]*\\)?\r?\n")
- "Regexp used to match either of the following including the newline.
-For example, the end-of-lines:
- ...
- ... % comment
-are matched.")
-
(defcustom matlab-fill-code nil
"*If true, `auto-fill-mode' causes code lines to be automatically continued."
:group 'matlab
@@ -1217,77 +1250,6 @@ This matcher will handle a range of variable features."
"Expressions to highlight in MATLAB mode.")
-;; -----------------
-;; | Imenu support |
-;; -----------------
-;; Example functions we match, f0, f1, f2, f3, f4, f5, F6, g4
-;; function f0
-;; function...
-;; a = f1
-;; function f2
-;; function x = ...
-;; f3
-;; function [a, ...
-;; b ] ...
-;; = ...
-;; f4(c)
-;; function a = F6
-;; function [ ...
-;; a, ... % comment for a
-;; b ... % comment for b
-;; ] ...
-;; = ...
-;; g4(c)
-;;
-(defvar matlab-imenu-generic-expression
- ;; Using concat to increase indentation and improve readability
- `(,(list nil (concat
- "^[[:blank:]]*"
- "function\\>"
-
- ;; Optional return args, function ARGS = NAME. Capture the
'ARGS ='
-
- ;; The following regexp is better for capturing 'ARGS ='. The
regexp allows for any
- ;; characters in the "% comments", however with Emacs 30.1,
running:
- ;; M-: (re-search-forward (cadar
matlab-imenu-generic-expression) nil t)
- ;; on this file with point at 0
- ;; function
foo123567890123567890123567890123567890123567890(fh)
- ;; end
- ;; gives us a hang. If you shorten the function name, Emacs
won't hang.
- ;;
- ;; (concat "\\(?:"
- ;; ;; ARGS can span multiple lines
- ;; (concat "\\(?:"
- ;; ;; valid ARGS chars: "[" "]" variables ","
space, tab
- ;; "[]\\[a-zA-Z0-9_,[:blank:]]*"
- ;; ;; Optional continue to next line "..." or
"... % comment"
- ;; "\\(?:" matlab--ellipsis-to-eol-re "\\)?"
- ;; "\\)+")
- ;; ;; ARGS must be preceeded by the assignment
operator, "="
- ;; "[[:blank:]]*="
- ;; "\\)?")
-
- ;; Capture 'ARGS = ' using a less accurate regexp that doesn't
handle ellipsis
- ;; due to performance problems with the above.
- (concat "\\(?:"
- ;; valid ARGS chars: "[" "]" variables "," space, tab
- "[]\\[a-zA-Z0-9_,[:blank:]]+"
- ;; ARGS must be preceeded by the assignment operator,
"="
- "="
- "\\)?")
-
- ;; Optional space/tabs, "...", or "... % comment" continuation
- (concat "\\(?:"
- "[[:blank:]]*"
- "\\(?:" matlab--ellipsis-to-eol-re "\\)?"
- "\\)*")
-
- "[\\.[:space:]\n\r]*"
- "\\([a-zA-Z][a-zA-Z0-9_]+\\)" ;; function NAME
- )
- 1))
- "Regexp to find function names in *.m files for `imenu'.")
-
;;; MATLAB mode entry point ==================================================
@@ -1447,8 +1409,9 @@ All Key Bindings:
(make-local-variable 'fill-paragraph-function)
(setq fill-paragraph-function 'matlab-fill-paragraph)
(make-local-variable 'fill-prefix)
- (make-local-variable 'imenu-generic-expression)
- (setq imenu-generic-expression matlab-imenu-generic-expression)
+
+ ;; Imenu
+ (setq-local imenu-create-index-function #'matlab--imenu-index)
;; Save hook for verifying src. This lets us change the name of
;; the function in `write-file' and have the change be saved.
@@ -3224,7 +3187,7 @@ desired."
;; LocalWords: keymap setq decl memq classdef's progn mw vf functionname
booleanp torkel fboundp
;; LocalWords: gud ebstop mlgud ebclear ebstatus mlg mlgud's subjob featurep
defface commanddual
;; LocalWords: docstring cdr animatedline rlim thetalim cartesian stackedplot
bubblechart
-;; LocalWords: swarmchart wordcloud bubblecloud heatmap parallelplot fcontour
anim polarplot Imenu
+;; LocalWords: swarmchart wordcloud bubblecloud heatmap parallelplot fcontour
anim polarplot
;; LocalWords: polarscatter polarhistogram polarbubblechart goeplot
geoscatter geobubble geodensity
;; LocalWords: fimplicit fsurf tiledlayout nexttile uicontext mld flintmax
keywordlist mapconcat
;; LocalWords: vardecl flb fle tmp blockmatch md tm newmdata repeat:md sw
imenu boundp aref alist
diff --git a/tests/metest-imenu-files/f0_expected.txt
b/tests/metest-imenu-files/f0_expected.txt
index f35cfaccdc..251b035c72 100644
--- a/tests/metest-imenu-files/f0_expected.txt
+++ b/tests/metest-imenu-files/f0_expected.txt
@@ -1,6 +1,8 @@
f0
+f1
f2
f3
+f4
f5
F6
f7
diff --git a/tests/metest-imenu-files/g0_expected.txt
b/tests/metest-imenu-files/g0_expected.txt
index df8fd65723..40f06c0373 100644
--- a/tests/metest-imenu-files/g0_expected.txt
+++ b/tests/metest-imenu-files/g0_expected.txt
@@ -1,4 +1,5 @@
g0
-g2
+g1
g3
+g4
g5
diff --git a/tests/metest-imenu-files/oneliner.m
b/tests/metest-imenu-files/oneliner.m
new file mode 100644
index 0000000000..e5df49b4ac
--- /dev/null
+++ b/tests/metest-imenu-files/oneliner.m
@@ -0,0 +1 @@
+function oneliner, x = abc; end
diff --git a/tests/metest-imenu-files/oneliner_expected.txt
b/tests/metest-imenu-files/oneliner_expected.txt
new file mode 100644
index 0000000000..8b13789179
--- /dev/null
+++ b/tests/metest-imenu-files/oneliner_expected.txt
@@ -0,0 +1 @@
+
diff --git a/tests/metest-imenu-files/symPosDef.m
b/tests/metest-imenu-files/symPosDef.m
new file mode 100644
index 0000000000..ebbd141aff
--- /dev/null
+++ b/tests/metest-imenu-files/symPosDef.m
@@ -0,0 +1,19 @@
+classdef symPosDef
+ properties
+ inputMatrix = [1 0; 0 1]
+ end
+
+ methods
+ function obj = set.inputMatrix(obj,val)
+ try chol(val)
+ obj.inputMatrix = val;
+ catch ME
+ error("inputMatrix must be symmetric positive definite.")
+ end
+ end
+
+ function m = get.inputMatrix(obj)
+ m = obj.inputMatrix;
+ end
+ end
+end
diff --git a/tests/metest-imenu-files/symPosDef_expected.txt
b/tests/metest-imenu-files/symPosDef_expected.txt
new file mode 100644
index 0000000000..458fbd8fa5
--- /dev/null
+++ b/tests/metest-imenu-files/symPosDef_expected.txt
@@ -0,0 +1,3 @@
+symPosDef
+set.inputMatrix
+get.inputMatrix
diff --git a/tests/metest-imenu.el b/tests/metest-imenu.el
index 688520b5e4..d73dc28f61 100644
--- a/tests/metest-imenu.el
+++ b/tests/metest-imenu.el
@@ -55,31 +55,32 @@ For debugging, you can run with a specified M-FILE,
(save-excursion
(message "START: (metest-imenu \"%s\")" m-file)
- (find-file m-file)
- (goto-char (point-min))
+ (let ((m-file-buf (find-file m-file)))
+ (with-current-buffer m-file-buf
- (let* ((imenu-re (cadar matlab-imenu-generic-expression))
- (got "")
- (expected-file (replace-regexp-in-string "\\.m$"
"_expected.txt" m-file))
- (got-file (concat expected-file "~"))
- (expected (when (file-exists-p expected-file)
- (with-temp-buffer
- (insert-file-contents-literally expected-file)
- (buffer-string))))
- (case-fold-search nil))
- (while (re-search-forward imenu-re nil t)
- (setq got (concat got (match-string 1) "\n")))
+ (let* ((index (matlab--imenu-index))
+ (got (concat (string-join
+ (mapcar (lambda (el) (substring-no-properties
(car el))) index)
+ "\n")
+ "\n"))
+ (expected-file (replace-regexp-in-string "\\.m$"
"_expected.txt" m-file))
+ (got-file (concat expected-file "~"))
+ (expected (when (file-exists-p expected-file)
+ (with-temp-buffer
+ (insert-file-contents-literally expected-file)
+ (buffer-string)))))
- (when (not (string= got expected))
- (let ((coding-system-for-write 'raw-text-unix))
- (write-region got nil got-file))
- (when (not expected)
- (error "Baseline for %s does not exists. See %s and if it looks
good rename it to %s"
- m-file got-file expected-file))
- (error "Baseline for %s does not match, got: %s, expected: %s"
- m-file got-file expected-file))
- (kill-buffer)))
- (message "PASS: (metest-imenu \"%s\")" m-file)))
+ (when (not (string= got expected))
+ (let ((coding-system-for-write 'raw-text-unix))
+ (write-region got nil got-file))
+ (when (not expected)
+ (error "Baseline for %s does not exist.
+See %s and if it looks good rename it to %s"
+ m-file got-file expected-file))
+ (error "Baseline for %s does not match, got: %s, expected: %s"
+ m-file got-file expected-file))))
+ (kill-buffer m-file-buf))
+ (message "PASS: (metest-imenu \"%s\")" m-file))))
"success")
(provide 'metest-imenu)