branch: externals/matlab-mode
commit a54d2061bd31c9cba32fc5e0e0ecf34b900e26d1
Author: John Ciolfi <john.ciolfi...@gmail.com>
Commit: John Ciolfi <john.ciolfi...@gmail.com>

    Fix imenu setup for function names in *.m files
    
    This adds a number of tests to lockdown the expected behavior:
       tests/metest-imenu-files/*.m
       tests/metest-imenu-files/*_expected.txt
    
    See: https://github.com/mathworks/Emacs-MATLAB-Mode/issues/42
---
 README.org                                    |  9 ++-
 doc/matlab-imenu.org                          | 51 ++++++++++++++++
 matlab.el                                     | 67 +++++++++++++++++++--
 tests/metest-imenu-files/f0.m                 | 49 +++++++++++++++
 tests/metest-imenu-files/f0_expected.txt      |  8 +++
 tests/metest-imenu-files/g0.m                 | 43 ++++++++++++++
 tests/metest-imenu-files/g0_expected.txt      |  6 ++
 tests/metest-imenu-files/myFunc1.m            | 17 ++++++
 tests/metest-imenu-files/myFunc1_expected.txt |  4 ++
 tests/metest-imenu.el                         | 86 +++++++++++++++++++++++++++
 tests/metest.el                               |  3 +
 11 files changed, 337 insertions(+), 6 deletions(-)

diff --git a/README.org b/README.org
index 35d52f0ca4..f06f88a772 100644
--- a/README.org
+++ b/README.org
@@ -12,8 +12,13 @@
    - Edit MATLAB code with syntax highlighting and smart indentation.
    - Lint MATLAB code with fix-it's using the MATLAB Code Analyzer.
 
-2. *[[https://github.com/mathworks/MATLAB-language-server][MATLAB Language 
Server]]*, matlabls, for code navigation, code completion, go to definition, 
find
-   references, and more. See 
[[file:doc/matlab-language-server-lsp-mode.org][doc/matlab-language-server-lsp-mode.org]].
+2. *Code navigation and more*
+
+   - [[https://github.com/mathworks/MATLAB-language-server][MATLAB Language 
Server]], matlabls, for code navigation, code completion, go to definition, find
+     references, and more. See 
[[file:doc/matlab-language-server-lsp-mode.org][doc/matlab-language-server-lsp-mode.org]].
+
+   - Imenu support for quickly jumping to function declarations in the current 
~*.m~ file.
+     See [[file:doc/matlab-imenu.org][doc/matlab-imenu.org]].
 
 3. *M-x matlab-shell* for running and debugging MATLAB within Emacs 
(Unix-only).
 
diff --git a/doc/matlab-imenu.org b/doc/matlab-imenu.org
new file mode 100644
index 0000000000..02ceb81e20
--- /dev/null
+++ b/doc/matlab-imenu.org
@@ -0,0 +1,51 @@
+# File: doc/matlab-imenu.org
+
+#+startup: showall
+#+options: toc:nil
+
+# 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:
+
+ : Index item:
+
+You can type TAB to complete MATLAB function names and after selecting one, 
the point
+is moved to that function. For example, given test1.m
+
+#+begin_src matlab
+  function test1
+      a = test2(2);
+      b = test3(a);
+      test4(b);
+  end
+
+  function out = test2(in)
+      out = in * 2;
+  end
+
+  function [out] = test3(in)
+      out = in * 3;
+  end
+
+  function test4(in)
+      disp(num2str(in))
+  end
+#+end_src
+
+Typing ~M-g i~ followed by a ~TAB~ at the prompt:
+
+ : Index item: <TAB>
+
+gives:
+
+#+begin_example
+  5 Possible completions:
+  *Rescan*
+  myFunc1
+  myFunc2
+  myFunc3
+  myFunc4
+#+end_example
+ 
+and you can select the one you'd like by clicking on it or by typing the name 
with tab completion.
diff --git a/matlab.el b/matlab.el
index f762747368..dd8235b9b4 100644
--- a/matlab.el
+++ b/matlab.el
@@ -399,12 +399,20 @@ This overcomes situations where the `fill-column' plus the
   :group 'matlab
   :type 'integer)
 
+;; TODO - matlab-ellipsis-string shouldn't be a defcustom because it cannot be 
changed.
 (defcustom matlab-ellipsis-string "..."
   "Text used to perform continuation on code lines.
 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
+  ...
+  ... % comment
+")
+
 (defcustom matlab-fill-code nil
   "*If true, `auto-fill-mode' causes code lines to be automatically continued."
   :group 'matlab
@@ -1205,11 +1213,62 @@ This matcher will handle a range of variable features."
    )
   "Expressions to highlight in MATLAB mode.")
 
-;; Imenu support.
+
+;; -----------------
+;; | 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
-  '((nil "^\\s-*function\\>[ \t\n.]*\\(\\(\\[[^]]*\\]\\|\\sw+\\)[ \t\n.]*\
-< =[ \t\n.]*\\)?\\([a-zA-Z0-9_]+\\)" 3))
-  "Expressions which find function headings in MATLAB M files.")
+  ;; Using concat to increase indentation and improve readability
+  `(,(list nil (concat
+                "^[[:blank:]]*"
+                "function\\>"
+
+                ;; Optional return args, function ARGS = NAME. Capture the 
'ARGS ='
+                (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:]]*="
+
+                        "\\)?")
+
+                ;; Optional space/tabs or '...' 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 ==================================================
diff --git a/tests/metest-imenu-files/f0.m b/tests/metest-imenu-files/f0.m
new file mode 100644
index 0000000000..4ec463e827
--- /dev/null
+++ b/tests/metest-imenu-files/f0.m
@@ -0,0 +1,49 @@
+function f0
+    f1
+    f2
+    x = f3;
+    [a, b] = f4(2);
+    a = F6;
+    f7;
+end
+
+function...
+        a = f1
+    disp('in f1')
+end
+
+function f2
+    disp('in f2')
+end
+
+function x = ...
+        f3
+    disp('in f3')
+    function2 = 1;
+    x = function2;
+end
+
+function [a, ...
+          ...
+          b ] ...
+          = ...
+          f4(c)
+    function f5
+        disp('in f5')
+    end
+
+    disp('in f4')
+    f5;
+    a = c;
+    b = c;
+end
+
+function a = F6
+    a = sqrt(2);
+    disp('in f6');
+end
+
+function...
+        f7
+    disp('in f7')
+end
diff --git a/tests/metest-imenu-files/f0_expected.txt 
b/tests/metest-imenu-files/f0_expected.txt
new file mode 100644
index 0000000000..00e0d2b5d5
--- /dev/null
+++ b/tests/metest-imenu-files/f0_expected.txt
@@ -0,0 +1,8 @@
+f0
+f1
+f2
+f3
+f4
+f5
+F6
+f7
diff --git a/tests/metest-imenu-files/g0.m b/tests/metest-imenu-files/g0.m
new file mode 100644
index 0000000000..49ea2374d8
--- /dev/null
+++ b/tests/metest-imenu-files/g0.m
@@ -0,0 +1,43 @@
+function ... % foo
+    g0
+    g1
+    g2
+    x = g3;
+    [a, b] = g4(2);
+end
+
+function...
+        a = ... % foo
+        g1
+    disp('in g1')
+end
+
+function ...
+...
+...%foo
+        g2
+    disp('in g2')
+end
+
+function x = ... % [a,b] =
+        g3
+    disp('in g3')
+    function2 = 1;
+    x = function2;
+end
+
+function [ ...
+    a, ... % comment for a
+    b  ... % comment for b
+         ] ... 
+         = ...
+         g4(c)
+    function g5
+        disp('in g5')
+    end
+
+    disp('in g4')
+    g5;
+    a = c;
+    b = c;
+end
diff --git a/tests/metest-imenu-files/g0_expected.txt 
b/tests/metest-imenu-files/g0_expected.txt
new file mode 100644
index 0000000000..2b9d3637b6
--- /dev/null
+++ b/tests/metest-imenu-files/g0_expected.txt
@@ -0,0 +1,6 @@
+g0
+g1
+g2
+g3
+g4
+g5
diff --git a/tests/metest-imenu-files/myFunc1.m 
b/tests/metest-imenu-files/myFunc1.m
new file mode 100644
index 0000000000..b42785f7bf
--- /dev/null
+++ b/tests/metest-imenu-files/myFunc1.m
@@ -0,0 +1,17 @@
+function myFunc1
+    a = myFunc2(2);
+    b = myFunc3(a);
+    myFunc4(b);
+end
+
+function out = myFunc2(in)
+    out = in * 2;
+end
+
+function [out] = myFunc3(in)
+    out = in * 3;
+end
+
+function myFunc4(in)
+    disp(num2str(in))
+end
diff --git a/tests/metest-imenu-files/myFunc1_expected.txt 
b/tests/metest-imenu-files/myFunc1_expected.txt
new file mode 100644
index 0000000000..52da7ea333
--- /dev/null
+++ b/tests/metest-imenu-files/myFunc1_expected.txt
@@ -0,0 +1,4 @@
+myFunc1
+myFunc2
+myFunc3
+myFunc4
diff --git a/tests/metest-imenu.el b/tests/metest-imenu.el
new file mode 100644
index 0000000000..688520b5e4
--- /dev/null
+++ b/tests/metest-imenu.el
@@ -0,0 +1,86 @@
+;;; metest-imenu.el --- Testing suite for MATLAB Emacs -*- 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:
+;;
+;; Tests to exercise the matlab-imenu-generic-expression regexp
+;;
+
+;;; Code:
+
+(require 'matlab)
+
+(defun metest-imenu-files ()
+  "Return list of full paths to each metest-imenu-files/*.m."
+  (directory-files "metest-imenu-files" t "\\.m$"))
+
+(defvar metest-imenu (cons "metest-imenu" (metest-imenu-files)))
+
+(defun metest-imenu (&optional m-file)
+  "Test MATLAB imenu support using ./metest-imenu-files/M-FILE.
+Compare ./metest-imenu-files/M-FILE against
+./metest-imenu-files/NAME_expected.txt, where NAME_expected.txt
+contains the matched functions of the matlab-imenu-generic-expression
+regexp, one per line.
+
+If M-FILE is not provided, loop comparing all
+  ./metest-imenu-files/*.m
+
+For debugging, you can run with a specified M-FILE,
+  M-: (metest-imenu \"metest-imenu-files/M-FILE\")"
+  (let* ((m-files (if m-file
+                      (progn
+                        (setq m-file (file-truename m-file))
+                        (when (not (file-exists-p m-file))
+                          (error "File %s does not exist" m-file))
+                        (list m-file))
+                    (metest-imenu-files))))
+    (dolist (m-file m-files)
+      (save-excursion
+        (message "START: (metest-imenu \"%s\")" m-file)
+
+        (find-file m-file)
+        (goto-char (point-min))
+
+        (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")))
+
+          (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)))
+  "success")
+
+(provide 'metest-imenu)
+;;; metest-imenu.el ends here
diff --git a/tests/metest.el b/tests/metest.el
index f46e4ad3d4..04b42f71a3 100644
--- a/tests/metest.el
+++ b/tests/metest.el
@@ -38,6 +38,7 @@
 (add-to-list 'load-path ".")
 (require 'metest-font-lock-test2)
 (require 'metest-indent-test2)
+(require 'metest-imenu)
 
 (defun metest-all-syntax-tests ()
   "Run all the syntax test cases in this file."
@@ -64,6 +65,8 @@
   (metest-indents-randomize-files)
   (metest-run 'metest-indents-test)
 
+  (metest-imenu)
+
   ;; Parsing and completion are high level tools
   (metest-run 'metest-complete-test)
 

Reply via email to