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)