branch: externals/matlab-mode commit 04b58e9f134541e7e818937eaf1733047496a1bc Author: John Ciolfi <john.ciolfi...@gmail.com> Commit: John Ciolfi <john.ciolfi...@gmail.com>
matlab-ts-mode: add initial support for code indenting --- contributing/treesit-mode-how-to.org | 245 +++++++++++++- matlab-ts-mode.el | 361 ++++++++++++++++++-- tests/metest.el | 4 +- tests/test-matlab-ts-mode-font-lock.el | 32 +- .../test-matlab-ts-mode-indent-files/indent_cell.m | 33 ++ .../indent_cell_expected.m | 33 ++ .../indent_comments.m | 47 +++ .../indent_comments_expected.m | 47 +++ .../indent_cont_statements.m | 41 +++ .../indent_cont_statements_expected.m | 41 +++ .../indent_copyright.m | 13 + .../indent_copyright_expected.m | 13 + .../indent_copyright_in_code.m | 17 + .../indent_copyright_in_code_expected.m | 17 + .../indent_fcn_calls.m | 21 ++ .../indent_fcn_calls_expected.m | 21 ++ .../indent_fcn_ellipsis.m | 28 ++ .../indent_fcn_ellipsis_expected.m | 28 ++ .../indent_function.m | 7 + .../indent_function_expected.m | 7 + .../indent_if_continued.m | 42 +++ .../indent_if_continued_expected.m | 42 +++ .../indent_if_else.m | 7 + .../indent_if_else_expected.m | 7 + .../indent_keywords.m | 76 +++++ .../indent_keywords_expected.m | 76 +++++ .../indent_matrix.m | 33 ++ .../indent_matrix_expected.m | 33 ++ .../indent_nested.m | 29 ++ .../indent_nested_expected.m | 29 ++ .../indent_old_indents.m | 375 +++++++++++++++++++++ .../indent_old_indents_expected.m | 375 +++++++++++++++++++++ .../indent_ranges.m | 15 + .../indent_ranges_expected.m | 15 + .../indent_switch.m | 25 ++ .../indent_switch_expected.m | 25 ++ .../indent_tab_between_fcns.m | 6 + .../indent_tab_between_fcns_expected.m | 6 + .../indent_tab_in_fcn.m | 5 + .../indent_tab_in_fcn_expected.m | 5 + tests/test-matlab-ts-mode-indent.el | 166 +++++++++ 41 files changed, 2388 insertions(+), 60 deletions(-) diff --git a/contributing/treesit-mode-how-to.org b/contributing/treesit-mode-how-to.org index 7066b62b89..bc2b747d12 100644 --- a/contributing/treesit-mode-how-to.org +++ b/contributing/treesit-mode-how-to.org @@ -16,22 +16,25 @@ # | along with this program. If not, see <http://www.gnu.org/licenses/>. # | # | Commentary: -# | -# | Use this as a template for creating org-files with MATLAB and other language code blocks. -# | The '#+COMMENT' lines configure org-mode. +# | Guidelines for writting a major mode powered by tree-sitter -#+title: MATLAB and Tree-Sitter +#+title: Tree-Sitter How To #+author: John Ciolfi #+date: Jun-13-2025 -* Overview +* TODO -This is a set of notes that I'm taking as I develop matlab-ts-mode.el with the goal of this -becoming a guide for writting a tree-sitter mode for Emacs 30 or later. +- [ ] Add how to setup comments and syntax table +- [ ] Add indent assert rule +- [ ] Add font-lock test +- [ ] Add indent test * Guide to building a tree-sitter mode -** Syntax trees and queries +This is a set of notes that I'm taking as I develop matlab-ts-mode.el with the goal of this +becoming a guide for writting a tree-sitter mode for Emacs 30 or later. + +* Syntax trees and queries If you are not familar with the concepts behind tree-sitter, see https://tree-sitter.github.io/tree-sitter. In particular, learn the notion of queries and try out @@ -68,12 +71,12 @@ tree is roughly 10 times the text size of the program being analyzed. However, t tree sitter are highly accuracte and fast syntax coloring (font-lock), indentation, code navigation via syntatic expressions, etc. -** Documentation +* Documentation - [[https://www.gnu.org/software/emacs/manual/html_node/elisp/Parsing-Program-Source.html][Emacs manual: Parsing Program Source]] - [[https://www.gnu.org/software/emacs/manual/html_node/elisp/Parser_002dbased-Indentation.html][Emacs manual: Parser-based Indentation]] -** libtree-sitter-LANGUAGE.EXT +* libtree-sitter-LANGUAGE.EXT Place the tree-sitter language library in =~/.emacs.d/tree-sitter/libtree-sitter-LANGUAGE.EXT= (EXT=.so on Linux, .dll on Windows, .dylib on Mac). There are other locations that this can @@ -117,7 +120,39 @@ You should now be able to use: : M-x treesit-inspect-mode : M-x treesit-explore-mode -** Setup font-lock +* Debugging tips + +- Incremental updates to your LANGUAGE-ts-mode + + As you update =LANUGAGE-ts-mode.el= you need to tell Emacs to pickup the updates. To do this, + + - Use *=C-x C-e=*. With the cursor =(point)= at the end of the syntatic expression of your *.el + file and run =C-x C-e= (or =M-x eval-last-sexp=) to evaluate the sexp prior to the cursor + point. + + - Alternatively, use *=C-M-x* (or =M-x eval-defun=). With the =(point)= in the =defvar=, + =defcusom=, or =defface=, run =C-M-x= to evaluate it. + + Note: =M-x eval-buffer= will not reevaluate already defined =defvar='s, so you must use + one of the above two to update a =defvar=. + +- =M-x LANGUAGE-ts-mode= + + - After making updates to =LANGUAGE-ts-mode.el= and evaluating them, you run =M-x LANGUAGE-ts-mode= + to re-load your mode in your =test.lang= file. For example, when writing the indent rules, you'll + need to run =M-x LANGUAGE-ts-mode= after =M=x eval-defun= on in your =(defvar + LANGUAGE-ts-mode--indent-rules ....)=. + +- Use =M-x ielm= + + In the =*ielm*= buffer created by =M-x ielm=, you can examine tree-sitter nodes, etc. For example: + + #+begin_example + ELISP> (with-current-buffer "test.lang" + (treesit-node-parent (treesit-node-at (point)))) + #+end_example + +* Font-lock Queries are needed to identify syntax tree nodes to fontify. See https://www.gnu.org/software/emacs/manual/html_node/elisp/Pattern-Matching.html @@ -181,14 +216,15 @@ and keywords. ;; names that correspond to the ;; :feature 'NAME ;; entries in LANGUAGE-ts-mode--font-lock-settings. For example, 'comment for comments, - ;; 'definition for function definitions, 'keyword for language keywords, etc. + ;; 'definition for function definitions, 'keyword for language keywords, etc. Below + ;; we have a few examples. You can use any names for your features. ;; Font-lock applies the faces defined in each sublist up to and including ;; `treesit-font-lock-level', which defaults to 3. (setq-local treesit-font-lock-feature-list - '((comment definition) + '((comment definition) (keyword string type) - (builtin constant escape-sequence label number) - (bracket delimiter error function operator property variable))) + (number) + (bracket delimiter error))) (treesit-major-mode-setup))) #+end_src @@ -198,6 +234,154 @@ applied to the items captured by the query. You can see available faces by using list-faces-display=. You'll probably want to stick with faces that come with stock Emacs to avoid dependenices on other packages or create your own face. +* Comments + +TODO + +* Indent + +Tree-sitter indentation is controlled by =treesit-simple-indent-rules=. We create a variable +containing our N indent rules and tell tree-sitter about them + +#+begin_src emacs-lisp + (defvar LANGUAGE-ts-mode--indent-rules + `((LANGUAGE + (MATCHER-1 ANCHOR-1 OFFSET-1) + (MATCHER-N ANCHOR-N OFFSET-N))) + "Tree-sitter indent rules for `LANGUAGE-ts-mode'.") + + ;;;###autoload + (define-derived-mode LANGUAGE-ts-mode prog-mode "LANGUAGE" + "Major mode for editing LANGUAGE files using tree-sitter." + + (when (treesit-ready-p 'LANGUAGE) + (treesit-parser-create 'LANGUAGE) + + ;; Indent + (setq-local treesit-simple-indent-rules LANGUAGE-ts-mode--indent-rules) + + (treesit-major-mode-setup))) +#+end_src + +To write the indent rules, we need to define the /matcher/, /anchor/, and /offset/ of each rule as +explained in the Emacs manual, "[[https://www.gnu.org/software/emacs/manual/html_node/elisp/Parser_002dbased-Indentation.html][Parser-based Indentation]]". The /matcher/ and /anchor/ are are +functions that take three arguments, =node=, =parent= node, and =bol=. =bol= is the +beginning-of-line buffer position. /matcher/ returns non-nil when the rule applies and /anchor/ +returns the buffer position which along with /offset/ determine the indent level of the line. + +Let's take this basic example of our LANGUAGE, =if_else.lang= file + +#+begin_example + if a > 1 + b = a * 2; + else + b = a; + end +#+end_example + +Running =M-x treesit-explore-mode= gives us: + +#+begin_example + (source_file + (if_statement if + condition: (comparison_operator (identifier) > (number)) + \n + (block + (assignment left: (identifier) = + right: (binary_operator left: (identifier) * right: (number))) + ;) + (else_clause else \n + (block + (assignment left: (identifier) = right: (identifier)) + ;)) + end) + \n) +#+end_example + +We start with + +#+begin_src emacs-lisp + (defvar tmp-debug-indent-rule + '((lambda (node parent bol) + (message "-->N:%S P:%S BOL:%S GP:%S NPS:%S" + node parent bol + (treesit-node-parent parent) + (treesit-node-prev-sibling node)) + nil) + nil + 0)) + + (defvar LANGUAGE-ts-mode--indent-rules + `((LANGUAGE + ,tmp-debug-indent-rule + ((parent-is "^source_file$") column-0 0) + )) + "Tree-sitter indent rules for `LANGUAGE-ts-mode'.") +#+end_src + +We set + +: M-: (setq treesit--indent-verbose t) + +and then hit the =TAB= key when vising a our =if_else.lang= file. + +The first rule, =((parent-is "source_file") column-0 0)= is the rule for the root node, which in our +LANGUAGE is "source_file" and says to sart on column 0. + +The two lambda debugging rules aid in writing rules will be removed when we have completed the +rules. For example, with the above and we type =TAB= on the "b = a * 2" line in the following +=if_else.lang= file. + +#+begin_example + if a > 1 + b = a * 2; + else + b = a; + end +#+end_example + +we'll see in the =*Messages*= buffer we'll see the error: + + : node: #<treesit-node block in 14-24> parent: #<treesit-node if_statement in 1-44> bol: 14 + +where point 14-24 is "b = a * 2" and we see it has node named "block". Thus, we update we add to our +indent rules, =((node-is "block") parent 4)= and a couple more rules as shown below. + +*Tip*: =C-M-x= in our =defvar= and re-run =M-x LANGUAGE-ts-mode= file to pickup the new indent +rules. + +#+begin_src emacs-lisp + (defvar LANGUAGE-ts-mode--indent-rules + `((LANGUAGE + ,tmp-debug-indent-rule + ((parent-is "^source_file$") column-0 0) + ((node-is "^block$") parent 4) + ((node-is "^else_clause$") parent 0) + ((node-is "%end$") parent 0) + )) + "Tree-sitter indent rules for `LANGUAGE-ts-mode'.") +#+end_src + +We can simplify this because the "else_clause" and "end" nodes have the same indent rules: + +#+begin_src emacs-lisp + (defvar LANGUAGE-ts-mode--indent-rules + `((LANGUAGE + ,tmp-debug-indent-rule + ((parent-is "^source_file$") column-0 0) + ((node-is "^block$") parent 4) + ((node-is ,(rx bol (or "else_clause" "end") eol)) parent 0) + )) + "Tree-sitter indent rules for `LANGUAGE-ts-mode'.") +#+end_src + +Following this process, we add additional rules and our indent engine is complete after we remove +the debugging rules. + +*Tip*: If you look at the defintion, =M-x find-variable RET treesit-simple-indent-presets RET=, you +can see how the built-in /matchers/ and /achors/ are written. From that, you can write your own as +needed. + * Issues - [ ] Building libtree-sitter-matlab.dll from src on Windows produces a DLL that fails. @@ -241,3 +425,34 @@ avoid dependenices on other packages or create your own face. makes it easy to install packages from ELPA, MELPA, etc. but how to we get libtree-sitter-LANUGAGE.EXT (EXT = .so, .dll, .dylib) installed? + +- [ ] In [[https://www.gnu.org/software/emacs/manual/html_node/elisp/Parser_002dbased-Indentation.html][Parser-Based Indentation]] we have prev-line which goes backward exactly one line + + Consider a programming lanugage with a few statements, e.g. + + #+begin_example + { + a = 1; + b = 2; + + + } + #+end_example + + If you use prev-line on the blank-line immediately after "b = 2;", you'll get the expected + point below "b". If you use prev-line on the second blank line after "b = 2;", you'll get + 0, which is unexpected in many languages. I suspect it may be safe to just update prev-real + line too look backwards to the first prior line with non-whitespace or if you are worried + about compatibility, introduce: + + #+begin_src emacs-lisp + (cons 'prev-real-line (lambda (_n _p bol &rest _) + (save-excursion + (goto-char bol) + (forward-line -1) + (while (and (not (bobp)) + (looking-at "^[ \t]*$")) + (forward-line -1)) + (skip-chars-forward " \t") + (point)))) + #+end_src diff --git a/matlab-ts-mode.el b/matlab-ts-mode.el index 2abba95231..95ad09ca0c 100644 --- a/matlab-ts-mode.el +++ b/matlab-ts-mode.el @@ -63,6 +63,13 @@ :bold t)) "Face for \"%% code section\" headings when NOT in matlab-sections-minor-mode.") +(defcustom matlab-ts-font-lock-level 4 + "Level of font lock, 1 for minimum 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))) + ;;--------------------;; ;; Section: font-lock ;; ;;--------------------;; @@ -78,7 +85,7 @@ ;; Keywords are documented here https://www.mathworks.com/help/matlab/ref/iskeyword.html ;; Note, arguments, methods, properties are semi-keywords in that in the right location ;; the are keywords, otherwise in the wrong location they are variables, but tree-sitter - ;; correctly handles them by letting use look for these as content of the nodes. + ;; correctly handles them by letting us look for these as content of the nodes. '("arguments" (break_statement) "case" @@ -118,48 +125,64 @@ "uint64") "MATLAB data type functions.") -(defun matlab-ts-mode--doc-comment-capture (comment-node override start end &rest _) - "Fontify function/classdef documentation comments. +(defun matlab-ts-mode--is-doc-comment (comment-node parent) + "Is the COMMENT-NODE under PARENT a help doc comment. In MATLAB, + + function out = myFunction + % The documentation help comment for myFunction immediately follows the + % function defintion. + + % code comments are preceded with a blank line + out = 1; + end + function out = myFunction % The documentation help comment for myFunction immediately follows the % function defintion. - % code comments are preceeded with a blank line + % copyright at column 0 and preceded by blank liness after the help comment + + % code comments are preceded with a blank line out = 1; end function out = myFunctionWithoutHelp - % code comments are preceeded with a blank line + % code comments are preceded with a blank line out = 1; end +Similar behavior for classdef's." + + (when (string-match-p (rx bol (or "function_definition" "class_definition") eol) + (treesit-node-type parent)) + (let ((definition-point (treesit-node-start parent))) + (save-excursion + (goto-char (treesit-node-start comment-node)) + (beginning-of-line) + + ;; Skip backwards over the copyright line to prior content. + (when (looking-at "^[ \t]*%[ \t]*copyright\\b") + (beginning-of-line) + (forward-line -1) + (while (looking-at "^[ \t]*$") + (forward-line -1))) + + ;; result - is doc comment? + (or (<= (point) definition-point) ;; at definition? + (and (> (point) definition-point) + (not (re-search-backward "^[ \t]*$" definition-point t)))))))) + +(defun matlab-ts-mode--doc-comment-capture (comment-node override start end &rest _) + "Fontify function/classdef documentation comments. COMMENT-NODE is the tree-sitter node from the \"doc comments\" treesit-font-lock-rules rule and OVERRIDE is from that rule. START and END specify the region to be fontified." - (let* ((prev-node (treesit-node-prev-sibling comment-node)) - (prev-node-type (treesit-node-type prev-node)) - (prev2-node (when (string= prev-node-type "identifier") - (treesit-node-prev-sibling prev-node))) - (real-prev-node-type (if prev2-node (treesit-node-type prev2-node) prev-node-type)) - (real-prev-node (or prev2-node prev-node)) - (is-doc-comment-candidate - (or (string= real-prev-node-type "function_arguments") ;; function foo(in) - (string= real-prev-node-type "function_output") ;; function out = foo - (string= real-prev-node-type "function") ;; function foo - (string= real-prev-node-type "classdef") ;; classdef foo - (string= real-prev-node-type "superclasses"))) ;; classdef foo < ParentClass - (is-doc-comment (and is-doc-comment-candidate - (save-excursion - (goto-char (treesit-node-start comment-node)) - (not (re-search-backward "^[ \t]*$" - (treesit-node-end real-prev-node) - t)))))) - (when is-doc-comment - (treesit-fontify-with-override - (treesit-node-start comment-node) (treesit-node-end comment-node) - font-lock-doc-face override start end)))) + (when (matlab-ts-mode--is-doc-comment comment-node (treesit-node-parent comment-node)) + (treesit-fontify-with-override + (treesit-node-start comment-node) (treesit-node-end comment-node) + font-lock-doc-face override start end))) (defvar matlab-ts-mode--font-lock-settings (treesit-font-lock-rules @@ -195,9 +218,9 @@ START and END specify the region to be fontified." (function_arguments arguments: (identifier) @font-lock-variable-name-face ("," (identifier) @font-lock-variable-name-face) :*) - ;; Function single output arugment: function out = functionName(in1, in2) + ;; Function single output argument: function out = functionName(in1, in2) (function_output (identifier) @font-lock-variable-name-face) - ;; Function multiple ouptut arguments: function [out1, out2] = functionName(in1, in2) + ;; Function multiple output arguments: function [out1, out2] = functionName(in1, in2) (function_output (multioutput_variable (identifier) @font-lock-variable-name-face)) ;; Fields of: arguments ... end , properties ... end (property (validation_functions (identifier) @font-lock-builtin-face)) @@ -263,6 +286,255 @@ START and END specify the region to be fontified." ) "MATLAB tree-sitter font-lock settings.") + +;;-----------------:; +;; Section: Indent ;; +;;-----------------;; + +;; We discourage customizing the indentation rules. Having one-style of consistent indentation makes +;; reading others' code easier. +(defvar matlab-ts-mode--indent-level 4 + "Indentation level.") +(defvar matlab-ts-mode--switch-indent-level (/ matlab-ts-mode--indent-level 2) + "Indentation level for switch-case statements.") +(defvar matlab-ts-mode--array-indent-level 2 + "Indentation level for elements in an array.") + +(defun matlab-ts-mode--prev-real-line (_n _p bol &rest _) + "Return point of first non-whitespace looking backward. +BOL, beginning-of-line point, is where to start from." + (save-excursion + (goto-char bol) + (forward-line -1) + (while (and (not (bobp)) + (looking-at "^[ \t]*$")) + (forward-line -1)) + (skip-chars-forward " \t") + (point))) + +(defun matlab-ts-mode--prev-real-line-is (node-type prev-real-line-node-type) + "Node type matcher and previous real line type matcher. +Returns non-nil if the current tree-sitter node matches NODE-TYPE and +the previous non-empty line tree-sitter node type matches +PREV-REAL-LINE-NODE-TYPE. NODE-TYPE can be nil when there's no current +node or a regular expression. PREV-REAL-LINE-NODE-TYPE is a regular +expression." + (lambda (node parent bol &rest _) + (when (or (and (not node-type) + (not node)) + (and node-type + (string-match-p node-type (or (treesit-node-type node) "")))) + (let* ((prev-real-line-bol (matlab-ts-mode--prev-real-line node parent bol)) + (p-node (treesit-node-at prev-real-line-bol))) + (string-match-p prev-real-line-node-type (or (treesit-node-type p-node) "")))))) + +(defvar tmp-debug-indent-rule + '((lambda (node parent bol) + (message "-->N:%S P:%S BOL:%S GP:%S NPS:%S" + node parent bol + (treesit-node-parent parent) + (treesit-node-prev-sibling node)) + nil) + nil + 0)) + +(defvar matlab-ts-mode--indent-assert nil + "Tests should set this to t to identify when we fail to find an indent rule.") + +(defvar matlab-ts-mode--indent-assert-rule + '((lambda (node parent bol) + (when matlab-ts-mode--indent-assert + (error "Assert no indent rule for: N:%S P:%S BOL:%S GP:%S NPS:%S BUF:%S" + node parent bol + (treesit-node-parent parent) + (treesit-node-prev-sibling node) + (buffer-name)))) + nil + 0)) + +(defvar matlab-ts-mode--indent-rules + `((matlab + + ;; ,tmp-debug-indent-rule + + ;; Rule: classdef's, function's, or code for a script that is at the top-level + ((parent-is ,(rx bol "source_file" eol)) column-0 0) + + ;; Rule: within a function/classdef doc block comment "%{ ... %}"? + ((lambda (node parent bol &rest _) + (and (not node) + (string= "comment" (treesit-node-type parent)) + (not (save-excursion (goto-char bol) + (looking-at "%"))) + (matlab-ts-mode--is-doc-comment parent (treesit-node-parent parent)))) + parent 2) + + ;; Rule: function/classdef doc comment? + ((lambda (node parent &rest _) + (or (and (string= "comment" (or (treesit-node-type node) "")) + (matlab-ts-mode--is-doc-comment node parent)) + (and (not node) + (string= "comment" (treesit-node-type parent)) + (matlab-ts-mode--is-doc-comment parent (treesit-node-parent parent))))) + parent 0) + + ;; Rule: within a code block comment "%{ ... %}"? + ((lambda (node parent bol &rest _) + (and (not node) + (string= "comment" (treesit-node-type parent)) + (not (save-excursion (goto-char bol) + (looking-at "%"))))) + parent + 2) + + ;; Rule: last line of code block coment "%{ ... %}"? + ((lambda (node parent bol &rest _) + (and (not node) + (string= "comment" (treesit-node-type parent)) + (save-excursion (goto-char bol) + (looking-at "%")))) + parent + 0) + + ;; Rule: switch case and otherwise statements + ((node-is ,(rx bol (or "case_clause" "otherwise_clause") eol)) + parent ,matlab-ts-mode--switch-indent-level) + + ;; Rule: first line of code witin a switch case or otherwise statement, node is block + ((parent-is ,(rx bol (or "case_clause" "otherwise_clause") eol)) + parent ,matlab-ts-mode--switch-indent-level) + + ;; Rule: nested functions + ((n-p-gp ,(rx bol "function_definition" eol) ,(rx bol "block" eol) + ,(rx bol "function_definition" eol)) + parent 0) + + ;; Rule: constructs within classdef or function's. + ((node-is ,(rx bol (or "arguments_statement" "block" "enumeration" "enum" "methods" "events" + "function_definition" "property" "properties") + eol)) + parent ,matlab-ts-mode--indent-level) + + ;; Rule: elseif, else, catch, end statements go back to parent level + ((node-is ,(rx bol (or "elseif_clause" "else_clause" "catch_clause" "end") eol)) parent 0) + + ;; Rule: code in if, for, methods, function, arguments statements + ((parent-is ,(rx bol (or "if_statement" "for_statement" "while_statement" + "methods" "events" "enumeration" + "function_definition" "arguments_statement") + eol)) + parent ,matlab-ts-mode--indent-level) + + ;; Rule: function a<RET> + ;; end + ((n-p-gp nil ,(rx bol "\n" eol) ,(rx bol (or "function_definition") eol)) + grand-parent ,matlab-ts-mode--indent-level) + + ;; Rule: case 10<RET> + ((n-p-gp nil ,(rx bol "\n" eol) ,(rx bol (or "switch_statement" "case_clause" + "otherwise_clause") + eol)) + grand-parent ,matlab-ts-mode--switch-indent-level) + + ;; Rule: if condition1 || ... | if condition1 + condition2 == ... + ;; <TAB> condition2 || ... | 2770000 ... + ((parent-is ,(rx bol (or "boolean_operator" "comparison_operator") eol)) parent 0) + + ;; Rule: elseif ... + ;; <TAB> condition2 || ... + ((parent-is ,(rx bol (or "else_clause" "elseif_clause") eol)) + parent ,matlab-ts-mode--indent-level) + + ;; Rule: if a ... + ;; <TAB> + ((n-p-gp nil ,(rx bol "\n" eol) + ,(rx bol (or "if_statement" "else_clause" "elseif_clause" ) eol)) + grand-parent ,matlab-ts-mode--indent-level) + + ;; Rule: disp(myMatrix(1: ... + ;; <TAB> end)); + ((parent-is ,(rx bol "range" eol)) parent 0) + + ;; Rule: try<RET> | catch<RET> + ((parent-is ,(rx bol (or "try_statement" "catch_clause") eol)) + parent ,matlab-ts-mode--indent-level) + + ;; Rule: function a + ;; x = 1; + ;; <TAB> y = 2; + ((parent-is ,(rx bol "block" eol)) parent 0) + + ;; Rule: "switch var" and we type RET after the var + (,(matlab-ts-mode--prev-real-line-is (rx bol "\n" eol) (rx bol "switch" eol)) + ,#'matlab-ts-mode--prev-real-line ,matlab-ts-mode--switch-indent-level) + + ;; Rule: "function foo()" and we type RET after the ")" + (,(matlab-ts-mode--prev-real-line-is nil (rx bol "function" eol)) + ,#'matlab-ts-mode--prev-real-line ,matlab-ts-mode--indent-level) + + ;; Rule: a = ... + ;; <TAB> 1; + ((parent-is ,(rx bol "assignment" eol)) parent ,matlab-ts-mode--indent-level) + + ;; Rule: a = 2 * ... + ;; <TAB> 1; + ((parent-is ,(rx bol "binary_operator" eol)) parent 0) + + ;; Rule: a = ( ... | a = [ ... | a = { ... + ;; 1 ... | 1 ... | 1 ... + ;; <TAB> ); | ]; | }; + ((node-is ,(rx bol (or ")" "]" "}") eol)) parent 0) + + ;; Rule: a = ( ... + ;; <TAB> 1 ... + ((parent-is ,(rx bol "parenthesis" eol)) parent 1) + + ;; Rule: a = [ ... | a = { ... + ;; <TAB> 2 ... | 2 ... + ((parent-is ,(rx bol (or "matrix" "cell") eol)) parent ,matlab-ts-mode--array-indent-level) + + ;; Rule: function [ ... | function name ( ... + ;; <TAB> a, ... % comment | a, ... % comment + ((parent-is ,(rx bol (or "multioutput_variable" "function_arguments") eol)) parent 1) + + ;; Rule: a = [ 2 ... | function a ... + ;; <TAB> 1 ... | = fcn + ((parent-is ,(rx bol (or "row" "function_output") eol)) parent 0) + + ;; Rule: a = ... + ;; <TAB> 1; + ((n-p-gp nil nil ,(rx bol "assignment" eol)) grand-parent ,matlab-ts-mode--indent-level) + + ;; Rule: a = my_function(1, ... + ;; <TAB> 2, ... + ((parent-is ,(rx bol "arguments" eol)) parent 0) + + ;; Rule: my_function( ... + ;; <TAB> 1, ... + ((node-is ,(rx bol "arguments" eol)) parent ,matlab-ts-mode--indent-level) + + ;; Rule: function indent_tab_between_fcns | function indent_tab_in_fcn + ;; end | disp('here') + ;; <TAB> | + ;; function b | end + ;; end | + ((lambda (node parent bol) + (and (not node) + (string= (treesit-node-type parent) "\n"))) + grand-parent 0) + + ;; Rule: In an empty line, string, etc. just maintain indent + ;; switch in + ;; case 10 + ;; disp('11'); + ;; <TAB> + (no-node ,#'matlab-ts-mode--prev-real-line 0) + + ;; Rule: Assert if no rule matched and asserts are enabled. + ,matlab-ts-mode--indent-assert-rule + )) + "Tree-sitter indent rules for `matlab-ts-mode'.") + ;;;###autoload (define-derived-mode matlab-ts-mode prog-mode "MATLAB:ts" "Major mode for editing MATLAB files with tree-sitter." @@ -270,13 +542,40 @@ START and END specify the region to be fontified." (when (treesit-ready-p 'matlab) (treesit-parser-create 'matlab) + ;; Comments + ;; TODO: M-; on code comments, then a 2nd M-; doesn't uncomment + ;; likely need to also set up the syntax table. + (setq-local comment-start "%") + (setq-local comment-end "") + (setq-local comment-start-skip "%\\s-+") + + ;; TODO function end handling + ;; TODO add strings to syntax table? + ;; TODO what about syntax table and electric keywords? + ;; TODO function / end match like matlab-mode + ;; TODO code folding + ;; TODO fill paragraph, etc. look at c-ts-common.el + ;; TODO outline: look at https://hg.sr.ht/~pranshu/perl-ts-mode/browse/perl-ts-mode.el?rev=tip + ;; TODO imenu: look at https://hg.sr.ht/~pranshu/perl-ts-mode/browse/perl-ts-mode.el?rev=tip + ;; TODO handle file name mismatch between function / classdef name + ;; TODO face for all built-in functions such as dbstop, quit, sin, etc. + ;; https://www.mathworks.com/help/matlab/referencelist.html?type=function&category=index&s_tid=CRUX_lftnav_function_index + ;; https://stackoverflow.com/questions/51942464/programmatically-return-a-list-of-all-functions/51946257 + ;; Maybe use completion api and complete on each letter? + ;; Maybe look at functionSignatures.json? + ;; Font-lock + (setq-local treesit-font-lock-level matlab-ts-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) - (number) - (bracket delimiter error))) + (number bracket delimiter) + ( error))) + + ;; Indent + (setq-local indent-tabs-mode nil ;; for consistency between Unix and Windows we don't use TABs. + treesit-simple-indent-rules matlab-ts-mode--indent-rules) (treesit-major-mode-setup))) diff --git a/tests/metest.el b/tests/metest.el index af924fd5fb..8e3d25adea 100644 --- a/tests/metest.el +++ b/tests/metest.el @@ -85,7 +85,9 @@ ;; matlab-ts-mode tests (when (>= emacs-major-version 30) (require 'test-matlab-ts-mode-font-lock) - (metest-run 'test-matlab-ts-mode-font-lock))) + (metest-run 'test-matlab-ts-mode-font-lock) + (require 'test-matlab-ts-mode-indent) + (metest-run 'test-matlab-ts-mode-indent))) (defun metest-run (test) "Run and time TEST." diff --git a/tests/test-matlab-ts-mode-font-lock.el b/tests/test-matlab-ts-mode-font-lock.el index dca7cd9966..6a5a514b4f 100644 --- a/tests/test-matlab-ts-mode-font-lock.el +++ b/tests/test-matlab-ts-mode-font-lock.el @@ -1,4 +1,4 @@ -;;; test-matlab-ts-mode-font-lock.el --- Testing suite for MATLAB Emacs -*- lexical-binding: t -*- +;;; test-matlab-ts-mode-font-lock.el --- Test matlab-ts-mode font-lock -*- lexical-binding: t -*- ;; ;; Copyright Free Software Foundation @@ -17,16 +17,22 @@ ;;; Commentary: ;; -;; Validate font-lock faces in matlab-ts-mode +;; Validate matlab-ts-mode font-lock faces. +;; Load ../matlab-ts-mode.el via require and run font-lock tests using +;; ./test-matlab-ts-mode-font-lock-files/NAME.m comparing against +;; ./test-matlab-ts-mode-font-lock-files/NAME_expected.txt +;; ;;; Code: +(require 'cl-macs) + +;; Add abs-path of ".." to load-path so we can (require 'matlab-ts-mode) (let* ((lf (or load-file-name (buffer-file-name (current-buffer)))) (d1 (file-name-directory lf)) (parent-dir (expand-file-name (file-name-directory (directory-file-name d1))))) (add-to-list 'load-path parent-dir t)) -(require 'cl-macs) (require 'matlab-ts-mode) (defun test-matlab-ts-mode-font-lock-files () @@ -37,26 +43,26 @@ (cons "test-matlab-ts-mode-font-lock" (test-matlab-ts-mode-font-lock-files))) (cl-defun test-matlab-ts-mode-font-lock (&optional m-file) - "Test font-lock using ./test-matlab-ts-mode-font-lock-files/M-FILE. -Compare ./test-matlab-ts-mode-font-lock-files/M-FILE against + "Test font-lock using ./test-matlab-ts-mode-font-lock-files/NAME.m. +Compare ./test-matlab-ts-mode-font-lock-files/NAME.m against ./test-matlab-ts-mode-font-lock-files/NAME_expected.txt, where -NAME_expected.txt is of same length as M-FILE and has a character for +NAME_expected.txt is of same length as NAME.m and has a character for each face setup by font-lock. -If M-FILE is not provided, loop comparing all - ./test-matlab-ts-mode-font-lock-files/*.m +If M-FILE NAME.m is not provided, loop comparing all +./test-matlab-ts-mode-font-lock-files/NAME.m files. -For example, given foo.m containing +For example, given foo.m containing: function a = foo a = 1; end -we'll have expected that looks like +we'll have expected that looks like: kkkkkkkk v d fff d d dd kkk -For debugging, you can run with a specified M-FILE, - M-: (test-matlab-ts-mode-font-lock \"test-matlab-ts-mode-font-lock-files/M-FILE\")" +For debugging, you can run with a specified NAME.m, + M-: (test-matlab-ts-mode-font-lock \"test-matlab-ts-mode-font-lock-files/NAME.m\")" (when (or (< emacs-major-version 30) (not (progn @@ -109,7 +115,7 @@ For debugging, you can run with a specified M-FILE, (font-lock-mode 1) (font-lock-flush (point-min) (point-max)) (font-lock-ensure (point-min) (point-max)) - + (goto-char (point-min)) (let* ((got "") (expected-file (replace-regexp-in-string "\\.m$" "_expected.txt" diff --git a/tests/test-matlab-ts-mode-indent-files/indent_cell.m b/tests/test-matlab-ts-mode-indent-files/indent_cell.m new file mode 100644 index 0000000000..8884883656 --- /dev/null +++ b/tests/test-matlab-ts-mode-indent-files/indent_cell.m @@ -0,0 +1,33 @@ +% -*- matlab-ts -*- +function a = indent_cell + a = { ... + 1 ... + + ... + 2 + }; + + a = { 2 ... + 1 ... + }; + + a = { ... + 2 + { 3 + 4, + 5 + { ... + 2 + } + } + }; + + a = { ... + 1; ... + 2 ... + }; + + long_variable_a = ... + { + 2, 123, 456 + 3, 2 7 + }; + +end diff --git a/tests/test-matlab-ts-mode-indent-files/indent_cell_expected.m b/tests/test-matlab-ts-mode-indent-files/indent_cell_expected.m new file mode 100644 index 0000000000..b7a2febac7 --- /dev/null +++ b/tests/test-matlab-ts-mode-indent-files/indent_cell_expected.m @@ -0,0 +1,33 @@ +% -*- matlab-ts -*- +function a = indent_cell + a = { ... + 1 ... + + ... + 2 + }; + + a = { 2 ... + 1 ... + }; + + a = { ... + 2 + { 3 + 4, + 5 + { ... + 2 + } + } + }; + + a = { ... + 1; ... + 2 ... + }; + + long_variable_a = ... + { + 2, 123, 456 + 3, 2 7 + }; + +end diff --git a/tests/test-matlab-ts-mode-indent-files/indent_comments.m b/tests/test-matlab-ts-mode-indent-files/indent_comments.m new file mode 100644 index 0000000000..ea2c8b88d1 --- /dev/null +++ b/tests/test-matlab-ts-mode-indent-files/indent_comments.m @@ -0,0 +1,47 @@ +% -*- matlab-ts -*- +function b = indent_comments(a) +% this the doc help +% comment + + % comment about fcn1 + [c, d] = fcn1(a, a); + + b = c + d; + + % comment about fcn2 + fcn2; +end + + +function [c, d] = fcn1(a, b) +%{ + help comment + + with blank lines + +%} + c = a; + d = b; +end + + +function fcn2 +%{ + help comment + + with blank lines + + +%} + + %{ + + block + comment + for following + + line + %} + + disp('2'); +end diff --git a/tests/test-matlab-ts-mode-indent-files/indent_comments_expected.m b/tests/test-matlab-ts-mode-indent-files/indent_comments_expected.m new file mode 100644 index 0000000000..60581922ab --- /dev/null +++ b/tests/test-matlab-ts-mode-indent-files/indent_comments_expected.m @@ -0,0 +1,47 @@ +% -*- matlab-ts -*- +function b = indent_comments(a) +% this the doc help +% comment + + % comment about fcn1 + [c, d] = fcn1(a, a); + + b = c + d; + + % comment about fcn2 + fcn2; +end + + +function [c, d] = fcn1(a, b) +%{ + help comment + + with blank lines + +%} + c = a; + d = b; +end + + +function fcn2 +%{ + help comment + + with blank lines + + +%} + + %{ + + block + comment + for following + + line + %} + + disp('2'); +end diff --git a/tests/test-matlab-ts-mode-indent-files/indent_cont_statements.m b/tests/test-matlab-ts-mode-indent-files/indent_cont_statements.m new file mode 100644 index 0000000000..4622afe840 --- /dev/null +++ b/tests/test-matlab-ts-mode-indent-files/indent_cont_statements.m @@ -0,0 +1,41 @@ +% -*- matlab-ts -*- +function a = indent_cont_statements + + a = ... + 1; + + a = ... + 1 + ... + 2 + ... + 2; + + a = 2 * ... + 1; + + a = ( ... + 1 + 2); + + a = ( ... + 1 ... + ); + + a = 2 * ... + ( ... + 3 + ... + 4 + ( ... + 5 * ... + 6 ... + ) ... + ); ... + + a = 1 + 2; + + % The matlab-tree-sitter, by design for simplicity treats "..." as comments so the following + % is an error in MATLAB, but doesn't generate a parse error and indents the same as if + % the ellipsis (...) were present. + a = + 1 + + 2; + + +end diff --git a/tests/test-matlab-ts-mode-indent-files/indent_cont_statements_expected.m b/tests/test-matlab-ts-mode-indent-files/indent_cont_statements_expected.m new file mode 100644 index 0000000000..667a88cfc4 --- /dev/null +++ b/tests/test-matlab-ts-mode-indent-files/indent_cont_statements_expected.m @@ -0,0 +1,41 @@ +% -*- matlab-ts -*- +function a = indent_cont_statements + + a = ... + 1; + + a = ... + 1 + ... + 2 + ... + 2; + + a = 2 * ... + 1; + + a = ( ... + 1 + 2); + + a = ( ... + 1 ... + ); + + a = 2 * ... + ( ... + 3 + ... + 4 + ( ... + 5 * ... + 6 ... + ) ... + ); ... + + a = 1 + 2; + + % The matlab-tree-sitter, by design for simplicity treats "..." as comments so the following + % is an error in MATLAB, but doesn't generate a parse error and indents the same as if + % the ellipsis (...) were present. + a = + 1 + + 2; + + +end diff --git a/tests/test-matlab-ts-mode-indent-files/indent_copyright.m b/tests/test-matlab-ts-mode-indent-files/indent_copyright.m new file mode 100644 index 0000000000..9ca4fd6823 --- /dev/null +++ b/tests/test-matlab-ts-mode-indent-files/indent_copyright.m @@ -0,0 +1,13 @@ +% -*- matlab-ts -*- + function indent_copyright +% indent_copyright help + % comment +% that is several + % lines long + + % Copyright after help comment shouldn't be indented + +% this is a comment about the following code + disp('here') + +end diff --git a/tests/test-matlab-ts-mode-indent-files/indent_copyright_expected.m b/tests/test-matlab-ts-mode-indent-files/indent_copyright_expected.m new file mode 100644 index 0000000000..4462551f22 --- /dev/null +++ b/tests/test-matlab-ts-mode-indent-files/indent_copyright_expected.m @@ -0,0 +1,13 @@ +% -*- matlab-ts -*- +function indent_copyright +% indent_copyright help +% comment +% that is several +% lines long + +% Copyright after help comment shouldn't be indented + + % this is a comment about the following code + disp('here') + +end diff --git a/tests/test-matlab-ts-mode-indent-files/indent_copyright_in_code.m b/tests/test-matlab-ts-mode-indent-files/indent_copyright_in_code.m new file mode 100644 index 0000000000..e03408228d --- /dev/null +++ b/tests/test-matlab-ts-mode-indent-files/indent_copyright_in_code.m @@ -0,0 +1,17 @@ +% -*- matlab-ts -*- + function b = indent_copyright_in_code + %{ + sadf + + asdfasd + %} + % foo + % foo + + + + % copyright blah + + % foo + b = 1; + end diff --git a/tests/test-matlab-ts-mode-indent-files/indent_copyright_in_code_expected.m b/tests/test-matlab-ts-mode-indent-files/indent_copyright_in_code_expected.m new file mode 100644 index 0000000000..ba21d358ea --- /dev/null +++ b/tests/test-matlab-ts-mode-indent-files/indent_copyright_in_code_expected.m @@ -0,0 +1,17 @@ +% -*- matlab-ts -*- +function b = indent_copyright_in_code +%{ + sadf + + asdfasd +%} + % foo + % foo + + + + % copyright blah + + % foo + b = 1; +end diff --git a/tests/test-matlab-ts-mode-indent-files/indent_fcn_calls.m b/tests/test-matlab-ts-mode-indent-files/indent_fcn_calls.m new file mode 100644 index 0000000000..d0287826b5 --- /dev/null +++ b/tests/test-matlab-ts-mode-indent-files/indent_fcn_calls.m @@ -0,0 +1,21 @@ +% -*- matlab-ts -*- +var_a = my_function(1, 2, 3); + +var_b = my_function(1, ... + 2, ... + 3); + +my_struct.var_c = my_function( ... + 1, ... + 2, ... + 3); + +my_other_function(1, ... + 2, ... + 3); + +% some extra spaces after a function call +my_other_function ( ... + 1, ... + 2, ... + 3); diff --git a/tests/test-matlab-ts-mode-indent-files/indent_fcn_calls_expected.m b/tests/test-matlab-ts-mode-indent-files/indent_fcn_calls_expected.m new file mode 100644 index 0000000000..d0287826b5 --- /dev/null +++ b/tests/test-matlab-ts-mode-indent-files/indent_fcn_calls_expected.m @@ -0,0 +1,21 @@ +% -*- matlab-ts -*- +var_a = my_function(1, 2, 3); + +var_b = my_function(1, ... + 2, ... + 3); + +my_struct.var_c = my_function( ... + 1, ... + 2, ... + 3); + +my_other_function(1, ... + 2, ... + 3); + +% some extra spaces after a function call +my_other_function ( ... + 1, ... + 2, ... + 3); diff --git a/tests/test-matlab-ts-mode-indent-files/indent_fcn_ellipsis.m b/tests/test-matlab-ts-mode-indent-files/indent_fcn_ellipsis.m new file mode 100644 index 0000000000..6e040266a9 --- /dev/null +++ b/tests/test-matlab-ts-mode-indent-files/indent_fcn_ellipsis.m @@ -0,0 +1,28 @@ +% -*- matlab-ts -*- +function ... + [ ... + a, ... comment about a + b ... comment about b + ] ... + = indent_ellipsis ... + ( ... + c, ... comment about c + d, ... comment about d + e ... comment about e + ) + + a = ... + 1 + ... + ( ... + c ... + * ... + d ... + ); + + b = [ ... + 1 + ... + 2 + ... + 3 ... + ]; + +end diff --git a/tests/test-matlab-ts-mode-indent-files/indent_fcn_ellipsis_expected.m b/tests/test-matlab-ts-mode-indent-files/indent_fcn_ellipsis_expected.m new file mode 100644 index 0000000000..6e040266a9 --- /dev/null +++ b/tests/test-matlab-ts-mode-indent-files/indent_fcn_ellipsis_expected.m @@ -0,0 +1,28 @@ +% -*- matlab-ts -*- +function ... + [ ... + a, ... comment about a + b ... comment about b + ] ... + = indent_ellipsis ... + ( ... + c, ... comment about c + d, ... comment about d + e ... comment about e + ) + + a = ... + 1 + ... + ( ... + c ... + * ... + d ... + ); + + b = [ ... + 1 + ... + 2 + ... + 3 ... + ]; + +end diff --git a/tests/test-matlab-ts-mode-indent-files/indent_function.m b/tests/test-matlab-ts-mode-indent-files/indent_function.m new file mode 100644 index 0000000000..a22813aab5 --- /dev/null +++ b/tests/test-matlab-ts-mode-indent-files/indent_function.m @@ -0,0 +1,7 @@ +% -*- matlab-ts -*- +function b = indent_function(a) +% doc comment + + % code comment + b = a; +end diff --git a/tests/test-matlab-ts-mode-indent-files/indent_function_expected.m b/tests/test-matlab-ts-mode-indent-files/indent_function_expected.m new file mode 100644 index 0000000000..a22813aab5 --- /dev/null +++ b/tests/test-matlab-ts-mode-indent-files/indent_function_expected.m @@ -0,0 +1,7 @@ +% -*- matlab-ts -*- +function b = indent_function(a) +% doc comment + + % code comment + b = a; +end diff --git a/tests/test-matlab-ts-mode-indent-files/indent_if_continued.m b/tests/test-matlab-ts-mode-indent-files/indent_if_continued.m new file mode 100644 index 0000000000..b33f6f1d21 --- /dev/null +++ b/tests/test-matlab-ts-mode-indent-files/indent_if_continued.m @@ -0,0 +1,42 @@ +% -*- matlab-ts -*- +function indent_if_continued + + if condition1 || ... + condition2 || ... + fcn_call(arg1, ... + arg2) + + line_in_if(); + + elseif condition1 + condition2 == ... + 2770000 ... + fcn_call(arg1, ... + arg2) + line_in_if(); + elseif (condition2 || ... + (condition3 && ... + condition4)) + disp('hello') + elseif ... + condition2 || ... + (condition3 && ... + condition4) + + disp('hello') + else ... + + + end + + if a + + + end + + if ... + foo + ... + bar + + end + +end diff --git a/tests/test-matlab-ts-mode-indent-files/indent_if_continued_expected.m b/tests/test-matlab-ts-mode-indent-files/indent_if_continued_expected.m new file mode 100644 index 0000000000..3247d19446 --- /dev/null +++ b/tests/test-matlab-ts-mode-indent-files/indent_if_continued_expected.m @@ -0,0 +1,42 @@ +% -*- matlab-ts -*- +function indent_if_continued + + if condition1 || ... + condition2 || ... + fcn_call(arg1, ... + arg2) + + line_in_if(); + + elseif condition1 + condition2 == ... + 2770000 ... + fcn_call(arg1, ... + arg2) + line_in_if(); + elseif (condition2 || ... + (condition3 && ... + condition4)) + disp('hello') + elseif ... + condition2 || ... + (condition3 && ... + condition4) + + disp('hello') + else ... + + + end + + if a + + + end + + if ... + foo + ... + bar + + end + +end diff --git a/tests/test-matlab-ts-mode-indent-files/indent_if_else.m b/tests/test-matlab-ts-mode-indent-files/indent_if_else.m new file mode 100644 index 0000000000..dc0f308d79 --- /dev/null +++ b/tests/test-matlab-ts-mode-indent-files/indent_if_else.m @@ -0,0 +1,7 @@ +% -*- matlab-ts -*- +if a > 1 + b = a * 2; + b = b + 1; +else + b = a; +end diff --git a/tests/test-matlab-ts-mode-indent-files/indent_if_else_expected.m b/tests/test-matlab-ts-mode-indent-files/indent_if_else_expected.m new file mode 100644 index 0000000000..dc0f308d79 --- /dev/null +++ b/tests/test-matlab-ts-mode-indent-files/indent_if_else_expected.m @@ -0,0 +1,7 @@ +% -*- matlab-ts -*- +if a > 1 + b = a * 2; + b = b + 1; +else + b = a; +end diff --git a/tests/test-matlab-ts-mode-indent-files/indent_keywords.m b/tests/test-matlab-ts-mode-indent-files/indent_keywords.m new file mode 100644 index 0000000000..889a0c4470 --- /dev/null +++ b/tests/test-matlab-ts-mode-indent-files/indent_keywords.m @@ -0,0 +1,76 @@ +% -*- matlab-ts -*- +classdef indent_keywords + properties + p1 = 0 + p2 = 0 + end + methods + function method1(in) + + arguments + in (1,1) double + end + + global gVar1 gVar2 + global pVar1 pVar2 + + try + + + switch in + case 10 + disp('10'); + + + disp('11'); + otherwise + + disp('~10'); + + end + catch me + + rethrow(me) + end + + j = 0; + for n = 1:in + if mod(n, 5) + x = 1; + continue + elseif mod(n, 7) + continue + else + j = j + 1; + end + disp(['Divisible by 5 or 7 : ' num2str(n)]) + end + + x = 0 + while x < 10 + x = x + 1; + end + end + + function method2() + n = 200; + A = 500; + a = zeros(1,n); + parfor i = 1:n + a(i) = max(abs(eig(rand(A)))); + end + + return + end + end + + events + e1 + e2 + end + + enumeration + one + two + end +end diff --git a/tests/test-matlab-ts-mode-indent-files/indent_keywords_expected.m b/tests/test-matlab-ts-mode-indent-files/indent_keywords_expected.m new file mode 100644 index 0000000000..bcabb8b9f7 --- /dev/null +++ b/tests/test-matlab-ts-mode-indent-files/indent_keywords_expected.m @@ -0,0 +1,76 @@ +% -*- matlab-ts -*- +classdef indent_keywords + properties + p1 = 0 + p2 = 0 + end + methods + function method1(in) + + arguments + in (1,1) double + end + + global gVar1 gVar2 + global pVar1 pVar2 + + try + + + switch in + case 10 + disp('10'); + + + disp('11'); + otherwise + + disp('~10'); + + end + catch me + + rethrow(me) + end + + j = 0; + for n = 1:in + if mod(n, 5) + x = 1; + continue + elseif mod(n, 7) + continue + else + j = j + 1; + end + disp(['Divisible by 5 or 7 : ' num2str(n)]) + end + + x = 0 + while x < 10 + x = x + 1; + end + end + + function method2() + n = 200; + A = 500; + a = zeros(1,n); + parfor i = 1:n + a(i) = max(abs(eig(rand(A)))); + end + + return + end + end + + events + e1 + e2 + end + + enumeration + one + two + end +end diff --git a/tests/test-matlab-ts-mode-indent-files/indent_matrix.m b/tests/test-matlab-ts-mode-indent-files/indent_matrix.m new file mode 100644 index 0000000000..c0c4fbdea4 --- /dev/null +++ b/tests/test-matlab-ts-mode-indent-files/indent_matrix.m @@ -0,0 +1,33 @@ +% -*- matlab-ts -*- +function a = indent_matrix + a = [ ... + 1 ... + + ... + 2 + ]; + + a = [ 2 ... + 1 ... + ]; + + a = [ ... + 2 + [ 3 + 4, + 5 + [ ... + 2 + ] + ] + ]; + + a = [ ... + 1; ... + 2 ... + ]; + + long_variable_a = ... + [ + 2, 123, 456 + 3, 2 7 + ]; + +end diff --git a/tests/test-matlab-ts-mode-indent-files/indent_matrix_expected.m b/tests/test-matlab-ts-mode-indent-files/indent_matrix_expected.m new file mode 100644 index 0000000000..c0c4fbdea4 --- /dev/null +++ b/tests/test-matlab-ts-mode-indent-files/indent_matrix_expected.m @@ -0,0 +1,33 @@ +% -*- matlab-ts -*- +function a = indent_matrix + a = [ ... + 1 ... + + ... + 2 + ]; + + a = [ 2 ... + 1 ... + ]; + + a = [ ... + 2 + [ 3 + 4, + 5 + [ ... + 2 + ] + ] + ]; + + a = [ ... + 1; ... + 2 ... + ]; + + long_variable_a = ... + [ + 2, 123, 456 + 3, 2 7 + ]; + +end diff --git a/tests/test-matlab-ts-mode-indent-files/indent_nested.m b/tests/test-matlab-ts-mode-indent-files/indent_nested.m new file mode 100644 index 0000000000..961815f5f7 --- /dev/null +++ b/tests/test-matlab-ts-mode-indent-files/indent_nested.m @@ -0,0 +1,29 @@ +% -*- matlab-ts -*- +function b = indent_nested(a) + + x = 1; + + b = nested1(a); + + function d = nested1(c) + % help comment for + % nested1 + + % comment about next line, y = + + y = 2; + + d = nested2(c) + x; + + function f = nested2(e) + % help comment for nested2 + + % comment about next line, f = + f = y + e * 3; + end + end + + function nested3 + end + +end diff --git a/tests/test-matlab-ts-mode-indent-files/indent_nested_expected.m b/tests/test-matlab-ts-mode-indent-files/indent_nested_expected.m new file mode 100644 index 0000000000..d4ba72c314 --- /dev/null +++ b/tests/test-matlab-ts-mode-indent-files/indent_nested_expected.m @@ -0,0 +1,29 @@ +% -*- matlab-ts -*- +function b = indent_nested(a) + + x = 1; + + b = nested1(a); + + function d = nested1(c) + % help comment for + % nested1 + + % comment about next line, y = + + y = 2; + + d = nested2(c) + x; + + function f = nested2(e) + % help comment for nested2 + + % comment about next line, f = + f = y + e * 3; + end + end + + function nested3 + end + +end diff --git a/tests/test-matlab-ts-mode-indent-files/indent_old_indents.m b/tests/test-matlab-ts-mode-indent-files/indent_old_indents.m new file mode 100644 index 0000000000..519e1f533a --- /dev/null +++ b/tests/test-matlab-ts-mode-indent-files/indent_old_indents.m @@ -0,0 +1,375 @@ +% -*- matlab-ts -*- +% See ../indents.m which is used in testing matlab-mode +function indent_old_indents(a,b,stuff,cmddual1fake,cmddual2fake) +% Help text +% !!0 +% of many lines +% !!0 + + % including a gap - comment for following code + % !!4 + + arguments (Repeating) % !!4 + a (1,1) {mustBeNumeric} % !!8 + b (:,:) double % !!8 + stuff {mustBeMember(stuff, { 'this' 'that' 'other' })} % !!8 + cmddual1fake double % !!8 + cmddual2fake int % !!8 + end % !!4 + + persistent var1 % !!4 + global var2 % !!4 + persistent var3 % !!4 + + + locala = a; %#ok + localb = b; %#ok + localstuff = stuff; %#ok + + if isempty(var1) var1=1; end %#ok !!4 + if isempty(var3) var3=2; end %#ok !!4 + + ends_in_comments_and_strings(var1, var2, var3); % !!4 has end in name + + % !!4 + + block_starts_in_comments_and_strings(cmddual1fake,cmddual2fake); + array_constant_decls(); + + % !!4 + + continuations_and_block_comments(); + + % $$$ !!0 + % $$$ special ignore comments + + has_nested_fcn(); % !!4 + + % !!4 - after ignore comments + +end % Comment with end in it + +%!!0 + +function B = ends_in_comments_and_strings() +% !!0 + + % >>6 + if foo + A = 1; + end % <<6 end in comment after end + + % !!4 + + symbol_with_end_in_it; + + B = A(1:end); %#ok + + %% cell start comment !!4 + if foo %!!4 + C = "this is the end of the line"; + % !!8 + else %!!4 + + % !!8 + end; D = "string end string"; + % !!4 + + E = [ D C]; + + if bar + + A = E; + + end; B = A(1:end); + % !!4 + + E = B; + + if baz + + A = C; + + end; B = [ 1 2 ... % is this the end? + 3 4 ]; % !!15 + + % !!4 + + if foo + + A = E; + + end ... the other end + % !! 4 + + B = [ B A ]; % !!4 + + str = 'This is a char array with ... in it'; + foo(str); % !!4 + + fcncall(arg1, '...', arg3); % !!4 + 1; % !!4 + + % Multi- end s + % >>8 + if foo %#ok + if bar %#ok + if baz + + A = B; + + else + + end; end; end % <<8 comment end thing + + % !!4 + B = A; + +end + +function out = array_constant_decls() + + A = [ 1 2 3 ]; %!!4 + + Blong = [ 1 2; %!!4 + 3 4; %!!14 + ]; %!!12 + + Csep = [ + 1 2; %!!8 + 3 4; %!!8 + ]; %!!11 + + multinest = { [ 1 2 %!!4 + 3 4 ]; %!!20 + { 5 6 7 ... %!!18 + 8 9 10 ... %!!20 + }; %!!18 + fcncall(10, ... %!!18 + 12, ... %!!26 + [ 13 14; %!!26 + 15 16 ]) %!!28 + } ; %!!16 + + nest = { ... %!!4 + 1 %!!8 + [ ... %!!8 + 2 3 %!!10 + ] ... %!!8 + 3 %!!8 + }; %!!11 + + cascade_long_name = ... %!!4 + { ... %!!8 + 1 %!!10 + 2 %!!10 + }; %!!8 + + % TODO + % I don't know why the below indents this way. + % It should either do all max indent, or all lined up with parens. + thing.thing.long.long.longname({ 'str' %!!4 + 'str' %!!37 + 'str' %!!37 + 'str' %!!37 + }); %!!35 + + thing.thing.long.long.longname('str', ... %!!4 + 'str', ... %!!35 + 'str', ... %!!35 + 'str' ... %!!35 + ); %!!34 + + % Line starting with end inside parens + disp(Csep(1: ... %!!4 + end)); %!!14 + + % This array has bad syntactic expression parsing due to the + % apostrophy + Closures = [ + 755009 ; ... % 21-Feb-2067 Washington's Birthday (Mon) + 755010 ; % !!8 + ]; + + dep = [ + root(info.function, factory, workspace, []), ... % likewise this isn't a keyword + fcn3.finalize % the single quote used to break [] scanning + ]; + + % This long fcn name last symbol starts with 'get' which + % used to confuse and move to indent past 1st arg. + if qesimcheck.utils.GetYesNoAnswer('Do ',... !!4 + 'n',... !!39 + 'once') %!!39 + code(); %!!8 + end %!!4 + + + + % !!4 + out = { A %!!4 + Blong %!!12 + Csep %!!12 + nest %!!12 + multinest%!!12 + cascade_long_name%!!12 + Closures%!!12 + dep %!!12 + }; %!!10 + +end + +function C = block_starts_in_comments_and_strings(varargin) +% !!0 + + C = 0; + + if varargin{1} % if true + + % !!8 + else % !!4 + + % !!8 + end % if true + + + % see previous function + % !!4 + for x=1:length(C) % !!4 + if varargin{2} % !!8 + continue % !!12 + end % !!8 + + break % !!8 + % !!14 + + %!!8 + end + + switch foo() %!!4 + case 1 %!!6 + + %!!8 + otherwise %!!6 + + %!!8 + end %!!4 + + try + % !!8 + catch %!!4 + + % !!8 + end + +end + +function B = continuations_and_block_comments +% !!0 +% !!0 +% !!0 +%{ + !!2 { } + !!2 +%} + + %{ + blank line means this block comment is not part of help and is for following code + !!6 { } + !!6 + %} + + arg1=1; + + %{ + % !!4 + !!6 + % !!4 + %} + + % Block comment indicators MUST be on a line by themselves. + %{ Not a block comment } + + foo(1); % !!4 - don't indent this special + + %} Not an end to a block comment { + + foo(arg1, ... %!!4 + arg2); %!!8 + + foo_long_fcn(arg1, ... %!!4 + arg2); %!!17 + + A = [ 1 2 % !!4 + 3 4 ]; % !!10 + + foo(['this is a very long string', ... %!!4 + 'with a continution to do something very exciting']);%!!9 + + set(gcf,'Position',[ 1 2 3 4], ... !!4 + 'Color', 'red'); % !!12 + + B = A + 1 + 4 ... + + 6; % !!8 + + foo_code(); % eol-comment !!4 + % continuation-comment !!17 + + % !!4 -blank between this & continuation comment + % !!4 - more comments + + if condition1 || ... % !!4 + fcn_call(arg1, ... % !!12 + arg2) % !!21 + line_in_if(); + end % !!4 + + + +end + +function has_nested_fcn + + plot(1:10); %!!4 + + A = 1; + + function am_nested_fcn() %!!4 + % help + % !!4 + code(A); + %!!8 + end + + %!!4 + am_nested_fcn(); + function_end_same_line(1); + function_after_end_same_line(); +end + +function b=function_end_same_line(a), b=a; end %!!0 + +function function_after_end_same_line()%!!0 + %!!0 + disp('foo');%!!4 + + debug_cmd_dual(); + +end%!!0 + +function debug_cmd_dual () +% These dbstop command dual content have 'if' blocks in them. +% The command dual detection needs to block these from being +% detected as block initiators which would cause indentaiton. + + dbstop in hRandomFile at 14 if func() % !!4 + dbstop in hRandomFile at 30@1 if x==1 % !!4 + dbstop in hPFile % !!4 + dbstop in hSimpleFile at 2 % !!4 + dbstop if error % !!4 + + %!!4 + + debug_cmd_dual(); %!!4 + +end diff --git a/tests/test-matlab-ts-mode-indent-files/indent_old_indents_expected.m b/tests/test-matlab-ts-mode-indent-files/indent_old_indents_expected.m new file mode 100644 index 0000000000..3abeed5292 --- /dev/null +++ b/tests/test-matlab-ts-mode-indent-files/indent_old_indents_expected.m @@ -0,0 +1,375 @@ +% -*- matlab-ts -*- +% See ../indents.m which is used in testing matlab-mode +function indent_old_indents(a,b,stuff,cmddual1fake,cmddual2fake) +% Help text +% !!0 +% of many lines +% !!0 + + % including a gap - comment for following code + % !!4 + + arguments (Repeating) % !!4 + a (1,1) {mustBeNumeric} % !!8 + b (:,:) double % !!8 + stuff {mustBeMember(stuff, { 'this' 'that' 'other' })} % !!8 + cmddual1fake double % !!8 + cmddual2fake int % !!8 + end % !!4 + + persistent var1 % !!4 + global var2 % !!4 + persistent var3 % !!4 + + + locala = a; %#ok + localb = b; %#ok + localstuff = stuff; %#ok + + if isempty(var1) var1=1; end %#ok !!4 + if isempty(var3) var3=2; end %#ok !!4 + + ends_in_comments_and_strings(var1, var2, var3); % !!4 has end in name + + % !!4 + + block_starts_in_comments_and_strings(cmddual1fake,cmddual2fake); + array_constant_decls(); + + % !!4 + + continuations_and_block_comments(); + + % $$$ !!0 + % $$$ special ignore comments + + has_nested_fcn(); % !!4 + + % !!4 - after ignore comments + +end % Comment with end in it + +%!!0 + +function B = ends_in_comments_and_strings() +% !!0 + + % >>6 + if foo + A = 1; + end % <<6 end in comment after end + + % !!4 + + symbol_with_end_in_it; + + B = A(1:end); %#ok + + %% cell start comment !!4 + if foo %!!4 + C = "this is the end of the line"; + % !!8 + else %!!4 + + % !!8 + end; D = "string end string"; + % !!4 + + E = [ D C]; + + if bar + + A = E; + + end; B = A(1:end); + % !!4 + + E = B; + + if baz + + A = C; + + end; B = [ 1 2 ... % is this the end? + 3 4 ]; % !!15 + + % !!4 + + if foo + + A = E; + + end ... the other end + % !! 4 + + B = [ B A ]; % !!4 + + str = 'This is a char array with ... in it'; + foo(str); % !!4 + + fcncall(arg1, '...', arg3); % !!4 + 1; % !!4 + + % Multi- end s + % >>8 + if foo %#ok + if bar %#ok + if baz + + A = B; + + else + + end; end; end % <<8 comment end thing + + % !!4 + B = A; + +end + +function out = array_constant_decls() + + A = [ 1 2 3 ]; %!!4 + + Blong = [ 1 2; %!!4 + 3 4; %!!14 + ]; %!!12 + + Csep = [ + 1 2; %!!8 + 3 4; %!!8 + ]; %!!11 + + multinest = { [ 1 2 %!!4 + 3 4 ]; %!!20 + { 5 6 7 ... %!!18 + 8 9 10 ... %!!20 + }; %!!18 + fcncall(10, ... %!!18 + 12, ... %!!26 + [ 13 14; %!!26 + 15 16 ]) %!!28 + } ; %!!16 + + nest = { ... %!!4 + 1 %!!8 + [ ... %!!8 + 2 3 %!!10 + ] ... %!!8 + 3 %!!8 + }; %!!11 + + cascade_long_name = ... %!!4 + { ... %!!8 + 1 %!!10 + 2 %!!10 + }; %!!8 + + % TODO + % I don't know why the below indents this way. + % It should either do all max indent, or all lined up with parens. + thing.thing.long.long.longname({ 'str' %!!4 + 'str' %!!37 + 'str' %!!37 + 'str' %!!37 + }); %!!35 + + thing.thing.long.long.longname('str', ... %!!4 + 'str', ... %!!35 + 'str', ... %!!35 + 'str' ... %!!35 + ); %!!34 + + % Line starting with end inside parens + disp(Csep(1: ... %!!4 + end)); %!!14 + + % This array has bad syntactic expression parsing due to the + % apostrophy + Closures = [ + 755009 ; ... % 21-Feb-2067 Washington's Birthday (Mon) + 755010 ; % !!8 + ]; + + dep = [ + root(info.function, factory, workspace, []), ... % likewise this isn't a keyword + fcn3.finalize % the single quote used to break [] scanning + ]; + + % This long fcn name last symbol starts with 'get' which + % used to confuse and move to indent past 1st arg. + if qesimcheck.utils.GetYesNoAnswer('Do ',... !!4 + 'n',... !!39 + 'once') %!!39 + code(); %!!8 + end %!!4 + + + + % !!4 + out = { A %!!4 + Blong %!!12 + Csep %!!12 + nest %!!12 + multinest%!!12 + cascade_long_name%!!12 + Closures%!!12 + dep %!!12 + }; %!!10 + +end + +function C = block_starts_in_comments_and_strings(varargin) +% !!0 + + C = 0; + + if varargin{1} % if true + + % !!8 + else % !!4 + + % !!8 + end % if true + + + % see previous function + % !!4 + for x=1:length(C) % !!4 + if varargin{2} % !!8 + continue % !!12 + end % !!8 + + break % !!8 + % !!14 + + %!!8 + end + + switch foo() %!!4 + case 1 %!!6 + + %!!8 + otherwise %!!6 + + %!!8 + end %!!4 + + try + % !!8 + catch %!!4 + + % !!8 + end + +end + +function B = continuations_and_block_comments +% !!0 +% !!0 +% !!0 +%{ + !!2 { } + !!2 +%} + + %{ + blank line means this block comment is not part of help and is for following code + !!6 { } + !!6 + %} + + arg1=1; + + %{ + % !!4 + !!6 + % !!4 + %} + + % Block comment indicators MUST be on a line by themselves. + %{ Not a block comment } + + foo(1); % !!4 - don't indent this special + + %} Not an end to a block comment { + + foo(arg1, ... %!!4 + arg2); %!!8 + + foo_long_fcn(arg1, ... %!!4 + arg2); %!!17 + + A = [ 1 2 % !!4 + 3 4 ]; % !!10 + + foo(['this is a very long string', ... %!!4 + 'with a continution to do something very exciting']);%!!9 + + set(gcf,'Position',[ 1 2 3 4], ... !!4 + 'Color', 'red'); % !!12 + + B = A + 1 + 4 ... + + 6; % !!8 + + foo_code(); % eol-comment !!4 + % continuation-comment !!17 + + % !!4 -blank between this & continuation comment + % !!4 - more comments + + if condition1 || ... % !!4 + fcn_call(arg1, ... % !!12 + arg2) % !!21 + line_in_if(); + end % !!4 + + + +end + +function has_nested_fcn + + plot(1:10); %!!4 + + A = 1; + + function am_nested_fcn() %!!4 + % help + % !!4 + code(A); + %!!8 + end + + %!!4 + am_nested_fcn(); + function_end_same_line(1); + function_after_end_same_line(); +end + +function b=function_end_same_line(a), b=a; end %!!0 + +function function_after_end_same_line()%!!0 + %!!0 + disp('foo');%!!4 + + debug_cmd_dual(); + +end%!!0 + +function debug_cmd_dual () +% These dbstop command dual content have 'if' blocks in them. +% The command dual detection needs to block these from being +% detected as block initiators which would cause indentaiton. + + dbstop in hRandomFile at 14 if func() % !!4 + dbstop in hRandomFile at 30@1 if x==1 % !!4 + dbstop in hPFile % !!4 + dbstop in hSimpleFile at 2 % !!4 + dbstop if error % !!4 + + %!!4 + + debug_cmd_dual(); %!!4 + +end diff --git a/tests/test-matlab-ts-mode-indent-files/indent_ranges.m b/tests/test-matlab-ts-mode-indent-files/indent_ranges.m new file mode 100644 index 0000000000..a7d9de26cf --- /dev/null +++ b/tests/test-matlab-ts-mode-indent-files/indent_ranges.m @@ -0,0 +1,15 @@ +% -*- matlab-ts -*- + +myMatrix = [ + 1 2; + 3 4; + ]; + +disp(myMatrix(1: ... + end)); + +disp(myMatrix(1: (1 + ... + 2))); + +disp(myMatrix(1: [1 + ... + 2])); diff --git a/tests/test-matlab-ts-mode-indent-files/indent_ranges_expected.m b/tests/test-matlab-ts-mode-indent-files/indent_ranges_expected.m new file mode 100644 index 0000000000..79a13962c7 --- /dev/null +++ b/tests/test-matlab-ts-mode-indent-files/indent_ranges_expected.m @@ -0,0 +1,15 @@ +% -*- matlab-ts -*- + +myMatrix = [ + 1 2; + 3 4; + ]; + +disp(myMatrix(1: ... + end)); + +disp(myMatrix(1: (1 + ... + 2))); + +disp(myMatrix(1: [1 + ... + 2])); diff --git a/tests/test-matlab-ts-mode-indent-files/indent_switch.m b/tests/test-matlab-ts-mode-indent-files/indent_switch.m new file mode 100644 index 0000000000..124d20aa0e --- /dev/null +++ b/tests/test-matlab-ts-mode-indent-files/indent_switch.m @@ -0,0 +1,25 @@ +% -*- matlab-ts -*- +function indent_switch(in) + + + switch in + + + case 10 + + disp('one') + + + disp('two'); + + + a = 1 + + otherwise + + disp('foo') + + + end + +end diff --git a/tests/test-matlab-ts-mode-indent-files/indent_switch_expected.m b/tests/test-matlab-ts-mode-indent-files/indent_switch_expected.m new file mode 100644 index 0000000000..124d20aa0e --- /dev/null +++ b/tests/test-matlab-ts-mode-indent-files/indent_switch_expected.m @@ -0,0 +1,25 @@ +% -*- matlab-ts -*- +function indent_switch(in) + + + switch in + + + case 10 + + disp('one') + + + disp('two'); + + + a = 1 + + otherwise + + disp('foo') + + + end + +end diff --git a/tests/test-matlab-ts-mode-indent-files/indent_tab_between_fcns.m b/tests/test-matlab-ts-mode-indent-files/indent_tab_between_fcns.m new file mode 100644 index 0000000000..8c4c836c70 --- /dev/null +++ b/tests/test-matlab-ts-mode-indent-files/indent_tab_between_fcns.m @@ -0,0 +1,6 @@ +% -*- matlab-ts -*- +function indent_tab_between_fcns +end + +function b +end diff --git a/tests/test-matlab-ts-mode-indent-files/indent_tab_between_fcns_expected.m b/tests/test-matlab-ts-mode-indent-files/indent_tab_between_fcns_expected.m new file mode 100644 index 0000000000..8c4c836c70 --- /dev/null +++ b/tests/test-matlab-ts-mode-indent-files/indent_tab_between_fcns_expected.m @@ -0,0 +1,6 @@ +% -*- matlab-ts -*- +function indent_tab_between_fcns +end + +function b +end diff --git a/tests/test-matlab-ts-mode-indent-files/indent_tab_in_fcn.m b/tests/test-matlab-ts-mode-indent-files/indent_tab_in_fcn.m new file mode 100644 index 0000000000..36854dc570 --- /dev/null +++ b/tests/test-matlab-ts-mode-indent-files/indent_tab_in_fcn.m @@ -0,0 +1,5 @@ +% -*- matlab-ts -*- +function indent_tab_in_fcn + disp('here') + +end diff --git a/tests/test-matlab-ts-mode-indent-files/indent_tab_in_fcn_expected.m b/tests/test-matlab-ts-mode-indent-files/indent_tab_in_fcn_expected.m new file mode 100644 index 0000000000..36854dc570 --- /dev/null +++ b/tests/test-matlab-ts-mode-indent-files/indent_tab_in_fcn_expected.m @@ -0,0 +1,5 @@ +% -*- matlab-ts -*- +function indent_tab_in_fcn + disp('here') + +end diff --git a/tests/test-matlab-ts-mode-indent.el b/tests/test-matlab-ts-mode-indent.el new file mode 100644 index 0000000000..60e87a631e --- /dev/null +++ b/tests/test-matlab-ts-mode-indent.el @@ -0,0 +1,166 @@ +;;; test-matlab-ts-mode-indent.el --- Test matlab-ts-mode indent -*- 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-indent-files/NAME.m comparing against +;; ./test-matlab-ts-mode-indent-files/NAME_expected.m +;; + +;;; Code: + +(require 'cl-seq) + +;; Add abs-path of ".." to load-path so we can (require 'matlab-ts-mode) +(let* ((lf (or load-file-name (buffer-file-name (current-buffer)))) + (d1 (file-name-directory lf)) + (parent-dir (expand-file-name (file-name-directory (directory-file-name d1))))) + (add-to-list 'load-path parent-dir t)) + +(require 'matlab-ts-mode) + +(setq matlab-ts-mode--indent-assert t) + +(defun test-matlab-ts-mode-indent-files () + "Return list of full paths to each test-matlab-ts-mode-indent-files/*.m." + (cl-delete-if (lambda (m-file) + (string-match "_expected\\.m$" m-file)) + (directory-files "test-matlab-ts-mode-indent-files" t "\\.m$"))) + +(defvar test-matlab-ts-mode-indent (cons "test-matlab-ts-mode-indent" + (test-matlab-ts-mode-indent-files))) + +(defun trim () + "Trim trailing whitespace and lines." + (setq buffer-file-coding-system 'utf-8-unix) + (let ((delete-trailing-lines t)) + (delete-trailing-whitespace (point-min) (point-max)))) + +(defun test-matlab-ts-mode-indent--typing (m-file expected expected-file) + "Exercise indent by simulating the creation of M-FILE via typing. +This compares the simulation of typing M-FILE line by line against +EXPECTED content in EXPECTED-FILE." + + (message "START: test-matlab-ts-mode-indent (typing) %s" m-file) + + (let* ((typing-m-file-name (concat "typing__" (file-name-nondirectory m-file))) + (contents (with-temp-buffer + (insert-file-contents-literally m-file) + (buffer-substring (point-min) (point-max)))) + (lines (split-string (string-trim contents) "\n"))) + (with-current-buffer (get-buffer-create typing-m-file-name) + (erase-buffer) + (matlab-ts-mode) + + ;; Insert the non-empty lines into typing-m-file-name buffer + (dolist (line lines) + (setq line (string-trim line)) + (when (not (string= line "")) + (insert line "\n"))) + + ;; Now indent each line and insert the empty ("") lines into typing-m-file-buffer + ;; as we indent. This exercises the RET and TAB behaviors which cause different + ;; tree-sitter nodes to be provided to the indent engine rules. + (goto-char (point-min)) + (while (not (eobp)) + + ;; Workaround https://github.com/acristoffers/tree-sitter-matlab/issues/32 + (let* ((node (treesit-node-at (point))) + (parent (and node (treesit-node-parent node)))) + (when (string= (treesit-node-type parent) "ERROR") + (insert " "))) + + (call-interactively #'indent-for-tab-command) ;; TAB on code just added + + ;; While next line in our original contents is a newline insert "\n" + (while (let ((next-line (nth (line-number-at-pos (point)) lines))) + (and next-line (string-match-p "^[ \t\r]*$" next-line))) + (goto-char (line-end-position)) + ;; RET to add blank line + (call-interactively #'newline) + ;; TAB on the same blank line can result in different tree-sitter nodes than + ;; the RET, so exercise that. + (call-interactively #'indent-for-tab-command)) + (forward-line)) + + (trim) + + (let ((typing-got (buffer-substring (point-min) (point-max)))) + (set-buffer-modified-p nil) + (kill-buffer) + (when (not (string= typing-got expected)) + (let ((coding-system-for-write 'raw-text-unix) + (typing-got-file (replace-regexp-in-string "\\.m$" "_typing.m~" m-file))) + (write-region typing-got nil typing-got-file) + (error "Typing %s line-by-line does not match %s, we got %s" m-file expected-file + typing-got-file))))))) + +(defun test-matlab-ts-mode-indent (&optional m-file) + "Test indent using ./test-matlab-ts-mode-indent-files/NAME.m. +Compare indent of ./test-matlab-ts-mode-indent-files/NAME.m against +./test-matlab-ts-mode-indent-files/NAME_expected.m + +If M-FILE (NAME.m) is not provided, loop comparing all +./test-matlab-ts-mode-indent-files/NAME.m files. + +For debugging, you can run with a specified NAME.m, + M-: (test-matlab-ts-mode-font-lock \"test-matlab-ts-mode-indent-files/NAME.m\")" + + (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)) + (test-matlab-ts-mode-indent-files)))) + (dolist (m-file m-files) + (let* ((expected-file (replace-regexp-in-string "\\.m$" "_expected.m" m-file)) + (expected (when (file-exists-p expected-file) + (with-temp-buffer + (insert-file-contents-literally expected-file) + (buffer-string))))) + + (save-excursion + (message "START: test-matlab-ts-mode-indent %s" m-file) + (find-file m-file) + (indent-region (point-min) (point-max)) + (trim) + (let ((got (buffer-substring (point-min) (point-max))) + (got-file (concat expected-file "~"))) + (set-buffer-modified-p nil) + (kill-buffer) + (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 - if %s 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)))) + + (when expected ;; expected-file exists? + (test-matlab-ts-mode-indent--typing m-file expected expected-file))) + + (message "PASS: test-matlab-ts-mode-indent %s" m-file))) + "success") + +(provide 'test-matlab-ts-mode-indent) +;;; test-matlab-ts-mode-indent.el ends here