branch: externals/matlab-mode commit e3e6952d57955764d48e9f9432247903672297dc Author: John Ciolfi <john.ciolfi...@gmail.com> Commit: John Ciolfi <john.ciolfi...@gmail.com>
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)